Source code for Karana.KUtils.MultibodyWebUI._infopanel

# Copyright (c) 2024-2026 Karana Dynamics Pty Ltd. All rights reserved.
#
# NOTICE TO USER:
#
# This source code and/or documentation (the "Licensed Materials") is
# the confidential and proprietary information of Karana Dynamics Inc.
# Use of these Licensed Materials is governed by the terms and conditions
# of a separate software license agreement between Karana Dynamics and the
# Licensee ("License Agreement"). Unless expressly permitted under that
# agreement, any reproduction, modification, distribution, or disclosure
# of the Licensed Materials, in whole or in part, to any third party
# without the prior written consent of Karana Dynamics is strictly prohibited.
#
# THE LICENSED MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# KARANA DYNAMICS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
#
# IN NO EVENT SHALL KARANA DYNAMICS BE LIABLE FOR ANY DAMAGES WHATSOEVER,
# INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR USE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, WHETHER IN CONTRACT, TORT,
# OR OTHERWISE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIALS.
#
# U.S. Government End Users: The Licensed Materials are a "commercial item"
# as defined at 48 C.F.R. 2.101, and are provided to the U.S. Government
# only as a commercial end item under the terms of this license.
#
# Any use of the Licensed Materials in individual or commercial software must
# include, in the user documentation and internal source code comments,
# this Notice, Disclaimer, and U.S. Government Use Provision.

"""Contains helper classes for creating info panel GUIs."""

from abc import abstractmethod, ABC
from typing import Callable, Generic, TypeVar, Type, cast, get_args, Any

import numpy as np
import textwrap
from collections.abc import Iterable, Sequence
import itertools
from pathlib import Path
from dataclasses import dataclass
import traceback

import Karana.Core as kc
import Karana.Math as km
import Karana.WebUI as kw
import Karana.Dynamics as kd
import Karana.Frame as kf
import Karana.Scene as ks
import Karana.Integrators as ki
import Karana.Models as kmdl
from Karana.KUtils import vizutils
import Karana.KUtils.visjs as vjs

from Karana.Math.Kquantities import ureg


from ._effects import (
    EffectItem,
    EffectManager,
    AxesParams,
    OutlineParams,
    HideBodyParams,
)
from ._helpers import widgetArray


T = TypeVar("T")


class WrappedTypeMixin(Generic[T]):
    """Adds the wrapped method."""

    @classmethod
    def wrapped(cls) -> Type[T]:
        """Get the concrete type used for T.

        Note that this requires a new derived class like so:

            >>> class MyWrapper(WrappedTypeMixin[T]):
            ...     pass
            ...
            >>> class IntWrapper(MyWrapper[int]):
            ...     pass
            ...
            >>> IntWrapper.wrapped() == int
            True

        But this WILL NOT work:

            >>> MyWrapper[int].wrapped() == int
            False

        """
        for base in getattr(cls, "__orig_bases__", []):
            if hasattr(base, "__origin__"):
                args = get_args(base)
                if args:
                    return args[0]
        raise TypeError(f"{cls.__name__} has not been specialized with a type.")


