Source code for simvx.editor.node_ops

"""Node manipulation operations mixin for EditorState."""

from __future__ import annotations

from typing import TYPE_CHECKING

from simvx.core import Node, Vec2

if TYPE_CHECKING:
    from .state import EditorState


[docs] class NodeOps: """Mixin providing node CRUD, placement, and scene-title operations. Methods in this class are designed to be mixed into EditorState, which provides the workspace, signals, and delegating properties they depend on. """ if TYPE_CHECKING: self: EditorState # type: ignore[assignment] # ------------------------------------------------------------------ # Node queries # ------------------------------------------------------------------
[docs] def get_scene_title(self) -> str: name = self.current_scene_path.stem if self.current_scene_path else "Untitled" return f"{name}{'*' if self._modified else ''}"
[docs] def find_node(self, path: str) -> Node | None: root = self.edited_scene.root if self.edited_scene else None if not root: return None if not path: return root try: return root[path] except (KeyError, ValueError): return None
# ------------------------------------------------------------------ # Undoable node mutations # ------------------------------------------------------------------
[docs] def rename_node(self, node: Node, new_name: str): from simvx.core import CallableCommand old_name = node.name cmd = CallableCommand( lambda: setattr(node, "name", new_name), lambda: setattr(node, "name", old_name), f"Rename {old_name} \u2192 {new_name}", ) self.undo_stack.push(cmd) self.modified = True
[docs] def set_node_property(self, node: Node, prop: str, value): from simvx.core import PropertyCommand old_value = getattr(node, prop, None) cmd = PropertyCommand(node, prop, old_value, value, f"Set {node.name}.{prop}") self.undo_stack.push(cmd) self.modified = True
[docs] def add_node(self, node: Node, parent: Node | None = None): from simvx.core import CallableCommand target = parent or (self.edited_scene.root if self.edited_scene else None) if not target: return cmd = CallableCommand( lambda: target.add_child(node), lambda: target.remove_child(node), f"Add {node.name}", ) self.undo_stack.push(cmd) self.modified = True
[docs] def remove_node(self, node: Node): from simvx.core import CallableCommand parent = node.parent if not parent: return cmd = CallableCommand( lambda: parent.remove_child(node), lambda: parent.add_child(node), f"Remove {node.name}", ) self.undo_stack.push(cmd) self.modified = True
[docs] def duplicate_node(self, node: Node) -> Node | None: parent = node.parent if not parent: return None from simvx.core.scene import _deserialize_node, _serialize_node data = _serialize_node(node) clone = _deserialize_node(data) if clone: clone.name = f"{node.name}_copy" self.add_node(clone, parent) return clone return None
# ------------------------------------------------------------------ # Mouse placement mode # ------------------------------------------------------------------
[docs] def enter_place_mode(self, node_class: type): self.pending_place_type = node_class self.place_mode_changed.emit()
[docs] def cancel_place_mode(self): if self.pending_place_type is not None: self.pending_place_type = None self.place_mode_changed.emit()
[docs] def place_node_at(self, x: float, y: float, parent: Node | None = None): cls = self.pending_place_type if cls is None: return None self.pending_place_type = None target = parent or (self.edited_scene.root if self.edited_scene else None) if target is None: self.place_mode_changed.emit() return None from simvx.core import CallableCommand, Node2D node = cls(name=cls.__name__) if isinstance(node, Node2D): node.position = Vec2(x, y) cmd = CallableCommand( lambda p=target, n=node: p.add_child(n), lambda p=target, n=node: p.remove_child(n), f"Place {node.name} at ({x:.0f}, {y:.0f})", ) self.undo_stack.push(cmd) self.modified = True self.selection.select(node) self.place_mode_changed.emit() return node