"""Scene-tree and property manipulation handlers."""
import logging
from simvx.core import Node
from simvx.core.scripted_demo import DemoRunner
from .._helpers import (
_drive_click,
_find_tree_item,
_get_editor,
_get_state,
_init_click,
_resolve_node,
)
from ..steps import (
AddNode,
AssertConfig,
AssertProperty,
AssertTree,
ClickTreeItem,
ConfigureNode,
PlaceNode,
PlayScene,
RenameNode,
SelectNode,
SetProperty,
SetScript,
StopScene,
SwitchViewport,
)
log = logging.getLogger(__name__)
def _handle_add_node(runner: DemoRunner, step: AddNode, dt: float):
state = _get_state(runner)
if not state:
runner._advance()
return
cls = Node._registry.get(step.type_name)
if not cls:
raise ValueError(f"Unknown node type: {step.type_name}")
node = cls(name=step.name or step.type_name)
parent = state.find_node(step.parent_path)
state.add_node(node, parent)
# Rebuild the scene tree panel if available
editor = _get_editor(runner)
if editor and editor.scene_tree_panel and hasattr(editor.scene_tree_panel, "_rebuild_tree"):
editor.scene_tree_panel._rebuild_tree()
runner._advance()
def _handle_rename_node(runner: DemoRunner, step: RenameNode, dt: float):
state = _get_state(runner)
if not state:
runner._advance()
return
node = state.find_node(step.path)
if not node:
raise ValueError(f"Node not found: {step.path}")
state.rename_node(node, step.new_name)
editor = _get_editor(runner)
if editor and editor.scene_tree_panel and hasattr(editor.scene_tree_panel, "_rebuild_tree"):
editor.scene_tree_panel._rebuild_tree()
runner._advance()
def _handle_select_node(runner: DemoRunner, step: SelectNode, dt: float):
state = _get_state(runner)
if not state:
runner._advance()
return
node = state.find_node(step.path)
if node:
state.selection.select(node)
editor = _get_editor(runner)
if editor and editor.scene_tree_panel and hasattr(editor.scene_tree_panel, "_rebuild_tree"):
editor.scene_tree_panel._rebuild_tree()
runner._advance()
def _handle_set_property(runner: DemoRunner, step: SetProperty, dt: float):
state = _get_state(runner)
if not state:
runner._advance()
return
node = state.find_node(step.path)
if not node:
raise ValueError(f"Node not found: {step.path}")
state.set_node_property(node, step.prop, step.value)
runner._advance()
def _handle_assert_property(runner: DemoRunner, step: AssertProperty, dt: float):
state = _get_state(runner)
node = _resolve_node(runner, step.path) if state else None
if not node:
msg = step.message or f"AssertProperty: node not found: {step.path}"
runner._failures.append(msg)
runner._failed = True
if runner._test_mode:
raise AssertionError(msg)
runner._advance()
return
actual = getattr(node, step.prop, None)
ok = False
if isinstance(step.expected, float) and actual is not None:
ok = abs(actual - step.expected) < step.tolerance
else:
ok = actual == step.expected
if not ok:
msg = step.message or f"{step.path}.{step.prop}: expected {step.expected!r}, got {actual!r}"
runner._failures.append(msg)
runner._failed = True
if runner._test_mode:
raise AssertionError(msg)
runner._advance()
def _handle_set_script(runner: DemoRunner, step: SetScript, dt: float):
state = _get_state(runner)
if state:
state.set_script_text(step.text)
runner._advance()
def _handle_play_scene(runner: DemoRunner, step: PlayScene, dt: float):
state = _get_state(runner)
if state:
state.play_scene()
runner._advance()
def _handle_stop_scene(runner: DemoRunner, step: StopScene, dt: float):
state = _get_state(runner)
if state:
state.stop_scene()
runner._advance()
def _handle_switch_viewport(runner: DemoRunner, step: SwitchViewport, dt: float):
editor = _get_editor(runner)
if editor:
editor._on_mode(step.mode)
runner._advance()
def _handle_assert_tree(runner: DemoRunner, step: AssertTree, dt: float):
state = _get_state(runner)
root = state.find_node("") if state else None
if not root:
msg = step.message or "AssertTree: no scene root"
runner._failures.append(msg)
runner._failed = True
if runner._test_mode:
raise AssertionError(msg)
runner._advance()
return
for path, spec in step.expected.items():
try:
n = root[path] if path else root
except (KeyError, ValueError):
msg = step.message or f"AssertTree: node not found: {path}"
runner._failures.append(msg)
runner._failed = True
if runner._test_mode:
raise AssertionError(msg) from None
runner._advance()
return
if isinstance(spec, tuple):
type_name, child_count = spec
actual_type = type(n).__name__
actual_count = len(list(n.children))
if actual_type != type_name or actual_count != child_count:
msg = step.message or (
f"AssertTree: {path or 'root'}: expected ({type_name}, {child_count}), "
f"got ({actual_type}, {actual_count})"
)
runner._failures.append(msg)
runner._failed = True
if runner._test_mode:
raise AssertionError(msg)
runner._advance()
return
else:
actual_type = type(n).__name__
if actual_type != spec:
msg = step.message or f"AssertTree: {path or 'root'}: expected {spec}, got {actual_type}"
runner._failures.append(msg)
runner._failed = True
if runner._test_mode:
raise AssertionError(msg)
runner._advance()
return
runner._advance()
def _handle_place_node(runner: DemoRunner, step: PlaceNode, dt: float):
state = _get_state(runner)
if not state:
runner._advance()
return
cls = Node._registry.get(step.type_name)
if not cls:
raise ValueError(f"Unknown node type: {step.type_name}")
parent = state.find_node(step.parent_path)
# Use place_node_at for position-aware placement
state.pending_place_type = cls
node = state.place_node_at(step.x, step.y, parent)
if node and step.name:
node.name = step.name
# Rebuild the scene tree panel if available
editor = _get_editor(runner)
if editor and editor.scene_tree_panel and hasattr(editor.scene_tree_panel, "_rebuild_tree"):
editor.scene_tree_panel._rebuild_tree()
runner._advance()
def _handle_configure_node(runner: DemoRunner, step: ConfigureNode, dt: float):
node = _resolve_node(runner, step.path)
if not node:
raise ValueError(f"ConfigureNode: node not found: {step.path}")
for k, v in step.props.items():
setattr(node, k, v)
runner._advance()
def _handle_assert_config(runner: DemoRunner, step: AssertConfig, dt: float):
node = _resolve_node(runner, step.path)
if not node:
msg = step.message or f"AssertConfig: node not found: {step.path}"
runner._failures.append(msg)
runner._failed = True
if runner._test_mode:
raise AssertionError(msg)
runner._advance()
return
for k, expected in step.props.items():
actual = getattr(node, k, None)
ok = True
if actual is None:
ok = False
elif isinstance(expected, float):
ok = abs(actual - expected) < 0.5
elif isinstance(expected, tuple):
ok = len(actual) == len(expected) and all(abs(a - e) < 0.02 for a, e in zip(actual, expected, strict=True))
else:
ok = actual == expected
if not ok:
msg = step.message or f"AssertConfig: {step.path}.{k}: expected {expected!r}, got {actual!r}"
runner._failures.append(msg)
runner._failed = True
if runner._test_mode:
raise AssertionError(msg)
runner._advance()
return
runner._advance()
def _handle_click_tree_item(runner: DemoRunner, step: ClickTreeItem, dt: float):
runner._action_desc = f"Select: {step.path.rsplit('/', 1)[-1] or 'root'}"
editor = _get_editor(runner)
if step._phase == 0:
if not editor or not editor.scene_tree_panel:
runner._advance()
return
target_node = editor.state.find_node(step.path)
if not target_node:
runner._advance()
return
tree_view = editor.scene_tree_panel._tree_view
for item, rx, ry, depth in tree_view._row_map:
if item.data is target_node:
click_x = rx + depth * tree_view.indent + tree_view.row_height + 20
click_y = ry + tree_view.row_height / 2
_init_click(step, runner, (click_x, click_y))
step._phase = 1
return
# Not visible in _row_map, find the TreeItem and expand ancestors
tree_item = _find_tree_item(tree_view.root, target_node) if tree_view.root else None
if not tree_item:
raise ValueError(f"ClickTreeItem: node not found in tree: {step.path}")
parent = tree_item.parent
while parent:
parent.expanded = True
parent = parent.parent
editor.scene_tree_panel._rebuild_tree()
# If _row_map is empty (headless, draw never runs), select through TreeView
if not tree_view._row_map:
tree_view.selected = tree_item
tree_view.item_selected.emit(tree_item)
runner._advance()
return
# Otherwise retry once on next frame (draw will populate _row_map)
if step._retried:
raise ValueError(f"ClickTreeItem: node not visible after expanding ancestors: {step.path}")
step._retried = True
elif step._phase == 1:
if _drive_click(runner, step, dt):
runner._advance()