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

"""PathCurveSection -- curve point editor for Path2D / Path3D.

Registered with the section registry via @register_inspector_section at
import time.
"""

from simvx.core import (
    Button,
    Control,
    HBoxContainer,
    Label,
    Path2D,
    Path3D,
    SpinBox,
    Vec2,
    Vec3,
)

from ._base import (
    InspectorSection,
    _font_size,
    _row_h,
    register_inspector_section,
)

_MAX_VISIBLE_POINTS = 10

[docs] @register_inspector_section class PathCurveSection(InspectorSection): """Compact curve point editor for Path2D and Path3D nodes.""" section_title = "Curve" priority = 6
[docs] def can_handle(self, node): return isinstance(node, Path2D | Path3D)
[docs] def handled_properties(self, node): return set()
[docs] def build_rows(self, node, ctx): curve = node.curve is_3d = isinstance(node, Path3D) rows: list[Control] = [] # -- Point count label -- count_lbl = Label(f"Curve Points: {curve.point_count} points") count_lbl.font_size = _font_size() count_lbl.size = Vec2(200, _row_h()) rows.append(count_lbl) ctx.register_widget("curve_count", count_lbl) # -- Add / Clear buttons -- btn_bar = HBoxContainer() btn_bar.separation = 4 btn_bar.size = Vec2(200, _row_h()) add_btn = Button("Add Point") add_btn.size = Vec2(80, _row_h()) add_btn.font_size = _font_size() add_btn.pressed.connect(lambda n=node, c=ctx: _curve_add_point(n, c)) btn_bar.add_child(add_btn) ctx.register_widget("curve_add", add_btn) clear_btn = Button("Clear Points") clear_btn.size = Vec2(90, _row_h()) clear_btn.font_size = _font_size() clear_btn.pressed.connect(lambda n=node, c=ctx: _curve_clear_points(n, c)) btn_bar.add_child(clear_btn) ctx.register_widget("curve_clear", clear_btn) rows.append(btn_bar) # -- Per-point coordinate rows (up to _MAX_VISIBLE_POINTS) -- visible = min(curve.point_count, _MAX_VISIBLE_POINTS) for idx in range(visible): pos = curve.get_point_position(idx) row = HBoxContainer() row.separation = 2 row.size = Vec2(280, _row_h()) # Index label idx_lbl = Label(f"[{idx}]") idx_lbl.font_size = _font_size() idx_lbl.size = Vec2(28, _row_h()) row.add_child(idx_lbl) # X spin spin_x = SpinBox(min_val=-1e6, max_val=1e6, value=float(pos.x), step=1.0) spin_x.font_size = _font_size() spin_x.size = Vec2(60, _row_h()) spin_x.value_changed.connect(_make_point_coord_handler(node, idx, 0, ctx)) row.add_child(spin_x) ctx.register_widget(f"curve_pt{idx}_x", spin_x) # Y spin spin_y = SpinBox(min_val=-1e6, max_val=1e6, value=float(pos.y), step=1.0) spin_y.font_size = _font_size() spin_y.size = Vec2(60, _row_h()) spin_y.value_changed.connect(_make_point_coord_handler(node, idx, 1, ctx)) row.add_child(spin_y) ctx.register_widget(f"curve_pt{idx}_y", spin_y) if is_3d: spin_z = SpinBox(min_val=-1e6, max_val=1e6, value=float(pos.z), step=1.0) spin_z.font_size = _font_size() spin_z.size = Vec2(60, _row_h()) spin_z.value_changed.connect(_make_point_coord_handler(node, idx, 2, ctx)) row.add_child(spin_z) ctx.register_widget(f"curve_pt{idx}_z", spin_z) # Remove button rm_btn = Button("x") rm_btn.size = Vec2(22, _row_h()) rm_btn.font_size = 10.0 rm_btn.pressed.connect(_make_point_remove_handler(node, idx, ctx)) row.add_child(rm_btn) ctx.register_widget(f"curve_pt{idx}_rm", rm_btn) rows.append(row) # Overflow label if curve.point_count > _MAX_VISIBLE_POINTS: extra = curve.point_count - _MAX_VISIBLE_POINTS more_lbl = Label(f"... and {extra} more") more_lbl.font_size = _font_size() more_lbl.size = Vec2(200, _row_h()) rows.append(more_lbl) return rows
def _curve_add_point(node, ctx): """Add a new control point to the node's curve with undo support.""" curve = node.curve is_3d = isinstance(node, Path3D) if curve.point_count > 0: last = curve.get_point_position(curve.point_count - 1) if is_3d: new_pos = Vec3(float(last.x) + 50, float(last.y), float(last.z)) else: new_pos = Vec2(float(last.x) + 50, float(last.y)) else: new_pos = Vec3() if is_3d else Vec2() def do_fn(): curve.add_point(new_pos) def undo_fn(): curve.remove_point(curve.point_count - 1) ctx.on_callable_command(do_fn, undo_fn, description=f"Add curve point to {node.name}") ctx.rebuild() def _curve_clear_points(node, ctx): """Remove all points from the node's curve with undo support.""" curve = node.curve saved = list(curve._points) saved_tilts = list(curve._tilts) if hasattr(curve, '_tilts') else None def do_fn(): curve.clear() def undo_fn(): curve.clear() for i, entry in enumerate(saved): if saved_tilts is not None: curve.add_point(entry[0], entry[1], entry[2], tilt=saved_tilts[i]) else: curve.add_point(entry[0], entry[1], entry[2]) ctx.on_callable_command(do_fn, undo_fn, description=f"Clear curve points on {node.name}") ctx.rebuild() def _make_point_coord_handler(node, point_idx: int, axis: int, ctx): """Return a closure that updates a single coordinate of a curve point with undo.""" def handler(value: float): curve = node.curve if point_idx >= curve.point_count: return old_pos = curve.get_point_position(point_idx) old_vals = [float(x) for x in old_pos] if old_vals[axis] == value: return new_vals = list(old_vals) new_vals[axis] = value is_3d = isinstance(node, Path3D) new_pos = Vec3(*new_vals) if is_3d else Vec2(*new_vals) old_pos_copy = Vec3(*old_vals) if is_3d else Vec2(*old_vals) def do_fn(): curve.set_point_position(point_idx, new_pos) def undo_fn(): curve.set_point_position(point_idx, old_pos_copy) ctx.on_callable_command(do_fn, undo_fn, description=f"Set {node.name} curve point [{point_idx}]") return handler def _make_point_remove_handler(node, point_idx: int, ctx): """Return a closure that removes a curve point with undo.""" def handler(): curve = node.curve if point_idx >= curve.point_count: return entry = curve._points[point_idx] tilt = curve._tilts[point_idx] if hasattr(curve, '_tilts') else None def do_fn(): curve.remove_point(point_idx) def undo_fn(): if tilt is not None: curve.add_point(entry[0], entry[1], entry[2], index=point_idx, tilt=tilt) else: curve.add_point(entry[0], entry[1], entry[2], index=point_idx) ctx.on_callable_command(do_fn, undo_fn, description=f"Remove {node.name} curve point [{point_idx}]") ctx.rebuild() return handler