Source code for simvx.editor.panels.inspector_sections._collision_section

"""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)