"""CollisionShapeSection -- shape type + parameters.
Edits the ``shape`` Property of a :class:`CollisionShape2D` / :class:`CollisionShape3D`
node. Geometry lives in a :class:`Shape` / :class:`Shape2D` resource (a plain value):
the dropdown swaps the resource kind and the SpinBox / VectorRow rows rebuild a fresh
resource of the same kind with the edited field, assigning it back through the ``shape``
Property so the change is undoable and serialised.
Registered with the section registry via @register_inspector_section at import time.
"""
from simvx.core import (
BoxShape3D,
CapsuleShape2D,
CapsuleShape3D,
CircleShape2D,
CollisionShape2D,
CollisionShape3D,
Control,
DropDown,
RectangleShape2D,
SphereShape3D,
SpinBox,
)
from ._base import (
InspectorSection,
_font_size,
_make_property_row,
_make_vector_row,
register_inspector_section,
)
# Per-dimension resource family for the "Sphere / Box / Capsule" dropdown. The
# 2D circle is labelled "Sphere" so the dropdown reads identically in both views.
_SHAPE_TYPES = ["Sphere", "Box", "Capsule"]
_RESOURCES_3D = {"Sphere": SphereShape3D, "Box": BoxShape3D, "Capsule": CapsuleShape3D}
_RESOURCES_2D = {"Sphere": CircleShape2D, "Box": RectangleShape2D, "Capsule": CapsuleShape2D}
def _resources_for(node) -> dict:
return _RESOURCES_3D if isinstance(node, CollisionShape3D) else _RESOURCES_2D
def _shape_type_name(node, shape) -> str:
res = _resources_for(node)
for name, cls in res.items():
if isinstance(shape, cls):
return name
return "Sphere"
[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):
# The section owns the whole shape resource; hide the generic row.
return {"shape"}
[docs]
def build_rows(self, node, ctx):
rows: list[Control] = []
is_3d = isinstance(node, CollisionShape3D)
shape = node.shape
type_name = _shape_type_name(node, shape)
dd = DropDown(items=list(_SHAPE_TYPES), selected=_SHAPE_TYPES.index(type_name))
dd.font_size = _font_size()
dd.item_selected.connect(
lambda idx, c=ctx, n=node: _change_shape_type(n, _SHAPE_TYPES[idx], c))
rows.append(_make_property_row("Shape", dd))
ctx.register_widget("shape_type", dd)
if type_name == "Box":
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):
spin.value_changed.connect(
lambda val, ax=i, c=ctx, n=node: _change_box_extents(n, ax, val, c))
rows.append(_make_property_row("Half Ext", he_row))
ctx.register_widget("shape_half_extents", he_row)
elif type_name == "Capsule":
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: _change_capsule_param(n, "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: _change_capsule_param(n, "height", val, c))
rows.append(_make_property_row("Height", height_spin))
ctx.register_widget("shape_capsule_height", height_spin)
else:
# Sphere / Circle
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: _change_sphere_radius(n, val, c))
rows.append(_make_property_row("Radius", rad_spin))
ctx.register_widget("shape_sphere_radius", rad_spin)
return rows
def _set_shape(node, new_shape, ctx):
"""Assign a new shape resource through the ``shape`` Property (undoable)."""
old_shape = node.shape
ctx.on_property_changed(node, "shape", old_shape, new_shape)
def _change_shape_type(node, shape_name, ctx):
res = _resources_for(node)
cls = res.get(shape_name)
if cls is None:
return
if cls in (SphereShape3D, CircleShape2D):
new_shape = cls(radius=0.5)
elif cls in (BoxShape3D, RectangleShape2D):
new_shape = cls() # default half_extents
else: # Capsule
new_shape = cls(radius=0.5, height=2.0)
_set_shape(node, new_shape, ctx)
ctx.rebuild()
def _change_box_extents(node, axis, value, ctx):
shape = node.shape
he = [float(c) for c in shape.half_extents]
if axis >= len(he) or he[axis] == value:
return
he[axis] = value
cls = type(shape)
new_shape = cls(half_extents=tuple(he))
_set_shape(node, new_shape, ctx)
def _change_capsule_param(node, param, value, ctx):
shape = node.shape
radius = shape.radius
height = shape.height
if param == "radius":
if radius == value:
return
radius = value
else:
if height == value:
return
height = value
new_shape = type(shape)(radius=radius, height=height)
_set_shape(node, new_shape, ctx)
def _change_sphere_radius(node, value, ctx):
shape = node.shape
if shape.radius == value:
return
new_shape = type(shape)(radius=value)
_set_shape(node, new_shape, ctx)