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, Sequence, cast
from pathlib import Path
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.Math as km
import Karana.KUtils.visjs as vjs
import numpy as np

try:
    from Karana.Adams import SimpleSpdp
except:
    SimpleSpdp = None

from ._infopanel import InfoPanel, GuiContext, 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
from ._notify import Notifier
from ._worker import AsyncWorker

__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[int | 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() # setup thread for long-running async callbacks self.worker = AsyncWorker() self.cleanups["closeWorker"] = self.worker.close # main server setup self.server = self._setupServer(port=port) self.router = kw.Router(self.server) # Inject custom CSS into the document self.router.channel(css=Path(__file__).parent / "static" / "style.css") # Setup error notifier popup self.error_count = 0 self.notifier = Notifier(self.router) self.server.setOnError(self._handleServerError) # 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 frame layer viz setup self.visjs_frames_server: vjs.GraphServer = self._setupFramesVisjs( self.mbody.frameContainer() ) self.visjs_frames_iframe = kw.IFrame(self.router, self.visjs_frames_server.getUrl()) # if non-null, invoke this callback instead of selecting when a # frame node is clicked # self.visjs_frames_noselect_cb = None # visjs graph viz setup self.visjs_label_map = name_to_label_map self.visjs_server: vjs.MultibodyGraphServer = self._setupVisjs(self.mbody) self.visjs_iframe = 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.subtrees_tree_view = self._setupSubTreesTreeView() if self.sp: self.sp_tree_view = self._setupStatePropagatorTreeView( sp=self.sp, options=SubTreeTreeViewOptions( subtree_bodies=True, subtree_subtrees=True, body_nodes=True, body_scene_parts=True, loop_constraints=True, ), ) """ 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 = [] if 1 or self.mbody.childrenSubTrees(): self.treeviews_options.append("SubTrees") self.treeviews_options = [ "Frames", ] 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[int(val)]), ) self.wtreeviews.setTooltip("Select the type of TreeView to create") # --------------------- 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.wwebscene_selection_mode.setTooltip( "Select 3D graphics pick mode - closest body, frame or scene part" ) # --------------------- webscene_view_modes = ["Free", "+X", "+Y", "+Z", "-X", "-Y", "-Z"] self.webscene_view_mode = "pers_free" # callback for the 3D pick mode menu def _websceneViewModeCB(val): mode = self.webscene_view_mode = webscene_view_modes[val] if self.webscene_view_mode == "Free": projection = ks.PerspectiveProjection() else: projection = ks.OrthographicProjection() target = [0, 0, 0] camera = self.scene.graphics().defaultCamera() if mode == "Free": offset = [3, 3, 3] up = [0, 0, 1] elif mode == "+X": offset = [5, 0, 0] up = [0, 0, 1] elif mode == "+Y": offset = [0, 5, 0] up = [0, 0, 1] elif mode == "+Z": offset = [0, 0, 5] up = [0, 1, 0] elif mode == "-X": offset = [-5, 0, 0] up = [0, 0, 1] elif mode == "-Y": offset = [0, -5, 0] up = [0, 0, 1] elif mode == "-Z": offset = [0, 0, -5] up = [0, 1, 0] camera.pointCameraAt(offset=offset, target=target, up=up) self.wwebscene_view_mode = kw.Dropdown( self.router, "3D View", webscene_view_modes, lambda val: _websceneViewModeCB(val), ) self.wwebscene_view_mode.setIndex(0) self.wwebscene_view_mode.setTooltip( "Select 3D graphics camera perspective and orthographic views" ) # ------------------------------------ self.wdeselect = kw.Button(self.router, "Deselect", lambda: self.deselect()) self.wreset_viz = kw.Button(self.router, "Clear Viz Effects", lambda: self.effects.clear()) # --------------------------------- """ # slider to scale the stick parts slider_opts = kw.SliderOptions() slider_opts.min = 0.01 slider_opts.max = 1 slider_opts.step = 0.01 """ self.waxes_scale = kw.FloatInput( self.router, "Axes scale", on_change=lambda scale: self.setAxesGlobalScale(float(scale), float(scale)), rapid_submit=True, ) self.waxes_scale.setTooltip("The global scaling factor for frame axes in 3D graphics") self.waxes_scale.setStep(0.01) self.waxes_scale.setMin(0) self.waxes_scale.setMax(1) self.waxes_scale.setValue(self.effects.frame_axes.part_scale) self.waxes_scale.setSizeClass(kw.SizeClass.NARROW) def _setCameraMask(cstate, mask): graphics = self.graphics camera = graphics.defaultCamera() if not camera: return cmask = camera.getMask() if cstate: cmask |= mask if mask == ks.LAYER_STICK_FIGURE: if not self.scene.getSceneParts(ks.LAYER_STICK_FIGURE): self.mbody.createStickParts() else: cmask ^= mask camera.setMask(cmask) self.wcamera_physical = kw.Toggle( self.router, "Physical", on_toggle=lambda cstate: _setCameraMask(cstate, ks.LAYER_PHYSICAL_GRAPHICS), tooltip="Show/hide the physical meshes in 3D graphics", render_as_button=True, ) self.wcamera_physical.setValue(True) self.wcamera_collision = kw.Toggle( self.router, "Collision", on_toggle=lambda cstate: _setCameraMask(cstate, ks.LAYER_COLLISION), tooltip="Show/hide the collision meshes in 3D graphics", render_as_button=True, ) self.wcamera_stick = kw.Toggle( self.router, "Stick", on_toggle=lambda cstate: _setCameraMask(cstate, ks.LAYER_STICK_FIGURE), tooltip="Show/hide the stick parts in 3D graphics", render_as_button=True, ) def _shadowsCB(cstate): radius = 1 if cstate else 0 pixels = 2048 if cstate else 0 self.graphics._setShadows(np.array([0, 0, -1]), radius=radius, pixels=pixels) self.wshadows = kw.Toggle( self.router, "Shadows", on_toggle=lambda cstate: _shadowsCB(cstate), tooltip="Turn shadows on/off", render_as_button=True, ) self.wlayout_3d = widgetArray( self.router, label="3D meshes", children=[ self.wcamera_physical, self.wcamera_collision, self.wcamera_stick, self.wshadows, ], kind="inputgroup", ) self.wlayout_3d.setTooltip("Select the meshes to display in 3D graphics") def _sceneCB(): self.scene.update() self.wscene = kw.Button( self.router, text="Update scene", on_press=lambda: _sceneCB(), ) self.wtoolbar = widgetArray( self.router, children=[ self.wdeselect, self.wreset_viz, self.wtreeviews, self.wwebscene_selection_mode, self.wwebscene_view_mode, self.waxes_scale, self.wlayout_3d, self.wscene, ], ) if sp is not None: def _stopCB(): cast(kd.StatePropagator, self.sp).stop() self.wstop = kw.Button( self.router, text="\u23f9", on_press=lambda: _stopCB(), ) self.wpause = kw.Button( self.router, "\u23f8/\u25b6", createPauseCb(cast(kd.StatePropagator, self.sp)) ) self.wtoolbar.addChild(self.wpause) self.wtoolbar.addChild(self.wstop) # 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.frameContainer().name(), widget=self.visjs_frames_iframe, relative_to=self.graphics_frame, direction="below", ) self.dock.addChild( title=self.mbody.name(), widget=self.visjs_iframe, relative_to=self.visjs_frames_iframe, direction="within", ) # 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="SubTrees", widget=self.subtrees_tree_view, relative_to=self.mbody_tree_view, direction="within", ) if self.sp: self.dock.addChild( title="StatePropagator", widget=self.sp_tree_view, relative_to=self.mbody_tree_view, direction="within", ) """ self.dock.addChild( title="ALL", widget=self.all_tree_view, relative_to=self.mbody_tree_view, direction="within", ) """ self.wmain.addChild(self.notifier.wroot) 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) if not any(km.isNotReadyNaN(x) for x in mbody.getQ()): self.scene.update()
[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: str): """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=cast(kd.StatePropagator, 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_frames_iframe, self.visjs_iframe, self.router, self.mbody, self.server, self.graphics, self.scene, self.dock, self.graphics_frame, self.visjs_frames_server, 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, display_name="Karana Viewer") 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.WebScenePart): # show the outline of this scene part # graphics = self.scene.graphics() self.effects.part_outliner.set( [EffectItem(obj=obj, params=OutlineParams(level="primary"))] ) psp = obj.getManager() frame = psp.ancestorFrame() self.effects.frame_axes.set([EffectItem(obj=frame, params=AxesParams())]) elif isinstance(obj, ks.WebSceneFileObject): # show the outline of this scene part # graphics = obj.graphics() frame = obj.getManager().ancestorFrame() if 0: # TODO - enable once outline() method is available for SFOs self.effects.part_outliner.set( [EffectItem(obj=graphics, params=OutlineParams(level="primary"))] ) else: bd = cast(kd.PhysicalBody, frame) self.effects.frame_outliner.set(_bodyOutlineEffects(bd, level="primary")) self.effects.frame_axes.set([EffectItem(obj=frame, params=AxesParams())]) elif isinstance(obj, ks.ProxyScenePart): # show the outline of this scene part frame = obj.ancestorFrame() self.effects.frame_axes.set([EffectItem(obj=frame, params=AxesParams())]) elif isinstance(obj, ks.ProxySceneFileObject): # show the outline of this scene part graphics = obj.graphics() frame = obj.ancestorFrame() if isinstance(graphics, ks.WebSceneFileObject): if 0: # TODO - enable once outline() method is available for SFOs self.effects.part_outliner.set( [EffectItem(obj=graphics, params=OutlineParams(level="primary"))] ) else: bd = cast(kd.PhysicalBody, frame) self.effects.frame_outliner.set( _bodyOutlineEffects(bd, level="primary") ) 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(style="lines", scale=1) ), EffectItem( obj=cf2f.pframe(), params=AxesParams(style="lines", scale=0.75) ), ] ) 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 fhge = obj.parentHinge() if isinstance(fhge, kd.PhysicalHinge): hge = cast(kd.PhysicalHinge, fhge) 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 _setupFramesVisjs( self, fc: kf.FrameContainer, use_id_labels: bool = False ) -> vjs.GraphServer: if not self.mbody: raise ValueError("GUI has been closed") selection_state = self.selection class ServerWithSelectCallback(vjs.GraphServer): def onClickNode(self, client_id: int, node_id: int | str): try: int_id = int(node_id) except ValueError: return # print("JJJ", self.visjs_frames_noselect_cb) if self.visjs_frames_noselect_cb: self.visjs_frames_noselect_cb(int_id) else: selection = kw.Selection(items=[kw.Selection.Item(id=int_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)]) selection_state.set(selection.dump()) graph = vjs.framesToGraph( fc, extra_chains=self.mbody.enabledConstraints(), use_id_labels=use_id_labels, ) server = ServerWithSelectCallback(graph=graph, port=0) server.visjs_frames_noselect_cb = None # register this graph server with the graph effects helpers self.effects.registerGraphServer(server) # Add a callback to cleanup the visjs web server def cleanupFramesVisjsServer(): self.effects.unregisterGraphServer(server) server.close() self.cleanups["cleanupFramesVisjsServer"] = cleanupFramesVisjsServer return server def _setupVisjs(self, st: kd.SubTree, use_id_labels: bool = False) -> 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 # print("PPP", self.visjs_noselect_cb) if self.visjs_noselect_cb: self.visjs_noselect_cb(int_id) else: 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()) label_map = self.visjs_label_map if use_id_labels: label_map = {} for bd in st.sortedBodiesList(): label_map[bd.id()] = str(bd.id()) server = ServerWithSelectCallback( subtree=st, label_map=label_map, # self.visjs_label_map, buttons=None, extra_edges=None, port=0, title=f"{st.name()} System", ) server.visjs_noselect_cb = None # 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: Sequence[kd.BodyBase], secondary: Sequence[kd.BodyBase] = [], tertiary: Sequence[kd.BodyBase] = [], ): # 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 = [] self.effects.graph_highlighter.clear() 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): self.effects.graph_highlighter.ensure([EffectItem(obj=obj, params=None)]) 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, ks.ProxyScenePart): # hightlight the frame this scene part is connected to frm = obj.ancestorFrame() assert isinstance(frm, kd.PhysicalBody) bd = cast(kd.PhysicalBody, frm) emphasis = _emphasizeBodies(primary=[bd], secondary=[]) elif isinstance(obj, ks.ProxySceneFileObject): # hightlight the frame this scene part is connected to frm = obj.ancestorFrame() assert isinstance(frm, kd.PhysicalBody) bd = cast(kd.PhysicalBody, frm) emphasis = _emphasizeBodies(primary=[bd], secondary=[]) elif isinstance(obj, ks.WebScenePart): # hightlight the frame this scene part is connected to psp = obj.getManager() frm = psp.ancestorFrame() assert isinstance(frm, kd.PhysicalBody) bd = cast(kd.PhysicalBody, frm) emphasis = _emphasizeBodies(primary=[bd], secondary=[]) elif isinstance(obj, ks.WebSceneFileObject): # hightlight the frame this scene part is connected to psp = obj.getManager() frm = psp.ancestorFrame() assert isinstance(frm, kd.PhysicalBody) bd = cast(kd.PhysicalBody, frm) emphasis = _emphasizeBodies(primary=[bd], secondary=[]) elif isinstance(obj, kd.FramePairHinge): # need this to keep the chained f2f part from kicking in (this edge already exists) self.effects.graph_highlighter.ensure([EffectItem(obj=obj, params=None)]) elif isinstance(obj, kf.ChainedFrameToFrame) or isinstance( obj, kf.OrientedChainedFrameToFrame ): # this is for new f2fs selected via pick mode # highlight the physical bodies involved f2f = cast(kf.FrameToFrame, obj) obd = self.mbody.getNodeAncestor(f2f.oframe()).parentBody() pbd = self.mbody.getNodeAncestor(f2f.pframe()).parentBody() emphasis = _emphasizeBodies(primary=[obd], secondary=[pbd]) self.effects.frame_outliner.set(emphasis) # self.frame_axes.clear() self.effects.graph_highlighter.set( [ EffectItem(obj=cast(kc.Base, frame), params=None) for frame in [f2f.oframe(), f2f.pframe()] ] ) params = GraphEdgeParams( color="#00ff00", dashed=True, id=f2f.id(), arrows=True, title=f"'{f2f.name()}' ({f2f.typeString()}) between\n the '{f2f.oframe().name()}/{f2f.pframe().name()}'\n frames", ) self.effects.graph_edge_adder.set( [EffectItem(obj=(f2f.oframe(), f2f.pframe()), params=params)] ) # self.effects.graph_highlighter.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() 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 SimpleSpdp is not None and 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.ensure(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") ps = self.mbody.getScene() if ps is None: raise ValueError("The scene has not been created yet.") return createScenesTreeView( router=self.router, gui_selection_state=self.selection, pscene=ps ) 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 = GuiContext( 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, subtrees_tree_view=self.subtrees_tree_view, setup_visjs=self._setupVisjs, setup_info_panel=self._setupInfoPanel, visjs_servers=self.visjs_servers, visjs_frames_server=self.visjs_frames_server, setup_frames_visjs=self._setupFramesVisjs, visjs_iframe=self.visjs_iframe, # visjs_frames_noselect_cb=self.visjs_frames_noselect_cb, graphics_frame=self.graphics_frame, signal_error=self._notifyError, worker=self.worker, ) 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()
def _handleServerError(self, error_msg: str): """Handle server errors. This is a handler to be registered with the HttpWsServer. The ensures that uncaught GUI error are logged and displayed on the frontend. """ kc.error(error_msg) self._notifyError() def _notifyError(self): """Notify the frontend that a new error has occurred.""" self.error_count += 1 markdown = f'<span style="color:red;">Error occurred ({self.error_count})</span>. _See console for details._' self.notifier.notify(markdown)