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