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 card GUIs."""

from abc import abstractmethod, ABC
from typing import (
    Callable,
    Generic,
    SupportsFloat,
    SupportsIndex,
    TypeVar,
    Type,
    cast,
    get_args,
    Any,
)

from Karana.KUtils.DataStruct import IdMixin
from networkx import immediate_dominators
import numpy as np
from numpy.typing import ArrayLike, NDArray
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
import Karana.Collision as kcoll


from Karana.Math.Kquantities import ureg


from ._effects import (
    EffectItem,
    EffectManager,
    AxesParams,
    OutlineParams,
    GraphEdgeParams,
)
from ._helpers import widgetArray
from ._worker import AsyncWorker


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 GuiContext: # 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 # tree view for the multibody system mbody_tree_view: kw.TreeView # tree view for subtrees subtrees_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 for subtree graphs visjs_servers: dict[int, tuple[vjs.MultibodyGraphServer, bool]] # visjs server for frames graph visjs_frames_server: vjs.NetworkGraph # method to create frames graph setup_frames_visjs: Callable # visjs iframe for all graphs visjs_iframe: kw.IFrame # the 3D graphics iframe graphics_frame: kw.IFrame # callable to signal that an error ocurred signal_error: Callable # background thread for long-running async callbacks worker: AsyncWorker
[docs] class AbstractCard(ABC, WrappedTypeMixin[T]): """Interface for an info card for a given item type.""" def __init__(self, context: GuiContext): """Create the AbstractCard. Derived classes SHOULD call this first in their constructor. """ self._context = context self._wroot = kw.Layout( context.router, style={"display": "flex", "flexDirection": "column", "gap": "0.5em"} ) # Add the single line header self._wheader = kw.Markdown(context.router, text="") self._wheader.addDomClass("karana-card-header") self._wheader.setVisible(False) self._wroot.addChild(self._wheader) # Add the markdown summary block self._wsummary = kw.Markdown(context.router, text="") self._wsummary.addDomClass("karana-card-summary") self._wsummary.setVisible(False) self._wroot.addChild(self._wsummary) @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 Card item before setting it") item = self._item_getter() if item is None: raise RuntimeError("Card item has gone out of scope!") return item @item.setter def item(self, item: T, /): """Set the current item for the card.""" 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 card.""" 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("Card item has gone out of scope!") pass return item
@property def context(self) -> GuiContext: return self._context @property @abstractmethod def label(self) -> str: """Get a text label for this card.""" @property def wroot(self) -> kw.Layout: """Get the root widget for this card.""" return self._wroot
[docs] def teardown(self, _: T, /): """Do any necessary cleanup when leaving the given item. Cards MAY override this if any cleanup is needed. """
[docs] def setup(self, item: T, item_context: kw.Json, /): """Set up the card for the new item. Derived Card classes SHOULD call this to update the header and summary. """ header = self.getHeader() self._wheader.setText(header) self._wheader.setVisible(bool(header)) summary = self.getSummary() # Cleanup any unintented whitespace and surrounding whitespace summary = textwrap.dedent(summary).strip() self._wsummary.setText(summary) self._wsummary.setVisible(bool(summary))
[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). Cards 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 Card knows how to display an item.""" result = isinstance(item, self.wrapped()) # print("AAAA", item, self.wrapped(), result) return result
[docs] def getHeader(self) -> str: """Get a single-line markdown header of the current item.""" item = self.item if isinstance(item, kc.Base): name = item.name() id_ = item.id() type_ = item.typeString() return f"### {name} [{type_}/{id_}]" return ""
[docs] def getSummary(self) -> str: """Get a markdown summary of the current item.""" # result = super().getSummary(self) return ""
def _doTbd(self): print("implementation TBD")
# Registry mapping item types to their card known_card_types = [] # Class decorator that saves the card to a list of known card types def register(cls): assert issubclass(cls, AbstractCard) known_card_types.append(cls) return cls @register class BaseCard(AbstractCard[kc.Base]): """Card to display info about any Base-derived object.""" @property def label(self) -> str: return "Base" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wdump = kw.Markdown(router, text="") self._wverbosity_error = kw.Toggle( router, text="ERROR", on_toggle=lambda cstate: self._setVerbosity( kc.LogLevel.ERROR ), # kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.ERROR), tooltip="Change verbosity to ERROR level", render_as_button=True, ) self._wverbosity_warn = kw.Toggle( router, text="WARNING", on_toggle=lambda cstate: self._setVerbosity(kc.LogLevel.WARN), # on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.WARN), tooltip="Change verbosity to WARNING level", render_as_button=True, ) self._wverbosity_debug = kw.Toggle( router, text="DEBUG", on_toggle=lambda cstate: self._setVerbosity(kc.LogLevel.DEBUG), # on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.DEBUG), tooltip="Change verbosity to DEBUG level", render_as_button=True, ) self._wverbosity_trace = kw.Toggle( router, text="TRACE", on_toggle=lambda cstate: self._setVerbosity(kc.LogLevel.TRACE), # on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.TRACE), tooltip="Change verbosity to TRACE level", render_as_button=True, ) # Setup widget topology # self._wmd_verbosity = kw.Markdown(router, text="**Verbosity**", in_line=True) self._wlayout_verbosity = widgetArray( router, label="Verbosity", children=[ self._wverbosity_error, self._wverbosity_warn, self._wverbosity_debug, self._wverbosity_trace, ], kind="inputgroup", ) 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="**Introspection**", in_line=True) self._wlayout_dump.addChild(self._wdump) def _setVerbosity(self, level): kc.MsgLogger.changeVerbosity("stdout", level) self._wverbosity_error.setValue(level == kc.LogLevel.ERROR) self._wverbosity_warn.setValue(level == kc.LogLevel.WARN) self._wverbosity_debug.setValue(level == kc.LogLevel.DEBUG) self._wverbosity_trace.setValue(level == kc.LogLevel.TRACE) def setup(self, item: kc.Base, item_context: kw.Json, /): # Update header/summary text super().setup(item, item_context) self._setVerbosity(kc.MsgLogger.getVerbosity("stdout")) # 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 BaseWithVarsCard(AbstractCard[kc.BaseWithVars]): """Card to display vars for any BaseWithVars-derived object.""" def __init__(self, context: GuiContext): super().__init__(context) self._wtree = kw.DataTree(context.router) self.wroot.addChild(self._wtree) @property def label(self) -> str: return "Vars" def setup(self, item: kc.BaseWithVars, item_context: kw.Json, /): # Update header/summary text super().setup(item, item_context) all_vars = item.getVars().getAllVars() def toJson(node: kc.NestedVars | kc.Var) -> kw.Json: if isinstance(node, kc.NestedVars): result = {} for leaf in node.local_vars: result[leaf.name()] = toJson(leaf) for branch in node.nested_vars: result[branch.name] = toJson(branch) return result try: return node.dumpString().replace("\\s+", " ").replace("\\", "") except Exception: msg = f"Error evaluating Var {node.name()}:\n{traceback.format_exc()}" kc.error(msg) self.context.signal_error() return "ERROR" json_data = toJson(all_vars) self._wtree.setData(json_data) """ def setup(self, item: kc.BaseWithVars, item_context: kw.Json, /): # Update header/summary text super().setup(item, item_context) 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) self.context.signal_error() 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 card 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 FrameCard(AbstractCard[kf.Frame]): """Card to display info about any Frame-derived object.""" @property def label(self) -> str: return "Frame" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------- 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", ) # Setup widget topology # self._wmd_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wlayout_highlight = widgetArray( router, label="Highlight", children=[self._waxes, self._waxes3d, self._wview_around], kind="inputgroup", ) # self.wroot.addChild(self._wlayout_highlight) # -------------------- # 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", ) self._wlayout_visrates = widgetArray( router, label="Visualize Rates", children=[ self._wvel_linear, self._wvel_angular, self._waccel_linear, self._waccel_angular, ], kind="inputgroup", ) # self.wroot.addChild(self._wlayout_visrates) # ---------- self._wlayout_viz = widgetArray( router, label="Visualize frame and its rates", children=[ self._wlayout_highlight, # highlight frames self._wlayout_visrates, # enable rates visualization ], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wlayout_viz.setTooltip( "Highlight a frame and turn on visualization of its Newtonian frame relative rates and accelerations" ) self._wlayout_viz.setOpen(True) self.wroot.addChild(self._wlayout_viz) # ------------------------- def _dumpChainCB(cstate): # print("HH", self.context.visjs_frames_server) if cstate: def f2fDumpAction(id): # look up frame from id other_frame = cast(kf.Frame, kc.BaseContainer.singleton().at(id)) print("") self.item.frameToFrame(other_frame).dump() print("") # keep the highlighting on the selected frame and not # change to the new picked frame. self.context.effects.graph_highlighter.set( [EffectItem(obj=cast(kc.Base, self.item), params=None)] ) print( "==> Pick a frame to dump the f2f from the selected frame to the picked frame" ) self.context.visjs_frames_server.visjs_frames_noselect_cb = f2fDumpAction # reset the other buttons # self._wdumpf2f.setValue(False) self._wpath.setValue(False) self._wselectf2f.setValue(False) else: print("==> Returning to regular frame selection mode") self.context.visjs_frames_server.visjs_frames_noselect_cb = None self._wdumpf2f = kw.Toggle( router, text="Dump chain", on_toggle=lambda cstate: _dumpChainCB(cstate), tooltip="Dump information about the f2f between the selected frame and this frame", render_as_button=True, ) def _pathChainCB(cstate): # print("HH", self.context.visjs_frames_server) if cstate: def f2fPathAction(id): # look up frame from id other_frame = cast(kf.Frame, kc.BaseContainer.singleton().at(id)) path = self.item.frameToFrame(other_frame).getPath() # frames_path = [x.pframe() for x in path] # frames_path += [x.oframe() for x in path] _highlightGraphFrames(True, path, self.context) # need to call this to keep the selection at this node # _selectObject(self.item, self.context.selection), print( "==> Pick a frame to highlight the path between the selected and picked frames" ) self.context.visjs_frames_server.visjs_frames_noselect_cb = f2fPathAction # reset the other buttons self._wdumpf2f.setValue(False) # self._wpath.setValue(False) self._wselectf2f.setValue(False) else: print("==> Returning to regular frame selection mode") self.context.visjs_frames_server.visjs_frames_noselect_cb = None self._wpath = kw.Toggle( router, text="Show path", on_toggle=lambda cstate: _pathChainCB(cstate), tooltip="Highlight the frames path to the next selected frame", render_as_button=True, ) def _selectChainCB(cstate): # print("HH", self.context.visjs_frames_server) if cstate: def f2fSelectAction(id): # look up frame from id other_frame = cast(kf.Frame, kc.BaseContainer.singleton().at(id)) _selectObject(self.item.frameToFrame(other_frame), self.context.selection) # reset self._wselectf2f.setValue(False) self.context.visjs_frames_server.visjs_frames_noselect_cb = None print( "==> Pick a frame to switch selection to the f2f between the selected and picked frames" ) self.context.visjs_frames_server.visjs_frames_noselect_cb = f2fSelectAction # reset the other buttons self._wdumpf2f.setValue(False) self._wpath.setValue(False) # self._wselectf2f.setValue(False) else: print("==> Returning to regular frame selection mode") self.context.visjs_frames_server.visjs_frames_noselect_cb = None self._wselectf2f = kw.Toggle( router, text="Go to f2f", on_toggle=lambda cstate: _selectChainCB(cstate), tooltip="Change selection to the fraame to frame for the next picked frame", render_as_button=True, ) self._wlayout_actions = widgetArray( router, label="FrameToFrame chain actions (via pick mode)", children=[self._wdumpf2f, self._wpath, self._wselectf2f], kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wlayout_actions) self._wlayout_actions.setTooltip("Explore relationship of this frame with other frames") # -------------------- 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", ) self._wlayout_select = widgetArray( router, label="Go to related frame", children=[ self._wselect_up, self._wselect_left, self._wselect_right, self._wselect_down, ], kind="accordion", # "layout", ) self._wlayout_select.setTooltip( "Switch selection to a related frame such as the parent or a child" ) self.wroot.addChild(self._wlayout_select) 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 getSummary(self) -> str: item = self.item nd = self.context.multibody.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"Anc node={nd_name} ({nd_str}) body={nd_bd_str}" return title_md def teardown(self, _: kf.Frame, /): self.context.visjs_frames_server.visjs_frames_noselect_cb = None # reset the noselect buttons self._wdumpf2f.setValue(False) self._wpath.setValue(False) self._wselectf2f.setValue(False) def setup(self, item: kf.Frame, item_context: kw.Json, /): super().setup(item, item_context) 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 FrameToFrameCard(AbstractCard[kf.FrameToFrame]): """Card to display info about any FrameToFrame-derived object.""" @property def label(self) -> str: return "FrameToFrame" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # Setup widget topology # 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) -> list[kf.Frame]: if isinstance(f2f, kf.EdgeFrameToFrame): return [f2f.oframe(), f2f.pframe()] 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() else: raise ValueError( f"Cannot get pathFrames for {f2f.name()} of type {f2f.typeString()}" ) # change selection to the first child body (drop down, or multiple buttons) self._wpath = kw.Toggle( router, text="Frames path", on_toggle=lambda cstate: _highlightFrames2( _pathFrames(self.item), cstate, gui_context=self.context, priority_level="primary" ), tooltip="Highlight the frames path connecting the oframe to the pframe", render_as_button=True, # addBorder=True, ) # self.wroot.addChild(self._wpath) # self._wmd_visualizerates = kw.Markdown(router, text="**Visualize Rates**", in_line=True) self._wlayout_visrates = widgetArray( router, label="Rates", children=[ self._wvel_linear, self._wvel_angular, self._waccel_linear, self._waccel_angular, ], kind="layout", # "inputgroup", ) self._wlayout_viz = widgetArray( router, label="Visualization", children=[ self._wpath, self._wlayout_visrates, ], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wlayout_viz.setTooltip( "Visualization options for the frame to frame such as highlighting the connection frames path, and visualizing its relative velocitities and accelerations." ) self._wlayout_viz.setOpen(True) self.wroot.addChild(self._wlayout_viz) # -------------------- # change selection to the pframe self._wselect_pframe = kw.Button( router, text="Go to 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="Go to 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="Switch to the from/to frames", children=[ self._wselect_oframe, self._wselect_pframe, ], kind="accordion", # "inputgroup", ) self._wlayout_select.setTooltip( "Switch selection to the oframe or pframe for this frame to frame" ) self.wroot.addChild(self._wlayout_select) def setup(self, item: kf.FrameToFrame, item_context: kw.Json, /): super().setup(item, item_context) has_path = isinstance(item, kf.OrientedChainedFrameToFrame) or isinstance( item, kf.ChainedFrameToFrame ) self._wpath.setVisible(has_path) def _firstChildBody(st: kd.SubTree, bd: kd.BodyBase) -> 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.BodyBase) -> 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.BodyBase, forward: bool) -> kd.BodyBase | None: """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 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: raise ValueError(f"Not sure what to do with item {item}.") if not shindex < hge.nSubhinges(): raise ValueError( f"The {shindex} subhinge index should be less than {hge.nSubhinges()} - the number of subhinges for the {hge.name()} hinge" ) sh = hge.subhinge(shindex) if not cindex < sh.nQ(): raise ValueError( f"The {cindex} coordinate index should be less than {sh.nQ()} - the number of coordinates for the {sh.name()} subhinge" ) 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: GuiContext, use_id_labels: bool = False): """Create a new tab with visjs layout for the subtree.""" server = gui_context.setup_visjs(st, use_id_labels) # 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_iframe, "within") if isinstance(st, kd.SubGraph): if len(st.enabledConstraints()) > 0: server.enableSubGraph("constraints") def _createFramesVisJs(use_id_labels: bool, gui_context: GuiContext): """Create a new tab with visjs layout for the frames.""" server = gui_context.setup_frames_visjs(gui_context.multibody.frameContainer(), use_id_labels) # gui_context.effects.registerGraphServer(server) gui_context.visjs_frames_server = server iframe = kw.IFrame(gui_context.router, server.getUrl()) nm = "frames" if use_id_labels else "frames_ids" gui_context.dock.addChild(nm, iframe, gui_context.visjs_iframe, "within") def _highlightBodyConstraints(body: kd.PhysicalBody, gui_context: GuiContext): """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: if sn := constraint.sourceNode(): src_body = sn.parentBody() else: raise ValueError(f"Cannot get source node from constraint {constraint.name()}") if tn := constraint.targetNode(): tgt_body = tn.parentBody() else: raise ValueError(f"Cannot get target node from constraint {constraint.name()}") params = GraphEdgeParams( color="#00ff00", dashed=True, id=constraint.id(), arrows=True, title=f"'{constraint.name()}' ({constraint.typeString()}) constraint between\n the '{src_body.name()}/{tgt_body.name()}'\n bodies", ) effect = EffectItem(obj=(src_body, tgt_body), params=params) effects.append(effect) gui_context.effects.graph_edge_adder.toggle(effects) def recreateStickParts(st: kd.SubTree, gui_context: GuiContext): st.removeStickParts() st.multibody().createStickParts() _toggleVisibleBodies( True, st.sortedPhysicalBodiesList(), gui_context, layers=ks.LAYER_STICK_FIGURE ) 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: GuiContext): """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.HingeBase | None], disable_ik: bool, gui_context: GuiContext ): """Articulate bodies in WebScene sequentially.""" for hinge in hinges: # Loop through and articulate each coordinate associated with a FramePairHinge 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) def _highlightFrames( cstate: bool, frames: Sequence[kf.Frame], # toggle_over_set: bool, gui_context: GuiContext, ): """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 _highlightFrames2( frames: Sequence[kf.Frame], cstate: bool, gui_context: GuiContext, priority_level: str ): """Highlight frame axes.""" axes = [] scale = 0 if cstate: if priority_level == "primary": scale = 1 elif priority_level == "secondary": scale = 0.75 elif priority_level == "tertiary": scale = 0.5 params = AxesParams(scale=scale) for f in frames: axes.append(EffectItem(obj=f, params=params)) gui_context.effects.frame_axes.ensure(axes) _highlightGraphFrames(True, frames, gui_context) def _getPhysicalBodies(bdlist: Sequence[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 _subTreeSim( st: kd.SubTree, integ_type: int, gravity_accel: NDArray[np.float64] | None, contact_force: kcoll.ContactForceBase | None, gui_context: GuiContext, ): # create an SP and put it in kinematics mode sp = kd.StatePropagator.create( st, ki.IntegratorType(integ_type), None, None, kd.MMSolverType.FORWARD_DYNAMICS ) # freeze coord # st.setU(init_U) if isinstance(st, kd.SubGraph) and st.enabledConstraints(): st.cks().solveU() ti = 0 sp.setTime(ti) x = sp.assembleState() sp.setState(x) # add a limit of hop size to force smooth visualization updates sp.setMaxHopSize(0.01) kmdl.UpdateProxyScene.create("scene", sp, gui_context.scene) kmdl.TimeDisplay.create("time_display", sp, gui_context.scene.graphics()) kmdl.SyncRealTime.create("sync_real", sp, 1.0) if gravity_accel is not None: ug = kmdl.Gravity("grav_model", sp, kmdl.UniformGravity("uniform_gravity"), st) ug.getGravityInterface().setGravity( np.array([0, 0, -3.73]), 0.0, kmdl.OutputUpdateType.PRE_HOP ) if contact_force is not None: col_scene = ks.CoalScene("collision_scene") fcoll = kcoll.FrameCollider(gui_context.scene, col_scene) gui_context.scene.registerClientScene( col_scene, gui_context.multibody.virtualRoot(), layers=ks.LAYER_COLLISION ) for bd1 in st.sortedPhysicalBodiesList(): bd2 = bd1.onode().parentBody() # ignore all inter-body collisions fcoll.ignoreFramePair(bd1, bd2) fcoll.ignoreAllCurrentlyTouchingPairs() kmdl.PenaltyContact( "penalty_contact", sp, st, [fcoll], contact_force, ) """ # 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) """ return sp # ------------------------------------ # mbody level kinematics sim callback def _mbodyKinematicsSim( sh: kd.SubhingeBase, cindex: int, deltaq: float, duration: float, gui_context: GuiContext ): # 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) # assert kc.allReady() if not kc.allReady(): print("WARNING: kc.allReady() is failing - call mb.resetData()") return # 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: SupportsFloat | SupportsIndex | float, _: ArrayLike): 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 kmdl.UpdateProxyScene.create("scene", sp, gui_context.scene) kmdl.TimeDisplay.create("time_display", sp, gui_context.scene.graphics()) kmdl.SyncRealTime.create("sync_real", sp, 1.0) # sp.registerModel(update_scene) # run a loop to advance time by # sp.advanceBy(duration) for _ 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: GuiContext, ): # shindex = self._coord_move_indices[0] # # get the selected sindex/cindex, and the Q values # if shindex is None: # return # assert kc.allReady() if not kc.allReady(): print("WARNING: kc.allReady() is failing - call mb.resetData()") 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) kmdl.TimeDisplay.create("time_display", sp, gui_context.scene.graphics()) kmdl.SyncRealTime.create("sync_real", sp, 1.0) # defined pre deriv CB for setting values def getNewCoord(t: SupportsFloat | SupportsIndex | float, _: ArrayLike): 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 kmdl.UpdateProxyScene.create("scene", sp, gui_context.scene) # run a loop to advance time by # sp.advanceBy(duration) for _ in range(10): print(f"Advancing to {sp.getTime()} ...") sp.advanceBy(duration / 10) # unfreeze coord # sg.cks().unfreezeCoord(sh, cindex) def _highlightGraphFrames( cstate: bool, frames: Sequence[kf.Frame], gui_context: GuiContext, ): """Highlight frames in visjs and outline in WebScene.""" if cstate: # print("GGGG", [x.name() for x in frames]) gui_context.effects.graph_highlighter.set( [EffectItem(obj=cast(kc.Base, frame), params=None) for frame in frames] ) else: gui_context.effects.graph_highlighter.set([]) def _highlightBodies( cstate: bool, bodies: Sequence[kd.BodyBase], # primary secondary_bodies: list[kd.BodyBase], tertiary_bodies: list[kd.BodyBase], # st: kd.SubTree, # toggle_over_set: bool, gui_context: GuiContext, ): """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. if isinstance(bodies, list): all_bodies = bodies + secondary_bodies + tertiary_bodies else: all_bodies = [x for x in bodies] + secondary_bodies + tertiary_bodies if False: # 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=cast(kc.Base, 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.ScenePart): """Toggle wireframe mode for a scene part.""" # TBD AG - add wireframe support assert sp pass def _transparent(sp: ks.ScenePart): """Toggle transparent mode for a scene part.""" # TBD AG - add transparency support assert sp pass def _toggleVisibleBodies(cstate: bool, bodies: list[kd.PhysicalBody], context: GuiContext, layers): """Toggle visibility of bodies' scene parts in WebScene.""" if not cstate: # hide the body's scene parts in the specified layer context.effects.body_part_hider.ensure( EffectItem(obj=(body, layers), params=None) for body in bodies ) else: # show the body's scene parts in the specified layer context.effects.body_part_hider.ensureRemoved((body, layers) for body in bodies) 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"Switching to '{obj.name()}' ({obj.typeString()}/{obj.id()})") selection.set(kw.Selection([kw.Selection.Item(obj.id(), context=st_context)]).dump()) @register class BodyBaseCard(AbstractCard[kd.BodyBase]): """Card to display info about any BodyBase-derived object.""" @property def label(self) -> str: return "BodyBase" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ # self._wnd_header = kw.Markdown(router, "---") # self.wroot.addChild(self._wnd_header) self._wbb_header1 = kw.Markdown(router, "**BodyBase**") self.wroot.addChild(self._wbb_header1) # ---------------------------- # highlight parent body, children body (different highlighting) # pbd = cast(kd.PhysicalBody, self.item) self._wbb_parentchildren = 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._wbb_downstream = kw.Toggle( router, text="Downstream", 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._wbb_upstream = kw.Toggle( router, text="Upstream", 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._wbb_md_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wbb_layout_highlight = widgetArray( router, label="**Highlight**", children=[self._wbb_parentchildren, self._wbb_upstream, self._wbb_downstream], kind="layout", # "inputgroup", ) # self.wroot.addChild(self._wbb_layout_highlight) # ---------------------------- # articulate parent self._wbb_swing_parent = kw.Button( router, text="Parent", on_press=lambda: _swingHinge( self.subtree, [self.item.physicalParentBody().parentHinge()], True, self.context ), tooltip="Auto articulate the parent body", ) # articulate childrent sequentially self._wbb_swing_children = kw.Button( router, text="Children", on_press=lambda: _swingHinge( self.subtree, [x.parentHinge() for x in self.subtree.childrenBodies(self.item)], True, self.context, ), tooltip="Auto articulate the children bodies sequentially", ) # articulate downstream bodies sequentially self._wbb_swing_downstream = kw.Button( router, text="Downstream", on_press=lambda: _swingHinge( self.subtree, [x.parentHinge() for x in _downstreamBodies(self.subtree, self.item)], False, self.context, ), tooltip="Auto articulate the downstream bodies sequentially", ) # self._wbb_md_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wbb_layout_articulate = widgetArray( router, label="**Articulate**", children=[self._wbb_swing_children, self._wbb_swing_parent, self._wbb_swing_downstream], kind="layout", ) # self.wroot.addChild(self._wbb_layout_articulate) self._wbb_layout_nbhd = widgetArray( router, label="Explore neighborhood bodies", children=[self._wbb_layout_highlight, self._wbb_layout_articulate], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wbb_layout_nbhd.setTooltip( "Highlight and articulate neighboring bodies to explore structure" ) self._wbb_layout_nbhd.setOpen(True) self.wroot.addChild(self._wbb_layout_nbhd) # ---------------------------- # change selection to the first child body (drop down, or multiple buttons) self._wbb_select_down = kw.Button( router, text="Down", on_press=lambda: _selectObject( cast(kc.Base | None, _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._wbb_select_up = kw.Button( router, text="Up", on_press=lambda: _selectObject( cast(kc.Base | None, _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._wbb_select_right = kw.Button( router, text="Right", on_press=lambda: _selectObject( cast(kc.Base | None, _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._wbb_select_left = kw.Button( router, text="Left", on_press=lambda: _selectObject( cast(kc.Base | None, _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._wbb_md_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wbb_layout_select = widgetArray( router, label="Go to other related body", children=[ self._wbb_select_up, self._wbb_select_left, self._wbb_select_right, self._wbb_select_down, ], kind="accordion", # "layout", ) self._wbb_layout_select.setTooltip( "Switch selection to a related body such as the parent, a child or a sibling body" ) self.wroot.addChild(self._wbb_layout_select) def getSummary(self) -> str: result = super().getSummary() item = self.item if item.isRootBody(): result += f""" Subtree: {self.subtree.name()}<br/> Is the root body """ else: hge_type = kd.HingeBase.hingeTypeString(item.parentHinge().hingeType()) result += f""" Subtree: '{self.subtree.name()}', parent body: '{self.subtree.parentBody(item).name()}'</br> hinge: {hge_type} """ return result def setup(self, item: kd.BodyBase, item_context: kw.Json, /): # need to set this for 'subtree' to work 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." ) super().setup(item, item_context) not_root_body = not item.isRootBody() not_basebody = not_root_body and not self.subtree.isBaseBody(item) has_children = not_root_body and len(self.subtree.childrenBodies(item)) > 0 if not_root_body: has_siblings = ( not_root_body and len(self.subtree.childrenBodies(self.subtree.parentBody(item))) > 1 ) else: has_siblings = False self._wbb_upstream.setVisible(not_basebody) self._wbb_swing_parent.setVisible(not_basebody) self._wbb_select_up.setVisible(not_basebody) self._wbb_downstream.setVisible(has_children) self._wbb_swing_downstream.setVisible(has_children) self._wbb_swing_children.setVisible(has_children) self._wbb_select_down.setVisible(has_children) self._wbb_select_right.setVisible(has_siblings) self._wbb_select_left.setVisible(has_siblings) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" if isinstance(item, kd.PhysicalBody) or isinstance(item, kd.CompoundBody): return False return isinstance(item, self.wrapped()) @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 PhysicalBodyCard(BodyBaseCard): # AbstractCard[kd.PhysicalBody]): """Card to display info about any PhysicalBody-derived object.""" @property def label(self) -> str: return "Body" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wheader = kw.Markdown(router, "---") self.wroot.addChild(self._wheader) self._wheader1 = kw.Markdown(router, "**PhysicalBody**") self.wroot.addChild(self._wheader1) # --------------------------------- # add articulation widgets self._frame_pair_widgets = FramePairHingeWidgets( self.context, group_tag=f"{self.wroot.domId()}-change_coord-group" ) self.wroot.addChild(self._frame_pair_widgets._wik_layout) self._frame_pair_widgets._wik_layout.setOpen(True) # --------------------------------- # show/hide graphics mesh scene parts (only webscene) self._wpbd_mesh = 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._wpbd_collision = 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 if not kc.allReady(): print("WARNING: kc.allReady() is failing - call mb.resetData()") return 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._wpbd_stick = 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._wpbd_wireframe = 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._wpbd_transparent = 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._wpbd_layout_geom_select = widgetArray( router, label="Geometry", children=[ self._wpbd_mesh, self._wpbd_collision, self._wpbd_stick, self._wpbd_wireframe, self._wpbd_transparent, ], kind="inputgroup", ) # --------------------------------- # for constraints involving the body show line between this body and the other bodies self._wpbd_constraints = 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.wroot.addChild(self._wpbd_constraints) self._wpbd_layout_geom = widgetArray( router, label="Visualize geomatry and structure", children=[self._wpbd_layout_geom_select, self._wpbd_constraints], alignment="column", alignItems="left", kind="accordion", # "inputgroup", ) self._wpbd_layout_geom.setTooltip( "Options to use highlighting to visualize system structure" ) # self.wroot.addChild(self._wpbd_layout_geom) # --------------------------------- # --------------------------------- self._wpbd_bdframe = kw.Toggle( router, text="Body", on_toggle=lambda cstate: _highlightFrames2( [self.item], cstate, gui_context=self.context, priority_level="primary" ), render_as_button=True, tooltip="Toggle the body frame axes", ) self._wpbd_pnodeframe = kw.Toggle( router, text="Pnode", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( [self.item.pnode()], cstate, gui_context=self.context, priority_level="primary" ), render_as_button=True, tooltip="Toggle the body pnode frame axes", ) self._wpbd_ponodeframe = kw.Toggle( router, text="Parent onode", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( [ self.item.onode() if (not self.item.isCompoundBody()) else cast(kd.CompoundBody, self.item).physicalParentBody() ], cstate, gui_context=self.context, priority_level="secondary", ), render_as_button=True, tooltip="Toggle the frame axes for the parent onode", ) self._wpbd_conodeframes = 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, priority_level="secondary", ), render_as_button=True, tooltip="Toggle the frame axes for all the children body onodes", ) self._wpbd_subhingeframes = 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, priority_level="tertiary", ), render_as_button=True, tooltip="Toggle the frame axes for all subhinge oframes and pframes for the body", ) self._wpbd_nodeframes = kw.Toggle( router, text="Nodes", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( self.item.nodeList(), cstate, gui_context=self.context, priority_level="tertiary" ), render_as_button=True, tooltip="Toggle the frame axes for all the regular nodes on the body", ) self._wpbd_cnodeframes = kw.Toggle( router, text="Constraint nodes", # on_press=lambda: _highlightFrames( on_toggle=lambda cstate: _highlightFrames2( self.item.constraintNodeList(), cstate, gui_context=self.context, priority_level="tertiary", ), render_as_button=True, tooltip="Toggle the frame axes for all the constraint nodes on the body", ) # self._wpbd_md_frames = kw.Markdown(router, text="**Frames**", in_line=True) self._wpbd_layout_frames = widgetArray( router, label="View body related frames", children=[ self._wpbd_bdframe, self._wpbd_ponodeframe, self._wpbd_pnodeframe, self._wpbd_conodeframes, self._wpbd_subhingeframes, self._wpbd_nodeframes, self._wpbd_cnodeframes, ], kind="accordion", # "inputgroup", ) # self.wroot.addChild(self._wpbd_layout_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 # 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._wpbd_interbody_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._wpbd_interbody_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._wpbd_external_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._wpbd_external_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._wpbd_md_forces = kw.Markdown(router, text="**Forces**", in_line=True) self._wpbd_layout_forces = widgetArray( router, label="Select body force/moment type", children=[ self._wpbd_interbody_force, self._wpbd_interbody_moment, self._wpbd_external_force, self._wpbd_external_moment, ], alignment="row", alignItems="left", kind="inputgroup", ) # self.wroot.addChild(self._wpbd_layout_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._wpbd_frc_scale = kw.Slider( router, "Viz Force Scale", _scaleForcesViz, slider_opts, ) self._wpbd_frc_scale.setValue(0.05) # self.wroot.addChild(self._wpbd_frc_scale) self._wpbd_layout_forces2 = widgetArray( router, label="Visualize forces", children=[self._wpbd_layout_forces, self._wpbd_frc_scale], alignment="column", alignItems="left", kind="accordion", # "inputgroup", ) # self.wroot.addChild(self._wpbd_layout_forces2) # --------------------------------- self._wpbd_layout_viz = widgetArray( router, label="Body related visualization", children=[ self._wpbd_layout_geom, # meshes and stick parts self._wpbd_layout_frames, # body related frames self._wpbd_layout_forces2, ], # body interaction forces alignment="column", alignItems="left", kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wpbd_layout_viz) # --------------------------------- def _tocutjointCB(): self.item.toCutJointConstraint() self.context.multibody.ensureHealthy() _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) self.context.mbody_tree_view.refresh() self._wpbd_tocutjoint = kw.Button( router, text="To cutjoint", on_press=lambda: _tocutjointCB(), tooltip="Convert the hinge into a cut-joint constraint", ) def _detachCB(): self.item.detach() self.context.multibody.ensureHealthy() _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) self.context.mbody_tree_view.refresh() if len(self.context.scene.getSceneParts(ks.LAYER_STICK_FIGURE)) > 0: recreateStickParts(self.context.multibody, self.context) self._wpbd_detach = kw.Button( router, text="Detach body", on_press=lambda: _detachCB(), tooltip="Detach the body from its current parent", ) def _makeBaseCB(): self.item.makeIntoBaseBody() self.context.multibody.ensureHealthy() _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) self.context.mbody_tree_view.refresh() if len(self.context.scene.getSceneParts(ks.LAYER_STICK_FIGURE)) > 0: recreateStickParts(self.context.multibody, self.context) self._wpbd_makebase = kw.Button( router, text="Make base body", on_press=lambda: _makeBaseCB(), tooltip="Convert this body into a base body", ) def _discardCB(): self.context.selection.set(kw.Selection().dump()) kc.discard(self.item) self.context.multibody.ensureHealthy() _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) self.context.mbody_tree_view.refresh() if len(self.context.scene.getSceneParts(ks.LAYER_STICK_FIGURE)) > 0: recreateStickParts(self.context.multibody, self.context) self._wpbd_discard = kw.Button( router, text="Discard body", on_press=lambda: _discardCB(), tooltip="Discard this body", ) self._wpbd_layout_constraints = widgetArray( router, label="Modify body's parent hinge", # self._wpbd_md_constraints, children=[ # self._wpbd_constraints, self._wpbd_tocutjoint, self._wpbd_detach, self._wpbd_discard, ], kind="accordion", # "inputgroup", ) # self.wroot.addChild(self._wpbd_layout_constraints) # ------------------------- self._node_sps_name = "dummy" def _setNodeSpNameCB(val): self._node_sps_name = val self._wpbd_ndname = kw.StringInput( router, "Name", on_change=lambda val: _setNodeSpNameCB(val), rapid_submit=True, ) self._wpbd_ndname.setTooltip("The name for the new node/scene part spec") self._wpbd_ndname.setSizeClass(kw.SizeClass.WIDE) self._wpbd_ndname.setValue(self._node_sps_name) # Add a node def _addNodeCB(): # nd = kd.Node.lookupOrCreate(self._node_sps_name, self.item) nd.setBodyToNodeTransform(km.HomTran()) _createFramesVisJs(use_id_labels=False, gui_context=self.context) _selectObject(nd, self.context.selection, {"subtree_id": self.subtree.id()}) self._wpbd_addnode = kw.Button( router, text="Add node", on_press=lambda: _addNodeCB(), tooltip="Add a new node to the body", ) geom_types = ["Box", "Capsule", "Cone", "Cylinder", "RoundFrustum", "Sphere", "File"] self.geom_selection = 1 self._sfo_filepath = "" # callback for the 3D pick mode menu def _geomSelectionCB(val): self.geom_selection = val self._wpbd_geomtype = kw.Dropdown( router, "Geometry", geom_types, lambda val: _geomSelectionCB(val), ) self._wpbd_geomtype.setIndex(self.geom_selection) # Add a node def _addScenePartSpecCB(): # get geometry type gtype = geom_types[self.geom_selection] # create a scene part spec if gtype != "File": spec = ks.ScenePartSpec() if gtype == "Box": spec.geometry = ks.BoxGeometry(1, 1, 1) elif gtype == "Capsule": spec.geometry = ks.CapsuleGeometry(1, 1) elif gtype == "Cone": spec.geometry = ks.ConeGeometry(1, 1) elif gtype == "Cylinder": spec.geometry = ks.CylinderGeometry(1, 1) elif gtype == "RoundFrustum": spec.geometry = ks.RoundFrustumGeometry(1, 1, 1) elif gtype == "Sphere": spec.geometry = ks.SphereGeometry(1) elif gtype == "File": spec.geometry = ks.SphereGeometry(0.1) else: assert 0 spec.material = ks.defaultMaterial() spec.name = self._node_sps_name spec.scale = np.ones(3) * 0.1 spec.transform = km.HomTran() spec.layers = ks.LAYER_PHYSICAL # add it to the body self.item.addScenePartSpec(spec) sp = self.item.getScenePart(spec.name) _selectObject(sp, self.context.selection, None) else: spec = ks.SceneFileObjectSpec() spec.filepath = Path(self._sfo_filepath) if not (spec.filepath.is_file() and spec.filepath.exists()): raise ValueError(f"The {self._sfo_filepath} file does not exist") spec.name = self._node_sps_name spec.scale = 1 spec.transform = km.HomTran() spec.layers = ks.LAYER_PHYSICAL # spec.filepath = self._sfo_filepath self.item.addSceneFileObjectSpec(spec) sfo = self.item.getSceneFileObject(spec.name) _selectObject(sfo, self.context.selection, None) self._wpbd_addsps = kw.Button( router, text="Add scene part", on_press=lambda: _addScenePartSpecCB(), tooltip="Add a new scene part spec to the body", ) self._sfo_filepath = "" def _setSfoFilePathCB(val): self._sfo_filepath = val self._wpbd_sfofile = kw.StringInput( router, "SFO Fliename", on_change=lambda val: _setSfoFilePathCB(val), rapid_submit=True, ) self._wpbd_sfofile.setTooltip("The file path for the new scene file object") self._wpbd_sfofile.setSizeClass(kw.SizeClass.WIDE) self._wpbd_sfofile.setValue("") self._wpbd_layout_nodesps = widgetArray( router, label="Add node, scene part", # self._wpbd_md_constraints, children=[ self._wpbd_addnode, self._wpbd_ndname, self._wpbd_addsps, self._wpbd_geomtype, self._wpbd_sfofile, ], kind="accordion", # "inputgroup", ) # self.wroot.addChild(self._wpbd_layout_nodesps) # ------------------------- self.hinge_selection: int = 0 hinge_types = ["REVOLUTE", "SLIDER", "BALL", "UJOINT", "GIMBAL", "CYLINDRICAL", "HELICAL"] hinge_types_enum = [ kd.HingeType.REVOLUTE, kd.HingeType.SLIDER, kd.HingeType.BALL, kd.HingeType.UJOINT, kd.HingeType.GIMBAL, kd.HingeType.CYLINDRICAL, kd.HingeType.HELICAL, ] def _attachChainCB(cstate): # print("HH", self.context.visjs_frames_server) vserver = self.context.visjs_servers[self.context.multibody.id()][0] # print("KKKK", cstate, vserver.visjs_noselect_cb) if cstate: # TODO - Highlight the 6dof bodies available for attachment def attachAction(id): # look up body from id obj = kc.BaseContainer.singleton().at(id) if not isinstance(obj, kd.PhysicalBody): raise ValueError( f"Only a physical body can be attached to another. '{obj.name()}' ({obj.typeString()}) is not a valid choice." ) obd = cast(kd.PhysicalBody, obj) assert obj.parentHinge().hingeType() == kd.HingeType.FULL6DOF if self.item.id() == obj.physicalParentBody().id(): if not self.item.isRootBody(): kc.warn( f"The '{obj.name()}' is already attached to the '{self.item.name()}' body. Nothing to do!" ) return # get the desired hinge type from the dropdown htype = hinge_types_enum[self.hinge_selection] # attach the body print( f"Attaching '{obd.name()}' to '{self.item.name()}' via '{hinge_types[self.hinge_selection]}' hinge" ) obd.reattach(self.item, htype) nhge = obd.parentHinge() if nhge.coordData().nU() == 1: cast(kd.Physical1DofSubhinge, nhge.subhinge(0)).setUnitAxis([1, 0, 0]) nhge.coordData().setQ(0) nhge.coordData().setU(0) nhge.coordData().setUdot(0) nhge.coordData().setT(0) obd.setBodyToJointTransform(km.HomTran()) if not self.item.isRootBody(): obd.onode().setBodyToNodeTransform(km.HomTran()) self.context.multibody.ensureHealthy() # update stick parts _createSubTreeVisJs(self.context.multibody, self.context) # set the noselect callback on the graph server as # well - TODO: do we need this? self.context.visjs_servers[self.context.multibody.id()][ 0 ].visjs_noselect_cb = attachAction # update highlighting to stay on the currently selected body _highlightBodies(True, [self.item], [], [], self.context) if len(self.context.scene.getSceneParts(ks.LAYER_STICK_FIGURE)) > 0: recreateStickParts(self.context.multibody, self.context) # TODO - not sure why this is needed, but need it to # get the new graph's highlighting updated # self.context.effects.graph_highlighter._syncGraphs() # _selectObject(self.item, self.context.selection) print("==> Pick a 6dof body to attach it to the selected body") vserver.visjs_noselect_cb = attachAction # reset the other no select button # self._wpbd_attachbd.setValue(False) # self._wpbd_add_cutjoint.setValue(False) else: # TODO - Disable the highlighting of the 6dof bodies available for attachment print("==> Returning to regular body selection mode") vserver.visjs_noselect_cb = None self._wpbd_attachbd = kw.Toggle( router, text="Attach 6dof body", on_toggle=lambda cstate: _attachChainCB(cstate), tooltip="Attach the other 6 dof body to this body via the specified hinge type", render_as_button=True, ) # callback for the 3D pick mode menu def _hingeSelectionCB(val): self.hinge_selection = val # hinge_types[val] self._wpbd_hinge_type = kw.Dropdown( router, "Hinge type", hinge_types, lambda val: _hingeSelectionCB(val), ) self._wpbd_hinge_type.setIndex(0) def _cutjointCB(cstate): # print("HH", self.context.visjs_frames_server) vserver = self.context.visjs_servers[self.context.multibody.id()][0] if cstate: def cutjointAction(id): # look up body from id obj = kc.BaseContainer.singleton().at(id) assert isinstance(obj, kd.PhysicalBody) if not isinstance(obj, kd.PhysicalBody): raise ValueError( f"Can create cut-joint loop constraints only between physical bodies. '{obj.name()}' ({obj.typeString()}) is not a valid choice." ) obd = cast(kd.PhysicalBody, obj) # assert obj.parentHinge().hingeType() == kd.HingeType.FULL6DOF # get the desired hinge type from the dropdown htype = hinge_types_enum[self.hinge_selection] # attach the body prefix = f"{self.item.name()}_{obd.name()}" print( f"Creating cut-joint loop constraint between '{obd.name()}' and '{self.item.name()}' with '{hinge_types[self.hinge_selection]}' hinge" ) if self.item.isRootBody(): cnd1 = kf.Frame.create(prefix + "cfrm1", self.item.container()) f2f = kf.PrescribedFrameToFrame.create(self.item, cnd1) else: cnd1 = kd.ConstraintNode.lookupOrCreate(prefix + "cnd1", self.item) if obd.isRootBody(): cnd2 = kf.Frame.create(prefix + "cfrm2", self.item.container()) f2f = kf.PrescribedFrameToFrame.create(obd, cnd2) else: cnd2 = kd.ConstraintNode.lookupOrCreate(prefix + "cnd2", obd) cj = kd.LoopConstraintCutJoint.create( prefix + "_cutjt", self.context.multibody, cnd1.frameToFrame(cnd2), htype ) nhge = cj.hinge() if nhge.coordData().nU() == 1: cast(kd.Physical1DofSubhinge, nhge.subhinge(0)).setUnitAxis([1, 0, 0]) nhge.coordData().setQ(0) nhge.coordData().setU(0) nhge.coordData().setUdot(0) nhge.coordData().setT(0) self.context.multibody.ensureHealthy() # update stick parts _createSubTreeVisJs(self.context.multibody, self.context) # change selection to the new loop constraint _selectObject(cj, self.context.selection) print( "==> Pick a body to create a cut-joint loop constraint between the selected body and the picked body" ) vserver.visjs_noselect_cb = cutjointAction # reset the other no select button self._wpbd_attachbd.setValue(False) # self._wpbd_add_cutjoint.setValue(False) else: print("==> Returning to regular body selection mode") vserver.visjs_noselect_cb = None self._wpbd_add_cutjoint = kw.Toggle( router, text="Add cut-joint constraint", on_toggle=lambda cstate: _cutjointCB(cstate), tooltip="Create a cut-joint loop constraint with the specified hinge type between this and the other body", render_as_button=True, ) self._wpbd_layout_attbd = widgetArray( router, label="Add hinges and cut-joints (via pick mode)", children=[self._wpbd_attachbd, self._wpbd_hinge_type, self._wpbd_add_cutjoint], kind="accordion", # "inputgroup", ) # self.wroot.addChild(self._wpbd_layout_attbd) # -------------------------- def _setInb2JointCB(T: km.HomTran): self.item.onode().setBodyToNodeTransform(T) self.context.scene.update() self._wpbd_ib2j = HomTranWidgets( "Inboard to joint transform", _setInb2JointCB, self.context ) # self.wroot.addChild(self._wpbd_ib2j._wlayout_homtran) # -------------------------- def _setBody2JointCB(T: km.HomTran): self.item.setBodyToJointTransform(T) self.context.scene.update() self._wpbd_b2j = HomTranWidgets("Body to joint transform", _setBody2JointCB, self.context) # self.wroot.addChild(self._wpbd_b2j._wlayout_homtran) self._wpbd_layout_transforms = widgetArray( router, label="Parent hinge location transforms", children=[self._wpbd_ib2j._wlayout_homtran, self._wpbd_b2j._wlayout_homtran], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wpbd_layout_transforms.setTooltip( "Modify the parent hinge's location transforms for the body" ) # self.wroot.addChild(self._wpbd_layout_transforms) self._wpbd_layout_structure = widgetArray( router, label="Modify body and its connectivity", children=[ self._wpbd_layout_transforms, self._wpbd_layout_constraints, # modify the parent hinge self._wpbd_layout_nodesps, # add nodes and scene parts self._wpbd_layout_attbd, # attach body, create cut-joint ], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self.wroot.addChild(self._wpbd_layout_structure) self._wpbd_layout_structure.setTooltip("Options to modify the body a0nd its connections") # --------------------------------- self._sg_branches = [] self._sg_stopats = [] self._sg_name = "dummy" def _setBranchesCB(cstate): vserver = self.context.visjs_servers[self.context.multibody.id()][0] if cstate: # self._wpbd_sgbranches.setValue(False) self._wpbd_sgstopats.setValue(False) self._sg_branches = [] def branchesAction(id): # look up body from id obj = kc.BaseContainer.singleton().at(id) assert isinstance(obj, kd.PhysicalBody) obd = cast(kd.PhysicalBody, obj) self._sg_branches.append(obd) print("==> Pick bodies to select branches to include in the subgraph") vserver.visjs_noselect_cb = branchesAction else: # TODO - Disable the highlighting of the 6dof bodies available for attachment print("==> Terminating branch selection") vserver.visjs_noselect_cb = None self._wpbd_sgbranches = kw.Toggle( router, text="Select branches", on_toggle=lambda cstate: _setBranchesCB(cstate), tooltip="Select bodies whose branches to include in the subgraph", render_as_button=True, ) def _setStopatsCB(cstate): vserver = self.context.visjs_servers[self.context.multibody.id()][0] if cstate: self._wpbd_sgbranches.setValue(False) # self._wpbd_sgstopats.setValue(False) self._sg_stopats = [] def stopatsAction(id): # look up body from id obj = kc.BaseContainer.singleton().at(id) assert isinstance(obj, kd.PhysicalBody) obd = cast(kd.PhysicalBody, obj) self._sg_stopats.append(obd) print("==> Pick 'stop at' bodies to limit the bodies included in the subgraph") vserver.visjs_noselect_cb = stopatsAction else: print("==> Terminating stop at selection") vserver.visjs_noselect_cb = None self._wpbd_sgstopats = kw.Toggle( router, text="Select stopats", on_toggle=lambda cstate: _setStopatsCB(cstate), tooltip="Select 'stop at' bodies to limit the bodies to include in the subgraph", render_as_button=True, ) def setSGName(val: str): self._sg_name = val self._wpbd_sgname = kw.StringInput( router, "Name", on_change=lambda val: setSGName(val), ) self._wpbd_sgname.setTooltip("The name for the new subgraph") self._wpbd_sgname.setSizeClass(kw.SizeClass.MEDIUM) self._wpbd_sgname.setValue(self._sg_name) # create a subgraph rooted at this body def _mksubgraphCB(): self._wpbd_sgbranches.setValue(False) self._wpbd_sgstopats.setValue(False) print("Root", self.item.name()) print("Branches", [x.name() for x in self._sg_branches]) print("Stop ats", [x.name() for x in self._sg_stopats]) if isinstance(self.subtree, kd.SubGraph): sg = kd.SubGraph.create( self._sg_name, self.subtree, self.item, self._sg_branches, self._sg_stopats ) else: sg = kd.SubGraph.create( self._sg_name, self.subtree.multibody(), self.item, self._sg_branches, self._sg_stopats, ) _highlightBodies(True, sg.sortedBodiesList(), [self.item], [], self.context) self.context.subtrees_tree_view.refresh() _createSubTreeVisJs(sg, self.context) self._sg_branches = [] self._sg_stopats = [] vserver = self.context.visjs_servers[self.context.multibody.id()][0] vserver.visjs_noselect_cb = None self._wpbd_sgbranches.setValue(False) self._wpbd_sgstopats.setValue(False) self._wpbd_mksubgraph = kw.Button( router, text="Make subgraph", on_press=lambda: _mksubgraphCB(), tooltip="Create a subgraph with this body as virtual root", ) # self._wpbd_md_constraints = kw.Markdown(router, text="**Constraints**", in_line=True) self._wpbd_layout_sg = widgetArray( router, label="Create new SubTree/SubGraph", # self._wpbd_md_constraints, children=[ # self._wpbd_constraints, self._wpbd_sgname, self._wpbd_sgbranches, self._wpbd_sgstopats, self._wpbd_mksubgraph, ], kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wpbd_layout_sg) # ---------------------------------------- self._wpbd_collision_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._wpbd_md_collision = kw.Markdown(router, text="**Collision**", in_line=True) self._wpbd_layout_collision = widgetArray( router, label="Collision", children=[self._wpbd_collision_filters], kind="inputgroup", ) # self.wroot.addChild(self._wpbd_layout_collision) # -------------------------------------------- def teardown(self, _: kd.PhysicalBody, /): self._wpbd_interbody_force.setValue(False) self._wpbd_interbody_moment.setValue(False) self._wpbd_external_force.setValue(False) self._wpbd_external_moment.setValue(False) self._wpbd_bdframe.setValue(False) self._wpbd_ponodeframe.setValue(False) self._wpbd_pnodeframe.setValue(False) self._wpbd_conodeframes.setValue(False) self._wpbd_subhingeframes.setValue(False) self._wpbd_nodeframes.setValue(False) self._wpbd_cnodeframes.setValue(False) self._wpbd_attachbd.setValue(False) self._wpbd_add_cutjoint.setValue(False) def getSummary(self) -> str: item = self.item result = super().getSummary() """ if item.isRootBody(): return "Root body" else: hge_type = kd.HingeBase.hingeTypeString(item.parentHinge().hingeType()) result = super().getSummary() return result # + f"Hinge: {hge_type}" """ return result def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" return isinstance(item, kd.PhysicalBody) def setup(self, item: kd.PhysicalBody, item_context: kw.Json, /): not_root_body = not item.isRootBody() # 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." ) super().setup(item, item_context) 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 has_scene_parts = len(item.getSceneParts()) > 0 has_stick_parts = len(self.context.scene.getSceneParts(ks.LAYER_STICK_FIGURE)) > 0 has_body_loop_constraints = len(self.context.multibody.getBodyLoopConstraints(item)) > 0 is_base_body = self.context.multibody.isBaseBody(item) is_floating_body = not_root_body and item.parentHinge().hingeType() == kd.HingeType.FULL6DOF parent_is_floating_body = ( not_root_body and (not item.physicalParentBody().isRootBody()) and item.physicalParentBody().parentHinge().hingeType() == kd.HingeType.FULL6DOF ) floating_bodies = [ x for x in self.context.multibody.sortedPhysicalBodiesList() if x.parentHinge().hingeType() == kd.HingeType.FULL6DOF ] has_6dof_bodies = len(floating_bodies) > 0 self._wpbd_ib2j._wlayout_homtran.setVisible(not_root_body and not is_base_body) self._wpbd_b2j._wlayout_homtran.setVisible(not_root_body and not is_floating_body) if not_root_body: self._wpbd_ib2j.setup(item.onode().getBodyToNodeTransform()) self._wpbd_b2j.setup(item.getBodyToJointTransform()) self._wpbd_attachbd.setEnabled(has_6dof_bodies) if not_root_body: self._frame_pair_widgets.setup( kc.CppWeakRef(item.parentHinge()), kc.CppWeakRef(self.subtree) ) else: assert self.subtree self._frame_pair_widgets.setup(None, kc.CppWeakRef(self.subtree)) self._wpbd_frc_scale.setVisible(not_root_body) self._wpbd_subhingeframes.setVisible(is_not_locked) self._wpbd_constraints.setVisible(has_body_loop_constraints) self._wpbd_tocutjoint.setVisible(not is_floating_body) self._wpbd_detach.setVisible(not is_floating_body) self._wpbd_makebase.setVisible(parent_is_floating_body) self._wpbd_layout_geom.setVisible(not_root_body) self._wpbd_mesh.setVisible(not_root_body) self._wpbd_mesh.setValue(True) self._wpbd_stick.setVisible(not_root_body) self._wpbd_stick.setValue(has_stick_parts) # self._wpbd_scale_stick.setVisible(not_root_body) # self._wpbd_md_collision.setVisible(not_root_body) self._wpbd_collision_filters.setVisible(not_root_body) self._wpbd_collision.setValue(False) # until implemented self._wpbd_wireframe.setVisible(not_root_body) self._wpbd_transparent.setVisible(not_root_body) # self._wpbd_md_forces.setVisible(not_root_body) self._wpbd_interbody_force.setVisible(not_root_body) self._wpbd_interbody_moment.setVisible(not_root_body) self._wpbd_external_force.setVisible(not_root_body) self._wpbd_external_moment.setVisible(not_root_body) self._wpbd_pnodeframe.setVisible(not_root_body) self._wpbd_subhingeframes.setVisible(not_root_body) self._wpbd_nodeframes.setVisible(not_root_body) self._wpbd_cnodeframes.setVisible(has_constraint_nodes) self._wpbd_layout_geom.setVisible(has_scene_parts) self._wpbd_mesh.setVisible(has_scene_parts) # TODO - add implementation if 1: # self._wpbd_md_collision.setVisible(False) self._wpbd_collision_filters.setVisible(False) # until implemented self._wpbd_wireframe.setVisible(False) self._wpbd_transparent.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._wpbd_bdframe.setValue(False) self._wpbd_pnodeframe.setValue(False) self._wpbd_conodeframes.setValue(False) self._wpbd_subhingeframes.setValue(False) self._wpbd_nodeframes.setValue(False) self._wpbd_cnodeframes.setValue(False) self._wpbd_interbody_force.setValue(False) self._wpbd_interbody_moment.setValue(False) self._wpbd_external_force.setValue(False) self._wpbd_external_moment.setValue(False) @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 CompoundBodyCard(BodyBaseCard): # AbstractCard[kd.CompoundBody]): """Card to display info about a CompoundBody.""" @property def label(self) -> str: return "CompoundBody" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wcbd_header = kw.Markdown(router, "---") self.wroot.addChild(self._wcbd_header) self._wcbd_header1 = kw.Markdown(router, "**CompoundBody**") self.wroot.addChild(self._wcbd_header1) # --------------------------- self._cmphge_widgets = CompoundHingeWidgets(self.context) self.wroot.addChild(self._cmphge_widgets._wik_layout) self._cmphge_widgets._wik_layout.setOpen(True) # ---------------------------- # open tab with visjs display of the bodies in the compound body self._wcbd_visjs_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._wcbd_visjs_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._wcbd_md_bodies = kw.Markdown(router, text="**Visjs graph**", in_line=True) self._wcbd_layout_visjs = widgetArray( router, label="Create Visjs Graph", children=[self._wcbd_visjs_bodies, self._wcbd_visjs_physical], # kind="inputgroup", ) # self.wroot.addChild(self._wcbd_layout_embedded) # ---------------------------- # create treeview for the subtree self._wcbd_treeview = 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._wcbd_treeview_physical = kw.Button( router, text="Physical bodies", 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._wcbd_md_treeview = kw.Markdown(router, text="**Create TreeView**", in_line=True) self._wcbd_layout_treeview = widgetArray( router, label="Create TreeView", children=[self._wcbd_treeview, self._wcbd_treeview_physical], # kind="accordion", # "inputgroup", ) self._wcbd_layout_treeview.setTooltip( "Create TreeViews for the bodes embedded within this compound body" ) self._wcbd_layout_views = widgetArray( router, label="Create views", children=[self._wcbd_layout_treeview, self._wcbd_layout_visjs], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wcbd_layout_treeview.setTooltip( "Create TreeViews and bodies graphs for the bodies embedded within this compound body" ) self.wroot.addChild(self._wcbd_layout_views) # ------------------------------------ def _flattenCB(sg): if not sg.hasCompoundBodies(): return sg.flattenCompoundBodies() # highlight bodies in aggregation graph for a constraint self._wcbd_flatten = kw.Button( router, text="Flatten compound body", on_press=lambda: _flattenCB(self.item), tooltip="Flatten the compound body to remove all nested compound bodies and replace with their physical bodies", ) # self._wcbd_md_flatten = kw.Markdown(router, text="**Flatten**", in_line=True) self._wcbd_layout_flatten = widgetArray( router, label="Transform the compound body", children=[self._wcbd_flatten], kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wcbd_layout_flatten) def getSummary(self) -> str: item = self.item hge_type = kd.HingeBase.hingeTypeString(item.parentHinge().hingeType()) result = super().getSummary() return result # + f"Hinge: {hge_type}" def setup(self, item: kd.CompoundBody, item_context: kw.Json, /): super().setup(item, item_context) # hide the physical subtree buttons if the physical bodies are # the same as the regular bodies has_compound = item.bodiesTree().hasCompoundBodies() self._wcbd_visjs_physical.setEnabled(has_compound) self._wcbd_treeview_physical.setEnabled(has_compound) self._wcbd_layout_flatten.setEnabled(has_compound) self._cmphge_widgets.setup(kc.CppWeakRef(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, ) """ def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" return isinstance(item, kd.CompoundBody) @register class ProxyScenePartCard(AbstractCard[ks.ProxyScenePart]): """Card to display info about a ScenePart.""" @property def label(self) -> str: return "ProxyScenePart" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # GRAPHICAL PART # --------------------------- # 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", ) # 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.Toggle( router, text="Show/hide part", on_toggle=lambda cstate: self.item.setVisible(cstate), tooltip="Toggle visibility of the scene part", render_as_button=True, ) def _getParentFrameList(): if isinstance(self.item, ks.ProxyScenePart): return [self.item.ancestorFrame()] else: return [] # show frame axes self._wframe = kw.Toggle( router, text="Parent frame", on_toggle=lambda cstate: _highlightFrames( cstate, _getParentFrameList(), gui_context=self.context ), tooltip="Enable the axes for the frame to which this scene part is attached", render_as_button=True, ) self._wlayout_highlight = widgetArray( router, label="Highlight", children=[ self._wvisible, self._wframe, self._wupstream, self._wdownstream, self._wwireframe, self._wtransparent, ], kind="inputgroup", ) self.wroot.addChild(self._wlayout_highlight) # --------------------------- # discard part def _discardCB(): frm = self.item.ancestorFrame() if not isinstance(frm, kd.PhysicalBody): raise ValueError( f"Only scene parts attached to physical bodies are removable this way. The parent frame '{frm.name()} ({frm.typeString()}) is not one." ) bd = cast(kd.PhysicalBody, frm) nm = self.item.name() self.context.selection.set(kw.Selection().dump()) bd.removeScenePartSpec(nm) self._wdiscard = kw.Button( router, text="Discard part", on_press=lambda: _discardCB(), tooltip="Discard the scene part", ) self.wroot.addChild(self._wdiscard) # -------------------------- if 1: def _setSimTransformCB(T: km.SimTran, scale): self.item.setSimTransform(T) self.item.setIntrinsicScale(scale) self.context.scene.update() self._wpart_trans = SimTranWidgets( "Transform and scaling", _setSimTransformCB, self.context ) self.wroot.addChild(self._wpart_trans._wlayout_simtran) def setup(self, item: ks.ProxyScenePart, item_context: kw.Json, /): super().setup(item, item_context) # self._wpart_trans._wlayout_scale.setVisible(False) self._wvisible.setValue(item.getVisible()) if 1: self._wpart_trans.setup(item.getSimTransform(), item.getIntrinsicScale()) if 1: # TODO - until implemented self._wupstream.setVisible(False) self._wdownstream.setVisible(False) self._wwireframe.setVisible(False) self._wtransparent.setVisible(False) # self._wvisible.setValue(True, trigger_own_callback=True) # self._wvisible.setValue(True) @register class ProxySceneFileObjectCard(AbstractCard[ks.ProxySceneFileObject]): """Card to display info about a SceneFileObject.""" @property def label(self) -> str: return "ProxySceneFileObject" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # GRAPHICAL PART # --------------------------- # SCENE NODE # Toggle scene part visibility (only webscene) self._wvisible = kw.Toggle( router, text="Show/hide part", on_toggle=lambda cstate: self.item.setVisible(cstate), tooltip="Toggle visibility of the scene file object", render_as_button=True, ) def _getParentFrameList(): if isinstance(self.item, ks.ProxySceneFileObject): return [self.item.ancestorFrame()] else: return [] # show frame axes self._wframe = kw.Toggle( router, text="Parent frame", on_toggle=lambda cstate: _highlightFrames( cstate, _getParentFrameList(), gui_context=self.context ), tooltip="Enable the axes for the frame to which this scene file object is attached", render_as_button=True, ) self._wlayout_highlight = widgetArray( router, label="Highlight", children=[ self._wvisible, self._wframe, ], kind="inputgroup", ) self.wroot.addChild(self._wlayout_highlight) # --------------------------- # discard part def _discardCB(): frm = self.item.ancestorFrame() if not isinstance(frm, kd.PhysicalBody): raise ValueError( f"Only scene file objects attached to physical bodies are removable this way. The parent frame '{frm.name()} ({frm.typeString()}) is not one." ) bd = cast(kd.PhysicalBody, frm) nm = self.item.name() self.context.selection.set(kw.Selection().dump()) bd.removeSceneFileObjectSpec(nm) self._wdiscard = kw.Button( router, text="Discard SFO", on_press=lambda: _discardCB(), tooltip="Discard the scene file object", ) self.wroot.addChild(self._wdiscard) # -------------------------- if 1: def _setSimTransformCB(T: km.SimTran, scale): self.item.setSimTransform(T) self.context.scene.update() self._wpart_trans = SimTranWidgets( "Transform and scaling", _setSimTransformCB, self.context ) self.wroot.addChild(self._wpart_trans._wlayout_simtran) def setup(self, item: ks.ProxySceneFileObject, item_context: kw.Json, /): super().setup(item, item_context) self._wpart_trans._wlayout_scale.setVisible(False) self._wvisible.setValue(item.getVisible()) @register class NodeCard(AbstractCard[kd.Node]): """Card to display info about any Node object.""" @property def label(self) -> str: return "Node" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ # self._wnd_header = kw.Markdown(router, "---") # self.wroot.addChild(self._wnd_header) self._wnd_header1 = kw.Markdown(router, "**Node**") self.wroot.addChild(self._wnd_header1) # ----------------- # highlight parent body self._wnd_body = kw.Toggle( router, text="Highlight parent body", on_toggle=lambda cstate: _highlightBodies( cstate, [self.item.parentBody()], [], [], gui_context=self.context ), tooltip="Toggle the highlighting of the node's parent body", render_as_button=True, ) # ------------------------ 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._wnd_force_vec = kw.Button( router, text="Force vector", # 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._wnd_moment_vec = kw.Button( router, text="Moment vector", on_press=lambda: _toggleForcesViz( self.item, 1, ks.Color.GREEN, self._af_cbs, ), tooltip="Toggle the visualization of the external moment on the node", ) # self._wnd_md_forces = kw.Markdown(router, text="Forces**", in_line=True) self._wnd_layout_forces = widgetArray( router, label="**Node's external spatial force**", children=[self._wnd_force_vec, self._wnd_moment_vec], kind="layout", # "inputgroup", ) # self.wroot.addChild(self._wnd_layout_forces) self._wnd_layout_viz = widgetArray( router, label="Introspection", children=[self._wnd_body, self._wnd_layout_forces], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wnd_layout_viz.setTooltip( "Highlight the parent body, and visualize forces at the node" ) self._wnd_layout_viz.setOpen(True) self.wroot.addChild(self._wnd_layout_viz) # -------------------------- def _setBodyToNodeCB(T: km.HomTran): # T.dump('TT') self.item.setBodyToNodeTransform(T) self.context.scene.update() self._wnd_bd2node = HomTranWidgets("Body to node transform", _setBodyToNodeCB, self.context) self.wroot.addChild(self._wnd_bd2node._wlayout_homtran) # --------------------------- # discard part def _discardCB(): bd = self.item.parentBody() _selectObject(bd, self.context.selection, None) kc.discard(self.item) _createFramesVisJs(False, self.context) self._wnd_discard = kw.Button( router, text="Discard", on_press=lambda: _discardCB(), tooltip="Discard the node", ) # self.wroot.addChild(self._wnd_detach) self._wnd_layout_discard = widgetArray( router, label="Discard the node", children=[ # self._wnd_body, self._wnd_discard, ], # self._wnd_constraint] kind="accordion", # "inputgroup", ) self._wnd_layout_discard.setTooltip("Discard this node (if not in use elsewhere)") self.wroot.addChild(self._wnd_layout_discard) # -------------------------- # change selection to the parent body self._wnd_select_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._wnd_select_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._wnd_select_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._wnd_constraint = 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._wnd_md_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) # self._wnd_md_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wnd_layout_select = widgetArray( router, label="Change selection to related node", children=[self._wnd_select_up, self._wnd_select_left, self._wnd_select_right], kind="accordion", # "inputgroup", ) self._wnd_layout_select.setTooltip("Change the selection to a sibling node or parent body") self.wroot.addChild(self._wnd_layout_select) def getSummary(self) -> str: body = self.item.parentBody() return f"Parent body: {body.name()}" def setup(self, item: kd.Node, item_context: kw.Json, /): super().setup(item, item_context) # hide the header is this is the most derived class if not isinstance(item, kd.ConstraintNode): self._wnd_header1.setVisible(False) is_force_node = item.isExternalForceNode() self._wnd_layout_forces.setVisible(is_force_node) self._wnd_bd2node.setup(item.getBodyToNodeTransform()) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" if isinstance(item, kd.ConstraintNode): return False return isinstance(item, self.wrapped()) 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 ConstraintNodeCard(NodeCard): # AbstractCard[kd.ConstraintNode]): """Card to display info about any Node object.""" @property def label(self) -> str: return "ConstraintNode" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wcnd_header = kw.Markdown(router, "---") self.wroot.addChild(self._wcnd_header) self._wcnd_header1 = kw.Markdown(router, "**ConstraintNode**") self.wroot.addChild(self._wcnd_header1) # ------------------------------ # for constraints for this node show line between this node and the other bodies self._wcnd_constraint = 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._wcnd_md_constraint = kw.Markdown(router, text="**Constraint**", in_line=True) self._wcnd_layout_constraint = widgetArray( router, label="Constraint", children=[self._wcnd_constraint], kind="inputgroup", ) # ------------------------------ # change selection to the previous sibling node self._wcnd_select_constraint = kw.Button( router, text="Go to loop constraint", on_press=lambda: _selectObject(self.item.loopConstraint(), self.context.selection), tooltip="Change the selection to the attached loop constraint", ) """ # show constraint attached to constraint node self._wcnd_constraint = 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._wcnd_md_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) # self._wcnd_md_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wcnd_layout_select = widgetArray( router, label="Go to the loop constraint", children=[self._wcnd_select_constraint], kind="accordion", # "inputgroup", ) self._wcnd_layout_select.setTooltip("Change the selection to attached loop constraint") self._wcnd_layout_select.setOpen(True) self.wroot.addChild(self._wcnd_layout_select) self.wroot.addChild(self._wcnd_layout_constraint) def getSummary(self) -> str: item = self.item lines = [] result = super().getSummary() lines.append(f"Body: {item.parentBody().name()}") if lc := item.loopConstraint(): lines.append(f"Loop constraint: {lc.name()}") else: lines.append("No loop constraint") return result + " \n".join(lines) def setup(self, item: kd.ConstraintNode, item_context: kw.Json, /): super().setup(item, item_context) has_loop_constraint = item.loopConstraint() is not None # not implemented yet self._wcnd_layout_constraint.setVisible(False) self._wcnd_layout_select.setEnabled(has_loop_constraint) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" return isinstance(item, kd.ConstraintNode) @register class BilateralConstraintBaseCard(AbstractCard[kd.BilateralConstraintBase]): """Card to display info about any Node object.""" @property def label(self) -> str: return "BilateralConstraintBase" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ # self._wbcb_header = kw.Markdown(router, "---") # self.wroot.addChild(self._wbcb_header) self._wbcb_header1 = kw.Markdown(router, "**BilateralConstraintBase**") self.wroot.addChild(self._wbcb_header1) """ 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._wbcb_enable = kw.Button( router, text="Toggle", on_press=lambda: _toggleConstraint(self.item), tooltip="Toggle enabling/disabling the constraint", ) # self._wbcb_md_enable = kw.Markdown(router, text="**Enable/Disable constraint**", in_line=True) self._wbcb_layout_enable = widgetArray( router, label="Enable/disable constraint", children=[self._wenable], kind="inputgroup", ) self.wroot.addChild(self._wlayout_enable) """ # ------------------------------------ def _createAggSubgraph(constraint: kd.BilateralConstraintBase): # 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(True, st.sortedPhysicalBodiesList(), [], [], self.context) self.context.subtrees_tree_view.refresh() # highlight bodies in aggregation graph for a constraint self._wbcb_aggsg = kw.Button( router, text="Create aggregation subgraph", on_press=lambda: _createAggSubgraph(self.item), tooltip="Create an aggregation subgraph for this constraint", ) def _disableCB(cstate): if cstate: cast(kd.SubGraph, self._subtree).enableConstraint(self.item) else: cast(kd.SubGraph, self._subtree).disableConstraint(self.item) _createSubTreeVisJs(self._subtree, self.context) self._wbcb_disable = kw.Toggle( router, text="Disable", on_toggle=lambda cstate: _disableCB(cstate), tooltip="Disable this constraint", render_as_button=True, ) self._wbcb_layout_sg = widgetArray( router, label="Sub-graph related changes", children=[ self._wbcb_aggsg, self._wbcb_disable, ], kind="accordion", # "inputgroup", ) self._wbcb_layout_sg.setTooltip("Make subgraph related changes based on the constraint") self._wbcb_layout_sg.setOpen(True) self.wroot.addChild(self._wbcb_layout_sg) # ------------------ def _discardCB(): self.context.selection.set(kw.Selection().dump()) kc.discard(self.item) self.context.multibody.ensureHealthy() _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) self._wbcb_discard = kw.Button( router, text="Discard", on_press=lambda: _discardCB(), tooltip="Discard this constraint", ) self._wbcb_layout_constraints = widgetArray( router, label="Discard the constraint", # self._wbcb_md_constraints, children=[ # self._wbcb_constraints, # self._wbcb_aggsg, # self._wbcb_disable, self._wbcb_discard, ], kind="accordion", # "inputgroup", ) self._wbcb_layout_constraints.setTooltip("Discard the constraint (cannot undo)") # self._wbcb_layout_constraints.setOpen(True) self.wroot.addChild(self._wbcb_layout_constraints) """ # for constraints for this node show line between this node and the other bodies self._wbcb_constraint = kw.Button( router, text="Constraints", on_press=lambda: _showLoopConstraints([self.item.loopConstraint()], self.context), ) self._wbcb_md_constraint = kw.Markdown(router, text="**Constraint**", in_line=True) self._wbcb_layout_constraint = widgetArray( router, label=self._wbcb_md_constraint, children=[self._wbcb_constraint] ) self.wroot.addChild(self._wbcb_layout_constraint) """ def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" # this is a non-concrete class return False """ if isinstance(item, kd.LoopConstraintBase): return False return isinstance(item, self.wrapped()) """ def setup(self, item: kd.BilateralConstraintBase, item_context: kw.Json, /): # 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: kc.warn( f"The subtree value is missing when selecting the {item.name()} constraint. Defaulting to multibody." ) self._subtree = self.context.multibody super().setup(item, item_context) # highlight the constraint nodes and bodies involved self._wbcb_disable.setValue(self._subtree.isEnabledConstraint(item)) @register class LoopConstraintBaseCard(BilateralConstraintBaseCard): # AbstractCard[kd.LoopConstraintBase]): """Card to display info about any Node object.""" @property def label(self) -> str: return "LoopConstraintBase" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wlcb_header = kw.Markdown(router, "---") self.wroot.addChild(self._wlcb_header) self._wlcb_header1 = kw.Markdown(router, "**LoopConstraintBase**") self.wroot.addChild(self._wlcb_header1) # ------------------------------ self._wlcb_srcframe = kw.Toggle( router, text="Source node", on_toggle=lambda cstate: _highlightFrames2( [self.item.constraintFrameToFrame().oframe()], cstate, gui_context=self.context, priority_level="primary", ), render_as_button=True, tooltip="Show the source node frame axes", ) self._wlcb_tgtframe = kw.Toggle( router, text="Target node", on_toggle=lambda cstate: _highlightFrames2( [self.item.constraintFrameToFrame().pframe()], cstate, gui_context=self.context, priority_level="secondary", ), render_as_button=True, tooltip="Show the target node frame axes", ) # should be in cutjoint card - TODO self._wlcb_errframe = kw.Toggle( router, text="Error frame", on_toggle=lambda cstate: _highlightFrames2( [self.item.hinge().pframe()], cstate, gui_context=self.context, priority_level="tertiary", ), render_as_button=True, tooltip="Show the error frame axes", ) # self._wlcb_md_frames = kw.Markdown(router, text="**Constraint nodes**", in_line=True) self._wlcb_layout_frames = widgetArray( router, label="Highlight loop constraint's constraint nodes and error frame", children=[ self._wlcb_srcframe, self._wlcb_tgtframe, self._wlcb_errframe, ], kind="accordion", # "inputgroup", ) self._wlcb_layout_frames.setTooltip( "Turn on highlighting of the constraint frames for this loop constraint" ) self._wlcb_layout_frames.setOpen(True) self.wroot.addChild(self._wlcb_layout_frames) # ----------------------------------- # change selection to the pframe self._wlcb_select_pframe = kw.Button( router, text="Go to target frame", on_press=lambda: _selectObject( self.item.constraintFrameToFrame().pframe(), self.context.selection ), tooltip="Change selection to the target frame for this loop constraint", ) # change selection to the oframe self._wlcb_select_oframe = kw.Button( router, text="Go to source frame", on_press=lambda: _selectObject(self.item.oframe(), self.context.selection), tooltip="Change selection to the source frame for this loop constraint", ) # self._wlcb_md_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wlcb_layout_select = widgetArray( router, label="Switch to the constraint frames", children=[ self._wlcb_select_oframe, self._wlcb_select_pframe, ], kind="accordion", # "inputgroup", ) self._wlcb_layout_select.setTooltip( "Switch selection to the constraint frames for this loop constraint" ) self.wroot.addChild(self._wlcb_layout_select) """ def getSummary(self) -> str: item = self.item if isinstance(item, kd.LoopConstraintCutJoint): hge_type = kd.HingeBase.hingeTypeString(item.hinge().hingeType()) return f"Hinge: {hge_type}" else: return "Not a cut joint" """ def setup(self, item: kd.LoopConstraintBase, item_context: kw.Json, /): super().setup(item, item_context) is_cutjoint = isinstance(item, kd.LoopConstraintCutJoint) self._wlcb_errframe.setVisible(is_cutjoint) def teardown(self, _: kd.LoopConstraintBase, /): self._wlcb_srcframe.setValue(False) self._wlcb_tgtframe.setValue(False) self._wlcb_errframe.setValue(False) @register class PhysicalSubhingeCard(AbstractCard[kd.PhysicalSubhinge]): """Card to display info about any Node object.""" @property def label(self) -> str: return "PhysicalSubhinge" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # --------------------------- # 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: list[kw.Slider] = [] 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) if not cindex < sh.nQ(): raise ValueError( f"The {cindex} coordinate index should be less than {sh.nQ()} - the number of coordinates for the {sh.name()} subhinge" ) 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): self._wcoord_sliders.append( 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" ) ) self._wcoord_sliders[-1].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, item_context: kw.Json, /): super().setup(item, item_context) """ _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: GuiContext, group_tag=""): router = gui_context.router self.context = gui_context # self.item = None # ---------------------------------- # the initial Q values (to use for reset) self.init_Q = np.array([]) self.full_Q = np.array([]) # -------------------------------------- # 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, ) # markdown to show status from IK self._wmd_coord_ik_status = kw.Markdown(router, text="**IK status:**") 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.item # print("JJJJ", shindex, cindex, hge.nSubhinges()) if not shindex < hge.nSubhinges(): raise ValueError( f"The {shindex} sughinge index should be less than {hge.nSubhinges()} - the number of subhinges in the {hge.name()} hinge" ) sh = hge.subhinge(shindex) nQ = sh.nQ() if not cindex < nQ: raise ValueError( f"The {cindex} coordinate index should be less than {nQ()} - the number of coordinates for the {sh.name()} subhinge" ) slider = self._wcoord_move_slider slider.setEnabled(True) self._wswing_kinsim.setEnabled(True) if isinstance(sh, kd.Physical1DofSubhinge): minQ, maxQ = sh.getJointLimits() if not math.isnan(minQ): slider.setMin(minQ) if not math.isnan(maxQ): slider.setMax(maxQ) slider.setValue(sh.getQ()[0]) 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) self._wlayout_coord_ik = widgetArray( router, label="Manually change coordinate", children=[self._wcoord_move_slider, self._wcoord_ik, self._wmd_coord_ik_status], kind="accordion", # accordion_group_tag=group_tag, ) self._wlayout_coord_ik.setTooltip("Manually change the body's selected hinge coordiante") self._wlayout_coord_ik.setOpen(True) # self._wik_layout.addChild(self._wlayout_coord_ik) # ------------------------------------- # 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", ) def _kinSim(): shindex = self._coord_move_indices[0] cindex = self._coord_move_indices[1] if shindex is None or cindex is None: return _mbodyKinematicsSim( self.item.subhinge(shindex), cindex, 0.3, 0.5, self.context, ) # constrained swing, with IK on self._wswing_kinsim = kw.Button( router, text="Kinematics sim", on_press=_kinSim, tooltip="Articulate the body using kinematics simulation mode", ) self._wlayout_articulate = widgetArray( router, label="Auto swing coordinate", children=[self._wswing, self._wswing_ik, self._wswing_kinsim], kind="accordion", alignment="row", accordion_group_tag=group_tag, ) self._wlayout_articulate.setTooltip("Articulate the body's selected hinge coordinate") # self._wik_layout.addChild(self._wlayout_articulate) # self.wroot.addChild(self._wik_layout) # ----------------------- """ 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: list[int | None] = [None, None] # callback to reset the selected coordinate def _localCoordReset(): shindex = self._coord_move_indices[0] cindex = self._coord_move_indices[1] if shindex is None or cindex is None: return 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="Single coordinate", 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="All coordinates", on_press=_fullCoordReset, tooltip="Reset all the coordinates to the original value", # render_as_button=True, ) self._wlayout_coord_reset = widgetArray( router, label="Coord reset", children=[ self._wcoord_local_reset, self._wcoord_full_reset, ], # self._wmd_coord_ik_status], kind="accordion", # "inputgroup", ) self._wlayout_coord_reset.setTooltip("Reset the hinge coordinates") # self._wik_layout.addChild(self._wlayout_coord_status) # -------------------------- # 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 # pyright: ignore 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) # --------------------------- def setAxesCB(shindex, val): axis = eval(val) if not isinstance(axis, Sequence): raise ValueError(f"Expecting 3 floats for the subhinge axis - got {axis}") if len(axis) != 3: raise ValueError(f"Expecting 3 floats for the subhinge axis - got {len(axis)}") naxis = np.array(axis) nrm = np.linalg.norm(naxis) if nrm == 0: raise ValueError(f"Expecting non-zero 3-vector for the subhinge axis - got {axis}") sh = cast(kd.Physical1DofSubhinge, self.item.subhinge(shindex)) if not sh.nU() == 1: raise ValueError( f"Can set subhinge axis for only 1 dof subhinges, and not for one with {sh.nU()} dofs." ) vaxis = naxis / nrm sh.setUnitAxis(vaxis) astr = np.array2string(vaxis, separator=", ") self.shaxes[shindex].setValue(astr) def setLimitsCB(shindex, val): limits = eval(val) assert isinstance(limits, Sequence) assert len(limits) == 2 if not isinstance(axis, Sequence): raise ValueError(f"Expecting 2 floats for the joint limits - got {limits}") if len(axis) != 2: raise ValueError(f"Expecting 2 floats for the joint limits - got {len(limits)}") if np.isnan(limits[0]): raise ValueError( f"Expecting legal float values for the joint limits - got {limits}" ) if np.isnan(limits[1]): raise ValueError( f"Expecting legal float values for the joint limits - got {limits}" ) if not limits[0] <= limits[1]: raise ValueError( f"Expecting first joint limit to be smaller than the second - got {limits}" ) sh = cast(kd.Physical1DofSubhinge, self.item.subhinge(shindex)) # assert sh.nU() == 1 if not sh.nU() == 1: raise ValueError( f"Can set joint limits for only 1 dof subhinges, and not for one with {sh.nU()} dofs." ) nlimits = np.array(limits) sh.setJointLimits(nlimits) astr = np.array2string(nlimits, separator=", ") self.shlimits[shindex].setValue(astr) self.shrows: list[kw.Layout | kw.InputGroup | kw.Accordion] = [None] * 6 # pyright: ignore self.shaxes: list[kw.StringInput] = [None] * 6 # pyright: ignore self.shlimits: list[kw.StringInput] = [None] * 6 # pyright: ignore w_shrows = [] 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]) waxis = kw.StringInput( router, "Change axis", on_change=lambda val, shindex=shindex: setAxesCB(shindex, val), ) waxis.setTooltip("The unit norm subhinge axis vector") waxis.setSizeClass(kw.SizeClass.WIDE) wlimits = kw.StringInput( router, "Joint limits", on_change=lambda val, shindex=shindex: setLimitsCB(shindex, val), ) wlimits.setTooltip("The joint limits for the 1 dof subhinge") wlimits.setSizeClass(kw.SizeClass.WIDE) # 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=f" **Subhinge[{shindex}]**", # wmd_shrow, children=wbtns + [waxis, wlimits], ) w_shrow.setTooltip("Coordinate selection, and related widgets") # self.shrows[shindex] = (w_shrow, wmd_shrow) self.shrows[shindex] = w_shrow self.shaxes[shindex] = waxis self.shlimits[shindex] = wlimits # self._wlayout_move_buttons.addChild(w_shrow) w_shrows.append(w_shrow) self._wlayout_move_buttons = widgetArray( router, label="Select move coordinate", children=w_shrows, alignment="column", alignItems="left", kind="inputgroup", ) # self._wik_layout.addChild(self._wlayout_move_buttons) self._wlayout_move_buttons.setVisible(True) self._wlayout_move_buttons.setTooltip("Coordinate selection buttons") self._wik_layout = widgetArray( router, [ self._wlayout_move_buttons, # select the coordinate self._wlayout_coord_ik, # manually move the coordinate self._wlayout_articulate, # swing the coordinate self._wlayout_coord_reset, # reset the states ], label="Kinematics", alignment="column", alignItems="left", kind="accordion", ) self._wik_layout.setTooltip("Controls for changing the body's hinge coordinates") @property def subtree(self): st = self._subtree_ref() if not st: raise ValueError("SubTree reference has been removed.") return st @subtree.setter def subtree(self, st: kd.SubTree): self._subtree_ref = kc.CppWeakRef(st) @property def item(self): if self._item_ref is None: raise RuntimeError("The associated FramePairHinge has gone out of scope.") item = self._item_ref() if item is None: raise RuntimeError("The associated FramePairHinge has gone out of scope.") return item @item.setter def item(self, item: kd.FramePairHinge | kc.CppWeakRef[kd.FramePairHinge] | None): if isinstance(item, kd.FramePairHinge): self._item_ref = kc.CppWeakRef(item) else: self._item_ref = item def setup(self, itemref: kc.CppWeakRef[kd.FramePairHinge] | None, stref: kc.CppWeakRef): self.item = itemref self._subtree_ref = stref # self.subtree st = stref() if itemref is None: item = None else: item = itemref() not_root_body = item is not None is_not_locked = not_root_body and 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 is_not_locked: hge = cast(kd.FramePairHinge, 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].setVisible(nQ != 0) self.shaxes[shindex].setVisible(nQ == 1) self.shlimits[shindex].setVisible(nQ == 1) if nQ == 1: sh1 = cast(kd.Physical1DofSubhinge, sh) astr = np.array2string(sh1.getUnitAxis(), separator=", ") self.shaxes[shindex].setValue(astr) astr = np.array2string(sh1.getJointLimits(), separator=", ") self.shlimits[shindex].setValue(astr) 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].setVisible(False) # print("JJJJ", shindex, cindex) # enable the first button self._wcoord_buttons[0].setValue(True, trigger_own_callback=True) self._coord_move_indices = [0, 0] class CompoundHingeWidgets: """Class to create compound hinge widgets.""" def __init__(self, gui_context: GuiContext): 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 = np.array([]) self.full_Q = np.array([]) # variable to track which coordinate index has been # chosen for motion via the slider self._coord_move_index = 0 # ------------------------------------ self.max_coord = 20 def _toggleCoordMove(cstate: bool, cindex: int): 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 # print("JJJJ", shindex, cindex, hge.nSubhinges()) sh = hge.subhinge(0) nQ = sh.nQ() if not cindex < nQ: raise ValueError( f"The {cindex} coordinate index should be less than {sh.nQ()} - the number of coordinates for the {sh.name()} subhinge" ) 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.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) 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) # --------------------------------- # callback to reset the selected coordinate def _localCoordReset(): cindex = self._coord_move_index hge = self.item 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", ) self._wlayout_coord_reset = widgetArray( router, label="Coord Reset", # self._wmd_coord_ik_status, children=[ self._wcoord_local_reset, self._wcoord_full_reset, ], # , self._wmd_coord_ik_status], kind="accordion", # "inputgroup", ) # self._wik_layout.addChild(self._wlayout_coord_status) self._wlayout_coord_reset.setTooltip("Reset the compound hinge coordinates") # --------------------------- # 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( cast(kd.SubGraph, self.item.compoundBody().bodiesTree().parentSubTree()), self.item.subhinge(0), self._coord_move_index, 0.3, 0.5, self.context, ), tooltip="Articulate the CE compound body using kinematics sim", ) # self._wmd_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wlayout_articulate = widgetArray( router, label="Articulate", children=[self._wswing, self._wswing_kinsim], kind="inputgroup", ) # self._wik_layout.addChild(self._wlayout_articulate) # -------------------------------------- # create subhinge and coordinate entries rowlen = 6 rowindex = 0 cindex = 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] = [ # pyright: ignore 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.shrows: list[kw.Layout | kw.InputGroup] = [None] * (int(self.max_coord / 6) + 1) # pyright: ignore w_shrows = [] 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=" **GGGGG*", in_line=True) w_shrow = widgetArray(router, children=wbtns) w_shrows.append(w_shrow) self.shrows[rowindex] = w_shrow # self.wroot.addChild(w_shrow) # self._wlayout_move_buttons.addChild(w_shrow) # w_shrow.setVisible(True) rowindex += 1 self._wlayout_move_buttons = widgetArray( router, label="Select move coordinate", children=w_shrows, alignment="column", alignItems="left", kind="inputgroup", ) # self._wik_layout.addChild(self._wlayout_move_buttons) self._wlayout_move_buttons.setVisible(True) self._wlayout_move_buttons.setTooltip("Coordinate selection buttons") self._wik_layout = widgetArray( router, [ # self._wlayout_articulate self._wlayout_move_buttons, # select the coordinate self._wlayout_coord_ik, # manually move the coordinate self._wlayout_articulate, # swing the coordinate self._wlayout_coord_reset, # reset the states ], label="Kinematics", alignment="column", alignItems="left", kind="accordion", # "inputgroup", ) # self.wroot.addChild(self._wik_layout) self._wik_layout.setTooltip("Controls for changing the compound body's hinge coordinates") @property def subtree(self): return self.item.compoundBody().bodiesTree().parentSubTree() @property def item(self): if self._item_ref is None: raise RuntimeError("The associated CompoundHinge has gone out of scope.") item = self._item_ref() if item is None: raise RuntimeError("The associated CompoundHinge has gone out of scope.") return item @item.setter def item(self, item: kd.CompoundHinge | kc.CppWeakRef[kd.CompoundHinge] | None): if isinstance(item, kd.CompoundHinge): self._item_ref = kc.CppWeakRef(item) else: self._item_ref = item def setup( self, itemref: kc.CppWeakRef[kd.CompoundHinge], # stref: kc.CppWeakRef # _: kw.Json, /, ): self.item = itemref hge = self.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) pass button.setVisible(False) # self.shrows[cindex].setVisible(False) # enable the first button -- it's ok to use state set bc we already have item ref set self._wcoord_buttons[0].setValue(True, trigger_own_callback=True) # self.shrows[0].setVisible(True) self.init_Q = hge.coordData().getQ() self.full_Q = self.context.multibody.getQ() class DynamicsSimWidgets: """Class to create dynamics sim related widgets.""" def __init__(self, gui_context: GuiContext): router = gui_context.router self.context = gui_context # self.item = None # constrained swing, with IK on self._external_sp = None self._dyn_sp: kd.StatePropagator | None = None self._dyn_integ_type = ki.IntegratorType.CVODE # default # self._dyn_init_U = 0 self._dyn_grav_accel: NDArray[np.float64] | None = None self._dyn_contact_force: kcoll.ContactForceBase | None = None # self._dyn_duration = 0.5 # same as self._advance_incr # ----------------------- # ---------------------------- adv_opts = kw.SliderOptions() adv_opts.min = -3 adv_opts.max = 3 # 10 steps per decade adv_opts.step = 0.5 # 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, "Duration", setincr) # Default to 0.1 s self._wadvinput.state("quantity_state").set({"value": 0.1, "units": "s"}) """ def setDuration(new_incr, _): # incr_seconds = (new_incr * ureg(units)).to("s") self._dyn_duration = new_incr self._wdyn_duration = kw.QuantityInput(router, "Duration", setDuration) # Default to 0.1 s self._wdyn_duration.state("quantity_state").set({"value": 0.5, "units": "s"}) """ # -------- color = "green" stxt = "Idle" status = f'**Status:** <span style="color:{color}">{stxt}</span>' self._wmd_status = kw.Markdown(router, text=status) # Advance by increment rounded to the nearest nanosecond self._wadvbtn = kw.Button( router, text="Advance by", on_press=lambda: createAdvanceByCallback( # self.item, np.timedelta64(int(self._advance_incr * 1e9), "ns") self._dyn_sp, self.context, np.timedelta64(int(self._advance_incr * 1e9), "ns"), self._wmd_status, ), ) # ---------------------------- self._wpause = kw.Button( router, text="\u23f8/\u25b6", on_press=lambda: createPauseCb(self._dyn_sp)() ) def _stopCB(sp): assert sp sp.stop() self._wstop = kw.Button( router, text="\u23f9", # on_press=lambda: self._dyn_sp.stop()) on_press=lambda: _stopCB(self._dyn_sp), ) # self._wmd_pause = kw.Markdown(router, text="**Sim run**", in_line=True) self._wlayout_pause = widgetArray( router, label="**Run sim**", children=[self._wadvinput, self._wadvbtn, self._wpause, self._wstop, self._wmd_status], kind="layout", # "inputgroup", ) # ------------- def _stepSizeCB(sp, new_val): assert sp sp.setMaxStepSize(new_val) 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, "Max step size", # lambda new_val: self._dyn_sp.setMaxStepSize(new_val), lambda new_val: _stepSizeCB(self._dyn_sp, new_val), slider_opts, ) self._wmaxstep_slider.setSizeClass(kw.SizeClass.MEDIUM) # ---------------------------- # Mapping of indices to integrator type self.integ_types = [ ki.IntegratorType.RK4, ki.IntegratorType.CVODE, ki.IntegratorType.CVODE_STIFF, ] def integSelect(sp, index): """Set the integrator according to a given index.""" integ_type = self.integ_types[index] self._dyn_integ_type = integ_type # old_type = sp.getIntegrator().getIntegratorType() # if old_type != integ_type: # old_is_cvode = old_type in [ # ki.IntegratorType.CVODE, # ki.IntegratorType.CVODE_STIFF, # ki.IntegratorType.CVODE_NEWTON, # ] # new_is_cvode = integ_type in [ # ki.IntegratorType.CVODE, # ki.IntegratorType.CVODE_STIFF, # ki.IntegratorType.CVODE_NEWTON, # ] # if old_is_cvode and new_is_cvode: # sp.setIntegrator(integ_type, sp.getIntegrator().getOptions()) # else: sp.setIntegrator(integ_type) self._winteg = kw.Dropdown( router, "", ["RK4", "CVode", "CVode Stiff"], lambda index: integSelect(self._dyn_sp, index), ) def createIntegratorPanel(sp): assert 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_iframe, "within", ) self._winteg_panel = kw.Button( router, text="Integrator panel", on_press=lambda: createIntegratorPanel(self._dyn_sp), tooltip="Create a separate info panel for the integrator", ) # self._wmd_integ = kw.Markdown(router, text="**Integrator**", in_line=True) self._wlayout_integ = widgetArray( router, label="**Integrator**", children=[self._winteg, self._winteg_panel, self._wmaxstep_slider], kind="layout", # "accordion", # "inputgroup", ) # ---------------------------- def _setupDynSimCB(cstate): if not kc.allReady(): print("WARNING: kc.allReady() is failing - call mb.resetData()") return # assert kc.allReady() # self._wdynsim_run.setEnabled(cstate) self._wadvbtn.setEnabled(cstate) self._wenable_gravity.setEnabled(not cstate) self._wenable_contact.setEnabled(not cstate) self._wcoord_full_reset.setEnabled(cstate) self._wmaxstep_slider.setEnabled(cstate) self._wsp_reset.setEnabled(cstate) if cstate: """ if self._dyn_grav_accel is None: raise ValueError("Gravity not set.") if self._dyn_contact_force is None: raise ValueError("Dynamic contact force not set.") """ self._dyn_sp = _subTreeSim( self.item, integ_type=self._dyn_integ_type, # init_U=self._dyn_init_U, gravity_accel=self._dyn_grav_accel, contact_force=self._dyn_contact_force, gui_context=self.context, ) self._winteg_panel.setEnabled(True) else: self._dyn_sp = None self._winteg_panel.setEnabled(False) self._wdynsim_setup = kw.Toggle( router, text="Setup", on_toggle=lambda cstate: _setupDynSimCB(cstate), tooltip="Set up dynamics simulation", render_as_button=True, ) def _enableContact(cstate): if cstate: hc = kcoll.HuntCrossleyContactForce("hunt_crossley_contact") hc.params.kp = 1e4 hc.params.kc = 5e3 hc.params.mu = 0.7 hc.params.n = 1.0 hc.params.linear_region_tol = 1e-3 self._dyn_contact_force = hc else: self._dyn_contact_force = None self._wenable_contact = kw.Toggle( router, text="Contact", on_toggle=lambda cstate: _enableContact(cstate), tooltip="Enable contact dynamics in dynamics sim", render_as_button=True, ) def _enableGravity(cstate): self._dyn_grav_accel = np.array([0, 0, -3.73]) if cstate else None self._wenable_gravity = kw.Toggle( router, text="Gravity", on_toggle=lambda cstate: _enableGravity(cstate), tooltip="Enable gravity in dynamics sim", render_as_button=True, ) # ----------------- self._wlayout_setup = widgetArray( router, label="**Setup propagator**", children=[ self._wdynsim_setup, self._wenable_gravity, self._wenable_contact, # self._wdynsim_run, # self._wdyn_duration, # self._winit_u, ], kind="layout", # "inputgroup", ) # -------------------- def setInitU(new_incr): # self._dyn_init_U = new_incr self.item.setU(new_incr) if isinstance(self.item, kd.SubGraph) and self.item.enabledConstraints(): self.item.cks().solveU() self._winit_u = kw.FloatInput( router, "Init U", "The initial generalized velocity to set for the coordinates", on_change=setInitU, ) self._winit_u.setSizeClass(kw.SizeClass.MEDIUM) # self._winit_u.state("quantity_state").set({"value": 0.0, "units": "s"}) self._winit_u.setStep(0.01) self._winit_u.setMin(-1) self._winit_u.setMax(1) self._winit_u.setValue(0) def _fullCoordReset(): assert self._dyn_sp self.item.setQ(self.full_Q) self.item.setU(self.full_U) x = self._dyn_sp.assembleState() self._dyn_sp.setState(x) self._dyn_sp.setTime(0) self.context.scene.update() self._wcoord_full_reset = kw.Button( router, text="Reset state", on_press=_fullCoordReset, tooltip="Reset the state to the one when this object was selected", # render_as_button=True, ) def _spReset(): assert self._dyn_sp x = self._dyn_sp.assembleState() self._dyn_sp.setState(x) self.context.scene.update() self._wsp_reset = kw.Button( router, text="SP set state", on_press=_spReset, tooltip="Sync the state propagator state from the SubTree's current Q/U values", # render_as_button=True, ) self._wlayout_reset = widgetArray( router, label="**Set state**", children=[ # self._wdynsim_setup, # self._wdyn_duration, self._wcoord_full_reset, self._winit_u, self._wsp_reset, ], kind="layout", # "inputgroup", ) # ---------------- # ----------------------- self._dsinitfile = "subtreeState.hdf5" def _setStateFile(x): self._dsinitfile = x self._wdsinitfile = kw.StringInput( router, "Filename", "The state filename", lambda x: _setStateFile(x), rapid_submit=True, ) self._wdsinitfile.setValue(self._dsinitfile) def _toDSInitCB(): st = self.item if isinstance(st, kd.SubGraph): from Karana.Dynamics.SOADyn_types import SubGraphStateDS sg = cast(kd.SubGraph, st) ds = SubGraphStateDS.fromSubGraph(sg) else: from Karana.Dynamics.SOADyn_types import SubTreeStateDS ds = SubTreeStateDS.fromSubTree(st) ds.toFile(self._dsinitfile) self._wsavedsinit = kw.Button( router, text="Save", on_press=lambda: _toDSInitCB(), tooltip="Save SubTree state to file", ) def _fromDSInitCB(): st = self.item if isinstance(st, kd.SubGraph): from Karana.Dynamics.SOADyn_types import SubGraphStateDS sg = cast(kd.SubGraph, st) ds = SubGraphStateDS.fromFile(self._dsinitfile) ds.toSubGraph(sg) else: from Karana.Dynamics.SOADyn_types import SubTreeStateDS ds = SubTreeStateDS.fromFile(self._dsinitfile) ds.toSubTree(st) self.context.scene.update() self._wloaddsinit = kw.Button( router, text="Load", on_press=lambda: _fromDSInitCB(), tooltip="Load SubTree state from file", ) self._wlayout_state = widgetArray( router, label="**State to/from file**", # self._wmd_views, children=[ self._wsavedsinit, self._wdsinitfile, self._wloaddsinit, ], kind="layout", # "inputgroup", ) # ------------------ self._wlayout_dynsim = widgetArray( router, label="Dynamics sim [with temporary StatePropagator]", children=[ self._wlayout_setup, self._wlayout_integ, # self._wlayout_maxstep, # self._wlayout_adv, self._wlayout_pause, self._wlayout_reset, self._wlayout_state, ], alignment="column", alignItems="left", kind="accordion", # "inputgroup", ) self._wlayout_dynsim.setTooltip( "Do a basic dynamics simulation of the sub-tree with initial rates, gravity and contact dynamics" ) # self.wroot.addChild(self._wlayout_dynsim) """ @property def subtree(self): return self.item.compoundBody().bodiesTree().parentSubTree() """ @property def item(self): if self._item_ref is None: raise RuntimeError("The associated SubTree has gone out of scope.") item = self._item_ref() if item is None: raise RuntimeError("The associated SubTree has gone out of scope.") return item @item.setter def item(self, item: kd.SubTree | kc.CppWeakRef[kd.SubTree] | None): if isinstance(item, kd.SubTree): self._item_ref = kc.CppWeakRef(item) else: self._item_ref = item def setup( self, itemref: kc.CppWeakRef[kd.SubTree], spref: kc.CppWeakRef[kd.StatePropagator] | None ): self.item = itemref if spref: self._dyn_sp = spref() sp = cast(kd.StatePropagator, self._dyn_sp) self._external_sp = True self._wmaxstep_slider.setValue( km.ktimeToSeconds(sp.getMaxStepSize()), trigger_own_callback=True, ) curr_type = sp.getIntegrator().getIntegratorType() 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}") else: self._dyn_sp = None self._external_sp = False self._winteg.setIndex(self.integ_types.index(self._dyn_integ_type)) # disable widgets not meant for external state propagator self._wlayout_setup.setVisible(not self._external_sp) # self._wdynsim_run.setEnabled(self._external_sp) self._wadvbtn.setEnabled(self._external_sp) self._winteg_panel.setEnabled(self._external_sp) self._wmaxstep_slider.setEnabled(self._external_sp) self._wsp_reset.setEnabled(self._external_sp) # self._wenable_gravity.setVisible(not self._external_sp) # self._wenable_contact.setVisible(not self._external_sp) self.full_Q = self.item.getQ() self.full_U = self.item.getU() """ self._dyn_sp = None self._wdynsim_setup.setValue(False) self._wdynsim_run.setEnabled(False) self._wcoord_full_reset.setEnabled(False) self._wenable_gravity.setEnabled(True) self._wenable_contact.setEnabled(True) """ class HomTranWidgets: """Class to create widgets to edit a HomTran.""" def __init__(self, title: str, cb: Callable, gui_context: GuiContext): router = gui_context.router # self.context = gui_context # self.item = None # -------------------------- # create roll, pitch, yaw sliders self.xyz = [0, 0, 0] self.rpy = [0, 0, 0] def _updateCB(): T = self.getTransform() cb(T) slider_opts = kw.SliderOptions() slider_opts.min = -3.12 slider_opts.max = 3.12 slider_opts.step = 0.01 slider_opts.log_scale = False def _rollCB(new_val): self.rpy[0] = new_val quat = km.UnitQuaternion( km.EulerAngles(self.rpy[0], self.rpy[1], self.rpy[2], km.EulerSystem.XYZ) ) astr = np.array2string(quat.toVector4(), separator=", ") self._wquat.setValue(astr) _updateCB() self._wroll = kw.Slider( router, "Roll", lambda new_val: _rollCB(new_val), slider_opts, ) self._wroll.setSizeClass(kw.SizeClass.MEDIUM) self._wroll.setTooltip("Roll angle (radians)") def _pitchCB(new_val): self.rpy[1] = new_val quat = km.UnitQuaternion( km.EulerAngles(self.rpy[0], self.rpy[1], self.rpy[2], km.EulerSystem.XYZ) ) astr = np.array2string(quat.toVector4(), separator=", ") self._wquat.setValue(astr) _updateCB() self._wpitch = kw.Slider( router, "Pitch", lambda new_val: _pitchCB(new_val), slider_opts, ) self._wpitch.setSizeClass(kw.SizeClass.MEDIUM) self._wpitch.setTooltip("Pitch angle (radians)") def _yawCB(new_val): self.rpy[2] = new_val quat = km.UnitQuaternion( km.EulerAngles(self.rpy[0], self.rpy[1], self.rpy[2], km.EulerSystem.XYZ) ) astr = np.array2string(quat.toVector4(), separator=", ") self._wquat.setValue(astr) _updateCB() self._wyaw = kw.Slider( router, "Yaw", lambda new_val: _yawCB(new_val), slider_opts, ) self._wyaw.setSizeClass(kw.SizeClass.MEDIUM) self._wyaw.setTooltip("Yaw angle (radians)") # ------------------ def setQuatCB(val): quat = eval(val) assert isinstance(quat, Sequence) assert len(quat) == 4 if not isinstance(axis, Sequence): raise ValueError(f"Expecting 4 floats for a unit quaternion - got {quat}") if len(quat) != 4: raise ValueError(f"Expecting 4 floats for a unit quaternion - got {len(quat)}") nquat = km.UnitQuaternion(quat) self.rpy = nquat.toEulerAngles().angles().m self._wxpos.setValue(self.rpy[0]) self._wypos.setValue(self.rpy[1]) self._wzpos.setValue(self.rpy[2]) _updateCB() self._wquat = kw.StringInput( router, "Unit quaternion", on_change=lambda val: setQuatCB(val), ) self._wquat.setTooltip("The orientation unit quaternion") self._wquat.setSizeClass(kw.SizeClass.EXTRA_WIDE) self._wlayout_rpy = widgetArray( router, label="", children=[self._wroll, self._wpitch, self._wyaw], kind="layout", ) self._wlayout_quat = widgetArray( router, label="Orientation", children=[self._wlayout_rpy, self._wquat], kind="inputgroup", alignment="column", alignItems="left", ) # -------------------------- # create X, Y, Z sliders slider_opts = kw.SliderOptions() slider_opts.min = -3 slider_opts.max = 3 slider_opts.step = 0.01 slider_opts.log_scale = False def _xposCB(new_val): self.xyz[0] = new_val _updateCB() self._wxpos = kw.Slider( router, "X", lambda new_val: _xposCB(new_val), slider_opts, ) self._wxpos.setSizeClass(kw.SizeClass.MEDIUM) self._wxpos.setTooltip("X position (m)") def _yposCB(new_val): self.xyz[1] = new_val _updateCB() self._wypos = kw.Slider( router, "Y", lambda new_val: _yposCB(new_val), slider_opts, ) self._wypos.setSizeClass(kw.SizeClass.MEDIUM) self._wypos.setTooltip("Y position (m)") def _zposCB(new_val): self.xyz[2] = new_val _updateCB() self._wzpos = kw.Slider( router, "Z", lambda new_val: _zposCB(new_val), slider_opts, ) self._wzpos.setSizeClass(kw.SizeClass.MEDIUM) self._wzpos.setTooltip("Z position (m)") self._wlayout_xyz = widgetArray( router, label="Position", children=[self._wxpos, self._wypos, self._wzpos], kind="inputgroup", ) # ------------------ self._wlayout_homtran = widgetArray( router, label=title, children=[ self._wlayout_xyz, # self._wlayout_rpy, self._wlayout_quat, ], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) def getTransform(self): ee = km.EulerAngles(self.rpy[0], self.rpy[1], self.rpy[2], km.EulerSystem.XYZ) return km.HomTran(km.UnitQuaternion(ee), self.xyz) def setup(self, T: km.HomTran): pos = T.getTranslation().m self._wxpos.setValue(pos[0]) self._wypos.setValue(pos[1]) self._wzpos.setValue(pos[2]) quat = T.getUnitQuaternion() rpy = quat.toEulerAngles(km.EulerSystem.XYZ).angles().m self._wroll.setValue(rpy[0]) self._wpitch.setValue(rpy[1]) self._wyaw.setValue(rpy[2]) astr = np.array2string(quat.toVector4(), separator=", ") self._wquat.setValue(astr) class SimTranWidgets: """Class to create widgets to edit a HomTran+Intrinsic scale.""" def __init__(self, title: str, cb: Callable, gui_context: GuiContext): router = gui_context.router # -------------------------- # create X, Y, Z scale sliders self.scale_xyz = [1, 1, 1] self.gscale = 1 def _updateCB(): T = km.SimTran(self._whomtran.getTransform(), self.gscale) cb(T, self.scale_xyz) slider_opts = kw.SliderOptions() slider_opts.min = -3 slider_opts.max = 1 slider_opts.step = 0.01 slider_opts.log_scale = True def _gscaleCB(new_val): self.gscale = new_val _updateCB() self._wgscale = kw.Slider( router, "Uniform", lambda new_val: _gscaleCB(new_val), slider_opts, ) self._wgscale.setSizeClass(kw.SizeClass.MEDIUM) self._wgscale.setTooltip("Uniform scaling factor") # ------------- def _xscaleCB(new_val): self.scale_xyz[0] = new_val _updateCB() self._wxscale = kw.Slider( router, "X", lambda new_val: _xscaleCB(new_val), slider_opts, ) self._wxscale.setSizeClass(kw.SizeClass.MEDIUM) self._wxscale.setTooltip("Intrinsic X axis scaling factor") def _yscaleCB(new_val): self.scale_xyz[1] = new_val _updateCB() self._wyscale = kw.Slider( router, "Y", lambda new_val: _yscaleCB(new_val), slider_opts, ) self._wyscale.setSizeClass(kw.SizeClass.MEDIUM) self._wyscale.setTooltip("Intrinsic Y axis scaling factor") def _zscaleCB(new_val): self.scale_xyz[2] = new_val _updateCB() self._wzscale = kw.Slider( router, "Z", lambda new_val: _zscaleCB(new_val), slider_opts, ) self._wzscale.setSizeClass(kw.SizeClass.MEDIUM) self._wzscale.setTooltip("Intrinsic Z axis scaling factor") self._wlayout_iscale = widgetArray( router, label="Intrinsic", children=[self._wxscale, self._wyscale, self._wzscale], kind="inputgroup", ) self._wlayout_scale = widgetArray( router, label="Scaling", children=[self._wgscale, self._wlayout_iscale], kind="inputgroup", alignment="column", alignItems="left", ) self._wlayout_scale.setTooltip("Uniform and intrinsic scaling for the scene part") # ------------------ self._whomtran = HomTranWidgets( title, lambda hT: cb(km.SimTran(hT, self.gscale), self.scale_xyz), gui_context ) # self._whomtran._wlayout_quat.addChild(self._wgscale) self._wlayout_simtran = self._whomtran._wlayout_homtran # self._wlayout_simtran.addChild(self._wgscale) self._wlayout_simtran.addChild(self._wlayout_scale) # def getTransform(self): # return km.SimTran(self._whomtran.getTransform(), self.scale_xyz) def setup(self, T: km.SimTran, iscale): self._whomtran.setup(T.getTransform()) self._wgscale.setValue(T.getScale()) self._wxscale.setValue(iscale[0]) self._wyscale.setValue(iscale[1]) self._wzscale.setValue(iscale[2]) @register class FramePairHingeCard(AbstractCard[kd.FramePairHinge]): """Card to display info about any Node object.""" @property def label(self) -> str: return "FramePairHinge" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # --------------------------- self._frame_pair_widgets = FramePairHingeWidgets( self.context, group_tag=f"{self.wroot.domId()}-change_coord-group" ) self.wroot.addChild(self._frame_pair_widgets._wik_layout) self._frame_pair_widgets._wik_layout.setOpen(True) """ # -------------------------------------- # 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, item_context: kw.Json, /): super().setup(item, item_context) """ _highlightFrames( [item.oframe(), item.pframe()], toggle_over_set=False, gui_context=self.context ) """ self._frame_pair_widgets.setup(kc.CppWeakRef(item), kc.CppWeakRef(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 CompoundHingeCard(AbstractCard[kd.CompoundHinge]): """Card to display info about a Compound hinge.""" @property def label(self) -> str: return "CompoundHinge" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # --------------------------- self._cmp_hge_widgets = CompoundHingeWidgets(self.context) self.wroot.addChild(self._cmp_hge_widgets._wik_layout) self._cmp_hge_widgets._wik_layout.setOpen(True) def setup(self, item: kd.CompoundHinge, item_context: kw.Json, /): super().setup(item, item_context) self._cmp_hge_widgets.setup(kc.CppWeakRef(item)) @register class LoopConstraintCutJointCard( LoopConstraintBaseCard ): # AbstractCard[kd.LoopConstraintCutJoint]): """Card to display info about a cut joint constraint.""" @property def label(self) -> str: return "LoopConstraintCutJoint" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wlcj_header = kw.Markdown(router, "---") self.wroot.addChild(self._wlcj_header) self._wlcj_header1 = kw.Markdown(router, "**Node**") self.wroot.addChild(self._wlcj_header1) # --------------------------- self._frame_pair_widgets = FramePairHingeWidgets( self.context, group_tag=f"{self.wroot.domId()}-change_coord-group" ) self.wroot.addChild(self._frame_pair_widgets._wik_layout) self._frame_pair_widgets._wik_layout.setOpen(True) # --------------------------- def _tohingeCB(): self.context.selection.set(kw.Selection().dump()) snd = self.item.sourceNode() if snd is None: raise ValueError(f"Cannot get source node for {self.item.name()}") sbd = snd.parentBody() if sbd.parentHinge().hingeType() == kd.HingeType.FULL6DOF: reverse = True else: tnd = self.item.targetNode() if tnd is None: raise ValueError(f"Cannot get target node for {self.item.name()}") tbd = tnd.parentBody() if tbd.parentHinge().hingeType() == kd.HingeType.FULL6DOF: reverse = False else: raise ValueError( f"One of the 2 ends of {self.item.name()} has to be 6 dof, should never get here. Cannot get target node for {self.item.name()}" ) hinge = kd.LoopConstraintCutJoint.toPhysicalHinge(self.item, reverse) self.context.multibody.ensureHealthy() _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) self.context.selection.set(kw.Selection([kw.Selection.Item(id=hinge.id())]).dump()) self._wlcj_tohinge = kw.Button( router, text="To hinge", on_press=lambda: _tohingeCB(), tooltip="Convert the cut-joint constraint into a physical hinge", ) self._wlcj_layout_tohinge = widgetArray( router, label="Convert the cut-joint into a hinge", children=[self._wlcj_tohinge], kind="accordion", # "layout", ) self._wlcj_layout_tohinge.setTooltip( "Convert this cut-joint loop constraint into a physical hinge if the necessary conditions to preserve a tree topology are met." ) self._wlcj_layout_tohinge.addChild(self._wlcj_tohinge) self.wroot.addChild(self._wlcj_layout_tohinge) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" return isinstance(item, kd.LoopConstraintCutJoint) def getSummary(self) -> str: item = self.item result = super().getSummary() is_cutjoint = item.type() == kd.BilateralConstraintType.CUTJOINT_LOOP if is_cutjoint: hge_type = kd.HingeBase.hingeTypeString(item.hinge().hingeType()) return result + f"Hinge: {hge_type}" else: return result + "Not a cut joint" def setup(self, item: kd.LoopConstraintCutJoint, item_context: kw.Json, /): # 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: kc.warn( f"The subtree value is missing when selecting the {item.name()} constraint. Defaulting to multibody." ) self._subtree = self.context.multibody # highlight the constraint nodes and bodies involved super().setup(item, item_context) self._frame_pair_widgets.setup( kc.CppWeakRef(item.hinge()), kc.CppWeakRef(self.context.multibody) ) # can the cutjoint be converted into a physical hinge? is_convertable_to_hinge = False snd = item.sourceNode() if snd: sbd = snd.parentBody() if sbd.parentHinge().hingeType() == kd.HingeType.FULL6DOF: is_convertable_to_hinge = True else: tnd = self.item.targetNode() if tnd: tbd = tnd.parentBody() if tbd.parentHinge().hingeType() == kd.HingeType.FULL6DOF: is_convertable_to_hinge = True self._wlcj_tohinge.setEnabled(is_convertable_to_hinge) @register class LoopConstraintConVelCard(LoopConstraintBaseCard): # AbstractCard[kd.LoopConstraintConVel]): """Card to display info about any Node object.""" @property def label(self) -> str: return "LoopConstraintConVel" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wlcc_header = kw.Markdown(router, "---") self.wroot.addChild(self._wlcc_header) self._wlcc_header1 = kw.Markdown(router, "**LoopConstraintConVel**") self.wroot.addChild(self._wlcc_header1) # --------------------------- # create slider for the constraint U coordinate # markdown to show status from IK self._wlcc_md_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.wroot.addChild(self._wlcc_md_coord_ik) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" return isinstance(item, kd.LoopConstraintConVel) def setup(self, item: kd.LoopConstraintConVel, item_context: kw.Json, /): # 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: kc.warn( f"The subtree value is missing when selecting the {item.name()} constraint. Defaulting to multibody." ) self._subtree = self.context.multibody super().setup(item, item_context) # disable until we have a way to do this for convels self._wlcc_md_coord_ik.setVisible(False) @register class CoordinateConstraintCard( BilateralConstraintBaseCard ): # AbstractCard[kd.CoordinateConstraint]): """Card to display info about any Node object.""" @property def label(self) -> str: return "CoordinateConstraint" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wcc_header = kw.Markdown(router, "---") self.wroot.addChild(self._wcc_header) self._wcc_header1 = kw.Markdown(router, "**CoordinateConstraint**") self.wroot.addChild(self._wcc_header1) # --------------------------- # create slider for the constraint Q coordinate # markdown to show status from IK self._wcc_md_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._wcc_coord_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._wcc_coord_slider = kw.Slider( router, text="Coordinate", on_change=lambda Q: changeCoord( self.item, self.context.multibody, self.context.scene, Q, self._wcc_md_coord_ik, ), opts=slider_opts, # tooltip="Change the scaling of the stick parts parts for the body in 3D graphics" ) self._wcc_coord_slider.setValue(0) self.wroot.addChild(self._wcc_md_coord_ik) self.wroot.addChild(self._wcc_coord_slider) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" return isinstance(item, kd.CoordinateConstraint) def setup(self, item: kd.CoordinateConstraint, item_context: kw.Json, /): # 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: kc.warn( f"The subtree value is missing when selecting the {item.name()} constraint. Defaulting to multibody." ) self._subtree = self.context.multibody super().setup(item, item_context) @register class SubTreeCard(AbstractCard[kd.SubTree]): """Card to display info about any SubTree object.""" @property def label(self) -> str: return "SubTree" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ # self._wst_sgheader = kw.Markdown(router, "---") # self.wroot.addChild(self._wst_sgheader) self._wst_header1 = kw.Markdown(router, "**SubTree**") self.wroot.addChild(self._wst_header1) # ---------------------------- # articulate all the bodies sequentially (only WebScene) self._wst_swing = kw.Button( router, text="Sequentially articulate (free)", on_press=lambda: _swingHinge( self.item, [x.parentHinge() for x in self.item.sortedBodiesList()], True, self.context, ), tooltip="Sequentially swing all the coordinates in this subtree (while ignroing constraints)", ) # self._wst_md_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wst_layout_articulate = widgetArray( router, label="**Articulate**", children=[self._wst_swing], kind="layout", # "inputgroup", ) # self.wroot.addChild(self._wst_layout_articulate) # ---------------------------- self._wst_mesh = 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._wst_collision = 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: if not kc.allReady(): print("WARNING: kc.allReady() is failing - call mb.resetData()") return # assert kc.allReady() 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._wst_stick = 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, ) # turn on/off stick parts for just the bodies in the subtree self._wst_stick_recreate = kw.Button( router, text="Recreate stick parts", on_press=lambda: recreateStickParts(self.item, self.context), tooltip="Recreate the stick parts in this subtree", ) # toggle wire frame/transparent view (only webscene) self._wst_wireframe = kw.Button( router, text="WireFrame (TBD)", on_press=lambda: _wireframeBodies( [x for x in self.item.sortedBodiesList() if isinstance(x, kd.PhysicalBody)] ), tooltip="Toggle wireframe mode for all the bodies in the subtree", ) # self._wst_md_highlight = kw.Markdown(router, text="**Highlight**", in_line=True) self._wst_layout_highlight = widgetArray( router, label="**Highlight**", children=[ self._wst_mesh, self._wst_collision, self._wst_stick, self._wst_stick_recreate, self._wst_wireframe, ], kind="layout", # "inputgroup", ) # self.wroot.addChild(self._wst_layout_highlight) # ---------------------------- # 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._wst_scale_stick = kw.Slider( router, text="Scale stick", on_change=lambda scale: self.item.scaleStickParts(scale), opts=slider_opts, ) self._wst_scale_stick.setValue(1) self.wroot.addChild(self._wst_scale_stick) # ---------------------------- # create visjs graph self._wst_visjs = kw.Button( router, text="Visjs Graph", on_press=lambda: _createSubTreeVisJs(self.item, self.context, False), tooltip="Create a visjs graph for the subtree (with names as labels)", ) self._wst_visjs_ids = kw.Button( router, text="Visjs Graph (IDs)", on_press=lambda: _createSubTreeVisJs(self.item, self.context, True), tooltip="Create a visjs graph for the subtree (with ids as labels)", ) # dictionary with all subtree treeviews created # self._treeview = {} # create treeview for the subtree self._wst_treeview = kw.Button( router, text="TreeView", on_press=lambda: _createSubTreeView(self.item, self.context), tooltip="Create and add TreeView for the subtree", ) self._wst_layout_views = widgetArray( router, label="**Create views**", # self._wst_md_views, children=[ self._wst_visjs, self._wst_visjs_ids, self._wst_treeview, ], kind="layout", # "inputgroup", ) # self.wroot.addChild(self._wst_layout_views) # ---------------------------- self._wst_cmframe = kw.Toggle( router, text="CM frame", on_toggle=lambda cstate: _highlightFrames( cstate, [self.item.cmFrame()], # toggle_over_set=True, gui_context=self.context, ), tooltip="Enable the axes for the center of mass frame for the subtree", render_as_button=True, ) # show node frames self._wst_nodes = 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._wst_md_frames = kw.Markdown(router, text="**Frames**", in_line=True) self._wst_layout_frames = widgetArray( router, label="**Frames**", children=[self._wst_cmframe, self._wst_nodes], kind="layout", # "inputgroup", ) # self.wroot.addChild(self._wst_layout_frames) # ---------------------------- self._wst_model = kw.Button( router, text="Model", on_press=lambda: self.item.displayModel(), tooltip="Run displayModel() to produce text display about the SubTree's bodies", ) self._wst_tree = kw.Button( router, text="Tree", on_press=lambda: self.item.dumpTree(), tooltip="Run dumpModel() to produce text display about the SubTree's body topology", ) def _dumpStateCB(): if isinstance(self.item, kd.SubGraph): self.item.graphCoordData().dumpState() else: self.item.treeCoordData().dumpState() self._wst_state = kw.Button( router, text="State", on_press=lambda: _dumpStateCB(), tooltip="Run dumpModel() to produce text display about the SubTree's body topology", ) # self._wst_md_info = kw.Markdown(router, text="**Info**", in_line=True) self._wst_layout_info = widgetArray( router, label="**Info (on terminal)**", # self._wst_md_info, children=[self._wst_model, self._wst_tree, self._wst_state], kind="layout", # "inputgroup", ) # self.wroot.addChild(self._wst_layout_info) self._wst_layout_explore = widgetArray( router, label="Introspect the sub-tree structure and content", children=[ self._wst_layout_articulate, # articulate bodies self._wst_layout_highlight, # hightlight bodies self._wst_layout_frames, # show CM frame, nodes self._wst_layout_views, # create visjs graphs, treeviews self._wst_layout_info, # dump tree, display model ], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wst_layout_explore.setTooltip( "Explore the sub-tree by highlighting and articulating bodies, dumping etc" ) self._wst_layout_explore.setOpen(True) self.wroot.addChild(self._wst_layout_explore) # ---------------------------- # compute forward dynamics self._wst_fwddyn = kw.Button( router, text="Fwd dynamics", on_press=lambda: kd.Algorithms.evalForwardDynamics(self.item), tooltip="Evaluate forward dynamics for this subtree", ) def _toggleAlgorithm(cstate: bool, st: kd.SubTree): if cstate: st.enableAlgorithmicUse() else: st.disableAlgorithmicUse() self._wst_algthmuse = kw.Toggle( router, text="Enable algorithmic", on_toggle=lambda cstate: _toggleAlgorithm(cstate, self.item), tooltip="Enable/disable algorithmic use for the subtree", render_as_button=True, ) 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._wst_debug = kw.Toggle( router, text="Enable 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._wst_layout_algthm = widgetArray( router, label="Forward dynamics", children=[self._wst_algthmuse, self._wst_debug, self._wst_fwddyn], kind="accordion", # "inputgroup", ) self._wst_layout_algthm.setTooltip("Run forward dynamics and examine the results") self.wroot.addChild(self._wst_layout_algthm) # ---------------------------- # dynamics simulation self._dynsim_widgets = DynamicsSimWidgets(self.context) self.wroot.addChild(self._dynsim_widgets._wlayout_dynsim) # ---------------------------- self._stdsfile = "subtreeModel.hdf5" def _setSTModelFile(x): self._stdsfile = x self._wst_dsfile = kw.StringInput( router, "Filename", "The model filename", lambda x: _setSTModelFile(x), rapid_submit=True, ) self._wst_dsfile.setValue(self._stdsfile) # self._wst_dsfile.setSizeClass(kw.SizeClass.MEDIUM) self._stdsprefix = "new_" def _setSTPrefix(x): self._stdsprefix = x self._wst_dsprefix = kw.StringInput( router, "Prefix", "The prefix to apply to the names of the new bodies created from loading the DS file", lambda x: _setSTPrefix(x), rapid_submit=True, ) self._wst_dsprefix.setValue(self._stdsprefix) self._wst_dsprefix.setSizeClass(kw.SizeClass.NARROW) def _toSTDSCB(): from Karana.Dynamics.SOADyn_types import SubTreeDS ds = SubTreeDS.fromSubTree(self.item) ds.toFile(self._stdsfile) self._wst_saveds = kw.Button( router, text="Save", on_press=lambda: _toSTDSCB(), tooltip="Save SubTree model to file", ) def _fromSTDSCB(): from Karana.Dynamics.SOADyn_types import SubTreeDS ds = SubTreeDS.fromFile(self._stdsfile) if self._stdsprefix: ds.renameBodies(self._stdsprefix) ds.toSubTree(self.context.multibody, self.context.multibody.virtualRoot()) _createSubTreeVisJs(self.context.multibody, self.context) self._wst_loadds = kw.Button( router, text="Load", on_press=lambda: _fromSTDSCB(), tooltip="Load SubTree model from file (adds bodies)", ) self._wst_layout_model = widgetArray( router, label="**Model data**", # self._wst_md_views, children=[ self._wst_saveds, self._wst_dsfile, self._wst_loadds, self._wst_dsprefix, ], kind="layout", # "inputgroup", ) # ----------------------- self._stdsinitfile = "subgraphState.hdf5" def _setSTStateFile(x): self._stdsinitfile = x self._wst_dsinitfile = kw.StringInput( router, "State filename", "The input/output state filename", lambda x: _setSTStateFile(x), rapid_submit=True, ) self._wst_dsinitfile.setValue(self._stdsinitfile) def _toSTDSInitCB(): from Karana.Dynamics.SOADyn_types import SubTreeStateDS ds = SubTreeStateDS.fromSubTree(self.item) ds.toFile(self._stdsinitfile) self._wst_savedsinit = kw.Button( router, text="Save", on_press=lambda: _toSTDSInitCB(), tooltip="Save SubTree state to file", ) def _fromSTDSInitCB(): from Karana.Dynamics.SOADyn_types import SubTreeStateDS ds = SubTreeStateDS.fromFile(self._stdsinitfile) ds.toSubTree(self.item) self._wst_loaddsinit = kw.Button( router, text="Load", on_press=lambda: _fromSTDSInitCB(), tooltip="Load SubTree state from file", ) self._wst_layout_state = widgetArray( router, label="**State data**", # self._wst_md_views, children=[ self._wst_savedsinit, self._wst_dsinitfile, self._wst_loaddsinit, ], ) self._wst_layout_ds = widgetArray( router, label="SubTree model and state data to/from file", # self._wst_md_views, children=[self._wst_layout_model, self._wst_layout_state], alignment="column", alignItems="left", kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wst_layout_ds) self._wst_layout_ds.setTooltip( "Save sub-tree model to file, and add a sub-tree of bodies from file. Also save and load state dato to and from a file" ) # ----------------------------------------------------- # ------------------ # create a subtree clone def _cloneCB(): st = self.item.cloneSubTree(f"{self.item.name()}_stclone") _createSubTreeVisJs(st, self.context) self.context.subtrees_tree_view.refresh() self._wst_clone = kw.Button( router, text="Clone SubTree", on_press=lambda: _cloneCB(), tooltip="Create a clone of the subtree", ) # clone the bodies in the subtree def _cloneBodiesCB(): from Karana.Dynamics.SOADyn_types import SubTreehDS ds = SubTreeDS.fromSubTree(self.item) new_st = ds.toSubTree(self.context.multibody, self.context.multibody.virtualRoot()) new_st.resetData() _createSubTreeVisJs(self.context.multibody, self.context) self.context.mbody_tree_view.refresh() self._wst_clone_bodies = kw.Button( router, text="Clone Bodies", on_press=lambda: _cloneBodiesCB(), tooltip="Create all the physical bodies in the subtree", ) # discard the subtree def _discardCB(): if self.context.multibody == self.item: print("WARNING: cannot discard multibody system") return # psg = self.item.parentSubTree() sg = self.item self.context.selection.set(kw.Selection().dump()) kc.discard(sg) """ self.context.selection.set( kw.Selection([kw.Selection.Item(id=self.context.multibody.id())]).dump() ) """ self.context.subtrees_tree_view.refresh() self._wst_discard = kw.Button( router, text="Discard", on_press=lambda: _discardCB(), tooltip="Discard the subtree/subgraph (requires that there not be any other dependencies)", ) self._wst_layout_transform = widgetArray( router, label="Transform the sub-tree", # self._wst_md_views, children=[ self._wst_clone, self._wst_clone_bodies, self._wst_discard, ], kind="accordion", # "inputgroup", ) self._wst_layout_transform.setTooltip( "Transform the system by cloning the sub-tree, its bodies or discarding it." ) self.wroot.addChild(self._wst_layout_transform) # ---------------------------- # change selection to one of the child subtree (drop down, or multiple buttons) # change selection to the parent subtree self._wst_select_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._wst_select_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._wst_select_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._wst_select_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._wst_select_child = kw.Button(router, text="Select child (TBD)", on_press=self._doTbd) # change selection to the parent subtree self._wst_select_parent = kw.Button(router, text="Select parent (TBD)", on_press=self._doTbd) # change selection to a sibling subtree self._wst_select_sibling = kw.Button(router, text="Select sibling (TBD)", on_press=self._doTbd) # change selection to a child body self._wst_select_body = kw.Button(router, text="Select body (TBD)", on_press=self._doTbd) """ # Setup widget topology """ self._wst_md_visjs = kw.Markdown(router, text="**Visjs**", in_line=True) self._wst_layout_visjs = widgetArray(router, label=self._wst_md_visjs, children=[self._wst_visjs]) self.wroot.addChild(self._wst_layout_visjs) """ # self._wst_md_selection = kw.Markdown(router, text="**Selection**", in_line=True) self._wst_layout_select = widgetArray( router, label="Go to a related sub-tree", # self._wst_md_selection, children=[ self._wst_select_up, self._wst_select_left, self._wst_select_right, self._wst_select_down, ], kind="accordion", # "inputgroup", ) self._wst_layout_select.setTooltip("Change selection to other related sub-trees") self.wroot.addChild(self._wst_layout_select) def getSummary(self) -> str: """Get a markdown summary of the current item.""" # result = super().getSummary() result = "" st = self.item result += f"num bodies={st.numBodies()}, num physical bodies={len(st.sortedPhysicalBodiesList())}, nQ={st.nQ()}, nU={st.nU()}</br>" result += f"Algorithmically enabled={self.item.getVars().enabled_algorithmically()}</br>" return result def setup(self, item: kd.SubTree, item_context: kw.Json, /): super().setup(item, item_context) # hide the header is this is the most derived class if not isinstance(item, kd.SubGraph): # self._wst_sgheader.setVisible(False) self._wst_header1.setVisible(False) scene = self.context.scene has_mesh = len(scene.getSceneParts(ks.LAYER_PHYSICAL_GRAPHICS)) > 0 has_stick = 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 has_compound_bodies = item.hasCompoundBodies() is_algorithm_enabled = item.getVars().enabled_algorithmically() self._wst_mesh.setVisible(has_mesh) self._wst_mesh.setValue(has_mesh) self._wst_clone_bodies.setVisible(not has_compound_bodies) # self._wst_stick.setVisible(has_stick) self._wst_stick.setValue(has_stick) self._wst_scale_stick.setVisible(has_stick) self._wst_collision.setVisible(has_collision) self._wst_collision.setValue(False) self._wst_algthmuse.setValue(is_algorithm_enabled) self._wst_select_up.setVisible(has_parent) self._wst_select_left.setVisible(has_parent) self._wst_select_right.setVisible(has_parent) self._wst_select_left.setVisible(has_siblings) self._wst_select_right.setVisible(has_siblings) # self._wst_md_selection.setVisible(has_children) self._wst_select_down.setVisible(has_children) self._wst_debug.setValue(item.enable_dump_dynamics) self.full_Q = self.context.multibody.getQ() self.full_U = self.context.multibody.getU() sp = None self._dynsim_widgets.setup(kc.CppWeakRef(item), sp) """ self._dyn_sp = None self._wst_dynsim_setup.setValue(False) self._wst_dynsim_run.setEnabled(False) self._wst_coord_full_reset.setEnabled(False) self._wst_enable_gravity.setEnabled(True) self._wst_enable_contact.setEnabled(True) """ """ # 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 False: 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._wst_wireframe.setVisible(False) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" if isinstance(item, kd.SubGraph): return False return isinstance(item, self.wrapped()) @register class SubGraphCard(SubTreeCard): # AbstractCard[kd.SubGraph]): # class SubGraphCard(AbstractCard[kd.SubGraph]): """Card to display info about a SubGraph instance.""" @property def label(self) -> str: return "SubGraph" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wsg_header = kw.Markdown(router, "---") self.wroot.addChild(self._wsg_header) self._wsg_header1 = kw.Markdown(router, "**SubGraph**") self.wroot.addChild(self._wsg_header1) # ------------------------------ # articulate all the bodies sequentially with IK solver on (only WebScene) self._wsg_swing_ik = kw.Button( router, text="Sequentially articulate (constrained)", on_press=lambda: _swingHinge( self.item, [x.parentHinge() for x in self.item.sortedBodiesList()], False, self.context, ), tooltip="Sequentially swing all the coordinates in the subgraph while enforcing bilateral constraints", ) # ---------------------------- def _toggleHighlightConstraints( cstate: bool, sg: kd.SubGraph, # bodies: list[kd.PhysicalBody], gui_context: GuiContext, ): """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_context.visjs_servers: # the graph does not exist, create it. It will show the # constraint edges by default. _createSubTreeVisJs(sg, gui_context) return server, _ = gui_context.visjs_servers[sg.id()] new_state = cstate gui_context.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._wsg_constraints = kw.Toggle( router, text="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._wsg_md_articulate = kw.Markdown(router, text="**Articulate**", in_line=True) self._wsg_layout_viz = widgetArray( router, label="Introspect the sub-graph structure and content", children=[self._wsg_swing_ik, self._wsg_constraints], kind="accordion", # "layout" # "inputgroup", ) self._wsg_layout_viz.setTooltip( "Explore the sub-graph by highlighting and articulating bodies with constraints " ) self._wsg_layout_viz.setOpen(True) self.wroot.addChild(self._wsg_layout_viz) # self.wroot.addChild(self._wsg_constraints) # ---------------------------- self._sgdsfile = "subgraphModel.hdf5" def _setSGModelFile(x): self._sgdsfile = x self._wsg_dsfile = kw.StringInput( router, "Model filename", "The input/output DataStruct filename", lambda x: _setSGModelFile(x), rapid_submit=True, ) self._wsg_dsfile.setValue(self._sgdsfile) self._sgdsprefix = "new_" def _setSGPrefix(x): self._sgdsprefix = x self._wsg_dsprefix = kw.StringInput( router, "Prefix", "The prefix to apply to the names of the new bodies created from loading the DS file", lambda x: _setSGPrefix(x), rapid_submit=True, ) self._wsg_dsprefix.setValue(self._sgdsprefix) self._wsg_dsprefix.setSizeClass(kw.SizeClass.NARROW) def _toSGDSCB(): from Karana.Dynamics.SOADyn_types import SubGraphDS ds = SubGraphDS.fromSubGraph(self.item) ds.toFile(self._sgdsfile) self._wsg_saveds = kw.Button( router, text="Save", on_press=lambda: _toSGDSCB(), tooltip="Save SubGraph model to file", ) def _fromSGDSCB(): from Karana.Dynamics.SOADyn_types import SubGraphDS ds = SubGraphDS.fromFile(self._sgdsfile) if self._sgdsprefix: ds.renameBodies(self._sgdsprefix) ds.toSubGraph(self.context.multibody, self.context.multibody.virtualRoot()) _createSubTreeVisJs(self.context.multibody, self.context) self._wsg_loadds = kw.Button( router, text="Load", on_press=lambda: _fromSGDSCB(), tooltip="Load SubGraph model from file (adds bodies and constraints)", ) self._wsg_layout_model = widgetArray( router, label="**Model file**", # self._wsg_md_views, children=[ self._wsg_saveds, self._wsg_dsfile, self._wsg_loadds, self._wsg_dsprefix, ], ) # self.wroot.addChild(self._wsg_layout_ds) # ----------------------- self._sgdsinitfile = "subgraphState.hdf5" def _setSGStateFile(x): self._sgdsinitfile = x self._wsg_dsinitfile = kw.StringInput( router, "State filename", "The input/output state filename", lambda x: _setSGStateFile(x), rapid_submit=True, ) self._wsg_dsinitfile.setValue(self._sgdsinitfile) def _toSGDSInitCB(): from Karana.Dynamics.SOADyn_types import SubGraphStateDS ds = SubGraphStateDS.fromSubGraph(self.item) ds.toFile(self._sgdsinitfile) self._wsg_savedsinit = kw.Button( router, text="Save", on_press=lambda: _toSGDSInitCB(), tooltip="Save SubGraph state to file", ) def _fromSGDSInitCB(): from Karana.Dynamics.SOADyn_types import SubGraphStateDS ds = SubGraphStateDS.fromFile(self._sgdsinitfile) ds.toSubGraph(self.item) self._wsg_loaddsinit = kw.Button( router, text="Load", on_press=lambda: _fromSGDSInitCB(), tooltip="Load SubGraph state from file", ) self._wsg_layout_state = widgetArray( router, label="**State data**", # self._wsg_md_views, children=[ self._wsg_savedsinit, self._wsg_dsinitfile, self._wsg_loaddsinit, ], ) self._wsg_layout_ds = widgetArray( router, label="SubGraph model and state data to/from file", # self._wsg_md_views, children=[self._wsg_layout_model, self._wsg_layout_state], alignment="column", alignItems="left", kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wsg_layout_ds) self._wsg_layout_ds.setTooltip( "Save sub-graph model to file, and add a sub-graph of bodies & constraints from file. Also save and load state dato to and from a file" ) # ------------------------------------ def _flattenCB(sg): if not sg.hasCompoundBodies(): return sg.flattenCompoundBodies() # create a subgraph clone def _cloneCB(): sg = self.item.cloneSubGraph(f"{self.item.name()}_sgclone") _createSubTreeVisJs(sg, self.context) self._wsg_clone = kw.Button( router, text="Clone SubGraph", on_press=lambda: _cloneCB(), tooltip="Create a clone of the subgraph", ) # clone the bodies in the subgraph def _cloneBodiesCB(): from Karana.Dynamics.SOADyn_types import SubGraphDS ds = SubGraphDS.fromSubGraph(self.item) IdMixin.resetAllObjectsFromIds() new_sg = ds.toSubGraph(self.context.multibody, self.context.multibody.virtualRoot()) new_sg.resetData() _createSubTreeVisJs(self.context.multibody, self.context) self._wsg_clone_bodies = kw.Button( router, text="Clone Bodies", on_press=lambda: _cloneBodiesCB(), tooltip="Create all the physical bodies and constraints in the subgraph", ) # highlight bodies in aggregation graph for a constraint self._wsg_flatten = kw.Button( router, text="Flatten compound bodies", on_press=lambda: _flattenCB(self.item), tooltip="Flatten all nested compound bodies in the subgraph", ) # self._wsg_md_flatten = kw.Markdown(router, text="**Flatten**", in_line=True) self._wsg_layout_transform = widgetArray( router, label="Transform the sub-graph", children=[self._wsg_clone, self._wsg_clone_bodies, self._wsg_flatten], kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wsg_layout_transform) self._wsg_layout_transform.setTooltip( "Transform the system by cloning a sub-graph, its bodies or flattening its compound bodies." ) # ------------------- def updateBaumDamping(damping) -> None: self.item.setBaumgarteDamping(damping) def updateBaumStiffness(stiffness) -> None: self.item.setBaumgarteDamping(stiffness) slider_opts = kw.SliderOptions() slider_opts.min = -4 slider_opts.max = 4 slider_opts.log_scale = True self._wsg_baum_damping = kw.Slider( router, text="Damping", on_change=updateBaumDamping, opts=slider_opts ) self._wsg_baum_stiffness = kw.Slider( router, text="Stiffness", on_change=updateBaumStiffness, opts=slider_opts ) self._wsg_baum_damping.setSizeClass(kw.SizeClass.MEDIUM) self._wsg_baum_stiffness.setSizeClass(kw.SizeClass.MEDIUM) self._wsg_baum_sliders = widgetArray( router, [self._wsg_baum_damping, self._wsg_baum_stiffness], alignment="row" ) def toggleBaum(enabled): self.item.setBaumgarteEnabled(enabled) self._wsg_baum_sliders.setEnabled(enabled) self._wsg_baum_enable = kw.Toggle(router, "Enable Baumgarte Stabilization", toggleBaum) self._wsg_baum_group = widgetArray( router, label="Baumgarte Stabilization", kind="accordion", # "inputgroup", children=[self._wsg_baum_enable, self._wsg_baum_sliders], alignment="column", alignItems="left", ) self._wsg_baum_group.setTooltip("Enabled Baumgarte stabilization for the dynamics") self.wroot.addChild(self._wsg_baum_group) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" if isinstance(item, kd.Multibody): result = False else: result = isinstance(item, kd.SubGraph) # self.wrapped()) # print(" BBB", item, self.wrapped(), result) return result def setup(self, item: kd.SubGraph, item_context: kw.Json, /): super().setup(item, item_context) has_constraints = len(item.enabledConstraints()) > 0 has_compound_bodies = item.hasCompoundBodies() # Setup baumgarte settings self._wsg_baum_enable.setValue(item.getBaumgarteEnabled()) self._wsg_baum_sliders.setEnabled(item.getBaumgarteEnabled()) if not km.isNotReadyNaN(item.getBaumgarteDamping()): self._wsg_baum_damping.setValue(item.getBaumgarteDamping()) if not km.isNotReadyNaN(item.getBaumgarteStiffness()): self._wsg_baum_stiffness.setValue(item.getBaumgarteStiffness()) # self._wsg_md_articulate.setVisible(has_constraints) self._wsg_swing_ik.setEnabled(has_constraints) # self._wsg_constraints.setVisible(has_constraints) self._wsg_constraints.setEnabled(has_constraints) if False: # 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._wsg_clone_bodies.setEnabled(not has_compound_bodies) self._wsg_flatten.setEnabled(has_compound_bodies) self._wsg_baum_group.setEnabled(has_constraints) """ # 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._wsg_constraints.setValue(True) @register class MultibodyCard(SubGraphCard): # AbstractCard[kd.Multibody]): """Card to display info about any Multibody-derived object.""" @property def label(self) -> str: return "Mbody" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------------ self._wmb_sgheader = kw.Markdown(router, "---") self.wroot.addChild(self._wmb_sgheader) self._wmb_sgheader1 = kw.Markdown(router, "**Multibody**") self.wroot.addChild(self._wmb_sgheader1) # ----------------------------- # select camera mask def _setCameraMask(cstate, mask): graphics = self.context.graphics camera = graphics.defaultCamera() if not camera: return cmask = camera.getMask() if cstate: cmask |= mask if mask == ks.LAYER_STICK_FIGURE: if not self.context.scene.getSceneParts(ks.LAYER_STICK_FIGURE): self.context.multibody.createStickParts() else: cmask ^= mask camera.setMask(cmask) self._wmb_camera_graphics = kw.Toggle( router, text="Physical", on_toggle=lambda cstate: _setCameraMask(cstate, ks.LAYER_PHYSICAL_GRAPHICS), # on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.TRACE), tooltip="Enable/disable physical graphics meshes in the 3D display", render_as_button=True, ) self._wmb_camera_ornamental = kw.Toggle( router, text="Ornamental", on_toggle=lambda cstate: _setCameraMask(cstate, ks.LAYER_ORNAMENTAL), # on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.TRACE), # tooltip="Change the 3D graphics camera mask", tooltip="Enable/disable ornamental meshes in the 3D display", render_as_button=True, ) self._wmb_camera_collision = kw.Toggle( router, text="Collision", on_toggle=lambda cstate: _setCameraMask(cstate, ks.LAYER_COLLISION), # on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.TRACE), # tooltip="Change the 3D graphics camera mask", tooltip="Enable/disable collision meshes in the 3D display", render_as_button=True, ) self._wmb_camera_stick = kw.Toggle( router, text="Stick", on_toggle=lambda cstate: _setCameraMask(cstate, ks.LAYER_STICK_FIGURE), # on_press=lambda: kc.MsgLogger.changeVerbosity("stdout", kc.LogLevel.TRACE), # tooltip="Change the 3D graphics camera mask", tooltip="Enable/disable stick figure meshes in the 3D display", render_as_button=True, ) self._wmb_layout_camera = widgetArray( router, label="Mesh types", children=[ self._wmb_camera_graphics, self._wmb_camera_ornamental, self._wmb_camera_collision, self._wmb_camera_stick, ], kind="layout", ) # ------------ # shadow parameters self._shadows_radius = 1 self._shadows_pixels = 2048 def _shadowsUpdate(): self.context.graphics._setShadows( np.array([0, 0, -1]), radius=self._shadows_radius, pixels=self._shadows_pixels ) # slider to scale the stick parts slider_opts = kw.SliderOptions() slider_opts.min = -1 slider_opts.max = 1 slider_opts.step = 0.01 slider_opts.log_scale = True def _shadowsRadiusCB(radius): self._shadows_radius = radius _shadowsUpdate() self._wmb_shadows_radius = kw.Slider( router, text="Radius", on_change=_shadowsRadiusCB, opts=slider_opts, ) self._wmb_shadows_radius.setValue(self._shadows_radius) self._wmb_shadows_radius.setTooltip("The shadows radius parameter") self._wmb_shadows_radius.setSizeClass(kw.SizeClass.MEDIUM) def _shadowsPixelsCB(val): self.branch_length = val _shadowsUpdate() self._wmb_shadows_pixels = kw.IntInput( router, "Pixels", tooltip="The shadows pixel parameter", on_change=_shadowsPixelsCB, ) self._wmb_shadows_pixels.setValue(self._shadows_pixels) self._wmb_shadows_pixels.setSizeClass(kw.SizeClass.MEDIUM) self._wmb_layout_shadows = widgetArray( router, label="Shadows settings", children=[ self._wmb_shadows_radius, self._wmb_shadows_pixels, ], kind="layout", ) # ------------ # --------------------------------- # 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._wmb_scale_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._wmb_scale_stick.setValue(1) self._wmb_scale_stick.setSizeClass(kw.SizeClass.MEDIUM) # self.wroot.addChild(self._wmb_scale_stick) # recreate/delete stick figures and scaling def _recreateStickParts(): self.item.removeStickParts() self.item.createStickParts() self._wmb_stick_recreate = kw.Button( router, text="Recreate", on_press=lambda: _recreateStickParts(), tooltip="Recreate the stick parts", ) self._wmb_stick_delete = kw.Button( router, text="Remove", on_press=lambda: self.item.removeStickParts(), tooltip="Delete and remove the stick parts", ) self._wmb_layout_stick = widgetArray( router, label="Stick parts", children=[ self._wmb_scale_stick, self._wmb_stick_recreate, self._wmb_stick_delete, ], kind="layout", ) # ---------------- # set background color - TODO def _background(clr): self.context.graphics._setBackgroundColor(clr) self._wmb_bg_black = kw.Button( router, text="Black", on_press=lambda: _background(ks.Color.BLACK), tooltip="Change the background color to BLACK", ) self._wmb_bg_skyblue = kw.Button( router, text="Skyblue", on_press=lambda: _background(ks.Color.SKYBLUE), tooltip="Change the background color to SKYBLUE", ) self._wmb_bg_palegoldenrod = kw.Button( router, text="Palegoldenrod", on_press=lambda: _background(ks.Color.PALEGOLDENROD), tooltip="Change the background color to PALEGOLDENROD", ) self._wmb_layout_bgclr = widgetArray( router, label="Background color", children=[ self._wmb_bg_black, self._wmb_bg_skyblue, self._wmb_bg_palegoldenrod, ], kind="layout", ) # ---------------- # show the 3D pane def _addWebScene(): self.context.dock.addChild( title="3D View", widget=self.context.graphics_frame, ) self._wmb_webscene = kw.Button( router, text="Show 3D graphics pane", on_press=lambda: _addWebScene(), tooltip="Make the 3D graphics panel visible is missing", ) self._wmb_layout_viz = widgetArray( router, label="Tailor 3D graphics content", children=[ self._wmb_layout_shadows, self._wmb_layout_camera, self._wmb_layout_stick, self._wmb_layout_bgclr, self._wmb_webscene, ], kind="accordion", # "layout", alignment="column", alignItems="left", ) self._wmb_layout_viz.setTooltip("Tailor the 3D graphics content") self._wmb_layout_viz.setOpen(True) self.wroot.addChild(self._wmb_layout_viz) # ----------------------------- def _createFramesGraph(use_names): _createFramesVisJs(use_names, self.context) self._wmb_framesgraph_ids = kw.Button( router, text="Id labels", on_press=lambda: _createFramesGraph(True), tooltip="Create the frames graph with frame ids as labels", ) self._wmb_framesgraph = kw.Button( router, text="Name labels", on_press=lambda: _createFramesGraph(False), tooltip="Create the frames graph with frame names as labels", ) self._wmb_layout_graphs = widgetArray( router, label="Create frames graph", children=[self._wmb_framesgraph, self._wmb_framesgraph_ids], kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wmb_layout_graphs) # ------------------------------------ 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() cegraph.flattenCompoundBodies() _createSubTreeVisJs(cegraph, self.context) _createSubTreeView(cegraph, self.context) # highlight bodies in aggregation graph for a constraint self._wmb_fullce = kw.Button( router, text="TA to CE model ", on_press=lambda: _setupCE(self.item), tooltip="Create CE compound bodies for all the multibody constraints", ) def _toFACB(): self.item.toFullyAugmentedModel() _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) self._wmb_fullyaug = kw.Button( router, text="TA to FA model", on_press=lambda: _toFACB(), tooltip="Convert the tree augmented multibody model into a fully augmnented model", ) def _toTACB(): mb = self.context.multibody mb.ensureHealthy() from KaranaTest.Dynamics.fa2ta import TreeConverter fa2ta = TreeConverter(verbose=True, parent_prefs={}, skip_prefs={}) # breadth first basebds = [] fa2ta.visited = list() fa2ta.todo = mb.sortedPhysicalBodiesList() while fa2ta.todo: while fa2ta.visited: fa2ta.convertBranchBF(basebds) if fa2ta.todo: fa2ta.visited = [fa2ta.todo[0]] mb.ensureHealthy() _createSubTreeVisJs(mb, self.context) self._wmb_ta = kw.Button( router, text="FA to TA model", on_press=lambda: _toTACB(), tooltip="Convert the full augmented multibody model into a tree augmnented model", ) self._wmb_layout_topol = widgetArray( router, label="Transform multibody topology", children=[ self._wmb_fullyaug, self._wmb_ta, self._wmb_fullce, ], # self._wmb_modelscript, self._wmb_mdloutfile], kind="accordion", # "inputgroup", ) self._wmb_layout_topol.setTooltip( "Transform the multibody system for constraint embedding and into a FA model" ) self.wroot.addChild(self._wmb_layout_topol) # ----------------------------- # add bodies self._flex_mode = False self._bd_prefix = "dummy" def _setPrefixCB(val): self._bd_prefix = val self._wmb_bdprefix = kw.StringInput( router, "Prefix", on_change=lambda val: _setPrefixCB(val), rapid_submit=True, ) self._wmb_bdprefix.setTooltip("The prefix to use when creating bodies") self._wmb_bdprefix.setSizeClass(kw.SizeClass.WIDE) self._wmb_bdprefix.setValue(self._bd_prefix) def _flexMode(cstate): self._flex_mode = cstate self._wmb_flex_body = kw.Toggle( router, text="Enable flex bodies", on_toggle=lambda cstate: _flexMode(cstate), tooltip="If on, bodies fill be flexible, else rigid", render_as_button=True, ) mass = 1 dist = 0.2 b2j = km.HomTran(km.UnitQuaternion(0.8, 0, 0.6, 0), np.array((0, dist, 0))) inb2j = km.HomTran(km.UnitQuaternion(0.5, 0.5, 0.5, 0.5), np.array((0, -dist, 0))) axis = np.array((0, 0, 1.0)) body_to_cm = np.array((0.1, 0.11, 0.13)) inertia = np.array(((1, 0, 0), (0, 1, 0), (0, 0, 1))) spI = km.SpatialInertia(mass, body_to_cm, inertia) rigid_bdparams = kd.PhysicalBodyParams( spI, b2j, inb2j, kd.PhysicalHingeParams( hinge_type=kd.HingeType.REVOLUTE, subhinge_params=[kd.PinSubhingeParams(unit_axis=np.array([0.0, 0.0, 1.0]))], ), ) htype = kd.HingeType.REVOLUTE # TODO - Not working current def _addSingleBody(): bdparams = kd.PhysicalBodyParams( spI, km.HomTran(), inb2j, hinge_params=kd.PhysicalHingeParams( hinge_type=kd.HingeType.FULL6DOF, subhinge_params=[kd.Linear3SubhingeParams(), kd.SphericalSubhingeParams()], ), ) bds = kd.PhysicalBody.addSerialChain( self._bd_prefix, 1, self.context.multibody.virtualRoot(), bdparams ) self.context.multibody.ensureHealthy() for bd in bds: hge = bd.parentHinge() hge.coordData().setQ(0) hge.coordData().setU(0) hge.coordData().setT(0) hge.coordData().setUdot(0) bd.setGravAccel([0, 0, 0]) _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) self._wmb_onebody = kw.Button( router, text="Single floating body", on_press=lambda: _addSingleBody(), tooltip="Add a single body", ) # add a tree of bodies self.branch_length = 3 self.nbranches = 2 # 4 self.depth = 2 # 3 def _setBranchLength(val): self.branch_length = val self._wmb_branch_length = kw.IntInput( router, "Branch length", "The branch length for the new serial chain/tree system", on_change=_setBranchLength, ) self._wmb_branch_length.setValue(self.branch_length) self._wmb_branch_length.setSizeClass(kw.SizeClass.NARROW) def _setNbranches(val): self.nbranches = val self._wmb_nbranches = kw.IntInput( router, "NBranches", "The number of branches for the new tree system", on_change=_setNbranches, ) self._wmb_nbranches.setValue(self.nbranches) self._wmb_nbranches.setSizeClass(kw.SizeClass.NARROW) def _setDepth(val): self.depth = val self._wmb_depth = kw.IntInput( router, "Depth", "The depth of the new tree system", on_change=_setDepth, ) self._wmb_depth.setValue(self.depth) self._wmb_depth.setSizeClass(kw.SizeClass.NARROW) # TODO - Not working current def _addSerialChain(): bds = kd.PhysicalBody.addSerialChain( self._bd_prefix, self.branch_length, self.context.multibody.virtualRoot(), rigid_bdparams, ) self.context.multibody.ensureHealthy() for bd in bds: hge = bd.parentHinge() hge.coordData().setQ(0) hge.coordData().setU(0) hge.coordData().setT(0) hge.coordData().setUdot(0) bd.setGravAccel([0, 0, 0]) _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) recreateStickParts(self.item, self.context) self._wmb_serialchain = kw.Button( router, text="Serial chain", on_press=lambda: _addSerialChain(), tooltip="Add a serial chain", ) def _addTree(): bds = kd.PhysicalBody.addTree( self._bd_prefix, self.branch_length, self.nbranches, self.depth, self.context.multibody.virtualRoot(), rigid_bdparams, ) self.context.multibody.ensureHealthy() for bd in bds: hge = bd.parentHinge() hge.coordData().setQ(0) hge.coordData().setU(0) hge.coordData().setT(0) hge.coordData().setUdot(0) bd.setGravAccel([0, 0, 0]) _createSubTreeVisJs(self.context.multibody, self.context) # _createSubTreeView(self.context.multibody, self.context) recreateStickParts(self.item, self.context) # TODO - Not working current self._wmb_bodiestree = kw.Button( router, text="Tree", on_press=lambda: _addTree(), tooltip="Add a tree of bodies", ) # TODO - add way to enter number of bodies, number of branches, depth self._wmb_layout_addbodies = widgetArray( router, label="Add bodies", children=[ self._wmb_bdprefix, self._wmb_onebody, self._wmb_serialchain, self._wmb_bodiestree, ], kind="inputgroup", ) self._wmb_layout_addprops = widgetArray( router, label="Serial Chain/Tree properties", children=[ self._wmb_flex_body, self._wmb_branch_length, self._wmb_nbranches, self._wmb_depth, ], kind="inputgroup", ) self._wmb_layout_procedural = widgetArray( router, label="Procedurally add physical bodies to the multibody system", children=[ self._wmb_layout_addbodies, self._wmb_layout_addprops, ], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wmb_layout_procedural.setTooltip( "Procedurally add single, serial chain, and sub-tree of rigid and deformable bodies to the multibody system" ) self.wroot.addChild(self._wmb_layout_procedural) # ----------------------------- """ self._wmb_gravity = 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._wmb_md_gravity = kw.Markdown(router, text="**Gravity**", in_line=True) self._wmb_layout_grav = widgetArray( router, label="Gravity", children=[self._wmb_gravity], kind="inputgroup", ) self.wroot.addChild(self._wmb_layout_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, item_context: kw.Json, /): super().setup(item, item_context) has_constraints = len(item.enabledConstraints()) > 0 self._wmb_fullce.setEnabled(has_constraints) graphics = self.context.graphics camera = graphics.defaultCamera() if camera: mask = camera.getMask() self._wmb_camera_graphics.setValue(mask & ks.LAYER_PHYSICAL_GRAPHICS) self._wmb_camera_ornamental.setValue(mask & ks.LAYER_ORNAMENTAL) self._wmb_camera_collision.setValue(mask & ks.LAYER_COLLISION) self._wmb_camera_stick.setValue(mask & ks.LAYER_STICK_FIGURE) def isCompatible(self, item: kd.Any) -> bool: """Check whether the Card knows how to display an item.""" result = isinstance(item, kd.Multibody) # self.wrapped()) # print(" CCC Multibody", item, self.wrapped(), result) return result @register class ModelManagerCard(AbstractCard[kd.ModelManager]): """Card to display info about any ModelManager-derived object.""" @property def label(self) -> str: return "ModelManager" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router 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="Trace mode", on_toggle=_toggleTrace, render_as_button=True, tooltip="Enable/disable trace mode for the model manager to display mode related information", ) # Setup widget topology # self._wmd_info = kw.Markdown(router, text="**Info**") self._wlayout_info = widgetArray( router, label="Introspect", children=[self._wtrace], kind="inputgroup", ) self.wroot.addChild(self._wlayout_info) def setup(self, item: kd.ModelManager, item_context: kw.Json, /): super().setup(item, item_context) 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. """ assert sp weakref = kc.CppWeakRef(sp) def inner(): sp = weakref() if sp: if sp.isPaused(): sp.resume() else: sp.pause() else: kc.error("Callback called after StatePropagator was destroyed.") return inner def createAdvanceByCallback( sp: kd.StatePropagator | None, gui_context: GuiContext, time: np.timedelta64, wstatus: kw.Markdown, ): """Callback for a pause/resume button. Parameters ---------- sp : kd.StatePropagator The StatePropagator to pause/resume. gui_context: GuiContext Handle to various GUI helpers time : np.timedelta64 The time to advance by. wstatus : kw.Markdown The markdown widget to display the status. """ assert sp async def task(): color = "red" stxt = "Running ..." status = f'**Status:** <span style="color:{color}">{stxt}</span>' wstatus.setText(status) 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) """ for _ in range(10): print(f"Advancing to {sp.getTime()} ...") sp.advanceBy(time / 10) """ color = "green" stxt = "Idle" status = f'**Status:** <span style="color:{color}">{stxt}</span>' wstatus.setText(status) # Run the task on a background thread to avoid blocking the GUI thread for a long time gui_context.worker.run(task) @register class StatePropagatorCard(AbstractCard[kd.StatePropagator]): """Card to display info about any StatePropagator-derived object.""" @property def label(self) -> str: return "StatePropagator" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ---------------------------- # dynamics simulation self._dynsim_widgets = DynamicsSimWidgets(self.context) self.wroot.addChild(self._dynsim_widgets._wlayout_dynsim) self._dynsim_widgets._wlayout_dynsim.setOpen(True) # ---------------------------- # ---------------------------- self._spdsfile = "statepropagatorModel.hdf5" def _setSPModelFile(x): self._spdsfile = x self._wsp_dsfile = kw.StringInput( router, "Filename", "The model filename", lambda x: _setSPModelFile(x), rapid_submit=True, ) self._wsp_dsfile.setValue(self._spdsfile) self._spdsprefix = "new_" def _setSPPrefix(x): self._spdsprefix = x def _toSPDSCB(): from Karana.Dynamics.SOADyn_types import StatePropagatorDS ds = StatePropagatorDS.fromStatePropagator(self.item) ds.toFile(self._spdsfile) self._wsp_saveds = kw.Button( router, text="Save", on_press=lambda: _toSPDSCB(), tooltip="Save StatePropagator model to file", ) self._wsp_layout_model = widgetArray( router, label="**Model data**", # self._wsp_md_views, children=[ self._wsp_saveds, self._wsp_dsfile, ], kind="layout", # "inputgroup", ) self._wsp_layout_ds = widgetArray( router, label="StatePropagator model data to file", # self._wst_md_views, children=[self._wsp_layout_model], alignment="column", alignItems="left", kind="accordion", # "inputgroup", ) self.wroot.addChild(self._wsp_layout_ds) self._wsp_layout_ds.setTooltip("Save state propagator model to file.") self.wroot.addChild(self._wsp_layout_model) self._wsp_layout_model.setTooltip("Save state propagator model to file.") def getSummary(self) -> str: """Get a markdown summary of the current item.""" result = super().getSummary() st = self.item sp = self.item st = sp.getSubTree() result += f"subtree='{st.name()}', algorthimically enabled={st.getVars().enabled_algorithmically()}</br>" result += f"num states={sp.nstates()}, time={km.ktimeToSeconds(sp.getTime())}</br>" return result def setup(self, item: kd.StatePropagator, item_context: kw.Json, /): super().setup(item, item_context) """ 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.setValue( km.ktimeToSeconds(self.item.getMaxStepSize()), trigger_own_callback=True, ) 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}") """ self._dynsim_widgets.setup(kc.CppWeakRef(item.getSubTree()), kc.CppWeakRef(item)) @register class IntegratorCard(AbstractCard[ki.Integrator]): """Card to display info about any Integrator-derived object.""" @property def label(self) -> str: return "Integrator" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router self._wtitle = kw.Markdown(router, text="") self.wroot.addChild(self._wtitle) def setup(self, item: ki.Integrator, item_context: kw.Json, /): super().setup(item, item_context) def isCompatible(self, item: Any) -> bool: """Check whether the Card 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 CVodeIntegratorCard(AbstractCard[ki.CVodeIntegrator]): """Card to display info about any CVodeIntegrator-derived object.""" @property def label(self) -> str: return "CVodeIntegrator" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # 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 ) slider_opts.min = 1 slider_opts.max = 10 slider_opts.log_scale = False self._nits_slider = kw.Slider( router, "Max NL Iters", lambda new_val: self.item.setMaxNLIters(int(new_val)), slider_opts, ) slider_opts.min = 0 self._alen_slider = kw.Slider( router, "Anderson Length", lambda new_val: self.item.setAndersonLength(int(new_val)), slider_opts, ) slider_opts.min = 0 slider_opts.max = 1 slider_opts.step = 0.01 self._adamp_slider = kw.Slider( router, "Anderson Damping", lambda new_val: self.item.setAndersonDamping(new_val), slider_opts, ) sliders = [ self._atol_slider, self._rtol_slider, self._nits_slider, self._alen_slider, self._adamp_slider, ] for slider in sliders: slider.setSizeClass(kw.SizeClass.MEDIUM) self.wroot.addChild(slider) def setup(self, item: ki.CVodeIntegrator, item_context: kw.Json, /): super().setup(item, item_context) opts = cast(ki.CVodeIntegratorOptions, item.getOptions()) self._atol_slider.setValue(opts.atol) self._rtol_slider.setValue(opts.rtol) self._nits_slider.setValue(opts.max_nl_iters) self._alen_slider.setValue(opts.anderson_length) self._adamp_slider.setValue(opts.anderson_damping) @register class BaseKModelCard(AbstractCard[kmdl.BaseKModel]): """Card to display info about any BaseKModel-derived object.""" @property def label(self) -> str: return "BaseKModel" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # ------------------------ def _toggleDebug(mdl, cstate): # mdl.debug_model = not mdl.debug_model mdl.debug_model = cstate # 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.Toggle( router, text="Debug mode", on_toggle=lambda cstate: _toggleDebug(self.item, cstate), render_as_button=True, tooltip="Enable/disable debug mode for the model to display model execution related information to the terminal", ) # self._wmd_debug = kw.Markdown(router, text="**Debug**", in_line=True) self._wlayout_debug = widgetArray( router, label="Enable/disable debug mode for the model", children=[self._wdebug], kind="accordion", # "inputgroup", ) self._wlayout_debug.setOpen(True) self._wlayout_debug.setTooltip( "Enable/disable debug mode for the model to bring out detailed execution information for the model" ) self.wroot.addChild(self._wlayout_debug) # ------------------------ def _toggleRegistration(cstate): mdl = self.item # reg = mdl.model_manager.getRegisteredModel(mdl.name()) if not cstate: mdl.model_manager.unregisterModel(mdl) else: mdl.model_manager.registerModel(mdl) self._wregister = kw.Toggle( router, text="Enable/diable", on_toggle=lambda cstate: _toggleRegistration(cstate), tooltip="Enable/disalbe the model", ) # Setup widget topology # self._wmd_active = kw.Markdown(router, text="**Activate**", in_line=True) self._wlayout_active = widgetArray( router, label="Enable/disable the model ", children=[self._wregister], kind="accordion", # "inputgroup", ) self._wlayout_active.setTooltip( "Enable/disable the model by registering/unregistering it from the state propagator" ) self.wroot.addChild(self._wlayout_active) # ------------------------ self._wselect_body = kw.Button( router, text="Body", on_press=lambda: _selectObject( self.item.multibodyObjs().physical_bodies[0], self.context.selection ), ) self._wselect_subhinge = kw.Button( router, text="Subhinge body", on_press=lambda: _selectObject( cast( kd.PhysicalHinge, self.item.multibodyObjs().physical_subhinges[0].parentHinge() ) .pnode() .parentBody(), self.context.selection, ), ) self._wselect_node = kw.Button( router, text="Node", on_press=lambda: _selectObject( self.item.multibodyObjs().nodes[0], self.context.selection ), ) self._wselect_subtree = kw.Button( router, text="Subtree", on_press=lambda: _selectObject( self.item.multibodyObjs().subtrees[0], self.context.selection ), ) # self._wmd_select = kw.Markdown(router, text="**Select**", in_line=True) self._wlayout_select = widgetArray( router, label="Model's multibody objects", children=[ self._wselect_body, self._wselect_node, self._wselect_subtree, self._wselect_subhinge, ], kind="accordion", alignItems="left", ) self._wlayout_select.setTooltip("Go to the multibody object in use by the model") self.wroot.addChild(self._wlayout_select) def setup(self, item: kmdl.BaseKModel, item_context: kw.Json, /): super().setup(item, item_context) mbobjs = item.multibodyObjs() self._wselect_body.setEnabled(len(mbobjs.physical_bodies) > 0) self._wselect_node.setEnabled(len(mbobjs.nodes) > 0) self._wselect_subtree.setEnabled(len(mbobjs.subtrees) > 0) self._wselect_subhinge.setEnabled(len(mbobjs.physical_subhinges) > 0) is_enabled = item.model_manager.getRegisteredModel(item.name()) is not None self._wregister.setValue(is_enabled) self._wdebug.setValue(item.debug_model) """ # 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, ) """ def getSummary(self) -> str: item = self.item result = super().getSummary() result += f""" period: {km.ktimeToSeconds(item.getPeriod())}s """ return result @register class PenaltyContactCard(AbstractCard[kmdl.PenaltyContact]): """Card to display info about any PIDModel-derived object.""" @property def label(self) -> str: return "PenaltyContact" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # -------------------------------- # 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 self.item.setCacheContacts(active) # 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._wlayout_viz = widgetArray( router, label="Visualize the model's forces", children=[self._wcfv_toggle, self._wcfv_scale], kind="accordion", # "inputgroup", ) self._wlayout_viz.setOpen(True) self._wlayout_viz.setTooltip( "Enable/disable the visualization of the contact force vectors computed by the model" ) self.wroot.addChild(self._wlayout_viz) # ------------------------------------------- # 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): cast(kcoll.HuntCrossleyContactForce, 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): cast(kcoll.HuntCrossleyContactForce, 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): cast(kcoll.HuntCrossleyContactForce, 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): cast(kcoll.HuntCrossleyContactForce, 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): cast( kcoll.HuntCrossleyContactForce, 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 # pyright: ignore 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 # pyright: ignore self._we_slider = kw.Slider( router, text="e", on_change=lambda e: sete(e), opts=slider_opts, ) self._wlayout_info = widgetArray( router, label="Contact force parameters", children=[ self._wkp_slider, self._wkc_slider, self._wn_slider, self._wmu_slider, self._wlinear_region_tol_slider, self._wdmax_slider, self._we_slider, ], kind="accordion", # "inputgroup", alignment="column", alignItems="left", ) self._wlayout_info.setTooltip("Set the contact force model's parameters") self.wroot.addChild(self._wlayout_info) """ 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, item_context: kw.Json, /): super().setup(item, item_context) # 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) frcmdl = item.getContactForceModel() from Karana.Collision import HuntCrossleyContactForce, DampedContactForce possible_params = ["kp", "kc", "n", "mu", "linear_region_tol", "dmax", "e"] params_damped = possible_params params_hc = ["kp", "kc", "n", "mu", "linear_region_tol"] params_dict = {p: False for p in possible_params} # Set-specific values if isinstance(frcmdl, DampedContactForce): # 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_damped: params_dict[k] = True if isinstance(frcmdl, HuntCrossleyContactForce): # 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 SpringDamperCard(AbstractCard[kmdl.SpringDamper]): """Card to display info about any PIDModel-derived object.""" @property def label(self) -> str: return "SpringDamper" def __init__(self, context: GuiContext): super().__init__(context) # Create widgets router = context.router # 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._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, item_context: kw.Json, /): super().setup(item, item_context) # 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, ) # @register # class PIDModelCard(AbstractCard[kmdl.PID]): # """Card to display info about any PIDModel-derived object.""" # @property # def label(self) -> str: # return "PIDModel" # def __init__(self, context: GuiContext): # super().__init__(context) # # Create widgets # router = context.router # 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._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) -> GuiContext: return self._context def _stepActiveCard(self, incr: int): """Switch to the card incr steps forward from the active card.""" compatible_cards = [card for card in self._cards if card.isCompatible(self._item)] # We need a nonempty list for all of this to work, so catch it # early in case this method somehow gets called in this state. if not compatible_cards: kc.warn("There are no cards compatible with the current item") return try: active_card_index = compatible_cards.index(self._active_card) # Compute next card index, wrapping around at the end new_card_index = (active_card_index + incr) % len(compatible_cards) except ValueError: # Happens if the active card somehow isn't compatible with # the current item. kc.warn("Cannot determine index of the active card") # Just reset back to the first card to get back into a valid # state new_card_index = 0 # Treat this card switch as a user request so that the choice # is sticky self._requestActivateCard(compatible_cards[new_card_index]) def __init__( self, context: GuiContext, cards: Iterable[AbstractCard] | 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 card (eg: to update numerics) self._wrefresh = kw.Button( router, text="Refresh", on_press=lambda: self.updateFor(self.getItem(), self._item_context), tooltip=f"Refresh the contents of this card", ) # Container for the button widgets to select the active card self._wbuttons = kw.Layout(router) self._wbuttons.addDomClass("selection-buttons") # Container for the card widgets self._wcards_area = kw.Layout(router) self._wcards_area.addDomClass("selection-cards") self._wcards = kw.Layout(router) self._button_list = [] """ self._wprev_card = kw.Button( router, text="Card Up ↑", on_press=lambda: self._stepActiveCard(-1) ) self._wnext_card = kw.Button( router, text="Card Down ↓", on_press=lambda: self._stepActiveCard(1) ) """ if cards is None: # By default use all card types that were registered, # sorting based on class hierarchy of their wrapped types, # with less derived coming first. self._cards = [cls(context) for cls in self._hierarchySort(known_card_types)] else: self._cards = list(cards) # Context for the currently selected item self._item_context = None # This is the last card the user requested by clicking a button. # If this card is valid for the current item then it should be # prioritized. This makes the user card selection 'sticky', so # that if the user selects a card, selects an incompatible # item, then goes back to compatible items, the user-selected # card isn't forgotten. self._user_card = None # This is the currently displayed card. It may differ from the # user card if the user card isn't valid for the current # selection. self._active_card = None # Creates a no-argument callable with a card bound to its # closure. This will be used to generate callbacks for buttons, # each using a distinct card. def activateCardClosure(card: AbstractCard): def activateCardClosureInner(): self._requestActivateCard(card) return activateCardClosureInner for card in self._cards: # Hide all cards initially card.wroot.setVisible(False) # Create the buttons to activate each card on_press = activateCardClosure(card) wbutton = kw.Button( router, text=card.label, on_press=on_press, tooltip=f"Switch to the '{card.label}' base class card", ) wbutton.addDomClass("karana-card-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 card in self._cards: self._wcards.addChild(card.wroot) # self._wcards_area.addChild(self._wprev_card) self._wcards_area.addChild(self._wcards) # self._wcards_area.addChild(self._wnext_card) self._wselection.addChild(self._wbuttons) self._wselection.addChild(self._wcards_area) self._wroot.addChild(self._wdefault) self._wroot.addChild(self._wselection) def getItem(self) -> Any | None: return getattr(self, "_item", None) def _requestActivateCard(self, card: AbstractCard): """Request from user that a card should be activated. This will store the requested card so that it will be active whenever it is compatible with the currently selected item. """ self._user_card = card # Only allow a card if it's compatible with the current item, # typically meaning it's for the current item's type or a # subclass. if not card.isCompatible(self.getItem()): return self._activateCard(card) def _activateCard(self, card: AbstractCard | None): """Display the given card. If card is None, hide all cards. This can happen in the edge case where the selected item has no compatible cards. Note that this function assumes the card is compatible with the current item, so this must be checked before calling. """ if self._active_card == card: return old_card = self._active_card self._active_card = card # Update card selection button styling so the one for the # active card is visually distinct for wbutton, candidate_card in zip(self._button_list, self._cards, strict=True): if candidate_card == old_card: wbutton.removeDomClass("karana-card-choice-selected") if candidate_card == self._active_card: wbutton.addDomClass("karana-card-choice-selected") if card is None: # Only doing this now since we are about to return. # Otherwise we should wait until the new card is ready to # display to minimize the amount of time that no card is # visible. # We don't need to check old_card is not None, because if it was None, # then old_card == card and we would have returned above cast(AbstractCard, old_card).wroot.setVisible(False) return # Update the new card and switch to it try: card.updateFor(self.getItem(), self._item_context) except Exception: cls_name = type(card).__name__ msg = f"Error updating {cls_name}:\n{traceback.format_exc()}" kc.error(msg) self.context.signal_error() finally: card.item = self.getItem() if old_card is not None: # Teardown the item on the old card to clean up any side-effects if old_item := old_card.getItem(): old_card.teardown(old_item) old_card.wroot.setVisible(False) card.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 card for the given item.""" old_item = self.getItem() 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 card self._wselection.setVisible(False) self._wdefault.setVisible(True) self._activateCard(None) return # Might need to switch cards based on compatibility. First check # if the user's preferred card is compatible with the new item. new_active_card = None if self._user_card and self._user_card.isCompatible(item): new_active_card = self._user_card if new_active_card is None: # Use the most derived card that is compatible for card in reversed(self._cards): if card.isCompatible(item): new_active_card = card break # At this point, new_active_card could still be None if # no card is compatible. This is supported by # _activateCard and should hide all cards. if new_active_card is not None and new_active_card == self._active_card: try: cast(AbstractCard, self._active_card).updateFor(item, item_context) except Exception: cls_name = type(self).__name__ msg = f"Error updating {cls_name}:\n{traceback.format_exc()}" kc.error(msg) self.context.signal_error() finally: cast(AbstractCard, self._active_card).item = item else: self._activateCard(new_active_card) # Only show activation buttons for compatible cards for wbutton, card in zip(self._button_list, self._cards, strict=True): wbutton.setVisible(card.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 addCard(self, card: AbstractCard): """Add an AbstractCard to be shown for compatible items.""" # Add the card to our bookkept list of cards self._cards.append(card) card.wroot.setVisible(False) # Create the buttons to activate the card wbutton = kw.Button( self.context.router, text=card.label, on_press=lambda: self._requestActivateCard(card), tooltip=f"Switch to the '{card.label}' base class card", ) wbutton.addDomClass("karana-card-choice") # Only show the button right away if its card is compatible with the current item wbutton.setVisible(card.isCompatible(self.getItem())) # Add the button to our bookkept list of card 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 card to the layout widget so it can be seen on the frontend self._wcards.addChild(card.wroot) def close(self): """Do any necessary cleanup.""" self.updateFor(None, None) for card in self._cards: card.close() self._cards = []