[docs] @dataclass class Context: # the Dock instance dock: kw.Dock # The router used to connect widgets router: kw.Router # The gui's shared selection state selection: kw.State # The multibody for the gui multibody: kd.Multibody # The scene manager scene: ks.ProxyScene # The scene used for 3d graphics graphics: ks.WebScene # Middleware for visual effects effects: EffectManager # the mbody treeview mbody_tree_view: kw.TreeView # the visjs creation method - this needs to be fixed (TODO) setup_visjs: Callable # the info pane creation method - this needs to be fixed (TODO) setup_info_panel: Callable # visjs servers visjs_servers: dict[int, tuple[vjs.MultibodyGraphServer, bool]] # visjs iframe visjs_frame: kw.IFrame # the 3D graphics iframe graphics_frame: kw.IFrame
[docs] class AbstractPane(ABC, WrappedTypeMixin[T]): """Interface for an info pane for a given item type.""" def __init__(self, context: Context): """Create the AbstractPane. Derived classes SHOULD call this first in their constructor. """ self._context = context @property def item(self) -> T: """Get the current item or throw an error if it isn't set.""" if not hasattr(self, "_item_getter"): raise RuntimeError("Accessed Pane item before setting it") item = self._item_getter() if item is None: raise RuntimeError("Pane item has gone out of scope!") return item @item.setter def item(self, item: T, /): """Set the current item for the pane.""" if isinstance(item, kc.Base): # Store the item as a weak reference to avoid causing # issues with cleanup self._item_getter = kc.CppWeakRef(item) else: # It's not a base, so just save a trivial lambda getter so # that the other methods can assume self._item_getter() # gets the item. self._item_getter = lambda: item @item.deleter def item(self): """Clear the current item from the pane.""" del self._item_getter
[docs] def getItem(self) -> T | None: """Get the current item or None if it isn't set. If the item was previously set but has gone out of scope, throws an error. """ if not hasattr(self, "_item_getter"): return None item = self._item_getter() if item is None: # This happens if if the item has gone out of scope raise RuntimeError("Pane item has gone out of scope!") return item
@property def context(self) -> Context: return self._context @property @abstractmethod def label(self) -> str: """Get a text label for this pane.""" @property @abstractmethod def wroot(self) -> kw.Widget: """Get the root widget for this pane."""
[docs] def teardown(self, _: T, /): """Do any necessary cleanup when leaving the given item. Panes MAY override this if any cleanup is needed. """
[docs] @abstractmethod def setup(self, item: T, item_context: kw.Json, /): """Setup the pane for the new item."""
[docs] def updateFor(self, item: T, item_context: kw.Json, /): """Set the item to display and refresh. By default this will teardown the old item and setup the new one (which may be the same item when refreshing). Panes MAY override this method to make optimizations. """ if old_item := self.getItem(): self.teardown(old_item) self.item = item self.setup(item, item_context)
[docs] def close(self): """Do any necessary cleanup."""
[docs] def isCompatible(self, item: Any) -> bool: """Check whether the Pane knows how to display an item.""" return isinstance(item, self.wrapped())
def _doTbd(self): print("implementation TBD")
# Registry mapping item types to their pane known_pane_types = [] # Class decorator that saves the pane to a list of known pane types def register(cls): assert issubclass(cls, AbstractPane) known_pane_types.append(cls) return cls @register class BasePane(AbstractPane[kc.Base]): """Pane to display info about any Base-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "Base" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wdump = kw.Markdown(router, text="") self._wverbosity_error = kw.Button( router, text="ERROR", on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.ERROR), tooltip="Change verbosity to ERROR level", ) self._wverbosity_warn = kw.Button( router, text="WARNING", on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.WARN), tooltip="Change verbosity to WARNING level", ) self._wverbosity_debug = kw.Button( router, text="DEBUG", on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.DEBUG), tooltip="Change verbosity to DEBUG level", ) self._wverbosity_trace = kw.Button( router, text="TRACE", on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.TRACE), tooltip="Change verbosity to TRACE level", ) # Setup widget topology self._wroot.addChild(self._wtitle) self._wmd_verbosity = kw.Markdown(router, text="**Verbosity**", in_line=True) self._wlayout_verbosity = widgetArray( router, label=self._wmd_verbosity, children=[ self._wverbosity_error, self._wverbosity_warn, self._wverbosity_debug, self._wverbosity_trace, ], ) self._wroot.addChild(self._wlayout_verbosity) self._wlayout_dump = kw.Layout(router) self._wroot.addChild(self._wlayout_dump) self._wmd_dump = kw.Markdown(router, text="**Info**", in_line=True) self._wlayout_dump.addChild(self._wdump) def setup(self, item: kc.Base, _: kw.Json, /): # Update the text in the title markdown widget name = item.name() id = item.id() title_md = f"### {name} [Base/{id}]" self._wtitle.setText(title_md) # Update the text in the dump markdown widget dump = item.dumpString() dump_md_template = textwrap.dedent( """ ### `dumpString` output: ``` {dump} ``` """ ).strip() dump_md = dump_md_template.format(dump=dump) self._wdump.setText(dump_md) @register class BaseWithVarsPane(AbstractPane[kc.BaseWithVars]): """Pane to display vars for any BaseWithVars-derived object.""" def __init__(self, context: Context): super().__init__(context) self._wtreeview = kw.TreeView(context.router) # List of Path instances representing vars from the last update self._var_paths = [] @property def wroot(self) -> kw.Widget: return self._wtreeview @property def label(self) -> str: return "Vars" def setup(self, item: kc.BaseWithVars, _: kw.Json, /): var_paths: list[Path] = [] nodes: list[kw.TreeView.Node] = [] edges: list[kw.TreeView.Edge] = [] # Generates integer ids id_gen = itertools.count(1) def _extendLeaf(leaf: kc.Var, parent_path: Path, parent_id: int | None): path = parent_path / leaf.name() var_paths.append(path) id_ = next(id_gen) if quantity := leaf.quantity(): quantity = f"[{quantity}]" try: value = leaf.dumpString() except Exception: msg = f"Error evaluating Var {leaf.name()}:\n{traceback.format_exc()}" kc.error(msg) value = "ERROR" nodes.append( kw.TreeView.Node( id=id_, label=f"{leaf.name()} {value} {quantity}", tooltip=leaf.description(), ) ) if parent_id is not None: edges.append(kw.TreeView.Edge(parent_id=parent_id, child_id=id_)) def _extendBranch( branch: kc.NestedVars, parent_path: Path, parent_id: int | None, top_level=False ): path = parent_path / branch.name var_paths.append(path) id_ = next(id_gen) nodes.append( kw.TreeView.Node( id=id_, label=branch.name, tooltip=branch.description, collapsed=not top_level, ) ) if parent_id is not None: edges.append(kw.TreeView.Edge(parent_id=parent_id, child_id=id_)) for leaf in branch.local_vars: _extendLeaf(leaf, path, id_) for ch_branch in branch.nested_vars: _extendBranch(ch_branch, path, id_) _extendBranch( item.getVars().getAllVars(), parent_path=Path("/"), parent_id=None, top_level=True ) # Check whether the topology/naming has changed if var_paths == self._var_paths: # Same structure so just update the labels for node in nodes: self._wtreeview.setNodeLabel(node.id, node.label) else: # Structure has changed so update the entire tree self._var_paths = var_paths self._wtreeview.setTree(nodes, edges) def isCompatible(self, item: Any) -> bool: if not isinstance(item, kc.BaseWithVars): return False # Don't show this panel for items without any vars if item.getVars() is None: return False all_vars = item.getVars().getAllVars() return bool(all_vars.local_vars or all_vars.nested_vars) @register class FramePane(AbstractPane[kf.Frame]): """Pane to display info about any Frame-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "Frame" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._waxes = kw.Button( router, text="2d axes", on_press=self._toggleAxes, tooltip="Toggle line axes for the frame", ) self._waxes3d = kw.Button( router, text="3d axes", on_press=self._toggleAxes3d, tooltip="Toggle 3D axes for the frame", ) self._wview_around = kw.Button( router, text="View around", on_press=self._viewAround, tooltip="Re-center 3D graphics camera to view this frame", ) # AG - the following should be for a selected f2f insstead of # being tied to the newtonian frame. # Record which vizutils visualizations are active self._lvel_cbs = {} self._avel_cbs = {} self._laccel_cbs = {} self._aaccel_cbs = {} def _toggleRatesViz(frame, rtype, color, cbsmap): id_ = frame.id() scene = self.context.scene if id_ in cbsmap: # clean up # print("Deleting") cbsmap[id_]() del cbsmap[id_] else: # print("Creating") dark = vizutils.visualizeFrameToFrameRates( self.context.multibody.virtualRoot().frameToFrame(frame), self.context.scene, rtype, ) dark.setRadius(0.01) dark.setColor(color) dark.registerCallback() cbsmap[id_] = dark scene.update() # enable linear velocity visualization for the frame (only webscene) self._wvel_linear = kw.Button( router, text="Linear velocity", on_press=lambda: _toggleRatesViz( self.item, vizutils.FrameToFrameRateType.VEL_LINEAR, ks.Color.RED, self._lvel_cbs, ), tooltip="Enable visualization of the linear velocity of the frame with respect to the inertial frame", ) # enable angular velocity visualization for the frame (only webscene) self._wvel_angular = kw.Button( router, text="Angular velocity", on_press=lambda: _toggleRatesViz( self.item, vizutils.FrameToFrameRateType.VEL_ANGULAR, ks.Color.GREEN, self._avel_cbs, ), tooltip="Enable visualization of the angular velocity of the frame with respect to the inertial frame", ) # enable linear acceleration visualization for the frame (only webscene) self._waccel_linear = kw.Button( router, text="Linear acceleration", on_press=lambda: _toggleRatesViz( self.item, vizutils.FrameToFrameRateType.ACCEL_LINEAR, ks.Color.BLUE, self._laccel_cbs, ), tooltip="Enable visualization of the linear acceleration of the frame with respect to the inertial frame", ) # enable angular acceleration visualization for the frame (only webscene) self._waccel_angular = kw.Button( router, text="Angular acceleration", on_press=lambda: _toggleRatesViz( self.item, vizutils.FrameToFrameRateType.ACCEL_ANGULAR, ks.Color.YELLOW, self._aaccel_cbs, ), tooltip="Enable visualization of the angular acceleration of the frame with respect to the inertial frame", ) def _firstChildFrame(f: kf.Frame): cf = f.childrenFrames() if len(cf) > 0: return cf[0] else: return None # change selection to the first child body (drop down, or multiple buttons) self._wselect_down = kw.Button( router, text="Down", on_press=lambda: _selectObject( _firstChildFrame(self.item), self.context.selection, tooltip="Change selection to a frame", ), ) # change selection to the parent frame self._wselect_up = kw.Button( router, text="Up", on_press=lambda: _selectObject(self.item.parentFrame(), self.context.selection), tooltip="Change selection to the parent frame", ) def _siblingFrame(f: kf.Frame, forward: bool): """Get the next/previous sibling frame.""" parent = f.parentFrame() siblingsPlus = parent.childrenFrames() if len(siblingsPlus) == 1: return None index = 0 for b in siblingsPlus: if b.id() == f.id(): break index += 1 nbodies = len(siblingsPlus) next_index = (index + 1 if forward else index - 1) % nbodies # print("index=", index, next_index) new_frame = siblingsPlus[next_index] return new_frame # change selection to the next sibling body self._wselect_right = kw.Button( router, text="Right", on_press=lambda: _selectObject(_siblingFrame(self.item, True), self.context.selection), tooltip="Change selection to a sibling frame on the right", ) # change selection to the previous sibling body self._wselect_left = kw.Button( router, text="Left", on_press=lambda: _selectObject(_siblingFrame(self.item, False), self.context.selection), tooltip="Change selection to a sibling frame on the left", ) # Setup widget topology self._wroot.addChild(self._wtitle) self._wmd_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wlayout_highlight = widgetArray( router, label=self._wmd_highlight, children=[self._waxes, self._waxes3d, self._wview_around], ) self._wroot.addChild(self._wlayout_highlight) self._wmd_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wlayout_select = widgetArray( router, label=self._wmd_selection, children=[ self._wselect_up, self._wselect_left, self._wselect_right, self._wselect_down, ], ) self._wroot.addChild(self._wlayout_select) self._wmd_visualizerates = kw.Markdown(router, text="**Visualize Rates**", in_line=True) self._wlayout_visrates = widgetArray( router, label=self._wmd_visualizerates, children=[ self._wvel_linear, self._wvel_angular, self._waccel_linear, self._waccel_angular, ], ) self._wroot.addChild(self._wlayout_visrates) def _toggleAxes(self): self.context.effects.frame_axes.toggle( [EffectItem(obj=self.item, params=AxesParams(scale=1.0, style="lines"))] ) def _toggleAxes3d(self): self.context.effects.frame_axes.toggle( [EffectItem(obj=self.item, params=AxesParams(style="parts"))] ) def _viewAround(self): self.context.scene.viewAroundFrame(self.item, offset=[3.0, 3.0, 3.0]) def setup(self, item: kf.Frame, _: kw.Json, /): name = item.name() id = item.id() mb = self.context.multibody nd = mb.getNodeAncestor(item) nd_name = nd.name() if nd is not None else "(No ancestor)" nd_str = nd.typeString() if nd is not None else "n/a" nd_bd_str = nd.parentBody().name() if nd is not None else "n/a" title_md = f"### {name} [Frame/{id}] [Anc node={nd_name} ({nd_str}) body={nd_bd_str}]" self._wtitle.setText(title_md) parent = item.parentFrame() not_root = parent is not None has_siblings = not_root and len(parent.childrenFrames()) > 1 has_children = len(item.childrenFrames()) > 0 self._wselect_up.setVisible(not_root) self._wselect_right.setVisible(has_siblings) self._wselect_left.setVisible(has_siblings) self._wselect_down.setVisible(has_children) @register class FrameToFramePane(AbstractPane[kf.FrameToFrame]): """Pane to display info about any FrameToFrame-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "FrameToFrame" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") # Setup widget topology self._wroot.addChild(self._wtitle) # AG - the following should be for a selected f2f insstead of # being tied to the newtonian frame. # Record which vizutils visualizations are active self._lvel_cbs = {} self._avel_cbs = {} self._laccel_cbs = {} self._aaccel_cbs = {} def _toggleRatesViz(f2f, rtype, color, cbsmap): id_ = f2f.id() scene = self.context.scene if id_ in cbsmap: # clean up # print("Deleting") cbsmap[id_]() del cbsmap[id_] else: # print("Creating") dark = vizutils.visualizeFrameToFrameRates( f2f, self.context.scene, rtype, ) dark.setRadius(0.01) dark.setColor(color) dark.registerCallback() cbsmap[id_] = dark scene.update() # enable linear velocity visualization for the frame (only webscene) self._wvel_linear = kw.Button( router, text="Linear velocity", on_press=lambda: _toggleRatesViz( self.item, vizutils.FrameToFrameRateType.VEL_LINEAR, ks.Color.RED, self._lvel_cbs, ), tooltip="Visualize the relative linear velocity across this frame to frame.", ) # enable angular velocity visualization for the frame (only webscene) self._wvel_angular = kw.Button( router, text="Angular velocity", on_press=lambda: _toggleRatesViz( self.item, vizutils.FrameToFrameRateType.VEL_ANGULAR, ks.Color.GREEN, self._avel_cbs, ), tooltip="Visualize the relative angular velocity across this frame to frame.", ) # enable linear acceleration visualization for the frame (only webscene) self._waccel_linear = kw.Button( router, text="Linear acceleration", on_press=lambda: _toggleRatesViz( self.item, vizutils.FrameToFrameRateType.ACCEL_LINEAR, ks.Color.BLUE, self._laccel_cbs, ), tooltip="Visualize the relative linear acceleration across this frame to frame.", ) # enable angular acceleration visualization for the frame (only webscene) self._waccel_angular = kw.Button( router, text="Angular acceleration", on_press=lambda: _toggleRatesViz( self.item, vizutils.FrameToFrameRateType.ACCEL_ANGULAR, ks.Color.YELLOW, self._aaccel_cbs, ), tooltip="Visualize the relative angular acceleration across this frame to frame.", ) def _pathFrames(f2f: kf.FrameToFrame): if isinstance(f2f, kf.EdgeFrameToFrame): return elif isinstance(f2f, kf.OrientedChainedFrameToFrame): of2f = cast(kf.OrientedChainedFrameToFrame, f2f) return of2f.getPath() elif isinstance(f2f, kf.ChainedFrameToFrame): cf2f = cast(kf.ChainedFrameToFrame, f2f) return cf2f.getPath() # change selection to the first child body (drop down, or multiple buttons) self._wpath = kw.Button( router, text="Highlight path", on_press=lambda: _selectObject(_pathFrames(self.item), self.context.selection), ) self._wroot.addChild(self._wpath) # change selection to the pframe self._wselect_pframe = kw.Button( router, text="Select pframe", on_press=lambda: _selectObject(self.item.pframe(), self.context.selection), tooltip="Change selection to the (to) pframe for this frame to frame", ) # change selection to the oframe self._wselect_oframe = kw.Button( router, text="Select oframe", on_press=lambda: _selectObject(self.item.oframe(), self.context.selection), tooltip="Change selection to the (from) oframe for this frame to frame", ) """ self._wmd_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wlayout_highlight = widgetArray( router, label=self._wmd_highlight, children=[self._waxes, self._waxes3d, self._wview_around], ) self._wroot.addChild(self._wlayout_highlight) """ self._wmd_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wlayout_select = widgetArray( router, label=self._wmd_selection, children=[ self._wselect_pframe, self._wselect_oframe, ], ) self._wroot.addChild(self._wlayout_select) self._wmd_visualizerates = kw.Markdown(router, text="**Visualize Rates**", in_line=True) self._wlayout_visrates = widgetArray( router, label=self._wmd_visualizerates, children=[ self._wvel_linear, self._wvel_angular, self._waccel_linear, self._waccel_angular, ], ) self._wroot.addChild(self._wlayout_visrates) def setup(self, item: kf.FrameToFrame, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody title_md = f"### {name} [FrameToFrame/{id}]" self._wtitle.setText(title_md) has_path = isinstance(item, kf.OrientedChainedFrameToFrame) or isinstance( item, kf.ChainedFrameToFrame ) self._wpath.setVisible(has_path) def _firstChildBody(st: kd.SubTree, bd: kd.PhysicalBody) -> kd.BodyBase | None: """Return the first child body if the body has children bodies.""" children = st.childrenBodies(bd) child = None if not children else children[0] return child def _parentBody(st: kd.SubTree, bd: kd.PhysicalBody) -> kd.BodyBase | None: """Return the parent body if the body has one.""" return None if st.isBaseBody(bd) else st.parentBody(bd) def _siblingNode(nd: kd.Node, forward: bool): """Get the next/previous sibling node.""" bd = nd.parentBody() siblingsPlus = bd.nodeList() if len(siblingsPlus) == 1: return None index = 0 for n in siblingsPlus: if n.id() == nd.id(): break index += 1 nbodies = len(siblingsPlus) next_index = (index + 1 if forward else index - 1) % nbodies # print("index=", index, next_index) new_node = siblingsPlus[next_index] return new_node def _siblingBody(st: kd.SubTree, bd: kd.PhysicalBody, forward: bool): """Get the next/previous sibling body.""" parent = st.parentBody(bd) siblingsPlus = st.childrenBodies(parent) if len(siblingsPlus) == 1: return None index = 0 for b in siblingsPlus: if b.id() == bd.id(): break index += 1 nbodies = len(siblingsPlus) next_index = (index + 1 if forward else index - 1) % nbodies # print("index=", index, next_index) new_body = siblingsPlus[next_index] return new_body def _firstChildSubtree(st: kd.SubTree) -> kd.SubTree | None: """Return the first child subtree if the subtree has children bodies.""" children = st.childrenSubTrees() child = None if not children else children[0] return child def _parentSubtree(st: kd.SubTree) -> kd.SubTree | None: """Return the parent subtree if the subtree has one.""" return st.parentSubTree() def _siblingSubtree(st: kd.SubTree, forward: bool) -> kd.SubTree | None: """Get the next/previous sibling subtree.""" parent = st.parentSubTree() if not parent: return None siblingsPlus = parent.childrenSubTrees() if len(siblingsPlus) == 1: return None index = 0 for b in siblingsPlus: if b.id() == st.id(): break index += 1 nsts = len(siblingsPlus) next_index = (index + 1 if forward else index - 1) % nsts # print("index=", index, next_index) new_subtree = siblingsPlus[next_index] return new_subtree # callback to create a subtree's tree view def _createSubTreeView(st, gui_context): if isinstance(st, kd.Multibody): return from ._treeview import createSubTreeBodiesTreeView tv = createSubTreeBodiesTreeView( router=gui_context.router, gui_selection_state=gui_context.selection, st=st, ) tv.refresh() gui_context.dock.addChild( title=f"{st.name()}", widget=tv, relative_to=gui_context.mbody_tree_view, direction="within", ) # callback to change subhinge coords set by the sliders def changeSubhingeCoord(item, st, scene, Q, shindex, cindex, with_ik, wstatus=None): if not item: return if shindex is None: print("WARNING: Please select a coordinate first ...") return hge = None if isinstance(item, kd.PhysicalBody): hge = item.parentHinge() elif isinstance(item, kd.LoopConstraintCutJoint): hge = item.hinge() elif isinstance(item, kd.FramePairHinge): hge = item elif isinstance(item, kd.CompoundBody): hge = item.parentHinge() elif isinstance(item, kd.CompoundHinge): hge = item else: assert 0 assert shindex < hge.nSubhinges() sh = hge.subhinge(shindex) assert cindex < sh.nQ() Qvec = sh.getQ() Qvec[cindex] = Q sh.setQ(Qvec) if isinstance(st, kd.SubGraph) and with_ik: # do IK # offset = st.coordOffsets(sh).Q st.cks().freezeCoord(sh, cindex, kd.CKFrozenCoordType.Q) err = st.cks().solveQ() if err < 1e-10: color = "green" stxt = "SUCCESS" extra = f"[Q={Q:.4}]" else: color = "red" stxt = "FAILED" extra = f"[Q={Q:.4}, err={err:.4e}]" status = f'**IK status:** <span style="color:{color}">{stxt}</span> {extra}' # print(" err=", err, color) st.cks().unfreezeCoord(sh, cindex, kd.CKFrozenCoordType.Q) else: status = "**IK status:** N/A" if wstatus: wstatus.setText(status) scene.update() def _createSubTreeVisJs(st: kd.SubTree, gui_context: Context): """Create a new tab with visjs layout for the subtree.""" server = gui_context.setup_visjs(st) gui_context.effects.registerGraphServer(server) gui_context.visjs_servers[st.id()] = (server, True) iframe = kw.IFrame(gui_context.router, server.getUrl()) gui_context.dock.addChild(st.name(), iframe, gui_context.visjs_frame, "within") if isinstance(st, kd.SubGraph): if len(st.enabledConstraints()) > 0: server.enableSubGraph("constraints") def _highlightBodyConstraints(body: kd.PhysicalBody, gui_context: Context): """Show a line between this body and other bodies with which it has constraints.""" # Get a list of all constraints including this body constraints = gui_context.multibody.getBodyLoopConstraints(body) effects = [] for constraint in constraints: assert constraint.sourceNode() src_body = constraint.sourceNode().parentBody() assert constraint.targetNode() tgt_body = constraint.targetNode().parentBody() effect = EffectItem(obj=(src_body, tgt_body), params=None) effects.append(effect) gui_context.effects.graph_edge_adder.toggle(effects) def _highlightBodyNodes(cstate: bool, bodies: list[kd.PhysicalBody], effects: EffectManager): """Toggle frames axes for nodes on a physical bodies.""" if cstate: axes_effects = [] axes_params = AxesParams() for body in bodies: # toggle axes for the nodes for node in body.nodeList(): axes_effects.append(EffectItem(obj=node, params=axes_params)) effects.frame_axes.set(axes_effects) else: effects.frame_axes.set([]) def _showLoopConstraints(lcs: list[kd.LoopConstraintBase], gui_context: Context): """Show loop constraints involving the pair of bodies in visjs.""" # TBD AG - show line in visjs connecting the pair of bodies for this loop constraint assert 0 """ mb = gui_context.multibody for c in lcs: snd = c.sourceNode() tnd = c.targetNode() bd1 = mb.virtualRoot() if not snd else snd.parentBody() bd2 = mb.virtualRoot() if not tnd else tnd.parentBody() _showConstraint(bd1, bd2)coo pass """ def _swingHinge( st: kd.SubTree, hinges: list[kd.FramePairHinge | None], disable_ik: bool, gui_context: Context ): """Articulate bodies in WebScene sequentially.""" # for body in bodies: for hinge in hinges: """ if body is None: continue if body.isRootBody(): continue hinge = body.parentHinge() """ # Loop through and articulate each coordinate if isinstance(hinge, kd.FramePairHinge): axes_params = AxesParams() outline_params = OutlineParams(level="secondary") for coord_offset in range(hinge.coordData().nU()): subhinge, subhinge_offset = hinge.coordData().coordAt(coord_offset) sh = cast(kd.PhysicalSubhinge, subhinge) pfrm = sh.pframe() axes_effects = [EffectItem(obj=pfrm, params=axes_params)] bd = gui_context.multibody.getNodeAncestor(pfrm).parentBody() outline_effects = [ EffectItem( obj=bd, # cast(kf.Frame, body.pnode().parentBody()), params=outline_params, ) ] gui_context.effects.frame_axes.toggle(axes_effects) gui_context.effects.frame_outliner.toggle(outline_effects) st.articulateSubhinge(sh, subhinge_offset, disable_ik) gui_context.effects.frame_axes.toggle(axes_effects) gui_context.effects.frame_outliner.toggle(outline_effects) else: sh = hinge.subhinge(0) for coord_offset in range(sh.nU()): st.articulateSubhinge(sh, coord_offset, disable_ik) def _highlightFrames( cstate: bool, frames: Sequence[kf.Frame], # toggle_over_set: bool, gui_context: Context, ): """Highlight frame axes.""" if cstate: axes_effects = [] axes_params = AxesParams() for f in frames: axes_effects.append(EffectItem(obj=f, params=axes_params)) if 0 and toggle_over_set: gui_context.effects.frame_axes.toggle(axes_effects) else: gui_context.effects.frame_axes.set(axes_effects) else: gui_context.effects.frame_axes.set([]) def _getPhysicalBodies(bdlist: list[kd.BodyBase]) -> list[kd.PhysicalBody]: result = [] for bd in bdlist: if not bd.isCompoundBody(): result.append(bd) else: result.extend(cast(kd.CompoundBody, bd).physicalBodiesTree().sortedPhysicalBodiesList()) return result # ------------------------------------ # mbody level kinematics sim callback def _mbodyKinematicsSim( sh: kd.SubhingeBase, cindex: int, deltaq: float, duration: float, gui_context: Context ): # shindex = self._coord_move_indices[0] # # get the selected sindex/cindex, and the Q values # if shindex is None: # return # cindex = self._coord_move_indices[1] # hge = self.item.parentHinge() # sh = hge.subhinge(shindex) # create an SP and put it in kinematics mode sg = cast(kd.SubGraph, gui_context.multibody) sp = kd.StatePropagator.create( sg, ki.IntegratorType.EULER, None, None, kd.MMSolverType.KINEMATICS ) # freeze coord sg.cks().clearFrozenCoords() sg.setU(0) sg.setUdot(0) sg.cks().freezeCoord(sh, cindex) ti = 0 tf = duration qi = sh.getQ()[cindex] qf = qi + deltaq ui = uf = 0 sp.setTime(ti) x = sp.assembleState() sp.setState(x) # create a profile generator pg = kmdl.FloatCubicSplineProfileGenerator.create("kinpg", ti, qi, ui, tf, qf, uf) # defined pre deriv CB for setting values def getNewCoord(t: float, x: np.array): q = pg.getQ(t) u = pg.getU(t) udot = pg.getUdot(t) # regular non-convel subhinge udotvec = np.zeros(sh.nU()) udotvec[cindex] = udot sh.setUdot(udotvec) sp.fns.pre_deriv_fns["newcoord"] = getNewCoord update_scene = kmdl.UpdateProxyScene.create("scene", sp, gui_context.scene) time_display = kmdl.TimeDisplay.create("time_display", sp, gui_context.scene.graphics()) sync_real = kmdl.SyncRealTime.create("sync_real", sp, 1.0) # sp.registerModel(update_scene) # run a loop to advance time by # sp.advanceBy(duration) for i in range(10): print(f"Advancing to {sp.getTime()} ...") sp.advanceBy(duration / 10) # unfreeze coord sg.cks().unfreezeCoord(sh, cindex) # cegraph level kinematics sim callback def _ceKinematicsSim( sg: kd.SubGraph, sh: kd.SubhingeBase, cindex: int, deltaq: float, duration: float, gui_context: Context, ): # shindex = self._coord_move_indices[0] # # get the selected sindex/cindex, and the Q values # if shindex is None: # return # cindex = self._coord_move_indices[1] # hge = self.item.parentHinge() # sh = hge.subhinge(shindex) # # create an SP and put it in kinematics mode # sg = cast(kd.SubGraph, self.context.multibody) sp = kd.StatePropagator.create( sg, ki.IntegratorType.EULER, None, None, kd.MMSolverType.KINEMATICS ) """ # freeze coord sg.cks().clearFrozenCoords() sg.cks().freezeCoord(sh, cindex) """ sg.setU(0) sg.setUdot(0) ti = 0 tf = duration qi = sh.getQ()[cindex] qf = qi + deltaq ui = uf = 0 sp.setTime(ti) x = sp.assembleState() sp.setState(x) # create a profile generator pg = kmdl.FloatCubicSplineProfileGenerator.create("kinpg", ti, qi, ui, tf, qf, uf) time_display = kmdl.TimeDisplay.create("time_display", sp, gui_context.scene.graphics()) sync_real = kmdl.SyncRealTime.create("sync_real", sp, 1.0) # defined pre deriv CB for setting values def getNewCoord(t: float, x: np.array): q = pg.getQ(t) u = pg.getU(t) udot = pg.getUdot(t) # regular non-convel subhinge udotvec = np.zeros(sh.nU()) udotvec[cindex] = udot sh.setUdot(udotvec) sp.fns.pre_deriv_fns["newcoord"] = getNewCoord update_scene = kmdl.UpdateProxyScene.create("scene", sp, gui_context.scene) # sp.registerModel(update_scene) # run a loop to advance time by # sp.advanceBy(duration) for i in range(10): print(f"Advancing to {sp.getTime()} ...") sp.advanceBy(duration / 10) # unfreeze coord # sg.cks().unfreezeCoord(sh, cindex) def _highlightBodies( cstate: bool, bodies: list[kd.BodyBase], # primary secondary_bodies: list[kd.BodyBase], tertiary_bodies: list[kd.BodyBase], # st: kd.SubTree, # toggle_over_set: bool, gui_context: Context, ): """Highlight bodies in visjs and outline in WebScene.""" if cstate: # do outline effects in 3D graphics outline_effects = [] # for bd in bodies: for bd in bodies: # _getPhysicalBodies(bodies): if isinstance(bd, kf.Frame): outline_effects.append(EffectItem(obj=bd, params=OutlineParams(level="primary"))) for bd in _getPhysicalBodies(bodies): outline_effects.append(EffectItem(obj=bd, params=OutlineParams(level="primary"))) for bd in secondary_bodies: # _getPhysicalBodies(secondary_bodies): if isinstance(bd, kf.Frame): outline_effects.append(EffectItem(obj=bd, params=OutlineParams(level="secondary"))) for bd in _getPhysicalBodies(secondary_bodies): outline_effects.append(EffectItem(obj=bd, params=OutlineParams(level="secondary"))) for bd in tertiary_bodies: # _getPhysicalBodies(tertiary_bodies): if isinstance(bd, kf.Frame): outline_effects.append(EffectItem(obj=bd, params=OutlineParams(level="tertiary"))) for bd in _getPhysicalBodies(tertiary_bodies): outline_effects.append(EffectItem(obj=bd, params=OutlineParams(level="tertiary"))) # Now also highlight those bodies in the visjs graph. Here we don't # have a notion of a 'level' so just highlight all of them equally. all_bodies = bodies + secondary_bodies + tertiary_bodies if 0 and toggle_over_set: gui_context.effects.graph_highlighter.toggle( [EffectItem(obj=bd, params=None) for bd in all_bodies] ) gui_context.effects.frame_outliner.toggle(outline_effects) else: gui_context.effects.graph_highlighter.set( [EffectItem(obj=bd, params=None) for bd in all_bodies] ) gui_context.effects.frame_outliner.set(outline_effects) else: gui_context.effects.graph_highlighter.set([]) gui_context.effects.frame_outliner.set([]) def _wireframeBodies(bodies: list[kd.PhysicalBody]): """Change all bodies' scene parts in WebScene to wireframe mode.""" for bd in bodies: for sp in bd.getSceneParts(): _wireframe(sp) def _transparentBodies(bodies: list[kd.PhysicalBody]): """Change all bodies' scene parts in WebScene to semi-transparent mode.""" for bd in bodies: for sp in bd.getSceneParts(): _transparent(sp) def _wireframe(sp: ks.ProxyScenePart): """Toggle wireframe mode for a scene part.""" # TBD AG - add wireframe support assert sp pass def _transparent(sp: ks.ProxyScenePart): """Toggle transparent mode for a scene part.""" # TBD AG - add transparency support assert sp pass def _toggleVisibleBodies(cstate: bool, bodies: list[kd.PhysicalBody], context: Context, layers): """Toggle visibility of bodies' scene parts in WebScene.""" if not cstate: params = HideBodyParams(layers=layers) context.effects.body_part_hider.set(EffectItem(obj=body, params=params) for body in bodies) else: context.effects.body_part_hider.set([]) # TODO: Old implementation. Remove after confirming above is correct. # sps = [] # for bd in bodies: # sps += scene.getPartsAttachedToFrame(bd, layers) # sps += scene.getPartsAttachedToFrame(bd.pnode(), layers) # sps += scene.getPartsAttachedToFrame(bd.onode(), layers) # for nd in bd.nodeList(): # sps += scene.getPartsAttachedToFrame(nd, layers) # for nd in bd.constraintNodeList(): # sps += scene.getPartsAttachedToFrame(nd) # lc = nd.loopConstraint() # if lc and lc.type() == kd.BilateralConstraintType.CUTJOINT_LOOP: # for i in range(lc.hinge().nSubhinges()): # sps += scene.getPartsAttachedToFrame(lc.hinge().subhinge(i).pframe(), layers) # for i in range(bd.parentHinge().nSubhinges()): # sps += scene.getPartsAttachedToFrame(bd.parentHinge().subhinge(i).pframe(), layers) # if len(sps): # # lsps = [x for x in sps if isinstance(x, ks.ProxyScenePart) and x.getLayers() & layer] # # get the visibility of the first scene part and use that to # # flip the state of the all the scene parts so that they are # # in sync # if len(sps) > 0: # visibility = sps[0].getVisible() # for sp in sps: # sp.setVisible(not visibility) def _selectObject(obj: kc.Base | None, selection: kw.State, st_context=None): """Change the selection to the specified object.""" if obj is None: return print(f"Selecting '{obj.name()}' ({obj.typeString()}/{obj.id()})") selection.set(kw.Selection([kw.Selection.Item(obj.id(), context=st_context)]).dump()) @register class BodyBasePane(AbstractPane[kd.BodyBase]): """Pane to display info about any BodyBase-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "BodyBase" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # ---------------------------- # highlight parent body, children body (different highlighting) # pbd = cast(kd.PhysicalBody, self.item) self._wparentchildren = kw.Toggle( router, text="Parent/Children", # on_press= lambda: _parentChildeCB(self.item) on_toggle=lambda cstate: _highlightBodies( cstate, bodies=[self.item], secondary_bodies=[ self.subtree.parentBody(self.item) ], # self.item.physicalParentBody()], tertiary_bodies=[ cast(kd.PhysicalBody, x) for x in self.subtree.childrenBodies(self.item) ], # toggle_over_set=False, gui_context=self.context, ), tooltip="Toggle the highlighting of the parent and children bodies", render_as_button=True, ) # highlight downstream bodies def _downstreamBodies(st: kd.SubTree, bd: kd.BodyBase) -> list[kd.BodyBase]: """Return the physical downstream bodies for a body within a subtree.""" # print("KKKKK", [x.name() for x in st.filteredBodies(bd)]) return st.filteredBodies(bd) self._wdownstream = kw.Toggle( router, text="Downstream bodies", on_toggle=lambda cstate: _highlightBodies( cstate, bodies=[self.item], secondary_bodies=_downstreamBodies(self.subtree, self.item), tertiary_bodies=[], # toggle_over_set=False, gui_context=self.context, ), tooltip="Toggle the highlighting of the downstream bodies", render_as_button=True, ) # highlight upstream bodies def _upstreamBodies(st: kd.SubTree, bd: kd.BodyBase) -> list[kd.BodyBase]: """Return the physical upstream bodies for a body.""" if st.isBaseBody(bd): return [] else: return st.filteredBodies( st.virtualRoot(), [bd], [st.parentBody(bd)] ) # [bd.physicalParentBody()]) self._wupstream = kw.Toggle( router, text="Upstream bodies", on_toggle=lambda cstate: _highlightBodies( cstate, bodies=[self.item], secondary_bodies=_upstreamBodies(self.subtree, self.item), tertiary_bodies=[], # toggle_over_set=False, gui_context=self.context, ), tooltip="Toggle the highlighting of the upstream bodies", render_as_button=True, ) self._wmd_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wlayout_highlight = widgetArray( router, label=self._wmd_highlight, children=[self._wparentchildren, self._wupstream, self._wdownstream], ) self._wroot.addChild(self._wlayout_highlight) # ---------------------------- # articulate parent self._wswing_parent = kw.Button( router, text="Parent body", on_press=lambda: __swingHinge( self.subtree, [self.item.physicalParentBody().parentHinge()], True, self.context ), tooltip="Auto articulate parent body", ) # articulate childrent sequentially self._wswing_children = kw.Button( router, text="Children bodies", on_press=lambda: __swingHinge( self.subtree, [x.parentHinge() for x in self.subtree.childrenBodies(self.item)], True, self.context, ), tooltip="Auto articulate children bodies in sequence", ) # articulate downstream bodies sequentially self._wswing_downstream = kw.Button( router, text="Downstream bodies", on_press=lambda: __swingHinge( self.subtree, [x.parentHinge() for x in _downstreamBodies(self.subtree, self.item)], False, self.context, ), tooltip="Auto articulate downstream bodies in sequence", ) self._wmd_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wlayout_articulate = widgetArray( router, label=self._wmd_articulate, children=[self._wswing_children, self._wswing_parent, self._wswing_downstream], ) self._wroot.addChild(self._wlayout_articulate) # ---------------------------- # change selection to the first child body (drop down, or multiple buttons) self._wselect_down = kw.Button( router, text="Down", on_press=lambda: _selectObject( _firstChildBody(self.subtree, self.item), self.context.selection, st_context={"subtree_id": self.subtree.id()}, ), tooltip="Change selection to a child body", ) # change selection to the parent body self._wselect_up = kw.Button( router, text="Up", on_press=lambda: _selectObject( _parentBody(self.subtree, self.item), self.context.selection, st_context={"subtree_id": self.subtree.id()}, ), tooltip="Change selection to the parent body", ) # change selection to the next sibling body self._wselect_right = kw.Button( router, text="Right", on_press=lambda: _selectObject( _siblingBody(self.subtree, self.item, True), self.context.selection, st_context={"subtree_id": self.subtree.id()}, ), tooltip="Change selection to the next sibling body", ) # change selection to the previous sibling body self._wselect_left = kw.Button( router, text="Left", on_press=lambda: _selectObject( _siblingBody(self.subtree, self.item, False), self.context.selection, st_context={"subtree_id": self.subtree.id()}, ), tooltip="Change selection to the previous sibling body", ) # Setup widget topology self._wmd_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wlayout_select = widgetArray( router, label=self._wmd_selection, children=[ self._wselect_up, self._wselect_left, self._wselect_right, self._wselect_down, ], ) self._wroot.addChild(self._wlayout_select) def setup(self, item: kd.BodyBase, item_context: kw.Json, /): name = item.name() id = item.id() if isinstance(item_context, dict): # set the context subtree st_id = item_context.get("subtree_id", None) if not isinstance(st_id, int): raise ValueError("Could not get the subtree ID.") self._subtree = cast(kd.SubTree, kc.BaseContainer.singleton().at(st_id)) if not isinstance(self._subtree, kd.SubTree): raise ValueError(f"Did not get a SubTree from ID {st_id}.") elif item and not item_context: # assert 0 # should never get here if not item.isCompoundBody(): kc.warn( f"The subtree value is missing when selecting the {item.name()} body. Defaulting to multibody." ) self._subtree = self.context.multibody else: kc.warn( f"The subtree value is missing when setting up the {item.name()} compound body." ) not_root_body = not item.isRootBody() # need to set this for 'subtree' to work self.item = item if not_root_body: hge_type = kd.HingeBase.hingeTypeString(item.parentHinge().hingeType()) title_md = f"### {name} [BodyBase/{id}/{hge_type}] ({self.subtree.name()})" else: title_md = f"### {name} [BodyBase/{id}] ({self.subtree.name()})" self._wtitle.setText(title_md) # if hasattr(self, "_subtree"): not_basebody = not_root_body and not self.subtree.isBaseBody(item) has_children = not_root_body and len(self.subtree.childrenBodies(item)) > 0 parent = not_root_body and self.subtree.parentBody(item) has_siblings = not_root_body and len(self.subtree.childrenBodies(parent)) > 1 self._wupstream.setVisible(not_basebody) self._wswing_parent.setVisible(not_basebody) self._wselect_up.setVisible(not_basebody) self._wdownstream.setVisible(has_children) self._wswing_downstream.setVisible(has_children) self._wswing_children.setVisible(has_children) self._wselect_down.setVisible(has_children) self._wselect_right.setVisible(has_siblings) self._wselect_left.setVisible(has_siblings) @property def subtree(self): # AG - needs to be updated to return true subgraph context # return self.context.multibody if self.item.isCompoundBody(): return cast(kd.CompoundBody, self.item).bodiesTree().parentSubTree() else: return self.context.multibody def close(self): if hasattr(self, "_subtree"): del self._subtree @register class PhysicalBodyPane(AbstractPane[kd.PhysicalBody]): """Pane to display info about any PhysicalBody-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "Body" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) """ # ---------------------------------- # the initial Q values (to use for reset) self.init_Q = [] self.full_Q = [] # free swing, no IK self._wswing = kw.Button( router, text="Swing free", on_press=lambda: _swingHinge(self.subtree, [self.item.parentHinge()], True, self.context), tooltip="Auto articulate the body while ignoring any constraints", ) # constrained swing, with IK on self._wswing_ik = kw.Button( router, text="Swing IK", on_press=lambda: _swingHinge(self.subtree, [self.item.parentHinge()], False, self.context), tooltip="Auto articulate the body with constraint inverse kinematics", ) # ------------------------------------ # kinematics sim callback def _kinematicsSimOBSOLETE(deltaq: float, duration: float): shindex = self._coord_move_indices[0] # get the selected sindex/cindex, and the Q values if shindex is None: return cindex = self._coord_move_indices[1] hge = self.item.parentHinge() sh = hge.subhinge(shindex) # create an SP and put it in kinematics mode sg = cast(kd.SubGraph, self.context.multibody) sp = kd.StatePropagator.create( sg, ki.IntegratorType.EULER, None, None, kd.MMSolverType.KINEMATICS ) # freeze coord sg.cks().clearFrozenCoords() sg.setU(0) sg.setUdot(0) sg.cks().freezeCoord(sh, cindex) ti = 0 tf = duration qi = sh.getQ()[cindex] qf = qi + deltaq ui = uf = 0 sp.setTime(ti) x = sp.assembleState() sp.setState(x) # create a profile generator pg = kmdl.FloatCubicSplineProfileGenerator.create("kinpg", ti, qi, ui, tf, qf, uf) # defined pre deriv CB for setting values def getNewCoord(t: float, x: np.array): q = pg.getQ(t) u = pg.getU(t) udot = pg.getUdot(t) # regular non-convel subhinge udotvec = np.zeros(sh.nU()) udotvec[cindex] = udot sh.setUdot(udotvec) sp.fns.pre_deriv_fns["newcoord"] = getNewCoord update_scene = kmdl.UpdateProxyScene.create("scene", sp, self.context.scene) # sp.registerModel(update_scene) # run a loop to advance time by # sp.advanceBy(duration) for i in range(10): print(f"Advancing to {sp.getTime()} ...") sp.advanceBy(duration / 10) # unfreeze coord sg.cks().unfreezeCoord(sh, cindex) # constrained swing, with IK on self._wswing_kinsim = kw.Button( router, text="Kinematics sim", on_press=lambda: _mbodyKinematicsSim( self.item.parentHinge().subhinge(self._coord_move_indices[0]), self._coord_move_indices[1], 0.3, 0.5, self.context, ), tooltip="Articulate the body using kinematics simulation mode", ) self._wik_layout = widgetArray( router, [], alignment="column", alignItems="left", addBorder=True ) self._wroot.addChild(self._wik_layout) self._wmd_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wlayout_articulate = widgetArray( router, label=self._wmd_articulate, children=[self._wswing, self._wswing_ik, self._wswing_kinsim], ) self._wik_layout.addChild(self._wlayout_articulate) # -------------------------------------- # create subhinge and coordinate entries # set IK mode on/off for coordinates self._coord_ik = False def _toggleCoordIK(cstate): self._coord_ik = cstate self._wcoord_ik = kw.Toggle( router, text="IK mode", on_toggle=_toggleCoordIK, tooltip="Toggle constraint IK mode for the slider coordinates mode", render_as_button=True, ) # self._wmd_coord_ik = kw.Markdown(router, text="**Articulate**", in_line=True) # markdown to show status from IK self._wmd_coord_ik_status = kw.Markdown(router, text="**IK status:**") # variable to track which subhinge/coordinate indices have been # chosen for motion via the slider self._coord_move_indices = [None, None] # callback to reset the selected coordinate def _localCoordReset(): shindex = self._coord_move_indices[0] # get the selected sindex/cindex, and the Q values if shindex is None: return cindex = self._coord_move_indices[1] hge = self.item.parentHinge() sh = hge.subhinge(shindex) Q = hge.coordData().getQ() offset = hge.coordData().coordOffsets(sh) val = self.init_Q[offset.Q + cindex] Q[offset.Q + cindex] = val hge.coordData().setQ(Q) # reset the slider state also self._wcoord_move_slider.setValue(val) self.context.scene.update() self._wcoord_local_reset = kw.Button( router, text="Coord reset", on_press=_localCoordReset, tooltip="Reset the selected coordinate to original value", # render_as_button=True, ) # callback to reset all the coordinates def _fullCoordReset(): self.context.multibody.setQ(self.full_Q) self.context.scene.update() self._wcoord_full_reset = kw.Button( router, text="Full reset", on_press=_fullCoordReset, tooltip="Reset all the coordinates to the original value", # render_as_button=True, ) import math def _toggleCoordMove(cstate, shindex=None, cindex=None): if cstate: # reset all the other move buttons # print("HHH", shindex, cindex) for shi in range(6): for ci in range(3): if shi == shindex and ci == cindex: continue else: # pass self._wcoord_buttons[shi * 3 + ci].setValue(False) # record the indices for the selecting coordinate self._coord_move_indices = [shindex, cindex] # change the range of the slider based on the subhinge joint limits hge = self.item.parentHinge() # print("JJJJ", shindex, cindex, hge.nSubhinges()) assert shindex < hge.nSubhinges() sh = hge.subhinge(shindex) nQ = sh.nQ() assert cindex < nQ slider = self._wcoord_move_slider slider.setEnabled(True) self._wswing_kinsim.setEnabled(True) if nQ == 1: minQ, maxQ = sh.getJointLimits() if not math.isnan(minQ): slider.setMin(minQ) if not math.isnan(maxQ): slider.setMax(maxQ) else: self._coord_move_indices = [None, None] # reset the slider limits slider = self._wcoord_move_slider slider.setEnabled(False) slider.setMin(-3.2) slider.setMax(3.2) self._wcoord_move_slider.setEnabled(False) self._wswing_kinsim.setEnabled(False) # slider to change coordinates slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 slider_opts.tooltip = "Set the selected coordinate's value" # the one slider to use to articulate the specified button self._wcoord_move_slider = kw.Slider( router, # text=f'<span style="color:#FF5733">Subhinge {shindex}/Coord {cindex}</span>', text=f"Set Q", on_change=lambda Q: changeSubhingeCoord( self.item, self.subtree, self.context.scene, Q, self._coord_move_indices[0], # subhinge index self._coord_move_indices[1], # coord index self._coord_ik, self._wmd_coord_ik_status, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) # slider.setValue(0) # create buttons for max of 6 subhinges, with each having max 3 # coords. we only make visible the ones that are applicable for # a body self._wcoord_buttons: list[kw.Toggle] = [None] * 18 # array with all the buttons for shindex in range(6): for cindex in range(3): coord_button = self._wcoord_buttons[shindex * 3 + cindex] = kw.Toggle( router, text=f"Coord[{cindex}]", on_toggle=lambda cstate, shindex=shindex, cindex=cindex: _toggleCoordMove( cstate, shindex, cindex ), tooltip=f"Select the subhinge {shindex}/coordinate index {cindex} for motion", render_as_button=True, ) coord_button.setValue(False) self._wlayout_coord_ik = widgetArray( router, # label=self._wmd_coord_ik_status, children=[self._wcoord_ik, self._wcoord_move_slider], ) self._wik_layout.addChild(self._wlayout_coord_ik) self._wlayout_coord_status = widgetArray( router, # label=self._wmd_coord_ik_status, children=[self._wcoord_local_reset, self._wcoord_full_reset, self._wmd_coord_ik_status], ) self._wik_layout.addChild(self._wlayout_coord_status) # self._wlayout_coord_ik.addChild(self._wmd_coord_ik) # TODO - should not need to create this explicitly and should do # so as a string. However it is disappearing if we do not create # explicitly self.wmd_move_buttons = kw.Markdown(router, text="**Select move coordinate**", in_line=True) self._wlayout_move_buttons = widgetArray( router, label=self.wmd_move_buttons, # "Select move coordinate", children=[], alignment="column", alignItems="left", ) self._wik_layout.addChild(self._wlayout_move_buttons) self._wlayout_move_buttons.setVisible(True) self.shrows = [None] * 6 for shindex in range(6): wbtns = [] for cindex in range(3): # self._wlayout_coord_ik.addChild(self._wcoord_sliders[shindex * 3 + cindex]) # self._wlayout_coord_ik.addChild(self._wcoord_buttons[shindex * 3 + cindex]) # w_shrow.addChild(self._wcoord_buttons[shindex * 3 + cindex]) wbtns.append(self._wcoord_buttons[shindex * 3 + cindex]) # self._wroot.addChild(self._wcoord_buttons[shindex * 3 + cindex]) # TODO - should not need to create this explicitly and should do # so as a string. However it is disappearing if we do not create # explicitly wmd_shrow = kw.Markdown(router, text=f" **Subhinge[{shindex}]**", in_line=True) w_shrow = widgetArray(router, label=wmd_shrow, children=wbtns) self.shrows[shindex] = [w_shrow, wmd_shrow] # self._wroot.addChild(w_shrow) self._wlayout_move_buttons.addChild(w_shrow) # w_shrow.setVisible(True) """ self._frame_pair_widgets = FramePairHingeWidgets(self.context) self._wroot.addChild(self._frame_pair_widgets._wik_layout) # --------------------------------- # show/hide graphics mesh scene parts (only webscene) self._wmesh = kw.Toggle( router, text="Mesh", on_toggle=lambda cstate: _toggleVisibleBodies( cstate, [self.item], self.context, ks.LAYER_PHYSICAL_GRAPHICS ), tooltip="Toggle visibility of mesh scene parts for the body in 3D graphics", render_as_button=True, ) # show/hide collision scene parts (only webscene) self._wcollision = kw.Toggle( router, text="Collision parts", on_toggle=lambda cstate: _toggleVisibleBodies( cstate, [self.item], self.context, ks.LAYER_COLLISION ), tooltip="Toggle visibility of collision scene parts for the body in 3D graphics", render_as_button=True, ) # turn on/off stick parts for just the bodies in the subtree def stickCB(cstate, bd): has_stick = len(self.context.scene.getSceneParts(ks.LAYER_STICK_FIGURE)) > 0 if not has_stick: mb = self.context.multibody mb.createStickParts() # hide all the stick parts """ _toggleVisibleBodies(cstate, mb.sortedPhysicalBodiesList() + [mb.virtualRoot()], self.context, layers=ks.LAYER_STICK_FIGURE, ) """ # show this bodies stick parts _toggleVisibleBodies( cstate, [bd], self.context, layers=ks.LAYER_STICK_FIGURE, ) # show/hide stick parts (only webscene) self._wstick = kw.Toggle( router, text="Stick parts", on_toggle=lambda cstate: stickCB(cstate, self.item), tooltip="Toggle visibility of stick parts for the body in 3D graphics", render_as_button=True, ) # toggle wire frame view (only webscene) self._wwireframe = kw.Button( router, text="WireFrame (TBD)", on_press=lambda: _wireframeBodies([self.item]), tooltip="Toggle wireframe mode for the 3D parts for the body", ) # toggle transparent view (only webscene) self._wtransparent = kw.Button( router, text="Semi-transparent (TBD)", on_press=lambda: _transparentBodies([self.item]), tooltip="Toggle transparent mode for the 3D parts for the body", ) self._wlayout_geom_select = widgetArray( router, label="**Geometry**", children=[ self._wmesh, self._wcollision, self._wstick, self._wwireframe, self._wtransparent, ], ) # --------------------------------- # slider to scale the stick parts slider_opts = kw.SliderOptions() slider_opts.min = 0.01 slider_opts.max = 3 slider_opts.step = 0.01 def _scaleCB(bd, scale): if bd: bd.scaleStickParts(scale) self._wscale_stick = kw.Slider( router, text="Scale stick", on_change=lambda scale: _scaleCB(self.item, scale), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) self._wscale_stick.setValue(1) # self._wroot.addChild(self._wscale_stick) self._wlayout_geom = widgetArray( router, label="Visualization", children=[self._wlayout_geom_select, self._wscale_stick], alignment="column", alignItems="left", addBorder=True, ) self._wroot.addChild(self._wlayout_geom) # --------------------------------- # for constraints involving the body show line between this body and the other bodies self._wconstraints = kw.Button( router, text="Toggle highlighting", on_press=lambda: _highlightBodyConstraints(body=self.item, gui_context=self.context), tooltip="Toggle the highlighting of the constraints and nodes involving the body", ) self._wmd_constraints = kw.Markdown(router, text="**Constraints**", in_line=True) self._wlayout_constraints = widgetArray( router, label=self._wmd_constraints, children=[ self._wconstraints, # self._wnodes, ], ) self._wroot.addChild(self._wlayout_constraints) # --------------------------------- def _highlightFrames2(frames: Sequence[kf.Frame], cstate: bool, gui_context: Context): """Highlight frame axes.""" axes = [] if cstate: params = AxesParams(scale=1) else: params = AxesParams(scale=0) for f in frames: axes.append(EffectItem(obj=f, params=params)) gui_context.effects.frame_axes.toggle(axes) self._wbdframe = kw.Toggle( router, text="Body", on_toggle=lambda cstate: _highlightFrames2( [self.item], cstate, gui_context=self.context ), render_as_button=True, tooltip="Toggle the body frame axes", ) self._wpnodeframe = kw.Toggle( router, text="Pnode", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( [self.item.pnode()], cstate, gui_context=self.context ), render_as_button=True, tooltip="Toggle the body pnode frame axes", ) self._wconodeframes = kw.Toggle( router, text="Child onodes", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( [ cast(kd.PhysicalBody, x).onode() for x in self.subtree.childrenBodies(self.item) if not x.isCompoundBody() ], cstate, gui_context=self.context, ), render_as_button=True, tooltip="Toggle the frame axes for all the children body onodes", ) self._wsubhingeframes = kw.Toggle( router, text="Subhinge frames", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( [self.item.onode()] + [x.pframe() for x in self.item.parentHinge().subhinges()], cstate, gui_context=self.context, ), render_as_button=True, tooltip="Toggle the frame axes for all subhinge oframes and pframes for the body", ) self._wnodeframes = kw.Toggle( router, text="Nodes", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( self.item.nodeList(), cstate, gui_context=self.context ), render_as_button=True, tooltip="Toggle the frame axes for all the regular nodes on the body", ) self._wcnodeframes = kw.Toggle( router, text="Constraint nodes", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( self.item.constraintNodeList(), cstate, gui_context=self.context ), render_as_button=True, tooltip="Toggle the frame axes for all the constraint nodes on the body", ) self._wmd_frames = kw.Markdown(router, text="**Frames**", in_line=True) self._wlayout_frames = widgetArray( router, label=self._wmd_frames, children=[ self._wbdframe, self._wpnodeframe, self._wconodeframes, self._wsubhingeframes, self._wnodeframes, self._wcnodeframes, ], ) self._wroot.addChild(self._wlayout_frames) # ------------------------------------- # Record which vizutils visualizations are active self._libf_cbs = {} self._aibf_cbs = {} self._lexf_cbs = {} self._aexf_cbs = {} use_ta = False # scale = 1 radius = 0.01 def _toggleForcesViz(bd, ftype, color, cbsmap, cstate): id_ = bd.id() scene = self.context.scene if not cstate and id_ not in cbsmap: return if id_ in cbsmap: dark = cbsmap.pop(id_) else: # print("CCCCC", id_, ftype, bd.name()) if ftype == 0: dark = vizutils.visualizeInterBodyForce(bd.onode(), scene, use_ta) elif ftype == 1: dark = vizutils.visualizeInterBodyTorque(bd.onode(), scene, use_ta) elif ftype == 2: dark = vizutils.ScaledVectorVisualizer( f"{bd}_force_viz", bd, scene, lambda: bd.externalSpatialForce(with_constraints).getv(), ) elif ftype == 3: dark = vizutils.ScaledVectorVisualizer( f"{bd}_torque_viz", bd, scene, lambda: bd.externalSpatialForce(with_constraints).getw(), ) else: raise ValueError("Type is unknown") scale = self._frc_scale # self._wfrc_scale.state("value_state").get() # print("FFFF", scale) dark.setColor(color) dark.setScale(scale) dark.setRadius(radius) if cstate: dark.registerCallback() else: dark.unregisterCallback() cbsmap[id_] = dark scene.update() # enable interbody force visualization for the body (only webscene) self._winterbody_force = kw.Toggle( router, text="Interbody force", on_toggle=lambda cstate: _toggleForcesViz( self.item, 0, ks.Color.RED, self._libf_cbs, cstate ), render_as_button=True, tooltip="Toggle the visualization of the interbody forces for the body", ) # enable interbody moment visualization for the body (only webscene) self._winterbody_moment = kw.Toggle( router, text="Interbody moment", # on_toggle=lambda: self._doTbd() on_toggle=lambda cstate: _toggleForcesViz( self.item, 1, ks.Color.GREEN, self._aibf_cbs, cstate ), render_as_button=True, tooltip="Toggle the visualization of the interbody moments for the body", ) # enable external force visualization for the body (only webscene) # enable force vector display for the node with_constraints = False self._wexternal_force = kw.Toggle( router, text="Ext force vector", # on_toggle=self._doTbd) on_toggle=lambda cstate: _toggleForcesViz( self.item, 2, ks.Color.BLUE, self._lexf_cbs, cstate ), render_as_button=True, tooltip="Toggle the visualization of the net external forces on the body from the force nodes", ) # enable external moment visualization for the body (only webscene) self._wexternal_moment = kw.Toggle( router, text="Ext moment vector", on_toggle=lambda cstate: _toggleForcesViz( self.item, 3, ks.Color.YELLOW, self._aexf_cbs, cstate ), render_as_button=True, tooltip="Toggle the visualization of the net external moment on the body from the force nodes", ) self._wmd_forces = kw.Markdown(router, text="**Forces**", in_line=True) self._wlayout_forces = widgetArray( router, label=self._wmd_forces, children=[ self._winterbody_force, self._winterbody_moment, self._wexternal_force, self._wexternal_moment, ], ) # self._wroot.addChild(self._wlayout_forces) # --------------------------------------- slider_opts = kw.SliderOptions() slider_opts.min = -3 slider_opts.max = 3 slider_opts.step = 0.01 slider_opts.log_scale = True slider_opts.tooltip = "Factor to scale up/down all body forces" self._frc_scale = 0 # def scaleContact(new_scale): def _scaleForcesViz(new_scale): # global scale id_ = self.item.id() if id_ in self._libf_cbs: self._libf_cbs[id_].setScale(new_scale) if id_ in self._aibf_cbs: self._aibf_cbs[id_].setScale(new_scale) if id_ in self._lexf_cbs: self._lexf_cbs[id_].setScale(new_scale) if id_ in self._aexf_cbs: self._aexf_cbs[id_].setScale(new_scale) self._frc_scale = new_scale # print('EEEE', self._frc_scale) # self.context.scene.update() self._wfrc_scale = kw.Slider( router, "Viz Force Scale", _scaleForcesViz, slider_opts, ) self._wfrc_scale.setValue(0.05) # self._wroot.addChild(self._wfrc_scale) self._wlayout_forces2 = widgetArray( router, label="Force Visualization", children=[self._wlayout_forces, self._wfrc_scale], alignment="column", alignItems="left", addBorder=True, ) self._wroot.addChild(self._wlayout_forces2) # ---------------------------------------- self._wcollision_filters = kw.Button( router, text="Collsion filters (TBD)", on_press=lambda: self._doTbd(), tooltip="Toggle highlighting of bodies whose collisions with this body are being ignored", ) self._wmd_collision = kw.Markdown(router, text="**Collision**", in_line=True) self._wlayout_collision = widgetArray( router, label=self._wmd_collision, children=[self._wcollision_filters] ) # self._wroot.addChild(self._wlayout_collision) # -------------------------------------------- def teardown(self, _: kd.PhysicalBody, /): self._winterbody_force.setValue(False) self._winterbody_moment.setValue(False) self._wexternal_force.setValue(False) self._wexternal_moment.setValue(False) def setup(self, item: kd.PhysicalBody, item_context: kw.Json, /): name = item.name() id = item.id() not_root_body = not item.isRootBody() if not_root_body: hge_type = kd.HingeBase.hingeTypeString(item.parentHinge().hingeType()) title_md = f"### {name} [PhysicalBody/{id}/{hge_type}]" else: title_md = f"### {name} [PhysicalBody/{id}]" self._wtitle.setText(title_md) # set the context subtree if isinstance(item_context, dict): # set the context subtree st_id = item_context.get("subtree_id", None) if not isinstance(st_id, int): raise ValueError("Could not get the subtree ID.") self._subtree = cast(kd.SubTree, kc.BaseContainer.singleton().at(st_id)) if not isinstance(self._subtree, kd.SubTree): raise ValueError(f"Did not get a SubTree from ID {st_id}.") else: if not item.isCompoundBody(): kc.warn( f"The subtree value is missing when selecting the {item.name()} body. Defaulting to multibody." ) self._subtree = self.context.multibody else: kc.warn( f"The subtree value is missing when setting up the {item.name()} compound body." ) if not_root_body: self.init_Q = item.parentHinge().coordData().getQ() self.full_Q = self.context.multibody.getQ() is_not_locked = not_root_body and item.parentHinge().hingeType() != kd.HingeType.LOCKED has_constraint_nodes = len(item.constraintNodeList()) > 0 scene = self.context.scene has_scene_parts = len(item.getSceneParts()) > 0 has_collision_parts = ( len(scene.getPartsAttachedToFrame(item, False, ks.LAYER_COLLISION)) > 0 ) has_constraints = ( isinstance(self.subtree, kd.SubGraph) and len(self.subtree.enabledConstraints()) > 0 ) """ has_constraints = isinstance(self.subtree, kd.SubGraph) and ( (len(self.subtree.getBodyLoopConstraints(item)) > 0) or (len(self.subtree.getBodyCoordinateConstraints(item)) > 0) ) """ if not_root_body: self._frame_pair_widgets.setup(item.parentHinge(), self.subtree) else: self._frame_pair_widgets.setup(None, self.subtree) self._wfrc_scale.setVisible(not_root_body) """ # print("MMM", has_constraints, self.subtree.name()) self._wlayout_coord_ik.setVisible(is_not_locked) self._wcoord_ik.setVisible(has_constraints) self._wmd_coord_ik_status.setVisible(is_not_locked) # show the toggle IK mode button only if we have constraints self._wik_layout.setVisible(not_root_body and is_not_locked) # self._wswing.setVisible(not_root_body) self._wswing_ik.setVisible(not_root_body and has_constraints) self._wlayout_articulate.setVisible(is_not_locked) """ # self._wswing.setVisible(is_not_locked) # self._wswing_ik.setVisible(is_not_locked) self._wsubhingeframes.setVisible(is_not_locked) # self._wmd_nodes.setVisible(not_root_body) # self._wnodes.setVisible(not_root_body) self._wlayout_constraints.setVisible(not_root_body and has_constraints) self._wlayout_geom.setVisible(not_root_body) self._wmesh.setVisible(not_root_body) self._wmesh.setValue(True) # self._wcollision.setVisible(not_root_body and has_collision_parts) # self._wcollision_filters.setVisible(not_root_body) self._wstick.setVisible(not_root_body) self._wstick.setValue(False) self._wscale_stick.setVisible(not_root_body) self._wmd_collision.setVisible(not_root_body) self._wcollision_filters.setVisible(not_root_body) self._wcollision.setValue(False) # until implemented self._wwireframe.setVisible(not_root_body) self._wtransparent.setVisible(not_root_body) self._wmd_forces.setVisible(not_root_body) self._winterbody_force.setVisible(not_root_body) self._winterbody_moment.setVisible(not_root_body) self._wexternal_force.setVisible(not_root_body) self._wexternal_moment.setVisible(not_root_body) self._wpnodeframe.setVisible(not_root_body) self._wsubhingeframes.setVisible(not_root_body) self._wnodeframes.setVisible(not_root_body) """ has_constraints = False if self.subtree.isSubGraph(): has_constraints = len(self.subtree.getBodyLoopConstraints(item)) > 0 # self._wmd_nodes.setVisible(has_nodes) #self._wnodes.setVisible(has_nodes) self._wconstraints.setVisible(has_constraints) """ self._wconstraints.setVisible(has_constraint_nodes) self._wcnodeframes.setVisible(has_constraint_nodes) self._wlayout_geom.setVisible(has_scene_parts) self._wmesh.setVisible(has_scene_parts) # self._wcollision.setVisible(has_collision_parts) # self._wcollision_filters.setVisible(has_collision_parts) """ has_stick = len(scene.getPartsAttachedToFrame(item, ks.LAYER_STICK_FIGURE)) > 0 self._wstick.setVisible(has_stick) self._wscale_stick.setVisible(has_stick) """ # TODO - add implementation if 1: self._wmd_collision.setVisible(False) self._wcollision_filters.setVisible(False) # until implemented self._wwireframe.setVisible(False) self._wtransparent.setVisible(False) # Assigning the new item explicitly and early because below we # are setting states that may trigger callbacks which expect # self.item to be current. self.item = item self._wbdframe.setValue(False) self._wpnodeframe.setValue(False) self._wconodeframes.setValue(False) self._wsubhingeframes.setValue(False) self._wnodeframes.setValue(False) self._wcnodeframes.setValue(False) self._winterbody_force.setValue(False) self._winterbody_moment.setValue(False) self._wexternal_force.setValue(False) self._wexternal_moment.setValue(False) # enable subhinge/coord sliders for the coords available for the body import math """ if not_root_body: hge = item.parentHinge() self._wlayout_move_buttons.setVisible(hge.coordData().nQ() != 0) for shindex in range(6): for cindex in range(3): if shindex < hge.nSubhinges(): sh = hge.subhinge(shindex) nQ = sh.nQ() self.shrows[shindex][0].setVisible(nQ != 0) if cindex < nQ: # slider = self._wcoord_sliders[shindex * 3 + cindex] button = self._wcoord_buttons[shindex * 3 + cindex] button.setValue(False) # slider.setVisible(False) button.setVisible(True) else: # self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) self._wcoord_buttons[shindex * 3 + cindex].setVisible(False) else: # self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) # self._wcoord_buttons[shindex * 3 + cindex].setVisible(False) self.shrows[shindex][0].setVisible(False) # print("JJJJ", shindex, cindex) # enable the first button self._wcoord_buttons[0].setValue(True) self._coord_move_indices = [0, 0] """ @property def subtree(self): # AG - needs to be updated to return true subgraph context # return self.context.multibody if not hasattr(self, "_subtree"): raise ValueError("Subtree has not been set yet.") return self._subtree def close(self): if hasattr(self, "_subtree"): del self._subtree for v in self._libf_cbs.values(): v() self._libf_cbs.clear() for v in self._aibf_cbs.values(): v() self._aibf_cbs.clear() for v in self._lexf_cbs.values(): v() self._lexf_cbs.clear() for v in self._aexf_cbs.values(): v() self._aexf_cbs.clear() @register class CompoundBodyPane(AbstractPane[kd.CompoundBody]): """Pane to display info about a CompoundBody.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "CompoundBody" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # --------------------------- self._cmphge_widgets = CompoundHingeWidgets(self.context) self._wroot.addChild(self._cmphge_widgets._wik_layout) # ---------------------------- # PhysicalBodyPane.__init__(self, router) # open tab with visjs display of the bodies in the compound body self._wvisjs_bodies = kw.Button( router, text="Bodies ", on_press=lambda: _createSubTreeVisJs(self.item.bodiesTree(), self.context), tooltip="Create, if needed, the visjs graph of the bodies in this subtree", ) # open tab with visjs display of the physical bodies in the compound body self._wvisjs_physical = kw.Button( router, text="Physical bodies", on_press=lambda: _createSubTreeVisJs(self.item.physicalBodiesTree(), self.context), tooltip="Create, if needed, the visjs graph of the physical bodies in this subtree", ) self._wmd_bodies = kw.Markdown(router, text="**Visjs graph**", in_line=True) self._wlayout_embedded = widgetArray( router, label=self._wmd_bodies, children=[self._wvisjs_bodies, self._wvisjs_physical] ) self._wroot.addChild(self._wlayout_embedded) # ---------------------------- # create treeview for the subtree self._wtreeview = kw.Button( router, text="Bodies", on_press=lambda: _createSubTreeView(self.item.bodiesTree(), self.context), tooltip="Create and add TreeView for the compound body's subtree", ) # create treeview for the subtree self._wtreeview_physical = kw.Button( router, text="Physical", on_press=lambda: _createSubTreeView(self.item.physicalBodiesTree(), self.context), tooltip="Create and add TreeView for the the compound body's phhysical bodies subtree", ) # Setup widget topology self._wmd_treeview = kw.Markdown(router, text="**Create TreeView**", in_line=True) self._wlayout_treeview = widgetArray( router, label=self._wmd_treeview, children=[self._wtreeview, self._wtreeview_physical] ) self._wroot.addChild(self._wlayout_treeview) # ------------------------------------ def _flattenCB(sg): if not sg.hasCompoundBodies(): return sg.flattenCompoundBodies() # highlight bodies in aggregation graph for a constraint self._wflatten = kw.Button( router, text="Flatten", on_press=lambda: _flattenCB(self.item), tooltip="Flatten all embedded compound bodies and replace with their physical bodies", ) self._wmd_flatten = kw.Markdown(router, text="**Flatten**", in_line=True) self._wlayout_flatten = widgetArray( router, label=self._wmd_flatten, children=[self._wflatten], ) self._wroot.addChild(self._wlayout_flatten) def setup(self, item: kd.CompoundBody, _: kw.Json, /): name = item.name() id = item.id() hge_type = kd.HingeBase.hingeTypeString(item.parentHinge().hingeType()) title_md = f"### {name} [CompoundBody/{id}/{hge_type}]" # title_md = f"### {name} [CompoundBody/{id}]" self._wtitle.setText(title_md) # hide the physical subtree buttons if the physical bodies are # the same as the regular bodies has_compound = item.bodiesTree().hasCompoundBodies() self._wvisjs_physical.setVisible(has_compound) self._wtreeview_physical.setVisible(has_compound) self._wlayout_flatten.setVisible(has_compound) self._cmphge_widgets.setup(item.parentHinge()) """ # highlight the embedded physical bodies in the multibody visjs graph _highlightBodies( bodies=item.physicalBodiesTree().sortedPhysicalBodiesList(), secondary_bodies=[], tertiary_bodies=[], toggle_over_set=False, gui_context=self.context, ) """ # @register class CECompoundBodyPaneOBSOLETE(AbstractPane[kd.CECompoundBody]): """Pane to display info about a CECompoundBody.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "CECompoundBody" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) """ # ---------------------------------- # the initial Q values (to use for reset) self.init_Q = [] self.full_Q = [] # variable to track which coordinate index has been # chosen for motion via the slider self._coord_move_index = 0 # free swing, no IK self._wswing = kw.Button( router, text="Swing", on_press=lambda: _swingHinge( self.item.bodiesTree().parentSubTree(), [self.item.parentHinge()], False, self.context, ), tooltip="Auto articulate the compound body", ) # ------------------------------------ # constrained swing, with IK on self._wswing_kinsim = kw.Button( router, text="Kinematics sim", on_press=lambda: _ceKinematicsSim( self.item.bodiesTree().parentSubTree(), # self.subtree, self.item.parentHinge().subhinge(0), self._coord_move_index, 0.3, 0.5, self.context, ), tooltip="Articulate the CE compound body using kinematics sim", ) self._wik_layout = widgetArray( router, [], alignment="column", alignItems="left", addBorder=True ) self._wroot.addChild(self._wik_layout) self._wmd_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wlayout_articulate = widgetArray( router, label=self._wmd_articulate, children=[self._wswing, self._wswing_kinsim], ) self._wik_layout.addChild(self._wlayout_articulate) # -------------------------------------- # create subhinge and coordinate entries # callback to reset the selected coordinate def _localCoordReset(): shindex = 0 cindex = self._coord_move_index hge = self.item.parentHinge() sh = hge.subhinge(shindex) Q = hge.coordData().getQ() # offset = hge.coordData().coordOffsets(sh) val = self.init_Q[cindex] Q[cindex] = val hge.coordData().setQ(Q) # reset the slider state also self._wcoord_move_slider.setValue(val) self.context.scene.update() self._wcoord_local_reset = kw.Button( router, text="Coord reset", on_press=_localCoordReset, tooltip="Reset the selected coordinate to original value", ) # callback to reset all the coordinates def _fullCoordReset(): self.context.multibody.setQ(self.full_Q) self.context.scene.update() self._wcoord_full_reset = kw.Button( router, text="Full reset", on_press=_fullCoordReset, tooltip="Reset all the coordinates to the original value", ) import math self.max_coord = 10 def _toggleCoordMove(cstate, cindex=None): if cstate: # reset all the other move buttons for ci in range(self.max_coord): if ci == cindex: continue else: # pass self._wcoord_buttons[ci].setValue(False) # record the indices for the selecting coordinate self._coord_move_index = cindex # change the range of the slider based on the subhinge joint limits hge = self.item.parentHinge() # print("JJJJ", shindex, cindex, hge.nSubhinges()) sh = hge.subhinge(0) nQ = sh.nQ() assert cindex < nQ slider = self._wcoord_move_slider self._wcoord_move_slider.setEnabled(True) self._wswing_kinsim.setEnabled(True) else: self._coord_move_index = None # reset the slider limits slider = self._wcoord_move_slider slider.setMin(-3.2) slider.setMax(3.2) self._wcoord_move_slider.setEnabled(False) self._wswing_kinsim.setEnabled(False) # slider to change coordinates slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 slider_opts.tooltip = "Set the selected coordinate's value" # the one slider to use to articulate the specified button self._wcoord_move_slider = kw.Slider( router, # text=f'<span style="color:#FF5733">Subhinge {shindex}/Coord {cindex}</span>', text=f"Set Q", on_change=lambda Q: changeSubhingeCoord( self.item, self.item.bodiesTree().parentSubTree(), # self.subtree, self.context.scene, Q, 0, # subhinge index self._coord_move_index, # coord index False, # self._coord_ik, None, # self._wmd_coord_ik_status, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) # slider.setValue(0) # create buttons for all the coordinates # we only make visible the ones that are applicable for the CE compound body self._wcoord_buttons: list[kw.Toggle] = [ None ] * self.max_coord # array with all the buttons for cindex in range(self.max_coord): coord_button = self._wcoord_buttons[cindex] = kw.Toggle( router, text=f"Coord[{cindex}]", on_toggle=lambda cstate, cindex=cindex: _toggleCoordMove(cstate, cindex), tooltip=f"Select the coordinate index {cindex} for motion", render_as_button=True, ) coord_button.setValue(False) self._wlayout_coord_ik = widgetArray( router, # label=self._wmd_coord_ik_status, children=[self._wcoord_move_slider], ) self._wik_layout.addChild(self._wlayout_coord_ik) self._wlayout_coord_status = widgetArray( router, # label=self._wmd_coord_ik_status, children=[self._wcoord_local_reset, self._wcoord_full_reset], ) self._wik_layout.addChild(self._wlayout_coord_status) # TODO - should not need to create this explicitly and should do # so as a string. However it is disappearing if we do not create # explicitly self.wmd_move_buttons = kw.Markdown(router, text="**Select move coordinate**", in_line=True) self._wlayout_move_buttons = widgetArray( router, label=self.wmd_move_buttons, # "Select move coordinate", children=[], alignment="column", alignItems="left", ) self._wik_layout.addChild(self._wlayout_move_buttons) self._wlayout_move_buttons.setVisible(True) rowlen = 6 rowindex = 0 cindex = 0 while rowindex < self.max_coord / rowlen: wbtns = [] ci = 0 while ci < 6 and cindex < self.max_coord: # self._wlayout_coord_ik.addChild(self._wcoord_sliders[shindex * 3 + cindex]) # self._wlayout_coord_ik.addChild(self._wcoord_buttons[shindex * 3 + cindex]) # w_shrow.addChild(self._wcoord_buttons[shindex * 3 + cindex]) wbtns.append(self._wcoord_buttons[cindex]) cindex += 1 ci += 1 # self._wroot.addChild(self._wcoord_buttons[shindex * 3 + cindex]) # TODO - should not need to create this explicitly and should do # so as a string. However it is disappearing if we do not create # explicitly # wmd_shrow = kw.Markdown(router, text=f" **Subhinge[{shindex}]**", in_line=True) w_shrow = widgetArray(router, children=wbtns) # self.shrows[shindex] = [w_shrow] # self._wroot.addChild(w_shrow) self._wlayout_move_buttons.addChild(w_shrow) # w_shrow.setVisible(True) rowindex += 1 # ---------------------------- """ def setup(self, item: kd.CECompoundBody, _: kw.Json, /): name = item.name() id = item.id() hge_type = kd.HingeBase.hingeTypeString(item.parentHinge().hingeType()) title_md = f"### {name} [CECompoundBody/{id}/{hge_type}]" # title_md = f"### {name} [CECompoundBody/{id}]" self._wtitle.setText(title_md) """ hge = item.parentHinge() self._wlayout_move_buttons.setVisible(hge.coordData().nQ() != 0) for cindex in range(self.max_coord): if 1: # shindex < hge.nSubhinges(): sh = hge.subhinge(0) nQ = sh.nQ() # self.shrows[shindex][0].setVisible(nQ != 0) if cindex < nQ: # slider = self._wcoord_sliders[shindex * 3 + cindex] button = self._wcoord_buttons[cindex] button.setValue(False) # slider.setVisible(False) button.setVisible(True) else: # self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) self._wcoord_buttons[cindex].setVisible(False) # enable the first button self._wcoord_buttons[0].setValue(True) self.init_Q = item.parentHinge().coordData().getQ() self.full_Q = self.context.multibody.getQ() """ """ # enable subhinge/coord sliders for the coords available for the body if 1: nsh = 1 csh = 20 hge = item.parentHinge() for shindex in range(nsh): for cindex in range(csh): if shindex < hge.nSubhinges() and cindex < hge.subhinge(shindex).nQ(): slider = self._wcoord_sliders[shindex * 3 + cindex] slider.setVisible(True) slider.setValue(hge.subhinge(shindex).getQ()[cindex]) else: self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) """ @register class ScenePartPane(AbstractPane[ks.ScenePart]): """Pane to display info about a ScenePart.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "ScenePart" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") # GRAPHICAL PART # toggle wire frame view (only webscene) self._wwireframe = kw.Button( router, text="WireFrame (TBD)", on_press=lambda: _wireframe(self.item), tooltip="Toggle wireframe mode for the scene part", ) # toggle transparent view (only webscene) self._wtransparent = kw.Button( router, text="Semi-transparent (TBD)", on_press=lambda: _transparent(self.item), tooltip="Toggle transparent mode for the scene part", ) # change color self._wcolor = kw.Button( router, text="Color (TBD)", on_press=self._doTbd, tooltip="Change the color for the scene part", ) # SCENE NODE # Toggle scene part visibility (only webscene) self._wvisible = kw.Button( router, text="Mesh", on_press=lambda: self.item.setVisible(not self.item.getVisible()), tooltip="Toggle visibility of the scene part", ) def _getParentFrameList(): if isinstance(self.item, ks.ProxyScenePart): return [self.item.ancestorFrame()] else: return [] # show frame axes self._wframe = kw.Button( router, text="Frame", on_press=lambda: _highlightFrames( _getParentFrameList(), toggle_over_set=True, gui_context=self.context ), tooltip="Toggle the axes for the frame to which this scene part is attached", ) # highlight parent node/part, children node/part (different highlighting) (TBD) self._wdownstream = kw.Button( router, text="Downstream (TBD)", on_press=self._doTbd, tooltip="Toggle highlighting of the parent and children scene parts", ) # highlight upstream scene parts self._wupstream = kw.Button( router, text="Upstream (TBD)", on_press=self._doTbd, tooltip="Toggle highlighting of the upstream scene parts", ) # change scale self._wscale = kw.Button( router, text="Scale (TBD)", on_press=self._doTbd, tooltip="Change the scaling of the scene part", ) # change transform position offset self._wposition = kw.Button( router, text="Position (TBD)", on_press=self._doTbd, tooltip="Change the position of the scene part", ) # change orientation position offset self._worientation = kw.Button( router, text="Orientation (TBD)", on_press=self._doTbd, tooltip="Change the orientation of the scene part", ) # Setup widget topology self._wroot.addChild(self._wtitle) self._wmd_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wlayout_highlight = widgetArray( router, label=self._wmd_highlight, children=[ self._wvisible, self._wframe, self._wupstream, self._wdownstream, self._wwireframe, self._wtransparent, ], ) self._wroot.addChild(self._wlayout_highlight) self._wmd_parameters = kw.Markdown(router, text="**Parameters**", in_line=True) self._wlayout_params = widgetArray( router, label=self._wmd_parameters, children=[self._wscale, self._wposition, self._worientation], ) self._wroot.addChild(self._wlayout_params) def setup(self, item: ks.ScenePart, _: kw.Json, /): name = item.name() id = item.id() title_md = f"### {name} [ProxyScenePart/{id}]" self._wtitle.setText(title_md) if 1: # TODO - until implemented self._wupstream.setVisible(False) self._wdownstream.setVisible(False) self._wwireframe.setVisible(False) self._wtransparent.setVisible(False) self._wmd_parameters.setVisible(False) self._wscale.setVisible(False) self._wposition.setVisible(False) self._worientation.setVisible(False) @register class NodePane(AbstractPane[kd.Node]): """Pane to display info about any Node object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "Node" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") # highlight parent body self._wbody = kw.Button( router, text="Parent body", on_press=lambda: _highlightBodies( [self.item.parentBody()], [], [], toggle_over_set=False, gui_context=self.context ), tooltip="Toggle the highlighting of the node's parent body", ) self._lf_cbs = {} self._af_cbs = {} scale = 1 radius = 0.01 def _toggleForcesViz(nd, ftype, color, cbsmap): id_ = nd.id() scene = self.context.scene if id_ in cbsmap: cbsmap.pop(id_)() # print("Creating") if ftype == 0: dark = vizutils.visualizeNodeForce(nd, self.context.scene) elif ftype == 1: dark = vizutils.visualizeNodeTorque(nd, self.context.scene) else: raise ValueError("Type not recognized") dark.setScale(scale) dark.setRadius(radius) dark.setColor(color) dark.registerCallback() cbsmap[id_] = dark scene.update() # enable force vector display for the node self._wforce_vec = kw.Button( router, text="External force", # on_press=self._doTbd) on_press=lambda: _toggleForcesViz( self.item, 0, ks.Color.RED, self._lf_cbs, ), tooltip="Toggle the visualization of the external force on the node", ) # enable moment vector display for the node self._wmoment_vec = kw.Button( router, text="External moment", on_press=lambda: _toggleForcesViz( self.item, 1, ks.Color.GREEN, self._af_cbs, ), tooltip="Toggle the visualization of the external moment on the node", ) # change selection to the parent body self._wselect_up = kw.Button( router, text="Up", on_press=lambda: _selectObject(self.item.parentBody(), self.context.selection), tooltip="Change the selection to the node's parent body", ) # change selection to the next sibling node self._wselect_right = kw.Button( router, text="Right", on_press=lambda: _selectObject(_siblingNode(self.item, True), self.context.selection), tooltip="Change the selection to the next node on the body", ) # change selection to the previous sibling node self._wselect_left = kw.Button( router, text="Left", on_press=lambda: _selectObject(_siblingNode(self.item, False), self.context.selection), tooltip="Change the selection to the previous node on the body", ) """ # show constraint attached to constraint node self._wconstraint = kw.Button(router, text="Loop constraint (TBD)", on_press=self._doTbd), tooltip="Toggle the highlighting of the constraint attached to this constraint node" """ # Setup widget topology self._wroot.addChild(self._wtitle) self._wmd_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wlayout_highlight = widgetArray( router, label=self._wmd_highlight, children=[ self._wbody, ], # self._wconstraint] ) self._wroot.addChild(self._wlayout_highlight) self._wmd_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wlayout_select = widgetArray( router, label=self._wmd_selection, children=[self._wselect_up, self._wselect_left, self._wselect_right], ) self._wroot.addChild(self._wlayout_select) self._wmd_forces = kw.Markdown(router, text="**Forces**", in_line=True) self._wlayout_forces = widgetArray( router, label=self._wmd_forces, children=[self._wforce_vec, self._wmoment_vec] ) self._wroot.addChild(self._wlayout_forces) def setup(self, item: kd.Node, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # nd = mb.getNodAncestor(item) title_md = f"### {name} [Node/{id} [Anc body={item.parentBody().name()}]" # title_md = f"### {name} [Node/{id}]" self._wtitle.setText(title_md) def close(self): for v in self._lf_cbs.values(): v() self._lf_cbs.clear() for v in self._af_cbs.values(): v() self._af_cbs.clear() @register class ConstraintNodePane(AbstractPane[kd.ConstraintNode]): """Pane to display info about any Node object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "Node" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") # for constraints for this node show line between this node and the other bodies self._wconstraint = kw.Button( router, text="Constraints", on_press=lambda: _showLoopConstraints([self.item.loopConstraint()], self.context), tooltip="Toggle the highlighting of the bilaateral constraint attached to this constraint node", ) self._wmd_constraint = kw.Markdown(router, text="**Constraint**", in_line=True) self._wlayout_constraint = widgetArray( router, label=self._wmd_constraint, children=[self._wconstraint] ) self._wroot.addChild(self._wtitle) self._wroot.addChild(self._wlayout_constraint) def setup(self, item: kd.ConstraintNode, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # nd = mb.getNodAncestor(item) if item.loopConstraint(): title_md = f"### {name} [ConstraintNode/{id} [body={item.parentBody().name()}] [lc={item.loopConstraint().name()}]" else: title_md = f"### {name} [ConstraintNode/{id} [body={item.parentBody().name()}]" # title_md = f"### {name} [Node/{id}]" self._wtitle.setText(title_md) @register class BilateralConstraintBasePane(AbstractPane[kd.BilateralConstraintBase]): """Pane to display info about any Node object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "BilateralConstraintBase" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") def _toggleConstraint(cn): mb = self.context.multibody if mb.getEnabledConstraint(cn.name()): # print("Disabling", cn.name()) mb.disableConstraint(cn) else: # print("Enabling", cn.name()) mb.enableConstraint(cn) # enable/disable the constraint self._wenable = kw.Button( router, text="Toggle", on_press=lambda: _toggleConstraint(self.item), tooltip="Toggle enabling/disabling the constraint", ) self._wroot.addChild(self._wtitle) self._wmd_enable = kw.Markdown(router, text="**Enable/Disable constraint**", in_line=True) self._wlayout_enable = widgetArray( router, label=self._wmd_enable, children=[self._wenable], ) self._wroot.addChild(self._wlayout_enable) # ------------------------------------ def _createAggSubgraph(constraint: kd.LoopConstraintBase): # first check that a cegraph exists, else create it mb = self.context.multibody nbodies = len(mb.sortedPhysicalBodiesList()) cegraph = None for st in mb.childrenSubTrees(): if ( isinstance(st, kd.SubGraph) and st.getEnabledConstraint(constraint.name()) and len(st.sortedPhysicalBodiesList()) == nbodies ): cegraph = st break if not cegraph: cegraph = kd.SubGraph.create(f"cegraph{constraint.id()}", mb, mb.virtualRoot()) # now create the agg subgraph st = cegraph.aggregationSubGraph(constraint.name() + "agg_sg", [constraint], True) _highlightBodies(st.sortedPhysicalBodiesList(), [], [], True, self.context) # highlight bodies in aggregation graph for a constraint self._waggsg = kw.Button( router, text="Create aggregation subgraph", on_press=lambda: _createAggSubgraph(self.item), tooltip="Create an aggregation subgraph for this constraint", ) self._wmd_aggsg = kw.Markdown(router, text="**Aggsg**", in_line=True) self._wlayout_aggsg = widgetArray( router, label=self._wmd_aggsg, children=[self._waggsg], ) self._wroot.addChild(self._wlayout_aggsg) """ # for constraints for this node show line between this node and the other bodies self._wconstraint = kw.Button( router, text="Constraints", on_press=lambda: _showLoopConstraints([self.item.loopConstraint()], self.context), ) self._wmd_constraint = kw.Markdown(router, text="**Constraint**", in_line=True) self._wlayout_constraint = widgetArray( router, label=self._wmd_constraint, children=[self._wconstraint] ) self._wroot.addChild(self._wlayout_constraint) """ def setup(self, item: kd.BilateralConstraintBase, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # highlight the constraint nodes and bodies involved # is_cutjoint = item.type() == kd.BilateralConstraintType.CUTJOINT_LOOP title_md = f"### {name} [{item.typeString()}/{id}]" # title_md = f"### {name} [Node/{id}]" self._wtitle.setText(title_md) if 1: # TODO - unitil implemented self._wlayout_aggsg.setVisible(True) # self._wlayout_enable.setVisible(False) @register class LoopConstraintBasePane(AbstractPane[kd.LoopConstraintBase]): """Pane to display info about any Node object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "LoopConstraintBase" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) """ self._wroot.addChild(self._waggsg) # enable/disable the constraint self._wenable = kw.Button( router, text="Enable/disable constraints (TBD)", on_press=lambda: _toggleConstraint(self.item), tooltip="Toggle enabling/disabling the constraint", ) self._wmd_enable = kw.Markdown(router, text="**Enable**", in_line=True) self._wlayout_enable = widgetArray( router, label=self._wmd_enable, children=[self._wenable], ) self._wroot.addChild(self._wlayout_enable) self._wmd_aggsg = kw.Markdown(router, text="**Aggsg**", in_line=True) self._wlayout_aggsg = widgetArray( router, label=self._wmd_aggsg, children=[self._waggsg], ) self._wroot.addChild(self._wlayout_aggsg) """ """ # for constraints for this node show line between this node and the other bodies self._wconstraint = kw.Button( router, text="Constraints", on_press=lambda: _showLoopConstraints([self.item.loopConstraint()], self.context), ) self._wmd_constraint = kw.Markdown(router, text="**Constraint**", in_line=True) self._wlayout_constraint = widgetArray( router, label=self._wmd_constraint, children=[self._wconstraint] ) self._wroot.addChild(self._wlayout_constraint) """ def setup(self, item: kd.LoopConstraintBase, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # highlight the constraint nodes and bodies involved if isinstance(item, kd.LoopConstraintCutJoint): title_md = f"### {name} [{item.typeString()}/{id}, hinge={kd.HingeBase.hingeTypeString(item.hinge().hingeType())}]" else: title_md = f"### {name} [{item.typeString()}/{id}]" # title_md = f"### {name} [Node/{id}]" self._wtitle.setText(title_md) """ src_node = item.sourceNode() src_bd = mb.virtualRoot() if not src_node else src_node.parentBody() tgt_node = item.targetNode() tgt_bd = mb.virtualRoot() if not tgt_node else tgt_node.parentBody() _highlightBodies([], [src_bd], [tgt_bd], toggle_over_set=False, gui_context=self.context) cf2f = item.constraintFrameToFrame() _highlightFrames( [cf2f.oframe(), cf2f.pframe()], toggle_over_set=False, gui_context=self.context ) """ """ if 1: # TODO - unitil implemented self._wlayout_aggsg.setVisible(False) self._wlayout_enable.setVisible(False) """ @register class PhysicalSubhingePane(AbstractPane[kd.PhysicalSubhinge]): """Pane to display info about any Node object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "PhysicalSubhinge" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # --------------------------- # create sliders for max of 6 subhinges, with each having max 3 # coords. we only make visible the ones that are applicable for # a body # slider to scale the stick parts # markdown to show status from IK self._wmd_coord_ik = kw.Markdown(router, text="**IK status:**") slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 self._wcoord_sliders = [None] * 3 # array with all the sliders with_ik = len(self.context.multibody.enabledConstraints()) > 0 # callback to change subhinge coords set by the sliders def changeCoord(sh, st, scene, Q, cindex, with_ik, wstatus=None): # print("SSS", shindex, cindex, bd) if not sh: return if 1: # print("TTTT", shindex, cindex, hge.nSubhinges(), item.name(), Q) assert cindex < sh.nQ() Qvec = sh.getQ() Qvec[cindex] = Q sh.setQ(Qvec) if with_ik: # do IK # offset = st.coordOffsets(sh).Q st.cks().freezeCoord(sh, cindex, kd.CKFrozenCoordType.Q) err = st.cks().solveQ() if err < 1e-10: color = "green" stxt = "SUCCESS" extra = f"[Q={Q:.4}]" else: color = "red" stxt = "FAILED" extra = f"[Q={Q:.4}, err={err:.4e}]" status = f'**IK status:** <span style="color:{color}">{stxt}</span> {extra}' # print(" err=", err, color) st.cks().unfreezeCoord(sh, cindex, kd.CKFrozenCoordType.Q) else: status = "**IK status:** N/A" if wstatus: wstatus.setText(status) scene.update() for cindex in range(3): slider = self._wcoord_sliders[cindex] = kw.Slider( router, text=f"Coord {cindex}", # need to use the shindes=shindex etc syntax to # avoid the variable itself beig capture by the # lambda (else only get the last value) on_change=lambda Q, cindex=cindex: changeCoord( self.item, self.context.multibody, self.context.scene, Q, cindex, with_ik, self._wmd_coord_ik, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) slider.setValue(0) self._wroot.addChild(self._wmd_coord_ik) for cindex in range(3): self._wroot.addChild(self._wcoord_sliders[cindex]) def setup(self, item: kd.PhysicalSubhinge, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # highlight the constraint nodes and bodies involved title_md = f"### {name} [{item.typeString()}/{id}]" # title_md = f"### {name} [Node/{id}]" self._wtitle.setText(title_md) """ _highlightFrames( [item.oframe(), item.pframe()], toggle_over_set=False, gui_context=self.context ) """ # enable subhinge/coord sliders for the coords available for the body for cindex in range(3): if cindex < item.nQ(): slider = self._wcoord_sliders[cindex] slider.setVisible(True) slider.setValue(item.getQ()[cindex]) else: self._wcoord_sliders[cindex].setVisible(False) # print("JJJJ", shindex, cindex) class FramePairHingeWidgets: """Class to create frame pair hinge widgets.""" def __init__(self, gui_context: Context): router = gui_context.router self.context = gui_context # self.item = None # variable to track which subhinge/coordinate indices have been # chosen for motion via the slider self._coord_move_indices = [None, None] # ---------------------------------- # the initial Q values (to use for reset) self.init_Q = [] self.full_Q = [] # free swing, no IK self._wswing = kw.Button( router, text="Swing free", on_press=lambda: _swingHinge(self.subtree(), [self.item], True, self.context), tooltip="Auto articulate the body while ignoring any constraints", ) # constrained swing, with IK on self._wswing_ik = kw.Button( router, text="Swing IK", on_press=lambda: _swingHinge(self.subtree(), [self.item], False, self.context), tooltip="Auto articulate the body with constraint inverse kinematics", ) # constrained swing, with IK on self._wswing_kinsim = kw.Button( router, text="Kinematics sim", on_press=lambda: _mbodyKinematicsSim( self.item.subhinge(self._coord_move_indices[0]), self._coord_move_indices[1], 0.3, 0.5, self.context, ), tooltip="Articulate the body using kinematics simulation mode", ) self._wik_layout = widgetArray( router, [], label="Kinematics", alignment="column", alignItems="left", addBorder=True ) # self._wroot.addChild(self._wik_layout) self._wlayout_articulate = widgetArray( router, label="**Articulate**", children=[self._wswing, self._wswing_ik, self._wswing_kinsim], ) self._wik_layout.addChild(self._wlayout_articulate) # -------------------------------------- # create subhinge and coordinate entries # set IK mode on/off for coordinates self._coord_ik = False def _toggleCoordIK(cstate): self._coord_ik = cstate self._wmd_coord_ik_status.setVisible(cstate) self._wcoord_ik = kw.Toggle( router, text="IK mode", on_toggle=_toggleCoordIK, tooltip="Toggle constraint IK mode for the slider coordinates mode", render_as_button=True, ) """ self._wcoord_ik = kw.Button( router, text="Toggle coord IK", on_press=lambda: _toggleCoordIK(), tooltip="Toggle constraint IK mode for the slider coordinates mode", ) """ # self._wmd_coord_ik = kw.Markdown(router, text="**Articulate**", in_line=True) # markdown to show status from IK self._wmd_coord_ik_status = kw.Markdown(router, text="**IK status:**") # variable to track which subhinge/coordinate indices have been # chosen for motion via the slider self._coord_move_indices = [None, None] # callback to reset the selected coordinate def _localCoordReset(): # print("KKKK", self._coord_move_indices) shindex = self._coord_move_indices[0] # get the selected sindex/cindex, and the Q values if shindex is None: return cindex = self._coord_move_indices[1] hge = self.item sh = hge.subhinge(shindex) Q = hge.coordData().getQ() offset = hge.coordData().coordOffsets(sh) val = self.init_Q[offset.Q + cindex] Q[offset.Q + cindex] = val hge.coordData().setQ(Q) # reset the slider state also self._wcoord_move_slider.setValue(val) self.context.scene.update() self._wcoord_local_reset = kw.Button( router, text="Coord reset", on_press=_localCoordReset, tooltip="Reset the selected coordinate to original value", # render_as_button=True, ) # callback to reset all the coordinates def _fullCoordReset(): gui_context.multibody.setQ(self.full_Q) gui_context.scene.update() self._wcoord_full_reset = kw.Button( router, text="Full reset", on_press=_fullCoordReset, tooltip="Reset all the coordinates to the original value", # render_as_button=True, ) import math def _toggleCoordMove(cstate, shindex, cindex): if cstate: # reset all the other move buttons # print("HHH", shindex, cindex) for shi in range(6): for ci in range(3): if shi == shindex and ci == cindex: continue else: # pass self._wcoord_buttons[shi * 3 + ci].setValue(False) # record the indices for the selecting coordinate self._coord_move_indices = [shindex, cindex] # change the range of the slider based on the subhinge joint limits hge = self # print("JJJJ", shindex, cindex, hge.nSubhinges()) assert shindex < hge.nSubhinges() sh = hge.subhinge(shindex) nQ = sh.nQ() assert cindex < nQ slider = self._wcoord_move_slider slider.setEnabled(True) self._wswing_kinsim.setEnabled(True) if nQ == 1: minQ, maxQ = sh.getJointLimits() if not math.isnan(minQ): slider.setMin(minQ) if not math.isnan(maxQ): slider.setMax(maxQ) else: self._coord_move_indices = [None, None] # reset the slider limits slider = self._wcoord_move_slider slider.setEnabled(False) slider.setMin(-3.2) slider.setMax(3.2) self._wcoord_move_slider.setEnabled(False) self._wswing_kinsim.setEnabled(False) # slider to change coordinates slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 slider_opts.tooltip = "Set the selected coordinate's value" # the one slider to use to articulate the specified button self._wcoord_move_slider = kw.Slider( router, # text=f'<span style="color:#FF5733">Subhinge {shindex}/Coord {cindex}</span>', text=f"Set Q", on_change=lambda Q: changeSubhingeCoord( self.item, self.subtree(), self.context.scene, Q, self._coord_move_indices[0], # subhinge index self._coord_move_indices[1], # coord index self._coord_ik, self._wmd_coord_ik_status, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) # slider.setValue(0) # create buttons for max of 6 subhinges, with each having max 3 # coords. we only make visible the ones that are applicable for # a body self._wcoord_buttons: list[kw.Toggle] = [None] * 18 # array with all the buttons for shindex in range(6): for cindex in range(3): coord_button = self._wcoord_buttons[shindex * 3 + cindex] = kw.Toggle( router, text=f"Coord[{cindex}]", on_toggle=lambda cstate, shindex=shindex, cindex=cindex: _toggleCoordMove( cstate, shindex, cindex ), tooltip=f"Select the subhinge {shindex}/coordinate index {cindex} for motion", render_as_button=True, ) coord_button.setValue(False) self._wlayout_coord_ik = widgetArray( router, # label=self._wmd_coord_ik_status, children=[self._wcoord_ik, self._wcoord_move_slider], ) self._wik_layout.addChild(self._wlayout_coord_ik) self._wlayout_coord_status = widgetArray( router, # label=self._wmd_coord_ik_status, children=[self._wcoord_local_reset, self._wcoord_full_reset, self._wmd_coord_ik_status], ) self._wik_layout.addChild(self._wlayout_coord_status) """ self._wlayout_coord_ik = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.1em"} ) self._wlayout_coord_ik.addChild(self._wcoord_ik) #self._wroot.addChild(self._wlayout_coord_ik) self._wik_layout.addChild(self._wlayout_coord_ik) """ # self._wlayout_coord_ik.addChild(self._wmd_coord_ik) # TODO - should not need to create this explicitly and should do # so as a string. However it is disappearing if we do not create # explicitly self._wlayout_move_buttons = widgetArray( router, label="Select Move Coordinate", children=[], alignment="column", alignItems="left", addBorder=True, ) self._wik_layout.addChild(self._wlayout_move_buttons) self._wlayout_move_buttons.setVisible(True) self.shrows = [None] * 6 for shindex in range(6): wbtns = [] for cindex in range(3): # self._wlayout_coord_ik.addChild(self._wcoord_sliders[shindex * 3 + cindex]) # self._wlayout_coord_ik.addChild(self._wcoord_buttons[shindex * 3 + cindex]) # w_shrow.addChild(self._wcoord_buttons[shindex * 3 + cindex]) wbtns.append(self._wcoord_buttons[shindex * 3 + cindex]) # self._wroot.addChild(self._wcoord_buttons[shindex * 3 + cindex]) # TODO - should not need to create this explicitly and should do # so as a string. However it is disappearing if we do not create # explicitly wmd_shrow = kw.Markdown(router, text=f" **Subhinge[{shindex}]**", in_line=True) w_shrow = widgetArray(router, label=wmd_shrow, children=wbtns) self.shrows[shindex] = [w_shrow, wmd_shrow] # self._wroot.addChild(w_shrow) self._wlayout_move_buttons.addChild(w_shrow) # w_shrow.setVisible(True) def subtree(self): assert self._subtree return self._subtree def setup( self, item: kd.FramePairHinge, st: kd.SubTree, /, ): self.item = item self._subtree = st # self.subtree not_root_body = item is not None is_not_locked = item.hingeType() != kd.HingeType.LOCKED has_constraints = isinstance(st, kd.SubGraph) and len(st.enabledConstraints()) > 0 if not_root_body: self.init_Q = item.coordData().getQ() self.full_Q = self.context.multibody.getQ() # print("MMM", has_constraints, self.subtree.name()) self._wlayout_coord_ik.setVisible(is_not_locked) self._wcoord_ik.setVisible(has_constraints) # hide IK status until IK mode is enabled self._wmd_coord_ik_status.setVisible(False) # show the toggle IK mode button only if we have constraints self._wik_layout.setVisible(not_root_body and is_not_locked) # self._wswing.setVisible(not_root_body) self._wswing_ik.setVisible(not_root_body and has_constraints) self._wlayout_articulate.setVisible(is_not_locked) if not_root_body: hge = item # .parentHinge() self._wlayout_move_buttons.setVisible(hge.coordData().nQ() != 0) for shindex in range(6): for cindex in range(3): if shindex < hge.nSubhinges(): sh = hge.subhinge(shindex) nQ = sh.nQ() self.shrows[shindex][0].setVisible(nQ != 0) if cindex < nQ: # slider = self._wcoord_sliders[shindex * 3 + cindex] button = self._wcoord_buttons[shindex * 3 + cindex] button.setValue(False) """ if nQ == 1: minQ, maxQ = sh.getJointLimits() if not math.isnan(minQ): slider.setMin(minQ) if not math.isnan(maxQ): slider.setMax(maxQ) slider.setVisible(True) slider.setValue(hge.subhinge(shindex).getQ()[cindex]) """ # slider.setVisible(False) button.setVisible(True) else: # self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) self._wcoord_buttons[shindex * 3 + cindex].setVisible(False) else: # self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) # self._wcoord_buttons[shindex * 3 + cindex].setVisible(False) self.shrows[shindex][0].setVisible(False) # print("JJJJ", shindex, cindex) # enable the first button self._wcoord_buttons[0].setValue(True) self._coord_move_indices = [0, 0] class CompoundHingeWidgets: """Class to create compound hinge widgets.""" def __init__(self, gui_context: Context): router = gui_context.router self.context = gui_context # self.item = None # variable to track which subhinge/coordinate indices have been # chosen for motion via the slider self._coord_move_indices = [None, None] # ---------------------------------- # the initial Q values (to use for reset) self.init_Q = [] self.full_Q = [] # variable to track which coordinate index has been # chosen for motion via the slider self._coord_move_index = 0 # free swing, no IK self._wswing = kw.Button( router, text="Swing", on_press=lambda: _swingHinge( self.item.compoundBody().bodiesTree().parentSubTree(), [self.item], False, self.context, ), tooltip="Auto articulate the compound body", ) # ------------------------------------ # constrained swing, with IK on self._wswing_kinsim = kw.Button( router, text="Kinematics sim", on_press=lambda: _ceKinematicsSim( self.item.compoundBody().bodiesTree().parentSubTree(), # self.subtree, self.item.subhinge(0), self._coord_move_index, 0.3, 0.5, self.context, ), tooltip="Articulate the CE compound body using kinematics sim", ) self._wik_layout = widgetArray( router, [], alignment="column", alignItems="left", addBorder=True ) # self._wroot.addChild(self._wik_layout) self._wmd_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wlayout_articulate = widgetArray( router, label=self._wmd_articulate, children=[self._wswing, self._wswing_kinsim], ) self._wik_layout.addChild(self._wlayout_articulate) # -------------------------------------- # create subhinge and coordinate entries # callback to reset the selected coordinate def _localCoordReset(): shindex = 0 cindex = self._coord_move_index hge = self.item sh = hge.subhinge(shindex) Q = hge.coordData().getQ() # offset = hge.coordData().coordOffsets(sh) val = self.init_Q[cindex] Q[cindex] = val hge.coordData().setQ(Q) # reset the slider state also self._wcoord_move_slider.setValue(val) self.context.scene.update() self._wcoord_local_reset = kw.Button( router, text="Coord reset", on_press=_localCoordReset, tooltip="Reset the selected coordinate to original value", ) # callback to reset all the coordinates def _fullCoordReset(): self.context.multibody.setQ(self.full_Q) self.context.scene.update() self._wcoord_full_reset = kw.Button( router, text="Full reset", on_press=_fullCoordReset, tooltip="Reset all the coordinates to the original value", ) import math self.max_coord = 20 def _toggleCoordMove(cstate, cindex=None): if cstate: # reset all the other move buttons for ci in range(self.max_coord): if ci == cindex: continue else: # pass self._wcoord_buttons[ci].setValue(False) # record the indices for the selecting coordinate self._coord_move_index = cindex # change the range of the slider based on the subhinge joint limits hge = self.item.parentHinge() # print("JJJJ", shindex, cindex, hge.nSubhinges()) sh = hge.subhinge(0) nQ = sh.nQ() assert cindex < nQ slider = self._wcoord_move_slider self._wcoord_move_slider.setEnabled(True) self._wswing_kinsim.setEnabled(True) """ if nQ == 1: minQ, maxQ = sh.getJointLimits() if not math.isnan(minQ): slider.setMin(minQ) if not math.isnan(maxQ): slider.setMax(maxQ) """ else: self._coord_move_index = None # reset the slider limits slider = self._wcoord_move_slider slider.setMin(-3.2) slider.setMax(3.2) self._wcoord_move_slider.setEnabled(False) self._wswing_kinsim.setEnabled(False) # slider to change coordinates slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 slider_opts.tooltip = "Set the selected coordinate's value" # the one slider to use to articulate the specified button self._wcoord_move_slider = kw.Slider( router, # text=f'<span style="color:#FF5733">Subhinge {shindex}/Coord {cindex}</span>', text=f"Set Q", on_change=lambda Q: changeSubhingeCoord( self.item, self.item.compoundBody().bodiesTree().parentSubTree(), # self.subtree, self.context.scene, Q, 0, # subhinge index self._coord_move_index, # coord index False, # self._coord_ik, None, # self._wmd_coord_ik_status, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) # slider.setValue(0) # create buttons for all the coordinates # we only make visible the ones that are applicable for the CE compound body self._wcoord_buttons: list[kw.Toggle] = [ None ] * self.max_coord # array with all the buttons for cindex in range(self.max_coord): coord_button = self._wcoord_buttons[cindex] = kw.Toggle( router, text=f"Coord[{cindex}]", on_toggle=lambda cstate, cindex=cindex: _toggleCoordMove(cstate, cindex), tooltip=f"Select the coordinate index {cindex} for motion", render_as_button=True, ) coord_button.setValue(False) self._wlayout_coord_ik = widgetArray( router, # label=self._wmd_coord_ik_status, children=[self._wcoord_move_slider], ) self._wik_layout.addChild(self._wlayout_coord_ik) self._wlayout_coord_status = widgetArray( router, # label=self._wmd_coord_ik_status, children=[self._wcoord_local_reset, self._wcoord_full_reset], ) self._wik_layout.addChild(self._wlayout_coord_status) # TODO - should not need to create this explicitly and should do # so as a string. However it is disappearing if we do not create # explicitly self.wmd_move_buttons = kw.Markdown(router, text="**Select move coordinate**", in_line=True) self._wlayout_move_buttons = widgetArray( router, label=self.wmd_move_buttons, # "Select move coordinate", children=[], alignment="column", alignItems="left", ) self._wik_layout.addChild(self._wlayout_move_buttons) self._wlayout_move_buttons.setVisible(True) rowlen = 6 rowindex = 0 cindex = 0 self.shrows = [None] * (int(self.max_coord / 6) + 1) while rowindex < self.max_coord / rowlen: wbtns = [] ci = 0 while ci < 6 and cindex < self.max_coord: # self._wlayout_coord_ik.addChild(self._wcoord_sliders[shindex * 3 + cindex]) # self._wlayout_coord_ik.addChild(self._wcoord_buttons[shindex * 3 + cindex]) # w_shrow.addChild(self._wcoord_buttons[shindex * 3 + cindex]) wbtns.append(self._wcoord_buttons[cindex]) cindex += 1 ci += 1 # self._wroot.addChild(self._wcoord_buttons[shindex * 3 + cindex]) # TODO - should not need to create this explicitly and should do # so as a string. However it is disappearing if we do not create # explicitly # wmd_shrow = kw.Markdown(router, text=" **GGGGG*", in_line=True) w_shrow = widgetArray(router, children=wbtns) self.shrows[rowindex] = w_shrow # self._wroot.addChild(w_shrow) self._wlayout_move_buttons.addChild(w_shrow) # w_shrow.setVisible(True) rowindex += 1 def subtree(self): return self.item.compoundBody().bodiesTree().parentSubTree() def setup( self, item: kd.CompoundHinge, _: kw.Json, /, ): self.item = item hge = item sh = hge.subhinge(0) nQ = sh.nQ() self._wlayout_move_buttons.setVisible(nQ != 0) max_rowindex = int(nQ / 6) + 1 print(f"nQ={nQ}, masrow={max_rowindex}") for cindex in range(self.max_coord): rowindex = int(cindex / 6) button = self._wcoord_buttons[cindex] if cindex < nQ: # slider = self._wcoord_sliders[shindex * 3 + cindex] button.setValue(False) """ if nQ == 1: minQ, maxQ = sh.getJointLimits() if not math.isnan(minQ): slider.setMin(minQ) if not math.isnan(maxQ): slider.setMax(maxQ) slider.setVisible(True) slider.setValue(hge.subhinge(shindex).getQ()[cindex]) """ # slider.setVisible(False) self.shrows[rowindex].setVisible(True) button.setVisible(True) # self.shrows[cindex].setVisible(False) else: # self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) if rowindex >= max_rowindex: self.shrows[rowindex].setVisible(False) button.setVisible(False) # self.shrows[cindex].setVisible(False) # enable the first button self._wcoord_buttons[0].setValue(True) # self.shrows[0].setVisible(True) self.init_Q = item.coordData().getQ() self.full_Q = self.context.multibody.getQ() @register class FramePairHingePane(AbstractPane[kd.FramePairHinge]): """Pane to display info about any Node object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "FramePairHinge" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # --------------------------- self._frame_pair_widgets = FramePairHingeWidgets(self.context) self._wroot.addChild(self._frame_pair_widgets._wik_layout) """ # -------------------------------------- # create subhinge and coordinate entries # set IK mode on/off for coordinates self._coord_ik = False def _toggleCoordIK(cstate): self._coord_ik = cstate self._wcoord_ik = kw.Toggle( router, text="IK mode", on_toggle=_toggleCoordIK, tooltip="Toggle constraint IK mode for the slider coordinates mode", render_as_button=True, ) # self._wmd_coord_ik = kw.Markdown(router, text="**Articulate**", in_line=True) # markdown to show status from IK self._wmd_coord_ik = kw.Markdown(router, text="**IK status:**") # slider to scale the stick parts slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 slider_opts.tooltip = "Scale the stick part's size by this factor" # create sliders for max of 6 subhinges, with each having max 3 # coords. we only make visible the ones that are applicable for # a body self._wcoord_sliders = [None] * 18 # array with all the sliders for shindex in range(6): for cindex in range(3): slider = self._wcoord_sliders[shindex * 3 + cindex] = kw.Slider( router, # text=f'<span style="color:#FF5733">Subhinge {shindex}/Coord {cindex}</span>', text=f"Subhinge {shindex}/Coord {cindex}", # need to use the shindes=shindex etc syntax to # avoid the variable itself beig capture by the # lambda (else only get the last value) on_change=lambda Q, shindex=shindex, cindex=cindex: changeSubhingeCoord( self.item, self.context.multibody, self.context.scene, Q, shindex, cindex, self._coord_ik, self._wmd_coord_ik, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) slider.setValue(0) self._wlayout_coord_ik = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.1em"} ) self._wroot.addChild(self._wlayout_coord_ik) self._wlayout_coord_ik.addChild(self._wcoord_ik) self._wlayout_coord_ik.addChild(self._wmd_coord_ik) for shindex in range(6): for cindex in range(3): self._wlayout_coord_ik.addChild(self._wcoord_sliders[shindex * 3 + cindex]) """ def setup(self, item: kd.FramePairHinge, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # highlight the constraint nodes and bodies involved title_md = f"### {name} [{item.typeString()}/{id}]" # title_md = f"### {name} [Node/{id}]" self._wtitle.setText(title_md) """ _highlightFrames( [item.oframe(), item.pframe()], toggle_over_set=False, gui_context=self.context ) """ self._frame_pair_widgets.setup(item, self.context.multibody) # has_constraints = len(self.context.multibody.enabledConstraints()) > 0 """ # enable subhinge/coord sliders for the coords available for the body for shindex in range(6): for cindex in range(3): if shindex < item.nSubhinges() and cindex < item.subhinge(shindex).nQ(): slider = self._wcoord_sliders[shindex * 3 + cindex] slider.setVisible(True) slider.setValue(item.subhinge(shindex).getQ()[cindex]) else: self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) # print("JJJJ", shindex, cindex) """ @register class CompoundHingePane(AbstractPane[kd.CompoundHinge]): """Pane to display info about a Compound hinge.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "CompoundHinge" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # --------------------------- self._cmp_hge_widgets = CompoundHingeWidgets(self.context) self._wroot.addChild(self._cmp_hge_widgets._wik_layout) def setup(self, item: kd.CompoundHinge, _: kw.Json, /): name = item.name() id = item.id() # highlight the constraint nodes and bodies involved title_md = f"### {name} [{item.typeString()}/{id}]" # title_md = f"### {name} [Node/{id}]" self._wtitle.setText(title_md) self._cmp_hge_widgets.setup(item) @register class LoopConstraintCutJointPane(AbstractPane[kd.LoopConstraintCutJoint]): """Pane to display info about a cut joint constraint.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "LoopConstraintCutJoint" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # --------------------------- self._frame_pair_widgets = FramePairHingeWidgets(self.context) self._wroot.addChild(self._frame_pair_widgets._wik_layout) """ # create sliders for max of 6 subhinges, with each having max 3 # coords. we only make visible the ones that are applicable for # a body # slider to scale the stick parts # markdown to show status from IK self._wmd_coord_ik = kw.Markdown(router, text="**IK status:**") slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 self._wcoord_sliders = [None] * 18 # array with all the sliders with_ik = True for shindex in range(6): for cindex in range(3): slider = self._wcoord_sliders[shindex * 3 + cindex] = kw.Slider( router, text=f"Subhinge {shindex}/Coord {cindex}", # need to use the shindes=shindex etc syntax to # avoid the variable itself beig capture by the # lambda (else only get the last value) on_change=lambda Q, shindex=shindex, cindex=cindex: changeSubhingeCoord( self.item, self.context.multibody, self.context.scene, Q, shindex, cindex, with_ik, self._wmd_coord_ik, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) slider.setValue(0) self._wroot.addChild(self._wmd_coord_ik) for shindex in range(6): for cindex in range(3): self._wroot.addChild(self._wcoord_sliders[shindex * 3 + cindex]) """ def setup(self, item: kd.LoopConstraintCutJoint, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # highlight the constraint nodes and bodies involved is_cutjoint = item.type() == kd.BilateralConstraintType.CUTJOINT_LOOP if is_cutjoint: title_md = f"### {name} [{item.typeString()}/{id}, hinge={kd.HingeBase.hingeTypeString(item.hinge().hingeType())}]" else: title_md = f"### {name} [{item.typeString()}/{id}]" # title_md = f"### {name} [Node/{id}]" self._wtitle.setText(title_md) self._frame_pair_widgets.setup(item.hinge(), self.context.multibody) """ # enable subhinge/coord sliders for the coords available for the body hge = item.hinge() for shindex in range(6): for cindex in range(3): if shindex < hge.nSubhinges() and cindex < hge.subhinge(shindex).nQ(): slider = self._wcoord_sliders[shindex * 3 + cindex] slider.setVisible(True) slider.setValue(hge.subhinge(shindex).getQ()[cindex]) else: self._wcoord_sliders[shindex * 3 + cindex].setVisible(False) # print("JJJJ", shindex, cindex) """ @register class LoopConstraintConVelPane(AbstractPane[kd.LoopConstraintConVel]): """Pane to display info about any Node object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "LoopConstraintConVel" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # --------------------------- # create slider for the constraint U coordinate # markdown to show status from IK self._wmd_coord_ik = kw.Markdown(router, text="**IK status:**") slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 # self._wcoord_sliders = [None] * 18 # array with all the sliders """ # callback to change subhinge coords set by the sliders def changeCoordRate(item, st, scene, U, wstatus=None): # print("SSS", shindex, cindex, bd) if not item: return # implementation TBD assert 0 # print("TTTT", shindex, cindex, hge.nSubhinges(), item.name(), U) # sh = item.osubhinge() sh.setU(U) # do IK # offset = st.coordOffsets(sh).U st.cks().freezeCoord(sh, 0, kd.CKFrozenCoordType.U) err = st.cks().solveU() if err < 1e-10: color = "green" stxt = "SUCCESS" extra = f"[U={U:.4}]" else: color = "red" stxt = "FAILED" extra = f"[U={U:.4}, err={err:.4e}]" status = f'**IK status:** <span style="color:{color}">{stxt}</span> {extra}' # print(" err=", err, color) st.cks().unfreezeCoord(sh, 0, kd.CKFrozenCoordType.U) if wstatus: wstatus.setText(status) scene.update() self._wcoord_slider = kw.Slider( router, text="Coordinate rate", on_change=lambda U: changeCoordRate( self.item, self.context.multibody, self.context.scene, U, self._wmd_coord_ik, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) self._wcoord_slider.setValue(0) """ self._wroot.addChild(self._wmd_coord_ik) # self._wroot.addChild(self._wcoord_slider) """ # for constraints for this node show line between this node and the other bodies self._wconstraint = kw.Button( router, text="Constraints", on_press=lambda: _showLoopConstraints([self.item.loopConstraint()], self.context), ) self._wmd_constraint = kw.Markdown(router, text="**Constraint**", in_line=True) self._wlayout_constraint = widgetArray( router, label=self._wmd_constraint, children=[self._wconstraint] ) self._wroot.addChild(self._wlayout_constraint) """ def setup(self, item: kd.LoopConstraintConVel, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # highlight the constraint nodes and bodies involved title_md = f"### {name} [{item.typeString()}/{id}]" self._wtitle.setText(title_md) # disable until we have a way to do this for convels self._wmd_coord_ik.setVisible(False) self._wcoord_slider.setVisible(False) """ src_node = item.sourceNode() src_bd = mb.virtualRoot() if not src_node else src_node.parentBody() tgt_node = item.targetNode() tgt_bd = mb.virtualRoot() if not tgt_node else tgt_node.parentBody() _highlightBodies([], [src_bd], [tgt_bd], toggle_over_set=False, gui_context=self.context) cf2f = item.constraintFrameToFrame() _highlightFrames( [cf2f.oframe(), cf2f.pframe()], toggle_over_set=False, gui_context=self.context ) """ @register class CoordinateConstraintPane(AbstractPane[kd.CoordinateConstraint]): """Pane to display info about any Node object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "CoordinateConstraint" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # --------------------------- # create slider for the constraint Q coordinate # markdown to show status from IK self._wmd_coord_ik = kw.Markdown(router, text="**IK status:**") slider_opts = kw.SliderOptions() slider_opts.min = -3.2 slider_opts.max = 3.2 slider_opts.step = 0.01 # self._wcoord_sliders = [None] * 18 # array with all the sliders # callback to change subhinge coords set by the sliders def changeCoord(item, st, scene, Q, wstatus=None): # print("SSS", shindex, cindex, bd) if not item: return # print("TTTT", shindex, cindex, hge.nSubhinges(), item.name(), Q) sh = item.osubhinge() sh.setQ(Q) # do IK # offset = st.coordOffsets(sh).Q st.cks().freezeCoord(sh, 0, kd.CKFrozenCoordType.Q) err = st.cks().solveQ() if err < 1e-10: color = "green" stxt = "SUCCESS" extra = f"[Q={Q:.4}]" else: color = "red" stxt = "FAILED" extra = f"[Q={Q:.4}, err={err:.4e}]" status = f'**IK status:** <span style="color:{color}">{stxt}</span> {extra}' # print(" err=", err, color) st.cks().unfreezeCoord(sh, 0, kd.CKFrozenCoordType.Q) if wstatus: wstatus.setText(status) scene.update() self._wcoord_slider = kw.Slider( router, text="Coordinate", on_change=lambda Q: changeCoord( self.item, self.context.multibody, self.context.scene, Q, self._wmd_coord_ik, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) self._wcoord_slider.setValue(0) self._wroot.addChild(self._wmd_coord_ik) self._wroot.addChild(self._wcoord_slider) """ # for constraints for this node show line between this node and the other bodies self._wconstraint = kw.Button( router, text="Constraints", on_press=lambda: _showLoopConstraints([self.item.loopConstraint()], self.context), ) self._wmd_constraint = kw.Markdown(router, text="**Constraint**", in_line=True) self._wlayout_constraint = widgetArray( router, label=self._wmd_constraint, children=[self._wconstraint] ) self._wroot.addChild(self._wlayout_constraint) """ def setup(self, item: kd.CoordinateConstraint, _: kw.Json, /): name = item.name() id = item.id() # mb = self.context.multibody # highlight the constraint nodes and bodies involved title_md = f"### {name} [{item.typeString()}/{id}]" self._wtitle.setText(title_md) """ oshg = item.osubhinge() src_bd = oshg.parentHinge().pnode().parentBody() pshg = item.psubhinge() tgt_bd = pshg.parentHinge().pnode().parentBody() _highlightBodies([], [src_bd], [tgt_bd], toggle_over_set=False, gui_context=self.context) _highlightFrames( [oshg.pframe(), pshg.pframe()], toggle_over_set=False, gui_context=self.context ) """ @register class SubTreePane(AbstractPane[kd.SubTree]): """Pane to display info about any SubTree object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "SubTree" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) """ # highlight all the bodies (show root body with different highlighting) self._wbodies = kw.Button( router, text="Bodies (TBD)", on_press=lambda: _highlightBodies(self.item.sortedBodiesList()), ) """ """ # highlight parent subgraph bodies def parentSubTreeBodies(): pst = self.item.parentSubTree() return [] if not pst else pst.sortedBodiesList() self._wparent = kw.Button( router, text="Parent (TBD)", on_press=lambda: _highlightBodies(parentSubTreeBodies()) ) # highlight children subgraph bodies self._wchildren = kw.Button(router, text="Children (TBD)", on_press=self._doTbd) # highlight sibling subgraph bodies self._wsibling = kw.Button(router, text="Sibling (TBD)", on_press=self._doTbd) """ """ # show/hide scene parts (only webscene) self._wvisible = kw.Button( router, text="Show/Hide", on_press=lambda: _toggleVisibleBodies( self.item.sortedPhysicalBodiesList(), self.context.scene, ks.LAYER_ALL) ) """ # ---------------------------- # articulate all the bodies sequentially (only WebScene) self._wswing = kw.Button( router, text="Articulate", on_press=lambda: _swingHinge( self.item, [x.parentHinge() for x in self.item.sortedBodiesList()], True, self.context, ), tooltip="Sequentially articulate all the coordinates in this subtree (while ignroing constraints)", ) # self._wmd_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wlayout_articulate = widgetArray( router, # label=self._wmd_articulate, children=[self._wswing], ) self._wroot.addChild(self._wlayout_articulate) # ---------------------------- self._wmesh = kw.Toggle( router, text="Mesh", on_toggle=lambda cstate: _toggleVisibleBodies( cstate, self.item.sortedPhysicalBodiesList(), self.context, layers=ks.LAYER_PHYSICAL_GRAPHICS, ), tooltip="Toggle the visibility of all the graphics scene parts in this subtree", render_as_button=True, ) # turn on/off stick parts for just the bodies in the subtree self._wcollision = kw.Toggle( router, text="Collision", on_toggle=lambda cstate: _toggleVisibleBodies( cstate, self.item.sortedPhysicalBodiesList(), self.context, layers=ks.LAYER_COLLISION, ), tooltip="Toggle the visibility of all the collison scene parts in this subtree", render_as_button=True, ) # turn on/off stick parts for just the bodies in the subtree def stickCB(cstate, st): has_stick = len(self.context.scene.getSceneParts(ks.LAYER_STICK_FIGURE)) > 0 if not has_stick: st.multibody().createStickParts() _toggleVisibleBodies( cstate, st.sortedPhysicalBodiesList(), self.context, layers=ks.LAYER_STICK_FIGURE, ) # turn on/off stick parts for just the bodies in the subtree self._wstick = kw.Toggle( router, text="Stick parts", on_toggle=lambda cstate: stickCB(cstate, self.item), tooltip="Toggle the visibility of all the stick parts in this subtree", render_as_button=True, ) # toggle wire frame/transparent view (only webscene) self._wwireframe = kw.Button( router, text="WireFrame (TBD)", on_press=lambda: _wireframeBodies(self.item.sortedBodiesList()), tooltip="Toggle wireframe mode for all the bodies in the subtree", ) self._wmd_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wlayout_highlight = widgetArray( router, label=self._wmd_highlight, children=[self._wmesh, self._wcollision, self._wstick, self._wwireframe], ) self._wroot.addChild(self._wlayout_highlight) # ---------------------------- """ lambda: _toggleVisibleBodies( self.item.sortedPhysicalBodiesList(), self.context, layers=ks.LAYER_STICK_FIGURE, ), """ # slider to scale the stick parts slider_opts = kw.SliderOptions() slider_opts.min = 0.01 slider_opts.max = 3 slider_opts.step = 0.01 self._wscale_stick = kw.Slider( router, text="Scale stick", on_change=lambda scale: self.item.scaleStickParts(scale), opts=slider_opts, ) self._wscale_stick.setValue(1) self._wroot.addChild(self._wscale_stick) # ---------------------------- # create visjs graph self._wvisjs = kw.Button( router, text="Visjs Graph", on_press=lambda: _createSubTreeVisJs(self.item, self.context), tooltip="Create a visjs graph for the subtree if there is not one already", ) # dictionary with all subtree treeviews created # self._treeview = {} # create treeview for the subtree self._wtreeview = kw.Button( router, text="Create TreeView", on_press=lambda: _createSubTreeView(self.item, self.context), tooltip="Create and add TreeView for the subtree", ) self._wmd_views = kw.Markdown(router, text="**SubTree views**", in_line=True) self._wlayout_views = widgetArray( router, label=self._wmd_views, children=[self._wvisjs, self._wtreeview], ) self._wroot.addChild(self._wlayout_views) # ---------------------------- self._wcmframe = kw.Toggle( router, text="CM frame", on_toggle=lambda cstate: _highlightFrames( cstate, [self.item.cmFrame()], # toggle_over_set=True, gui_context=self.context, ), tooltip="Toggle the axes for the center of mass frame for the subtree", render_as_button=True, ) # show node frames self._wnodes = kw.Toggle( router, text="Nodes", on_toggle=lambda cstate: _highlightBodyNodes( cstate, bodies=self.item.sortedPhysicalBodiesList(), effects=self.context.effects ), tooltip="Toggle the axes for all the regular nodes in the subtree", # on_press=lambda: _showBodyNodes(self.item.sortedBodiesList()), render_as_button=True, ) self._wmd_frames = kw.Markdown(router, text="**Frames**", in_line=True) self._wlayout_frames = widgetArray( router, label=self._wmd_frames, children=[self._wcmframe, self._wnodes] ) self._wroot.addChild(self._wlayout_frames) # ---------------------------- self._wmodel = kw.Button( router, text="Model", on_press=lambda: self.item.displayModel(), tooltip="Run displayModel() to produce text display about the subtree's bodies", ) self._wtree = kw.Button( router, text="Tree", on_press=lambda: self.item.dumpTree(), tooltip="Run dumpModel() to produce text display about the subtree's body topology", ) # compute forward dynamics self._wfwddyn = kw.Button( router, text="Fwd dynamics", on_press=lambda: kd.Algorithms.evalForwardDynamics(self.item), tooltip="Evaluate forward dynamics for this subtree", ) def _toggleDumpDynamics(cstate: bool, st): # cstate = st.enable_dump_dynamics # if not cstate: # kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.TRACE) # else: # kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.WARN) st.enable_dump_dynamics = not cstate self._wdebug = kw.Toggle( router, text="Toggle dump dynamics", on_toggle=lambda cstate: _toggleDumpDynamics(cstate, self.item), tooltip="Toggle the 'dump dynamics' debug mode for the subtree", render_as_button=True, ) self._wmd_info = kw.Markdown(router, text="**Info**", in_line=True) self._wlayout_info = widgetArray( router, label=self._wmd_info, children=[self._wmodel, self._wtree, self._wdebug, self._wfwddyn], ) self._wroot.addChild(self._wlayout_info) # ---------------------------- # change selection to one of the child subtree (drop down, or multiple buttons) # change selection to the parent subtree self._wselect_down = kw.Button( router, text="Down", on_press=lambda: _selectObject(_firstChildSubtree(self.item), self.context.selection), tooltip="Change selection to a shild subtree", ) # change selection to the parent subtree self._wselect_up = kw.Button( router, text="Up", on_press=lambda: _selectObject(_parentSubtree(self.item), self.context.selection), tooltip="Change selection to the parent subtree", ) # change selection to the next sibling subtree self._wselect_right = kw.Button( router, text="Right", on_press=lambda: _selectObject( _siblingSubtree(self.item, True), self.context.selection ), tooltip="Change selection to the next sibling subtree", ) # change selection to the previous sibling subtree self._wselect_left = kw.Button( router, text="Left", on_press=lambda: _selectObject( _siblingSubtree(self.item, False), self.context.selection ), tooltip="Change selection to the previous sibling subtree", ) """ self._wselect_child = kw.Button(router, text="Select child (TBD)", on_press=self._doTbd) # change selection to the parent subtree self._wselect_parent = kw.Button(router, text="Select parent (TBD)", on_press=self._doTbd) # change selection to a sibling subtree self._wselect_sibling = kw.Button(router, text="Select sibling (TBD)", on_press=self._doTbd) # change selection to a child body self._wselect_body = kw.Button(router, text="Select body (TBD)", on_press=self._doTbd) """ # Setup widget topology """ self._wmd_visjs = kw.Markdown(router, text="**Visjs**", in_line=True) self._wlayout_visjs = widgetArray(router, label=self._wmd_visjs, children=[self._wvisjs]) self._wroot.addChild(self._wlayout_visjs) """ self._wmd_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wlayout_select = widgetArray( router, label=self._wmd_selection, children=[ self._wselect_up, self._wselect_left, self._wselect_right, self._wselect_down, ], ) self._wroot.addChild(self._wlayout_select) def setup(self, item: kd.SubTree, _: kw.Json, /): name = item.name() id = item.id() title_md = f"### {name} [SubTree/{id}]" self._wtitle.setText(title_md) scene = self.context.scene has_mesh = len(scene.getSceneParts(ks.LAYER_PHYSICAL_GRAPHICS)) > 0 has_stick = True # len(scene.getSceneParts(ks.LAYER_STICK_FIGURE)) > 0 has_collision = len(scene.getSceneParts(ks.LAYER_COLLISION)) > 0 parent = item.parentSubTree() has_parent = parent is not None has_siblings = has_parent and len(parent.childrenSubTrees()) > 1 has_children = len(item.childrenSubTrees()) > 0 self._wmesh.setVisible(has_mesh) self._wmesh.setValue(True) self._wstick.setVisible(has_stick) self._wstick.setValue(False) self._wscale_stick.setVisible(has_stick) self._wcollision.setVisible(has_collision) self._wcollision.setValue(False) self._wselect_up.setVisible(has_parent) self._wselect_left.setVisible(has_parent) self._wselect_right.setVisible(has_parent) self._wselect_left.setVisible(has_siblings) self._wselect_right.setVisible(has_siblings) # self._wmd_selection.setVisible(has_children) self._wselect_down.setVisible(has_children) self._wdebug.setValue(item.enable_dump_dynamics) """ # hightlight the subtree bodies in the multibody visjs graph _highlightBodies( bodies=item.sortedPhysicalBodiesList(), secondary_bodies=[], tertiary_bodies=[], toggle_over_set=False, gui_context=self.context, ) """ # create a visjs graph for the subtree if there is not one if 0: gui = self.context.gui if item.id() not in gui.visjs_servers: # the graph does not exist, create it _createSubTreeVisJs(item, self.context) if 1: # TODO - until implemented self._wwireframe.setVisible(False) @register class SubGraphPane(AbstractPane[kd.SubGraph]): """Pane to display info about a SubGraph instance.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "SubGraph" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") # SubTreePane.__init__(self, router) # articulate all the bodies sequentially with IK solver on (only WebScene) self._wswing_ik = kw.Button( router, text="Articulate IK", on_press=lambda: _swingHinge( self.item, [x.parentHinge() for x in self.item.sortedBodiesList()], False, self.context, ), tooltip="Sequentially auto articulate the bodies in the subgraph while enforcing bilateral constraints", ) self._wmd_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wlayout_articulate = widgetArray( router, label=self._wmd_articulate, children=[self._wswing_ik] ) self._wroot.addChild(self._wlayout_articulate) # ---------------------------- def _toggleHighlightConstraints( cstate, sg: kd.SubGraph, # bodies: list[kd.PhysicalBody], gui_context: Context, ): """Highlight constraints and the constraint nodes involving the list of bodies in visjs.""" # AG - this should be toggleable, so can turn off the frame axes gui = gui_context.gui if sg.id() not in gui.visjs_servers: # the graph does not exist, create it _createSubTreeVisJs(sg, gui_context) return server, state = gui.visjs_servers[sg.id()] new_state = cstate gui.visjs_servers[sg.id()] = [server, new_state] # label = "constraints" if self._constraints_state else "tree" if new_state: server.enableSubGraph("constraints") else: server.disableSubGraph("constraints") # show constraints (for loop constraint show nodes, for coordinate perhaps a line in webscene) # self._constraints_state = True self._wconstraints = kw.Toggle( router, text="Visjs graph constraint edges", on_toggle=lambda cstate: _toggleHighlightConstraints( cstate, sg=self.item, gui_context=self.context ), # render_as_button=True, tooltip="Toggle the bilateral constraint edges in the visjs graph for this subgraph", ) self._wroot.addChild(self._wconstraints) # ------------------------------------ def _flattenCB(sg): if not sg.hasCompoundBodies(): return sg.flattenCompoundBodies() # highlight bodies in aggregation graph for a constraint self._wflatten = kw.Button( router, text="Flatten compound bodies", on_press=lambda: _flattenCB(self.item), tooltip="Flatten all nested compound bodies in the subgraph", ) self._wmd_flatten = kw.Markdown(router, text="**Flatten**", in_line=True) self._wlayout_flatten = widgetArray( router, label=self._wmd_flatten, children=[self._wflatten], ) self._wroot.addChild(self._wlayout_flatten) # Setup widget topology def setup(self, item: kd.SubGraph, _: kw.Json, /): name = item.name() id = item.id() title_md = f"### {name} [SubGraph/{id}]" self._wtitle.setText(title_md) has_constraints = len(item.enabledConstraints()) > 0 has_compound_bodies = item.hasCompoundBodies() self._wmd_articulate.setVisible(has_constraints) self._wswing_ik.setVisible(has_constraints) # self._wconstraints.setVisible(has_constraints) self._wconstraints.setVisible(has_constraints) if 0: # create a visjs graph for the subgraph if there is not one gui = self.context.gui if item.id() not in gui.visjs_servers: # the graph does not exist, create it _createSubTreeVisJs(item, self.context) self._wlayout_flatten.setVisible(has_compound_bodies) """ # hightlight the subtree bodies in the multibody visjs graph _highlightBodies( bodies=item.sortedPhysicalBodiesList(), secondary_bodies=[], tertiary_bodies=[], toggle_over_set=False, gui_context=self.context, ) """ # set the default state. Skipping this for now, since it is # triggering the creation of the graph for the subtree with the # subtree is selected if 0: self._wconstraints.setValue(True) @register class MultibodyPane(AbstractPane[kd.Multibody]): # AbstractPane[kd.Multibody]): """Pane to display info about any Multibody-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "Mbody" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # ------------------------------------ def _setupCE(mb): # first check that a cegraph exists, else create it # nbodies = len(mb.sortedPhysicalBodiesList()) if not mb.enabledConstraints(): return import random suff = int(random.random() * 1000) cegraph = kd.SubGraph.create(f"cegraph{suff}", mb, mb.virtualRoot()) cegraph.setupConstraintEmbedding() # highlight bodies in aggregation graph for a constraint self._wfullce = kw.Button( router, text="Create", on_press=lambda: _setupCE(self.item), tooltip="Create CE compound bodies for all the multibody constraints", ) self._wmd_fullce = kw.Markdown(router, text="**Full CE**", in_line=True) self._wlayout_fullce = widgetArray( router, label=self._wmd_fullce, children=[self._wfullce], ) self._wroot.addChild(self._wlayout_fullce) # ----------------------------- def _addWebScene(): self.context.dock.addChild( title="3D View", widget=self.context.graphics_frame, ) self._wwebscene = kw.Button( router, text="Add 3D graphics pane", on_press=lambda: _addWebScene(), tooltip="Add/readd the 3D graphics pane to the gui", ) self._wroot.addChild(self._wwebscene) # ----------------------------- self._wgravity = kw.Button( router, text="Show gravity vector (TBD)", on_press=lambda: self._doTbd(), tooltip="Toggle visualization of the gravity acceleration vector", ) # Setup widget topology self._wmd_gravity = kw.Markdown(router, text="**Gravity**", in_line=True) self._wlayout_grav = widgetArray(router, label=self._wmd_gravity, children=[self._wgravity]) self._wroot.addChild(self._wlayout_grav) def _toggleStickParts(self): graphics = self.context.graphics camera = graphics.defaultCamera() if not camera: return mask = camera.getMask() mask ^= ks.LAYER_STICK_FIGURE camera.setMask(mask) def setup(self, item: kd.Multibody, _: kw.Json, /): has_constraints = len(item.enabledConstraints()) > 0 self._wlayout_fullce.setVisible(has_constraints) @register class ModelManagerPane(AbstractPane[kd.ModelManager]): """Pane to display info about any ModelManager-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "ModelManager" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") # Setup widget topology self._wroot.addChild(self._wtitle) def _toggleTrace(cstate): mm = self.item # Might happen if the toggle state gets set before we are ready if mm is None: return if not cstate: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.TRACE) else: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.WARN) mm.trace_state_propagator = cstate self._wtrace = kw.Toggle( router, text="Toggle trace mode", on_toggle=_toggleTrace, render_as_button=True ) # Setup widget topology self._wroot.addChild(self._wtitle) self._wmd_info = kw.Markdown(router, text="**Info**") self._wlayout_info = widgetArray(router, label=self._wmd_info, children=[self._wtrace]) self._wroot.addChild(self._wlayout_info) def setup(self, item: kd.ModelManager, _: kw.Json, /): pass def createPauseCb(sp: kd.StatePropagator) -> Callable[[], None]: """Return the callback for a pause/resume button. Parameters ---------- sp : kd.StatePropagator The StatePropagator to pause/resume. Returns ------- Callable[[], None] The pause/resume callback. """ def inner(): if sp.isPaused(): sp.resume() else: sp.pause() return inner def createAdvanceByCallback(sp: kd.StatePropagator, time: np.timedelta64) -> Callable[[], None]: """Return the callback for a pause/resume button. Parameters ---------- sp : kd.StatePropagator The StatePropagator to pause/resume. time : np.timedelta64 The time to advance by. Returns ------- Callable[[], None] The advanceTo callback. """ def inner(): if sp.isPaused(): pause_at = time + sp.getTimeKeeper().getTime() te = kd.TimedEvent(f"pause_at_{pause_at}", pause_at, lambda _: sp.pause(), False) sp.registerTimedEvent(te) sp.resume() else: sp.advanceBy(time) return inner @register class StatePropagatorPane(AbstractPane[kd.StatePropagator]): """Pane to display info about any StatePropagator-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "StatePropagator" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # ---------------------------- adv_opts = kw.SliderOptions() adv_opts.min = -3 adv_opts.max = 3 # 10 steps per decade adv_opts.step = 0.1 # Use log scale on slider adv_opts.log_scale = True self._advance_incr = 0.1 def setincr(new_incr, units): incr_seconds = (new_incr * ureg(units)).to("s") self._advance_incr = incr_seconds.magnitude self._wadvinput = kw.QuantityInput(router, "", setincr) # Default to 0.1 s self._wadvinput.state("quantity_state").set({"value": 0.1, "units": "s"}) # Advance by increment rounded to the nearest nanosecond self._wadvbtn = kw.Button( router, text="Advance", on_press=lambda: createAdvanceByCallback( self.item, np.timedelta64(int(self._advance_incr * 1e9), "ns") )(), ) # Advance by increment rounded to the nearest nanosecond self._wadvbtn = kw.Button( router, text="Advance", on_press=lambda: createAdvanceByCallback( self._item, np.timedelta64(int(self._advance_incr * 1e9), "ns") )(), ) self._wmd_adv = kw.Markdown(router, text="**Advance**", in_line=True) self._wlayout_adv = widgetArray( router, label=self._wmd_adv, children=[self._wadvinput, self._wadvbtn], ) # self._wlayout_adv.addChild(self._wadv_slider) # ---------------------------- self._wpause = kw.Button( router, text="\u23f8/\u25b6", on_press=lambda: createPauseCb(self.item)() ) self._wstop = kw.Button(router, text="\u23f9", on_press=lambda: self.item.stop()) self._wmd_pause = kw.Markdown(router, text="**Sim run**", in_line=True) self._wlayout_pause = widgetArray( router, label=self._wmd_pause, children=[self._wpause, self._wstop] ) slider_opts = kw.SliderOptions() slider_opts.min = -6 slider_opts.max = 2 slider_opts.step = 1 slider_opts.log_scale = True self._wmaxstep_slider = kw.Slider( router, "", lambda new_val: self._item.setMaxStepSize(new_val), slider_opts, ) self.wmd_step = kw.Markdown(router, text="**Max step size**", in_line=True) self._wlayout_maxstep = widgetArray( router, label=self.wmd_step, children=[self._wmaxstep_slider] ) self._wlayout_prop = widgetArray( router, children=[self._wlayout_adv, self._wlayout_pause, self._wlayout_maxstep], alignment="column", alignItems="left", ) self._wlayout_prop.addDomClass("karana-fancy-border") self._wroot.addChild(self._wlayout_prop) # ---------------------------- # Mapping of indices to integrator type self.integ_types = [ ki.IntegratorType.RK4, ki.IntegratorType.CVODE, ki.IntegratorType.CVODE_STIFF, ] def integSelect(index): """Set the integrator according to a given index.""" if self.item.getIntegrator().getIntegratorType() != self.integ_types[index]: self.item.setIntegrator(self.integ_types[index]) self._winteg = kw.Dropdown(router, "", ["RK4", "CVode", "CVode Stiff"], integSelect) def createIntegratorPane(sp): integ = sp.getIntegrator() selection = kw.Selection(items=[kw.Selection.Item(id=integ.id(), context={})]) integ_panel = self.context.setup_info_panel(selection) # integ_panel.selection.onChange(lambda x: None) integ_panel.updateFor(integ, {}) """ self.dock.addChild( title="Integrator Info", widget=integ_panel.wroot, relative_to=self.mbody_tree_view, direction="below", ) """ self.context.dock.addChild( "Integrator Info", integ_panel.wroot, # self.wroot, self.context.visjs_frame, "within", ) self._winteg_pane = kw.Button( router, text="Integrator pane", on_press=lambda: createIntegratorPane(self.item), tooltip="Create a separate info pane for the integrator", ) self._wmd_integ = kw.Markdown(router, text="**Integrator**", in_line=True) self._wlayout_integ = widgetArray( router, label=self._wmd_integ, children=[self._winteg, self._winteg_pane] ) self._wroot.addChild(self._wlayout_integ) def setup(self, item: kd.StatePropagator, _: kw.Json, /): curr_type = item.getIntegrator().getIntegratorType() # Setting item early in case setting the below widget states # triggers a callback relying on self.item to be current. self.item = item self._wmaxstep_slider.state("value_state").set( km.ktimeToSeconds(self._item.getMaxStepSize()) ) if curr_type in self.integ_types: self._winteg.setIndex(self.integ_types.index(curr_type)) else: print(f"Warning: unsupported dropdown integrator type {curr_type}") @register class IntegratorPane(AbstractPane[ki.Integrator]): """Pane to display info about any Integrator-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "Integrator" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) def setup(self, item: ki.Integrator, _: kw.Json, /): title_md = f"### {item.typeString()}" self._wtitle.setText(title_md) def isCompatible(self, item: Any) -> bool: """Check whether the Pane knows how to display an item.""" # Check if parent compatible and if not another type that is already managed return super().isCompatible(item) and not isinstance(item, ki.CVodeIntegrator) @register class CVodeIntegratorPane(AbstractPane[ki.CVodeIntegrator]): """Pane to display info about any CVodeIntegrator-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "CVodeIntegrator" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # Set up sliders for tolerances, etc slider_opts = kw.SliderOptions() slider_opts.min = -8 slider_opts.max = -2 slider_opts.log_scale = True self._atol_slider = kw.Slider( router, "Atol", lambda new_val: self.item.setAtol(new_val), slider_opts ) self._rtol_slider = kw.Slider( router, "Rtol", lambda new_val: self.item.setRtol(new_val), slider_opts ) self._wroot.addChild(self._atol_slider) self._wroot.addChild(self._rtol_slider) def setup(self, item: ki.CVodeIntegrator, _: kw.Json, /): title_md = f"### CVODE Integrator" self._wtitle.setText(title_md) opts = cast(ki.CVodeIntegratorOptions, item.getOptions()) self._atol_slider.setValue(opts.atol) self._rtol_slider.setValue(opts.rtol) @register class BaseKModelPane(AbstractPane[kmdl.BaseKModel]): """Pane to display info about any BaseKModel-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "BaseKModel" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") def _toggleRegistration(mdl): reg = mdl.model_manager.getRegisteredModel(mdl.name()) if reg: mdl.model_manager.unregisterModel(mdl) else: mdl.model_manager.registerModel(mdl) self._wregister = kw.Button( router, text="Toggle active", on_press=lambda: _toggleRegistration(self.item) ) def _toggleDebug(mdl): mdl.debug_model = not mdl.debug_model # Update verbosity to be at least DEBUG if it is not already if mdl.debug_model and kc.MsgLogger.getVerbosity("stdout") > kc.LogLevel.DEBUG: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.DEBUG) self._wdebug = kw.Button( router, text="Toggle debug mode", on_press=lambda: _toggleDebug(self.item) ) self._wselect_body = kw.Button( router, text="Select body", on_press=lambda: _selectObject( self.item.multibodyObjs().physical_bodies[0], self.context.selection ), ) self._wselect_subhinge = kw.Button( router, text="Select subhinge body", on_press=lambda: _selectObject( self.item.multibodyObjs().physical_subhinges[0].parentHinge().pnode().parentBody(), self.context.selection, ), ) self._wselect_node = kw.Button( router, text="Select node", on_press=lambda: _selectObject( self.item.multibodyObjs().nodes[0], self.context.selection ), ) self._wselect_subtree = kw.Button( router, text="Select subtree", on_press=lambda: _selectObject( self.item.multibodyObjs().subtrees[0], self.context.selection ), ) # Setup widget topology self._wroot.addChild(self._wtitle) self._wmd_active = kw.Markdown(router, text="**Activate**", in_line=True) self._wlayout_active = widgetArray( router, label=self._wmd_active, children=[self._wregister] ) self._wroot.addChild(self._wlayout_active) self._wmd_debug = kw.Markdown(router, text="**Debug**", in_line=True) self._wlayout_debug = widgetArray(router, label=self._wmd_debug, children=[self._wdebug]) self._wroot.addChild(self._wlayout_debug) self._wmd_select = kw.Markdown(router, text="**Select**", in_line=True) self._wlayout_select = widgetArray( router, label=self._wmd_select, children=[ self._wselect_body, self._wselect_node, self._wselect_subtree, self._wselect_subhinge, ], ) self._wroot.addChild(self._wlayout_select) def setup(self, item: kmdl.BaseKModel, _: kw.Json, /): title_md = f"### {item.name()} [BaseKModel/{item.id()}]" self._wtitle.setText(title_md) mbobjs = item.multibodyObjs() self._wselect_body.setVisible(len(mbobjs.physical_bodies) > 0) self._wselect_node.setVisible(len(mbobjs.nodes) > 0) self._wselect_subtree.setVisible(len(mbobjs.subtrees) > 0) self._wselect_subhinge.setVisible(len(mbobjs.physical_subhinges) > 0) """ # highlight bodies and nodes invoved with the model bodies = set() mbobj = item.multibodyObjs() bodies.update(mbobj.physical_bodies) bodies.update([x.parentBody() for x in mbobj.nodes]) for st in mbobj.subtrees: bodies.update(st.sortedPhysicalBodiesList()) for sh in mbobj.physical_subhinges: obd = sh.parentHinge().onode().parentBody() pbd = sh.parentHinge().pnode().parentBody() bodies.update([obd, pbd]) # hightlight the subtree bodies in the multibody visjs graph _highlightBodies( bodies=list(bodies), secondary_bodies=[], tertiary_bodies=[], toggle_over_set=False, gui_context=self.context, ) """ @register class PenaltyContactPane(AbstractPane[kmdl.PenaltyContact]): """Pane to display info about any PIDModel-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "PenaltyContact" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # Hunt Crossley: kp, kc, n, mu, linear_region_tol # Hunt Crossley: kp, kc, n, mu, linear_region_tol, dmax, e, # ------------------------------------------- # slider for kp slider_opts = kw.SliderOptions() slider_opts.min = 1 slider_opts.max = 6 slider_opts.step = 0.01 slider_opts.log_scale = True slider_opts.tooltip = "Stiffness parameter" def setkp(val): self.item.getContactForceModel().params.kp = val self._wkp_slider = kw.Slider( router, text="kp", on_change=lambda kp: setkp(kp), opts=slider_opts, ) # ------------------------------------------- # slider for kc slider_opts.tooltip = "Damping parameter" def setkc(val): self.item.getContactForceModel().params.kc = val self._wkc_slider = kw.Slider( router, text="kc", on_change=lambda kc: setkc(kc), opts=slider_opts, ) # ------------------------------------------- # slider for n slider_opts.min = -3 slider_opts.max = 3 slider_opts.step = 0.01 slider_opts.log_scale = False slider_opts.tooltip = "Normal penetration exponent" def setn(val): self.item.getContactForceModel().params.n = val self._wn_slider = kw.Slider( router, text="n", on_change=lambda n: setn(n), opts=slider_opts, ) # ------------------------------------------- # slider for mu slider_opts.min = -3 slider_opts.max = 3 slider_opts.step = 0.01 slider_opts.log_scale = False slider_opts.tooltip = "Coefficient of Friction" def setmu(val): self.item.getContactForceModel().params.mu = val self._wmu_slider = kw.Slider( router, text="mu", on_change=lambda mu: setmu(mu), opts=slider_opts, ) # ------------------------------------------- # slider for linear_region_tol slider_opts = kw.SliderOptions() slider_opts.min = -2 slider_opts.max = 2 slider_opts.step = 0.01 slider_opts.log_scale = True slider_opts.tooltip = "Width of linear interpolation band for friction forces" def setLinRegTol(val): self.item.getContactForceModel().params.linear_region_tol = val self._wlinear_region_tol_slider = kw.Slider( router, text="linear_region_tol", on_change=lambda linear_region_tol: setLinRegTol(linear_region_tol), opts=slider_opts, ) # ------------------------------------------- # slider for dmax slider_opts.min = -2 slider_opts.max = 2 slider_opts.step = 0.01 slider_opts.log_scale = True slider_opts.tooltip = "Maximum penetration depth before damping plateaus" def setdmax(val): self.item.getContactForceModel().params.dmax = val self._wdmax_slider = kw.Slider( router, text="dmax", on_change=lambda dmax: setdmax(dmax), opts=slider_opts, ) # ------------------------------------------- # slider for e slider_opts = kw.SliderOptions() slider_opts.min = -3 slider_opts.max = 3 slider_opts.step = 0.01 slider_opts.log_scale = False slider_opts.tooltip = "Damping exponent" def sete(val): self.item.getContactForceModel().params.e = val self._we_slider = kw.Slider( router, text="e", on_change=lambda e: sete(e), opts=slider_opts, ) # set up cfv var for future use and make a button self._cfv: vizutils.ContactForceVisualizer | None = None def toggleForceViz(active: bool): # Only register if we've already set up if self._cfv is None: return # Let the cfv register itself with scene if active: self._cfv.registerCallback() # Force redraw with new arrows self.context.scene.update() else: self._cfv.unregisterCallback() self._wcfv_toggle = kw.Toggle( router, "Visualize Contact Forces", toggleForceViz, tooltip="Visualize forces resulting from contact", render_as_button=True, ) # Default to off self._wcfv_toggle.setValue(False) slider_opts = kw.SliderOptions() slider_opts.min = -3 slider_opts.max = 3 slider_opts.step = 0.01 slider_opts.log_scale = True slider_opts.tooltip = "Factor to scale up/down contact forces" def scaleContact(new_scale): # Only register if we've already set up if self._cfv is None: return self._cfv.setScale(new_scale) self._wcfv_scale = kw.Slider( router, "Viz Force Scale", scaleContact, slider_opts, ) # Default to unit scale self._wcfv_scale.setValue(0.05) self._wroot.addChild(self._wtitle) self._wroot.addChild(self._wcfv_toggle) self._wroot.addChild(self._wcfv_scale) self._wroot.addChild(self._wkp_slider) self._wroot.addChild(self._wkc_slider) self._wroot.addChild(self._wn_slider) self._wroot.addChild(self._wmu_slider) self._wroot.addChild(self._wlinear_region_tol_slider) self._wroot.addChild(self._wdmax_slider) self._wroot.addChild(self._we_slider) def setup(self, item: kmdl.PenaltyContact, _: kw.Json, /): # Lazy-setup of cfv if self._cfv is None: # Default to off, so no registration yet self._cfv = vizutils.ContactForceVisualizer( "cfv", self.context.multibody.virtualRoot(), self.context.scene, item ) self._cfv.setRadius(0.05) title_md = f"### {item.name()} [PenaltyContact/{item.id()}]" self._wtitle.setText(title_md) frcmdl = item.getContactForceModel() from Karana.Collision import HuntCrossley from Karana.Adams.Adams_Py import AdamsContact possible_params = ["kp", "kc", "n", "mu", "linear_region_tol", "dmax", "e"] params_adams = possible_params params_hc = ["kp", "kc", "n", "mu", "linear_region_tol"] params_dict = {p: False for p in possible_params} # Set Adams-specific values if isinstance(frcmdl, AdamsContact): # Set initial values self._we_slider.setValue(frcmdl.params.e) self._wdmax_slider.setValue(frcmdl.params.dmax) self._wkp_slider.setValue(frcmdl.params.kp) self._wkc_slider.setValue(frcmdl.params.kc) self._wn_slider.setValue(frcmdl.params.n) self._wmu_slider.setValue(frcmdl.params.mu) self._wlinear_region_tol_slider.setValue(frcmdl.params.linear_region_tol) # Mark active params for k in params_adams: params_dict[k] = True if isinstance(frcmdl, HuntCrossley): # TODO for @kelly can we avoid code duplication from above # Set initial values self._wkp_slider.setValue(frcmdl.params.kp) self._wkc_slider.setValue(frcmdl.params.kc) self._wn_slider.setValue(frcmdl.params.n) self._wmu_slider.setValue(frcmdl.params.mu) self._wlinear_region_tol_slider.setValue(frcmdl.params.linear_region_tol) # Mark active params for k in params_hc: params_dict[k] = True self._wkp_slider.setVisible(params_dict["kp"]) self._wkc_slider.setVisible(params_dict["kc"]) self._wn_slider.setVisible(params_dict["n"]) self._wmu_slider.setVisible(params_dict["mu"]) self._wlinear_region_tol_slider.setVisible(params_dict["linear_region_tol"]) self._wdmax_slider.setVisible(params_dict["dmax"]) self._we_slider.setVisible(params_dict["e"]) @register class SpringDamperPane(AbstractPane[kmdl.SpringDamper]): """Pane to display info about any PIDModel-derived object.""" @property def wroot(self) -> kw.Widget: return self._wroot @property def label(self) -> str: return "SpringDamper" def __init__(self, context: Context): super().__init__(context) # Create widgets router = context.router self._wroot = kw.Layout( router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) self._wtitle = kw.Markdown(router, text="") self._wroot.addChild(self._wtitle) # Hunt Crossley: kp, kc, n, mu, linear_region_tol # Hunt Crossley: kp, kc, n, mu, linear_region_tol, dmax, e, # ------------------------------------------- # slider for kp slider_opts = kw.SliderOptions() slider_opts.min = 1 slider_opts.max = 6 slider_opts.step = 0.01 slider_opts.log_scale = True slider_opts.tooltip = "Stiffness parameter" def setk(val): self.item.params.k = val self._wk_slider = kw.Slider( router, text="k", on_change=lambda k: setk(k), opts=slider_opts, ) # ------------------------------------------- # slider for kc slider_opts.tooltip = "Damping parameter" def setd(val): self.item.params.d = val self._wd_slider = kw.Slider( router, text="d", on_change=lambda d: setd(d), opts=slider_opts, ) # set up nd1v var for future use and make a button self._nd1v: vizutils.ScaledVectorVisualizer | None = None self._nd2v: vizutils.ScaledVectorVisualizer | None = None def toggleForceViz(active: bool): # Only register if we've already set up if self._nd1v is None or self._nd2v is None: return # Let the nd1v register itself with scene if active: self._nd1v.registerCallback() self._nd2v.registerCallback() # Force redraw with new arrows self.context.scene.update() else: self._nd1v.unregisterCallback() self._nd2v.unregisterCallback() self._wndv_toggle = kw.Toggle( router, "Visualize Forces", toggleForceViz, tooltip="Visualize the spring damper forces at the nodes", render_as_button=True, ) # Default to off self._wndv_toggle.setValue(False) slider_opts = kw.SliderOptions() slider_opts.min = -3 slider_opts.max = 3 slider_opts.step = 0.01 slider_opts.log_scale = True slider_opts.tooltip = "Factor to scale up/down forces" def scaleForces(new_scale): # Only register if we've already set up if self._nd1v is None or self._nd2v is None: return self._nd1v.setScale(new_scale) self._nd2v.setScale(new_scale) self._wndv_scale = kw.Slider( router, "Viz Force Scale", scaleForces, slider_opts, ) # Default to unit scale self._wndv_scale.setValue(0.05) self._wroot.addChild(self._wtitle) self._wroot.addChild(self._wk_slider) self._wroot.addChild(self._wd_slider) self._wroot.addChild(self._wndv_toggle) self._wroot.addChild(self._wndv_scale) def setup(self, item: kmdl.SpringDamper, _: kw.Json, /): # Lazy-setup of nd1v if self._nd1v is None: mbobjs = item.multibodyObjs() # Default to off, so no registration yet self._nd1v = vizutils.visualizeNodeForce( mbobjs.nodes[0], self.context.scene, ) self._nd2v = vizutils.visualizeNodeForce( mbobjs.nodes[1], self.context.scene, ) title_md = f"### {item.name()} [SpringDamper/{item.id()}]" self._wtitle.setText(title_md) # @register # class PIDModelPane(AbstractPane[kmdl.PID]): # """Pane to display info about any PIDModel-derived object.""" # @property # def wroot(self) -> kw.Widget: # return self._wroot # @property # def label(self) -> str: # return "PIDModel" # def __init__(self, context: Context): # super().__init__(context) # # Create widgets # router = context.router # self._wroot = kw.Layout(router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"}) # self._wtitle = kw.Markdown(router, text="") # self._wshow_body = kw.Button( # router, # text="Show body", # on_press=lambda: _highlightBodies( # bodies=[ # self.item.multibodyObjs() # .physical_subhinges[0] # .parentHinge() # .pnode() # .parentBody() # ], # secondary_bodies=[], # tertiary_bodies=[], # gui_context=self.context, # ), # ) # """ # self._wselect_body = kw.Button( # router, # text="Select body", # on_press=lambda: _selectObject( # self.item.multibodyObjs().physical_subhinges[0].parentHinge().pnode().parentBody(), # self.context.selection, # ), # ) # """ # # Setup widget topology # self._wroot.addChild(self._wtitle) # # self._wmd_gravity = kw.Markdown(router, text="**Gravity**") # # self._wroot.addChild(self._wmd_gravity) # self._wroot.addChild(self._wshow_body) # #self._wroot.addChild(self._wselect_body) # def updateFor(self, item: kf.Frame, item_context: kw.Json): # pass class InfoPanel: @property def context(self) -> Context: return self._context def __init__( self, context: Context, panes: Iterable[AbstractPane] | None = None, ): """Create a new InfoPanel.""" self._context = context router = context.router self._wroot = kw.Layout(router, style={"height": "100%"}) # Container for content when nothing is selected self._wdefault = kw.Layout(router, style={"padding": "0.5em"}) self._wdefault_text = kw.Markdown(router, text="*No items selected*") # Container for content when something is selected self._wselection = kw.Layout(router) self._wselection.addDomClass("selection-panel") self._wselection.setVisible(False) # Button to refresh the active pane (eg: to update numerics) self._wrefresh = kw.Button( router, text="Refresh", on_press=lambda: self.updateFor(self._item, self._item_context), tooltip=f"Refresh the contents of this pane", ) # Container for the button widgets to select the active pane self._wbuttons = kw.Layout(router) self._wbuttons.addDomClass("selection-buttons") # Container for the pane widgets self._wpanes = kw.Layout(router) self._wpanes.addDomClass("selection-panes") self._button_list = [] if panes is None: # By default use all pane types that were registered, # sorting based on class hierarchy of their wrapped types, # with less derived coming first. self._panes = [cls(context) for cls in self._hierarchySort(known_pane_types)] else: self._panes = list(panes) # The currently selected item self._item = None self._item_context = None # This is the last pane the user requested by clicking a button. # If this pane is valid for the current item then it should be # prioritized. This makes the user pane selection 'sticky', so # that if the user selects a pane, selects an incompatible # item, then goes back to compatible items, the user-selected # pane isn't forgotten. self._user_pane = None # This is the currently displayed pane. It may differ from the # user pane if the user pane isn't valid for the current # selection. self._active_pane = None # Creates a no-argument callable with a pane bound to its # closure. This will be used to generate callbacks for buttons, # each using a distinct pane. def activatePaneClosure(pane: AbstractPane): def activatePaneClosureInner(): self._requestActivatePane(pane) return activatePaneClosureInner for pane in self._panes: # Hide all panes initially pane.wroot.setVisible(False) # Create the buttons to activate each pane on_press = activatePaneClosure(pane) wbutton = kw.Button( router, text=pane.label, on_press=on_press, tooltip=f"Switch to the '{pane.label}' base class pane widgets", ) wbutton.addDomClass("karana-pane-choice") self._button_list.append(wbutton) # Setup widget topology self._wdefault.addChild(self._wdefault_text) self._wbuttons.addChild(self._wrefresh) for wbutton in self._button_list: self._wbuttons.addChild(wbutton) for pane in self._panes: self._wpanes.addChild(pane.wroot) self._wselection.addChild(self._wbuttons) self._wselection.addChild(self._wpanes) self._wroot.addChild(self._wdefault) self._wroot.addChild(self._wselection) def _requestActivatePane(self, pane: AbstractPane): """Request from user that a pane should be activated. This will store the requested pane so that it will be active whenever it is compatible with the currently selected item. """ self._user_pane = pane # Only allow a pane if it's for the current item's type or a # subclass if not pane.isCompatible(self._item): return self._activatePane(pane) def _activatePane(self, pane: AbstractPane | None): """Display the given pane. If pane is None, hide all panes. This can happen in the edge case where the selected item has no compatible panes. Note that this function assumes the pane is compatible with the current item, so this must be checked before calling. """ if self._active_pane == pane: return old_pane = self._active_pane self._active_pane = pane # Update pane selection button styling so the one for the # active pane is visually distinct for wbutton, candidate_pane in zip(self._button_list, self._panes, strict=True): if candidate_pane == old_pane: wbutton.removeDomClass("karana-pane-choice-selected") if candidate_pane == self._active_pane: wbutton.addDomClass("karana-pane-choice-selected") if pane is None and old_pane is not None: # Only doing this now since we are about to return. # Otherwise we should wait until the new pane is ready to # display to minimize the amount of time that no pane is # visible. old_pane.wroot.setVisible(False) return # Update the new pane and switch to it try: pane.updateFor(self._item, self._item_context) except Exception: cls_name = type(pane).__name__ msg = f"Error updating {cls_name}:\n{traceback.format_exc()}" kc.error(msg) finally: pane.item = self._item if old_pane is not None: # Teardown the item on the old pane to clean up any side-effects if old_item := old_pane.getItem(): old_pane.teardown(old_item) old_pane.wroot.setVisible(False) pane.wroot.setVisible(True) @property def wroot(self) -> kw.Widget: """Get the root widget.""" return self._wroot def updateFor(self, item: Any | None, item_context: kw.Json): """Update the active pane for the given item.""" old_item = self._item self._item = item self._item_context = item_context if item is None: if old_item is None: return # We are deselecting, so switch to the default widget and # deactivate any active pane self._wselection.setVisible(False) self._wdefault.setVisible(True) self._activatePane(None) return # Might need to switch panes based on compatibility. First check # if the user's preferred pane is compatible with the new item. new_active_pane = None if self._user_pane and self._user_pane.isCompatible(item): new_active_pane = self._user_pane if new_active_pane is None: # Use the most derived pane that is compatible for pane in reversed(self._panes): if pane.isCompatible(item): new_active_pane = pane break # At this point, new_active_pane could still be None if # no pane is compatible. This is supported by # _activatePane and should hide all panes. if new_active_pane and new_active_pane == self._active_pane: try: self._active_pane.updateFor(item, item_context) except Exception: cls_name = type(self).__name__ msg = f"Error updating {cls_name}:\n{traceback.format_exc()}" kc.error(msg) finally: self._active_pane.item = item else: self._activatePane(new_active_pane) # Only show activation buttons for compatible panes for wbutton, pane in zip(self._button_list, self._panes, strict=True): wbutton.setVisible(pane.isCompatible(item)) # If there wasn't a selection prior we need to switch to # the selection widget if old_item is None: self._wdefault.setVisible(False) self._wselection.setVisible(True) @staticmethod def _hierarchySort(types: Iterable[type]) -> Sequence[type]: """Sort the given types so that less derived ones come first. We sort first by the length of each type's mro (method resolution order) in ascending order, meaning that less derived types will come earlier. We use the item's position in the original iterator as a tie-breaker to preserve order where possible. """ # Build a list of (index, type) tuples items = list(enumerate(types)) def keyFunc(item): index, type_ = item # This sorts first by ascending mro length, then by index return (len(type_.__mro__), index) # Sort the list of (index, type) tuples items.sort(key=keyFunc) # Extract the types in order from the sorted item tuples return [item[1] for item in items] def addPane(self, pane: AbstractPane): """Add an AbstractPane to be shown for compatible items.""" # Add the pane to our bookkept list of panes self._panes.append(pane) pane.wroot.setVisible(False) # Create the buttons to activate the pane wbutton = kw.Button( self.context.router, text=pane.label, on_press=lambda: self._requestActivatePane(pane), tooltip=f"Switch to the '{pane.label}' base class pane widgets", ) wbutton.addDomClass("karana-pane-choice") # Only show the button right away if its pane is compatible with the current item wbutton.setVisible(pane.isCompatible(self._item)) # Add the button to our bookkept list of pane buttons self._button_list.append(wbutton) # Add the button to the layout widget so it can be seen on the frontend self._wbuttons.addChild(wbutton) # Add the pane to the layout widget so it can be seen on the frontend self._wpanes.addChild(pane.wroot) def close(self): """Do any necessary cleanup.""" self.updateFor(None, None) for pane in self._panes: pane.close() self._panes = []