"""CollisionShapeSection -- shape type + parameters.
Registered with the section registry via @register_inspector_section at
import time.
"""
import numpy as np
from simvx.core import (
CollisionShape2D,
CollisionShape3D,
Control,
DropDown,
SpinBox,
)
from simvx.core.collision import BoxShape, CapsuleShape, SphereShape
from ._base import (
InspectorSection,
_font_size,
_make_property_row,
_make_vector_row,
register_inspector_section,
)
[docs]
@register_inspector_section
class CollisionShapeSection(InspectorSection):
section_title = "Collision Shape"
priority = 30
[docs]
def can_handle(self, node):
return isinstance(node, CollisionShape2D | CollisionShape3D)
[docs]
def handled_properties(self, node):
return {"radius"}
[docs]
def build_rows(self, node, ctx):
rows: list[Control] = []
is_3d = isinstance(node, CollisionShape3D)
shape = getattr(node, "_collision_shape", None)
shape_types = ["Sphere", "Box", "Capsule"]
current_idx = 0
if isinstance(shape, BoxShape):
current_idx = 1
elif isinstance(shape, CapsuleShape):
current_idx = 2
dd = DropDown(items=shape_types, selected=current_idx)
dd.font_size = _font_size()
dd.item_selected.connect(
lambda idx, c=ctx, n=node, st=shape_types: _change_shape_type(n, st[idx], c))
rows.append(_make_property_row("Shape", dd))
ctx.register_widget("shape_type", dd)
if isinstance(shape, BoxShape):
he = shape.half_extents
comps = 3 if is_3d else 2
he_vals = tuple(float(he[i]) for i in range(comps))
he_row = _make_vector_row("", comps, he_vals, step=0.1, min_val=0.01)
for i, spin in enumerate(he_row._spinboxes):
axis = i
spin.value_changed.connect(
lambda val, ax=axis, c=ctx, n=node, s=shape: _change_box_extents(n, s, ax, val, c))
rows.append(_make_property_row("Half Ext", he_row))
ctx.register_widget("shape_half_extents", he_row)
elif isinstance(shape, CapsuleShape):
rad_spin = SpinBox(min_val=0.01, max_val=10000, value=shape.radius, step=0.1)
rad_spin.font_size = _font_size()
rad_spin.value_changed.connect(
lambda val, c=ctx, n=node, s=shape: _change_capsule_param(n, s, "radius", val, c))
rows.append(_make_property_row("Radius", rad_spin))
ctx.register_widget("shape_capsule_radius", rad_spin)
height_spin = SpinBox(min_val=0.01, max_val=10000, value=shape.height, step=0.1)
height_spin.font_size = _font_size()
height_spin.value_changed.connect(
lambda val, c=ctx, n=node, s=shape: _change_capsule_param(n, s, "height", val, c))
rows.append(_make_property_row("Height", height_spin))
ctx.register_widget("shape_capsule_height", height_spin)
else:
# Sphere
rad_spin = SpinBox(min_val=0.01, max_val=10000, value=node.radius, step=0.1)
rad_spin.font_size = _font_size()
rad_spin.value_changed.connect(
lambda val, c=ctx, n=node: c.on_property_changed(n, "radius", n.radius, val))
rows.append(_make_property_row("Radius", rad_spin))
ctx.register_widget("shape_sphere_radius", rad_spin)
return rows
def _change_shape_type(node, shape_name, ctx):
old_shape = getattr(node, "_collision_shape", None)
isinstance(node, CollisionShape3D)
if shape_name == "Sphere":
new_shape = SphereShape(radius=node.radius)
elif shape_name == "Box":
new_shape = BoxShape(half_extents=(0.5, 0.5, 0.5))
elif shape_name == "Capsule":
new_shape = CapsuleShape(radius=0.5, height=2.0)
else:
return
def do_fn():
node._collision_shape = new_shape
def undo_fn():
node._collision_shape = old_shape
ctx.on_callable_command(do_fn, undo_fn, description=f"Set {node.name} shape to {shape_name}")
ctx.property_changed_signal.emit(node, "_collision_shape", old_shape, new_shape)
ctx.rebuild()
def _change_box_extents(node, shape, axis, value, ctx):
if not isinstance(shape, BoxShape):
return
old_he = np.copy(shape.half_extents)
new_he = np.copy(shape.half_extents)
new_he[axis] = value
def do_fn():
shape.half_extents = np.copy(new_he)
def undo_fn():
shape.half_extents = np.copy(old_he)
ctx.on_callable_command(do_fn, undo_fn, description=f"Set {node.name} box half_extents")
def _change_capsule_param(node, shape, param, value, ctx):
if not isinstance(shape, CapsuleShape):
return
old_val = getattr(shape, param)
if old_val == value:
return
def do_fn():
setattr(shape, param, value)
def undo_fn():
setattr(shape, param, old_val)
ctx.on_callable_command(do_fn, undo_fn, description=f"Set {node.name} capsule {param}")