Source code for Karana.KUtils.MultibodyWebUI._main

# 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 the MultibodyWebUI class."""

from typing import Optional, Literal, TYPE_CHECKING, cast, TYPE_CHECKING
import Karana.Dynamics as kd
import Karana.Core as kc
import Karana.Scene as ks
import Karana.WebUI as kw
import Karana.Frame as kf
import Karana.Models as kmdl
import Karana.KUtils.visjs as vjs

from ._infopanel import InfoPanel, Context, createPauseCb
from ._treeview import (
    SubTreeTreeViewOptions,
    createSubTreeTreeView,
    createSubTreeBodiesTreeView,
    createSubTreesTreeView,
    createScenesTreeView,
    createFramesTreeView,
    createStatePropagatorTreeView,
    createActiveContactNodesTreeView,
    createBilateralConstraintsTreeView,
)
from ._effects import (
    EffectManager,
    EffectItem,
    OutlineParams,
    OutlineLevel,
    AxesParams,
    GraphEdgeParams,
)
from ._helpers import widgetArray

__all__ = ["MultibodyWebUI"]

if TYPE_CHECKING:
    from Karana.KUtils.Sim import Sim


[docs] class MultibodyWebUI: """MultibodyWebUI class. This class creates a Multibody-centric web-based GUI """ def __init__( self, mbody: kd.Multibody, *, port: int = 29534, stick_parts: Literal["auto", "always", "never"] = "auto", stick_parts_config: Optional[kd.StickPartsConfig] = None, graphics_origin_frame: kf.Frame | None = None, name_to_label_map: dict[str, str] | None = None, sp: kd.StatePropagator | None = None, sim: "Sim | None" = None, time_display_period: float | None = 0.01, ): """Create a MultibodyWebUI instance. Parameters ---------- mbody: Multibody The Multibody to create a GUI for port: int The port to run the GUI server on. Defaults to 29534. stick_parts : Literal["auto", "always", "never"] = "auto" Policy for creating stick parts. Defaults to "auto". "auto": create stick parts if the ProxyScene is empty "always": unconditionally create stick parts "never": unconditionally don't create stick parts stick_parts_config : StickPartsConfig Configuration parameters for the stick parts. graphics_origin_frame: kf.Frame | None If given, and the GUI needs to setup graphics, use this as the origin frame instead of the multibody vroot. name_to_label_map: dict[str, str] | None Dictionary defining the labels to use for each body in the visjs graphs display sp: StatePropagator | None The state propagator to use if available. sim : Sim | None The Sim instance if available time_display_period: float | None If not None and sp is given, add a time display refreshing with this period in simulation time. Defaults to 0.01. """ self.sim = sim # the Sim instance alread has sp and mbody members, and if # the sim is specified, use the members if sim: if sp and not sp.id() == sim.sp.id(): raise ValueError( "The sim state propagator and state propagator given are not consistent." ) self.sp = sim.sp if mbody.id() != sim.mb.id(): raise ValueError("The sim multibody and multibody given are not consistent.") self.mbody = sim.mb else: self.mbody = mbody self.sp = sp self.cleanups = kc.VoidCallbackRegistry() # main server setup self.server = self._setupServer(port=port) self.router = kw.Router(self.server) # Inject custom CSS into the document self.router.channel() # Shared state for the selected item(s) self.selection: kw.State = kw.State(self.router) # Shared state for the hovered item self.hovered: kw.State = kw.State(self.router) # can be one of ScenePart, Frame, Body to control what gets # selected when a geometry is clicked on self.webscene_selection_mode = "Body" # 3d graphics setup self.scene: ks.ProxyScene = self._ensureScene() self.graphics: ks.WebScene = self._ensureGraphics(graphics_origin_frame) self.graphics_frame = kw.IFrame(self.router, self.graphics.server().guessUrl()) # create stick parts if requested should_create_stick_parts = False if stick_parts == "auto": should_create_stick_parts = self.scene.empty() elif stick_parts == "always": should_create_stick_parts = True if should_create_stick_parts: self.mbody.createStickParts(stick_parts_config or kd.StickPartsConfig()) # Effect helpers setup self.effects = EffectManager(graphics=self.graphics, scene=self.scene) self.cleanups["cleanupEffectManager"] = self.effects.close # visjs graph viz setup self.visjs_label_map = name_to_label_map self.visjs_server: vjs.MultibodyGraphServer = self._setupVisjs(self.mbody) self.visjs_frame = kw.IFrame(self.router, self.visjs_server.getUrl()) # keep track of all SubTree graph servers along with the status # of whether constraints are on or off self.visjs_servers: dict[int, tuple[vjs.MultibodyGraphServer, bool]] = { self.mbody.id(): (self.visjs_server, True) } # tree view setup self.mbody_tree_view = self._setupMultibodyTreeView() self.all_tree_view = self._setupSubTreeTreeView( options=SubTreeTreeViewOptions( subtree_bodies=True, subtree_subtrees=True, body_nodes=True, body_scene_parts=True, loop_constraints=True, collapse_depth=1, ) ) # TODO - for some reason we need to create this for the menu # SubTree option to work properly. If we do not call the below, # selections made from the menu created SubTree tree view seg # fault if self.mbody.childrenSubTrees(): self._setupSubTreeTreeView(options=SubTreeTreeViewOptions(subtree_subtrees=True)) """ # TODO - we should really use the following since it has refresh # capability, but it is segfaulting for some reason self._setupSubTreeTreeView( options=SubTreeTreeViewOptions(subtree_bodies=True) ) """ """ self.subtrees_tree_view = self._setupSubTreesTreeView() """ """ if self.mbody.getScene(): self.scenes_tree_view = self._setupScenesTreeView() self.contact_nodes_tree_view = None if self.mbody.getScene(): css = self.mbody.getScene().clientScenes() from Karana.Scene import CoalScene for cs in css: if isinstance(cs, CoalScene): self.contact_nodes_tree_view = self._setupActiveContactNodesTreeView(self.mbody) break self.biconstraints_tree_view = None if len(self.mbody.enabledConstraints()) > 0: self.biconstraints_tree_view = self._setupBilateralConstraintsTreeView(self.mbody) if sp is not None: self.sp_tree_view = self._setupStatePropagatorTreeView( sp=sp, options=SubTreeTreeViewOptions( subtree_bodies=True, subtree_subtrees=True, body_nodes=True, body_scene_parts=True, loop_constraints=True, ), ) self.frames_tree_view = self._setupFramesTreeView() """ self.wmain = kw.Layout( self.router, style={"display": "flex", "flexDirection": "column", "height": "100vh"} ) # ------------------------------------ # menu in toolbar to add different TreeViews # self.treeviews_options = ['SubTrees', 'Scenes', 'ALL', 'Frames', 'StatePropagator', 'Constraints', 'Contact nodes'] self.treeviews_options = [ "Frames", ] if 1 or self.mbody.childrenSubTrees(): self.treeviews_options.append("SubTrees") if self.sim: self.treeviews_options.append("Sim") if sp: self.treeviews_options.append("StatePropagator") if self.mbody.enabledConstraints(): self.treeviews_options.append("Constraints") if ps := self.mbody.getScene(): self.treeviews_options.append("Scenes") self.treeviews_options.append("FrameSceneParts") # add contact nodes option if CoalScene is registered css = ps.clientScenes() from Karana.Scene import CoalScene for cs in css: if isinstance(cs, CoalScene): self.treeviews_options.append("Contact nodes") break self.treeviews_options.extend(["ALL"]) self.wtreeviews = kw.Dropdown( self.router, "Tree Views", self.treeviews_options, lambda val: self.treeviews(self.treeviews_options[val]), ) webscene_selection_modes = ["Frame", "Body", "ScenePart"] # callback for the 3D pick mode menu def _websceneSelectionModeCB(val): self.webscene_selection_mode = webscene_selection_modes[val] self.wwebscene_selection_mode = kw.Dropdown( self.router, "3D Selection", webscene_selection_modes, lambda val: _websceneSelectionModeCB(val), ) self.wwebscene_selection_mode.setIndex(1) # ------------------------------------ self.wdeselect = kw.Button(self.router, "Deselect", lambda: self.deselect()) self.wreset_viz = kw.Button(self.router, "Clear Viz Effects", lambda: self.effects.clear()) self.wtoolbar = widgetArray( self.router, children=[ self.wdeselect, self.wreset_viz, self.wtreeviews, self.wwebscene_selection_mode, ], ) if sp is not None: self.wpause = kw.Button(self.router, "\u23f8/\u25b6", createPauseCb(sp)) self.wtoolbar.addChild(self.wpause) # dock layout setup self.dock = kw.Dock(self.router, style={"flexGrow": "1"}) self.dock.addChild( title="3D View", widget=self.graphics_frame, ) self.dock.addChild( title="Multibody", widget=self.mbody_tree_view, relative_to=self.graphics_frame, direction="right", ) self.dock.addChild( title=self.mbody.name(), # "Graph View", widget=self.visjs_frame, relative_to=self.graphics_frame, direction="below", ) # selection info panel setup self.info_panel = self._setupInfoPanel(self.selection) self.selection.onChange(lambda x: self.updateInfoPanel(x, self.info_panel)) self.dock.addChild( title="Selection Info", widget=self.info_panel.wroot, relative_to=self.mbody_tree_view, direction="below", ) self.dock.addChild( title="ALL", widget=self.all_tree_view, relative_to=self.mbody_tree_view, direction="within", ) self.wmain.addChild(self.wtoolbar) self.wmain.addChild(self.dock) self.wmain.addToDomRoot() self.cleanups["clobberFields"] = self._clobberFields if sp: self._setupModels(sp, time_display_period) """ # doing this here because of circular imports issue from ._infopanel import known_pane_types, SimpleSpdpPane # from Karana.Adams.Adams_Py import SimpleSpdp known_pane_types.append(SimpleSpdpPane) """
[docs] def setAxesGlobalScale(self, part: float, line: float | None = None): """Set the global scaling factor for axes.""" self.effects.frame_axes.part_scale = part if not line: line = part self.effects.frame_axes.line_scale = line
[docs] def deselect(self): """Make it so nothing is selected.""" self.selection.set(kw.Selection().dump())
[docs] def treeviews(self, selected_label): """Create the selected TreeView.""" tv = None if selected_label == "SubTrees": """ tv = self._setupSubTreeTreeView( options=SubTreeTreeViewOptions(subtree_subtrees=True) ) """ tv = self._setupSubTreesTreeView() elif selected_label == "Scenes": tv = self._setupScenesTreeView() elif selected_label == "ALL": tv = self._setupSubTreeTreeView( options=SubTreeTreeViewOptions( subtree_bodies=True, subtree_subtrees=True, body_nodes=True, body_scene_parts=True, loop_constraints=True, collapse_depth=1, ) ) elif selected_label == "Frames": tv = self._setupFramesTreeView() elif selected_label == "FrameSceneParts": from ._treeview import createFrameScenePartsTreeView tv = createFrameScenePartsTreeView( router=self.router, f=self.mbody.frameContainer().root(), scene=self.scene, gui_selection_state=self.selection, ) elif selected_label == "Sim": assert self.sim from ._treeview import createSimTreeView tv = createSimTreeView( router=self.router, sim=self.sim, gui_selection_state=self.selection ) elif selected_label == "StatePropagator": tv = self._setupStatePropagatorTreeView( sp=self.sp, options=SubTreeTreeViewOptions( subtree_bodies=True, subtree_subtrees=True, body_nodes=True, body_scene_parts=True, loop_constraints=True, ), ) elif selected_label == "Constraints": tv = self._setupBilateralConstraintsTreeView(self.mbody) elif selected_label == "Contact nodes": tv = self._setupActiveContactNodesTreeView(self.mbody) else: print("MMM", selected_label) if tv: self.dock.addChild( title=selected_label, # "Bilateral constraints", widget=tv, relative_to=self.mbody_tree_view, direction="within", ) tv.refresh()
def _clobberFields(self): """Drop references to fields so they can be discarded.""" del ( self.mbody_tree_view, # self.subtrees_tree_view, self.all_tree_view, self.info_panel, self.visjs_frame, self.router, self.mbody, self.server, self.graphics, self.scene, self.dock, self.graphics_frame, self.visjs_server, self.wtoolbar, self.wmain, self.sp, self.sim, ) if hasattr(self, "wpause"): del self.wpause import gc gc.collect() def _setupServer(self, port: int) -> kw.HttpWsServer: # main server setup server = kw.HttpWsServer(port=port) frontend = kc.findShareDir() / "WebUI" / "frontend" server.serveFile("/", frontend / "index.html") server.serveFile("/main.js", frontend / "router-main-bundle.js") server.serveFile("/style.css", frontend / "style.css") # Disabling this. If outside code adds extra widgets not # managed by this class, they'll need the server to keep # running. # self.cleanups["cleanupServer"] = server.close return server def _ensureScene(self) -> ks.ProxyScene: mbody = self.mbody if not mbody: raise ValueError("GUI has been closed") scene = mbody.getScene() if scene is None: # Scene doesn't already exist, so we will create it scene = ks.ProxyScene("webui_auto_scene", mbody.virtualRoot()) # Add a callback to cleanup the Scene we created def clearScene(): mbody.setScene(None) kc.discard(scene) self.cleanups["clearScene"] = clearScene # Add the new scene to the multibody mbody.setScene(scene) return scene def _ensureGraphics(self, origin_frame: kf.Frame | None) -> ks.WebScene: if not self.scene or not self.mbody: raise ValueError("GUI has been closed") mbid = self.mbody.id() # Scan for an existing WebScene instances for client in self.scene.clientScenes(): if isinstance(client, ks.WebScene): break else: # No existing WebScene, so we create one client = ks.WebScene("webui_auto_web_scene", kw.Server(port=0)) # Add the WebScene as a client of ProxyScene self.scene.registerClientScene(client, origin_frame or self.mbody.virtualRoot()) scene = self.scene def clearGraphics(): scene.unregisterClientScene(client) kc.discard(client) self.cleanups["clearGraphics"] = clearGraphics def onPick(part: ks.WebScenePart | None): # If nothing was picked, clear the selection state if not part: self.selection.set(kw.Selection().dump()) return proxy_part = self.scene.lookupProxyFromImpl(part) if self.webscene_selection_mode == "Frame": obj = proxy_part.ancestorFrame() elif self.webscene_selection_mode == "Body": frame = proxy_part.ancestorFrame() nd = self.mbody.getNodeAncestor(frame) if not nd: obj = self.mbody.virtualRoot() else: obj = nd.parentBody() elif self.webscene_selection_mode == "ScenePart": obj = proxy_part else: raise ValueError("Cannot determine webscene selection mode.") selection = kw.Selection( items=[kw.Selection.Item(id=obj.id(), context={"subtree_id": mbid})] ) self.selection.set(selection.dump()) def onHover(part: ks.WebScenePart | None): # If nothing was hovered, clear the hovered state if not part: self.hovered.set(kw.Selection().dump()) return proxy_part = self.scene.lookupProxyFromImpl(part) if self.webscene_selection_mode == "Frame": obj = proxy_part.ancestorFrame() elif self.webscene_selection_mode == "Body": frame = proxy_part.ancestorFrame() nd = self.mbody.getNodeAncestor(frame) if not nd: obj = self.mbody.virtualRoot() else: obj = nd.parentBody() elif self.webscene_selection_mode == "ScenePart": obj = proxy_part else: raise ValueError("Cannot determine webscene selection mode.") self.hovered.set( kw.Selection( items=[kw.Selection.Item(id=obj.id(), context={"subtree_id": mbid})] ).dump() ) client.setOnPick(onPick) client.setOnHover(onHover) # Create a weak ref for the closure to simplify cleanup client_weakref = kc.CppWeakRef(client) hover_overlay_id = client.addOverlayText("", 0.5, 0.05, ks.Alignment.CENTER) def _handleHoverChange(raw: kw.Json): # Display the name of the currently hovered item as overlay text selection = kw.Selection.parse(raw) if not selection.items: if client := client_weakref(): client.setOverlayText(hover_overlay_id, "") client.pushChanges() return item = selection.items[0] base_container = kc.BaseContainer.singleton() try: obj = base_container.at(item.id) except IndexError: return if client := client_weakref(): client.setOverlayText(hover_overlay_id, f"Hover: {obj.name()}") client.pushChanges() self.hovered.onChange(_handleHoverChange) def _bodyOutlineEffects(bd: kd.PhysicalBody, level: OutlineLevel = "primary"): # helper method to get list of outline effect items for a # physical body that include stick parts as well if bd.isRootBody(): return [] outline_effects = [ EffectItem(obj=bd, params=OutlineParams(level=level)), # include the below so stick parts also get # outline since they are connected to the pnode EffectItem(obj=bd.pnode(), params=OutlineParams(level=level)), ] # add these to include stick parts attached to the body nodes for nd in bd.nodeList(): outline_effects.append(EffectItem(obj=nd, params=OutlineParams(level=level))) for nd in bd.constraintNodeList(): outline_effects.append(EffectItem(obj=nd, params=OutlineParams(level=level))) return outline_effects def _update(raw: kw.Json): # TODO maybe this should be move somewhere after both the # effects helpers and graphics are setup selection = kw.Selection.parse(raw) if not selection.items: self.effects.part_outliner.clear() self.effects.frame_axes.clear() for item in selection.items: # FIXME: this will currently clobber effects for all but the last selected item base_container = kc.BaseContainer.singleton() try: obj = base_container.at(item.id) except IndexError: return # add appropriate effects based on object type if isinstance(obj, kd.PhysicalBody): outline_effects = _bodyOutlineEffects(obj, "primary") self.effects.frame_outliner.set(outline_effects) frm = obj if obj.isRootBody() else obj.pnode() self.effects.frame_axes.set([EffectItem(obj=frm, params=AxesParams())]) elif isinstance(obj, kf.Frame): # show the frame axes self.effects.frame_outliner.clear() self.effects.frame_axes.set([EffectItem(obj=obj, params=AxesParams())]) elif isinstance(obj, ks.ProxyScenePart): # show the outline of this scene part self.effects.part_outliner.set( [EffectItem(obj=obj, params=OutlineParams(level="primary"))] ) frame = obj.ancestorFrame() self.effects.frame_axes.set([EffectItem(obj=frame, params=AxesParams())]) elif isinstance(obj, kd.SubTree): # show the outline for all bodies in the subtree (TODO) outline_effects = [] for bd in obj.sortedPhysicalBodiesList(): outline_effects.extend(_bodyOutlineEffects(bd, level="primary")) self.effects.frame_outliner.set(outline_effects) self.effects.frame_axes.clear() elif isinstance(obj, kd.CompoundBody): # show the outline for all bodies in the compound # body's physical bodies subtree outline_effects = [] for bd in obj.physicalBodiesTree().sortedPhysicalBodiesList(): outline_effects.extend(_bodyOutlineEffects(bd, level="primary")) self.effects.frame_outliner.set(outline_effects) self.effects.frame_axes.clear() elif isinstance(obj, kd.LoopConstraintBase): # highlight the physical bodies involved src_node = obj.sourceNode() tgt_node = obj.targetNode() if src_node is not None: mb = src_node.parentBody().multibody() else: mb = cast(kd.ConstraintNode, tgt_node).parentBody().multibody() src_bd = mb.virtualRoot() if not src_node else src_node.parentBody() tgt_bd = mb.virtualRoot() if not tgt_node else tgt_node.parentBody() outline_effects = _bodyOutlineEffects( src_bd, level="primary" ) + _bodyOutlineEffects(tgt_bd, level="secondary") self.effects.frame_outliner.set(outline_effects) # show axes for the frames involved cf2f = obj.constraintFrameToFrame() self.effects.frame_axes.set( [ EffectItem(obj=cf2f.oframe(), params=AxesParams()), EffectItem(obj=cf2f.pframe(), params=AxesParams()), ] ) elif isinstance(obj, kd.CoordinateConstraint): # highlight the physical bodies involved oshg = obj.osubhinge() src_bd = cast(kd.PhysicalHinge, oshg.parentHinge()).pnode().parentBody() pshg = obj.psubhinge() tgt_bd = cast(kd.PhysicalHinge, pshg.parentHinge()).pnode().parentBody() outline_effects = _bodyOutlineEffects( src_bd, level="primary" ) + _bodyOutlineEffects(tgt_bd, level="secondary") self.effects.frame_outliner.set(outline_effects) # show axes for the frames involved self.effects.frame_axes.set( [ EffectItem(obj=oshg.pframe(), params=AxesParams()), EffectItem(obj=pshg.pframe(), params=AxesParams()), ] ) elif isinstance(obj, kd.PhysicalSubhinge): # highlight the physical bodies involved hge = cast(kd.PhysicalHinge, obj.parentHinge()) outline_effects = _bodyOutlineEffects( hge.onode().parentBody(), "primary" ) + _bodyOutlineEffects(hge.pnode().parentBody(), "secondary") self.effects.frame_outliner.set(outline_effects) # highlight the frames involved self.effects.frame_axes.set( [ EffectItem(obj=obj.oframe(), params=AxesParams()), EffectItem(obj=obj.pframe(), params=AxesParams()), ] ) elif isinstance(obj, kmdl.BaseKModel): # show the outline for all bodies involved with the KModel, and axes for all the nodes outline_effects = [] axes_effects = [] for bd in obj.multibodyObjs().physical_bodies: outline_effects.append( EffectItem(obj=bd, params=OutlineParams(level="primary")) ) for nd in obj.multibodyObjs().nodes: axes_effects.append(EffectItem(obj=nd, params=AxesParams())) outline_effects.append( EffectItem(obj=nd.parentBody(), params=OutlineParams(level="primary")) ) for sh in obj.multibodyObjs().physical_subhinges: outline_effects.append( EffectItem( obj=cast(kd.PhysicalHinge, sh.parentHinge()).pnode().parentBody(), params=OutlineParams(level="primary"), ) ) for st in obj.multibodyObjs().subtrees: for bd in st.sortedPhysicalBodiesList(): outline_effects.append( EffectItem(obj=bd, params=OutlineParams(level="primary")) ) self.effects.frame_outliner.set(outline_effects) self.effects.frame_axes.set(axes_effects) self.selection.onChange(_update) return client def _setupVisjs(self, st: kd.SubTree) -> vjs.MultibodyGraphServer: if not self.mbody: raise ValueError("GUI has been closed") selection_state = self.selection st_id = st.id() class ServerWithSelectCallback(vjs.MultibodyGraphServer): def onClickNode(self, client_id: int, node_id: int | str): try: int_id = int(node_id) except ValueError: return selection = kw.Selection( items=[kw.Selection.Item(id=int_id, context={"subtree_id": st_id})] ) selection_state.set(selection.dump()) def onClickEdge(self, client_id: int, edge_id: int | str): try: int_id = int(edge_id) except ValueError: return selection = kw.Selection( items=[kw.Selection.Item(id=int_id, context={"subtree_id": st_id})] ) selection_state.set(selection.dump()) server = ServerWithSelectCallback( subgraph=st, # self.mbody, label_map=self.visjs_label_map, buttons=None, extra_edges=None, port=0, title=f"{st.name()} System", ) # register this graph server with the graph effects helpers self.effects.registerGraphServer(server) # enable constraints by default in the graph server (can toggle # from subtree info panel) if server.hasSubGraph("constraints"): server.enableSubGraph("constraints") def _setGraphSelection(raw: kw.Json): """Update the multibody visjs graph for the newly selected object.""" selection = kw.Selection.parse(raw) if not selection.items: self.effects.graph_edge_adder.clear() self.effects.graph_highlighter.clear() # helper method to emphasize a model in mbody visjs graph def _emphasizeBaseKModel(mdl): # show the outline for all bodies involved with the KModel, and axes for all the nodes emphasis = [] mbobjs = mdl.multibodyObjs() emphasis.extend(_emphasizeBodies(mbobjs.physical_bodies)) emphasis.extend(_emphasizeBodies([x.parentBody() for x in mbobjs.nodes])) emphasis.extend( _emphasizeBodies( [x.parentHinge().pnode().parentBody() for x in mbobjs.physical_subhinges] ) ) for st in mbobjs.subtrees: emphasis.extend(_emphasizeBodies(st.sortedPhysicalBodiesList())) self.effects.frame_outliner.set(emphasis) return emphasis # helper method to emphasize bodies in mbody visjs graph def _emphasizeBodies( primary: list[kd.PhysicalBody], secondary: list[kd.PhysicalBody] = [], tertiary: list[kd.PhysicalBody] = [], ): # highlight the subtree physical bodies in the multibody visjs graph for bd in primary: emphasis.append(EffectItem(obj=bd, params=OutlineParams(level="primary"))) for bd in secondary: emphasis.append(EffectItem(obj=bd, params=OutlineParams(level="secondary"))) for bd in tertiary: emphasis.append(EffectItem(obj=bd, params=OutlineParams(level="tertiary"))) return emphasis emphasis = [] from Karana.Adams import SimpleSpdp for item in selection.items: base_container = kc.BaseContainer.singleton() try: obj = base_container.at(item.id) except IndexError: continue if isinstance(obj, kd.SubTree): # highlight the subtree physical bodies in the multibody visjs graph # generalize to handle compound body virtual roots (TODO) bds = obj.sortedPhysicalBodiesList() emphasis = _emphasizeBodies(primary=bds, secondary=[obj.virtualRoot()]) elif isinstance(obj, kd.CompoundBody): # highlight the embedded physical bodies emphasis = _emphasizeBodies( primary=[obj] + obj.physicalBodiesTree().sortedPhysicalBodiesList(), # secondary=[obj.physicalParentBody()], ) elif isinstance(obj, kd.LoopConstraintBase): """ # highlight the physical bodies involved src_node = obj.sourceNode() tgt_node = obj.targetNode() if src_node is not None: mb = src_node.parentBody().multibody() else: mb = cast(kd.ConstraintNode, tgt_node).parentBody().multibody() src_bd = mb.virtualRoot() if not src_node else src_node.parentBody() tgt_bd = mb.virtualRoot() if not tgt_node else tgt_node.parentBody() #_emphasizeBodies(primary=[src_bd], secondary=[tgt_bd]) params = GraphEdgeParams( # color="#00ff00", color="magenta", dashed=True, id=obj.id(), arrows=True, title=f"'{obj.name()}' ({obj.typeString()})\n the '{src_bd.name()}'\n and '{tgt_bd.name()}'\n bodies", ) graph_edge_emphasis = [EffectItem(obj=[src_bd, tgt_bd], params=params)] self.graph_edge_adder.set(graph_edge_emphasis) """ pass elif isinstance(obj, kd.CoordinateConstraint): """ # highlight the physical bodies involved oshg = obj.osubhinge() src_bd = cast(kd.PhysicalHinge, oshg.parentHinge()).pnode().parentBody() pshg = obj.psubhinge() tgt_bd = cast(kd.PhysicalHinge, pshg.parentHinge()).pnode().parentBody() _emphasizeBodies(primary=[src_bd], secondary=[tgt_bd]) """ pass elif isinstance(obj, kd.PhysicalSubhinge): # highlight the physical bodies involved hge = cast(kd.PhysicalHinge, obj.parentHinge()) emphasis = _emphasizeBodies( primary=[hge.onode().parentBody()], secondary=[hge.pnode().parentBody()] ) self.effects.frame_outliner.set(emphasis) # self.frame_axes.clear() elif isinstance(obj, kmdl.SpringDamper): # show the outline for all bodies involved with the # KModel, and axes for all the nodes mbobjs = obj.multibodyObjs() # emphasis = _emphasizeBaseKModel(obj) nd0 = mbobjs.nodes[0] nd1 = mbobjs.nodes[1] bd0 = nd0.parentBody() bd1 = nd1.parentBody() # visualize the ride spring element via a green dashed line params = GraphEdgeParams( color="#00ff00", dashed=True, id=obj.id(), arrows=True, title=f"'{obj.name()}' ({obj.typeString()}) model connecting\n the '{nd0.name()}/{bd0.name()}'\n and '{nd1.name()}/{bd1.name()}'\n body nodes", ) graph_edge_emphasis = [EffectItem(obj=(bd0, bd1), params=params)] self.effects.graph_edge_adder.set(graph_edge_emphasis) elif isinstance(obj, SimpleSpdp): # show the outline for all bodies involved with the # KModel, and axes for all the nodes mbobjs = obj.multibodyObjs() # emphasis = _emphasizeBaseKModel(obj) nd0 = mbobjs.nodes[0] nd1 = mbobjs.nodes[1] bd0 = nd0.parentBody() bd1 = nd1.parentBody() # visualize the ride spring element via a green dashed line params = GraphEdgeParams( color="#ff0000", dashed=True, arrows=True, id=obj.id(), title=f"'{obj.name()}' ({obj.typeString()}) model connecting\n the '{nd0.name()}/{bd0.name()}'\n and '{nd1.name()}/{bd1.name()}'\n body nodes", ) graph_edge_emphasis = [EffectItem(obj=[bd0, bd1], params=params)] self.effects.graph_edge_adder.set(graph_edge_emphasis) elif isinstance(obj, (kmdl.BaseKModel)): # fallback for a BaseKModel emphasis # show the outline for all bodies involved with the # KModel, and axes for all the nodes emphasis = _emphasizeBaseKModel(obj) else: emphasis.append(EffectItem(obj=obj, params=None)) pass # Update the visjs graph to emphasize whatever is selected self.effects.graph_highlighter.set(emphasis) selection_state.onChange(_setGraphSelection) # Add a callback to cleanup the visjs web server def cleanupVisjsServer(): self.effects.unregisterGraphServer(server) server.close() self.cleanups["cleanupVisjsServer"] = cleanupVisjsServer return server def _setupSubTreeTreeView(self, options: SubTreeTreeViewOptions) -> kw.TreeView: """Create NON-refreshable multibody level subtrees hierarchy.""" if not self.mbody or not self.router: raise ValueError("GUI has been closed") return createSubTreeTreeView( router=self.router, gui_selection=self.selection, subtree=self.mbody, options=options ) def _setupSubTreesTreeView(self) -> kw.TreeView: """Create refreshable multibody subtrees tree view. Selections made from this view are current seg faulting unless the context version of the SubTree tree view is created first. Wierd. TODO - fix. """ if not self.mbody or not self.router: raise ValueError("GUI has been closed") self.mbody.ensureHealthy() return createSubTreesTreeView( router=self.router, gui_selection_state=self.selection, mb=self.mbody, ) def _setupStatePropagatorTreeView( self, sp: kd.StatePropagator, options: SubTreeTreeViewOptions ) -> kw.TreeView: if not self.mbody or not self.router: raise ValueError("GUI has been closed") return createStatePropagatorTreeView( router=self.router, sp=sp, gui_selection_state=self.selection, ) def _setupActiveContactNodesTreeView(self, st: kd.SubTree) -> kw.TreeView: if not self.mbody or not self.router: raise ValueError("GUI has been closed") return createActiveContactNodesTreeView( router=self.router, st=st, gui_selection_state=self.selection ) def _setupBilateralConstraintsTreeView(self, st: kd.SubTree) -> kw.TreeView: if not self.mbody or not self.router: raise ValueError("GUI has been closed") return createBilateralConstraintsTreeView( router=self.router, st=st, gui_selection_state=self.selection ) def _setupScenesTreeView(self) -> kw.TreeView: """Create scene tree view.""" if not self.mbody or not self.router: raise ValueError("GUI has been closed") return createScenesTreeView( router=self.router, gui_selection_state=self.selection, pscene=self.mbody.getScene() ) def _setupFramesTreeView(self) -> kw.TreeView: """Create frame tree view.""" if not self.mbody or not self.router: raise ValueError("GUI has been closed") self.mbody.frameContainer().ensureHealthy() return createFramesTreeView( router=self.router, gui_selection_state=self.selection, f=self.mbody.frameContainer().root(), ) def _setupMultibodyTreeView(self) -> kw.TreeView: """Create multibody tree view.""" if not self.mbody or not self.router: raise ValueError("GUI has been closed") self.mbody.ensureHealthy() return createSubTreeBodiesTreeView( router=self.router, gui_selection_state=self.selection, st=self.mbody, ) def _setupInfoPanel(self, state: kw.State) -> InfoPanel: if not self.mbody or not self.router: raise ValueError("GUI has been closed") context = Context( dock=self.dock, router=self.router, selection=state, # self.selection, multibody=self.mbody, scene=self.scene, graphics=self.graphics, effects=self.effects, mbody_tree_view=self.mbody_tree_view, setup_visjs=self._setupVisjs, setup_info_panel=self._setupInfoPanel, visjs_servers=self.visjs_servers, visjs_frame=self.visjs_frame, graphics_frame=self.graphics_frame, ) info_panel = InfoPanel(context=context) # Register cleanup callback def _cleanup(): info_panel.close() del context.graphics del context.scene del context.multibody self.cleanups["cleanupInfoPanel"] = _cleanup """ def _update(raw: kw.Json): obj = None context = None # Parse the raw json into a Selection struct selection = kw.Selection.parse(raw) if selection.items: base_container = kc.BaseContainer.singleton() try: item = selection.items[0] obj = base_container.at(item.id) context = item.context except IndexError: pass info_panel.updateFor(obj, context) else: info_panel.updateFor(None, None) self.selection.onChange(_update) """ return info_panel
[docs] def updateInfoPanel(self, raw: kw.Json, info_panel: InfoPanel): """Update the specified info panel based on selection change.""" obj = None context = None # Parse the raw json into a Selection struct selection = kw.Selection.parse(raw) if selection.items: base_container = kc.BaseContainer.singleton() try: item = selection.items[0] obj = base_container.at(item.id) context = item.context except IndexError: pass info_panel.updateFor(obj, context) else: info_panel.updateFor(None, None)
def _setupModels(self, sp: kd.StatePropagator, time_display_period: float | None): if time_display_period is not None: time_display = kmdl.TimeDisplay("auto_time_display", sp, self.graphics) time_display.setPeriod(time_display_period)
[docs] def close(self): """Idempotently close the GUI and cleanup created objects.""" self.cleanups.executeAndPopReverse()
[docs] def __del__(self): """Idempotently close the GUI and cleanup created objects.""" self.close()