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