# 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()