Scenes¶
Scenes are Python source files. A scene file defines a Node subclass; loading imports the module and instantiates the class. There is no separate scene format — JSON and pickle exist for save-game data, not scenes.
A scene is just a Node (or Node subclass) intended to be the root of a tree. Nesting one scene inside another is a plain self.add_child(OtherScene()) in __init__. Custom behaviour is a Python class; instances configure via __init__ kwargs.
Loading¶
from simvx.core.scene_io import load_scene
scene = load_scene("scenes/level1.py") # imports the module, instantiates the primary class
tree.set_root(scene)
The loader prefers, in order: a class whose name matches the file’s stem (level1.py → class Level1), Root, then the only top-level Node subclass in the file.
Folder-as-scene is also supported: a directory with __init__.py (or <folder>/<folder>.py) is loaded as a package and the same naming heuristic applies.
Saving from source¶
The editor’s save path lives in simvx.core.scene_io:
from simvx.core.scene_io import SceneFile
# Greenfield: emit a fresh .py from a runtime tree.
SceneFile.from_runtime(root).save("scenes/level1.py")
# Preserve user formatting: parse the existing source, reconcile the tree
# against it, write back. Comments, blank lines, hand-written code, quote
# style and import ordering survive the round-trip.
sf = SceneFile.load("scenes/level1.py")
from simvx.editor.scene_diff import apply_runtime_diff
apply_runtime_diff(sf.scene_class(), root)
sf.save()
The editor’s state.save_scene() chooses between these paths automatically — greenfield for new scenes, preserve-mode for existing ones.
Reusable prefabs¶
A “prefab” is a Python class. Multiple instances are multiple constructor calls:
from .enemy import Enemy
self.add_child(Enemy(position=Vec2(10, 0)))
self.add_child(Enemy(position=Vec2(20, 0)))
self.add_child(Enemy(position=Vec2(30, 0)))
Variants are subclasses (class FastEnemy(Enemy):) or factory functions (def spawn_enemy(): return Enemy(...)).
Project-wide refactoring¶
simvx.core.scene_io.symbols exposes pure CST queries for class definition + use-site analysis, plus the in-place rename primitives those builds on:
from simvx.core.scene_io import (
find_class_definitions, find_class_uses,
rename_class_in_source, rename_module_in_imports,
)
tree = parse_source(open("src/player.py").read())
defs = find_class_definitions(tree) # top-level class refs
uses = find_class_uses(tree, "Player") # every reference, classified
rename_class_in_source(tree, "Player", "Hero") # in-place mutation
Use-site kind covers import, import_alias, base_class, instantiation, isinstance_arg, annotation, bare_reference. Scope-aware: a local Player = MockPlayer shadow inside a function suppresses uses in that scope; aliased imports (Player as P) don’t propagate the rename to P calls.
The editor wraps these in simvx.editor.project_classes.rename_class(project_index, old, new, *, rename_file=False) — orchestrates the per-file rewrite + (optionally) renames the defining file and updates importers’ module paths via trailing-segment match. Atomic-ish: collects every new source in memory, then writes; rolls back on partial failure.
File ↔ folder refactoring¶
simvx.editor.refactor_extract.extract_to_folder(file_path, project_index) splits a multi-class .py into a sibling package — one file per class plus an __init__.py re-exporting each, so absolute imports keep resolving. Inverse: simvx.editor.refactor_inline.inline_to_file(folder_path, project_index, *, force=False). Both refuse cleanly on unsupported constructs (top-level free functions, side-effecting imports, conditional / control-flow blocks); force=True proceeds best-effort and returns an InlineResult.flagged audit trail of (file, line, reason) tuples for the editor’s review panel.
Identity-preserving rename on save¶
When the editor renames a node mid-session, the runtime canonical var name (derived from Node.name) no longer matches the source’s. Without a hint, apply_runtime_diff would emit remove + add at save time, losing the original source position. The editor builds an identity_hints: dict[Node, str] mapping at scene-load time keyed by Node identity:
apply_runtime_diff(scene_class, root, identity_hints=hints)
When a hint exists and the runtime canonical differs, the diff issues SceneClass.rename_child(...) in place. Backward compat: omitting identity_hints (or passing None) keeps the canonical-name-only behaviour for non-editor callers.
API Reference¶
simvx.core.scene_io — load_scene, SceneFile, SceneClass, SceneModule, parse_source, emit_scene.
simvx.core.scene_io.symbols — find_class_definitions, find_class_uses, rename_class_in_source, rename_module_in_imports.
simvx.editor.scene_diff — apply_runtime_diff (with identity_hints).
simvx.editor.project_classes — ProjectClassIndex, rename_class, RenameResult.
simvx.editor.refactor_extract — extract_to_folder, ExtractRefused.
simvx.editor.refactor_inline — inline_to_file, FolderInlineRefused.
simvx.core.scene_tree — SceneTree.change_scene, add_autoload.