simvx.core.scene_io.emitter

Greenfield scene emitter: live Node tree → canonical .py source.

Produces a single class definition whose __init__ reconstructs the tree via add_child calls, with non-default :class:Property values passed as keyword arguments. The output is a string; round-trip identity through

func:

simvx.core.scene_io.parse_source is guaranteed by construction because the emitted form is canonical and parso preserves any text it parses verbatim under :meth:SourceTree.dump.

The emitter is intentionally string-based rather than parso-based: building a fresh tree is the right tool when there is no source to preserve. The diff-and-edit layer (Tier 3b) handles the round-trip case where source already exists on disk.

Every line it writes that has anything to break goes through

mod:

~simvx.core.scene_io.layout: the constructions, the add_child calls, the class header and the from ... import ... lines are laid out the way a formatter run at the project’s width lays them out, so the file reads like code someone wrote rather than one 220-column line per node.

Values are spelled the way a formatter spells them – double quotes where they cost no extra escaping, an exponent without its + – and a value that is written is written whole: the number carries the digits that read it back as the value stored, at the precision it is stored at, in the notation repr uses for a float that size. So a scene this emitter wrote is a scene the project’s formatter has nothing to say about, and a value it writes loses no digit on the way to the file.

Whether a value is written at all is a separate question, and one this module answers less finely: a position, rotation or scale within 1e-9 of its default counts as that default and is left off the call entirely, so a position of 5e-10 reaches disk as no position at all.

What it cannot write, it refuses: a value with no source form raises

class:

UnemittableValueError rather than reaching the file as a flattened, truncated or hollowed-out version of itself. :func:emit_scene takes a report list for callers that would rather keep the rest of the save.

Module Contents

Functions

emit_scene

Emit a complete .py source file for the live tree rooted at root.

emit_node_construction

Emit the var_name = Type(kwargs...) statement for node.

iter_runtime_kwargs

Return the (kwarg_name, formatted_expr) pairs the emitter would emit for node in greenfield mode.

structural_type_name

Importable type name the emitter writes for node.

emit_value

Format val as Python source, or None when it has no source form.

helper_import_module

The module an emitted helper name must be imported from.

var_name_base

The local a scene file binds name to, before de-duplication.

expression_describes

Does a scene file’s expression already build the value a node holds?

expression_is_opaque

Does expression build value’s kind of shape without saying which one?

expression_reads_a_shape

Does expression build a collision shape with its geometry written out?

engine_computed

Is node.name still holding exactly what the engine worked out for it?

Data

API

simvx.core.scene_io.emitter.log

‘getLogger(…)’

simvx.core.scene_io.emitter.__all__

[‘UnemittableValueError’, ‘emit_node_construction’, ‘emit_scene’, ‘emit_value’, ‘expression_describe…

exception simvx.core.scene_io.emitter.UnemittableValueError(refusals: list[str])[source]

Bases: ValueError

A live value has no source form, so the scene cannot be written whole.

Raised by :func:emit_scene (and therefore by

Meth:

~simvx.core.scene_io.SceneFile.from_runtime) unless the caller passes a report list, which selects keep-and-report instead: the file is emitted without the refused values and every refusal is appended to that list for the caller to show. :attr:refusals carries the same messages.

Initialization

Initialize self. See help(type(self)) for accurate signature.

class __cause__
class __context__
__delattr__()
__dir__()
__eq__()
__format__()
__ge__()
__getattribute__()
__getstate__()
__gt__()
__hash__()
__le__()
__lt__()
__ne__()
__new__()
__reduce__()
__reduce_ex__()
__repr__()
__setattr__()
__setstate__()
__sizeof__()
__str__()
__subclasshook__()
class __suppress_context__
class __traceback__
add_note()
class args
with_traceback()
simvx.core.scene_io.emitter.emit_scene(root: simvx.core.node.Node, *, class_name: str | None = None, extra_imports: list[str] | None = None, report: list[str] | None = None) str[source]

Emit a complete .py source file for the live tree rooted at root.

A value with no source form – a texture built from pixels in memory, a multi-dimensional array, a node reference – stops the emission by default: writing the file without it would produce a scene that silently loads back different from the one that was saved. Pass report to choose the other policy: emit anyway, leaving the refused values out, and append one message per refusal to that list so the caller can show them and keep the document marked unsaved.

Args: root: Root node of the tree. class_name: Class name to use. Defaults to root.name when it is a valid identifier and differs from the engine type name; otherwise falls back to the engine type name. extra_imports: Verbatim import lines to include in place of the auto-generated ones. Empty/None → generated imports, one from <module> import ... line per module the emitted source names (see :func:helper_import_module). report: Mutated in place, one message per refused value, and the emission continues. None (the default) raises :class:UnemittableValueError instead.

Returns: Complete Python source string, laid out to the line limit: a statement that does not fit on one line is broken across several, in the shape :mod:~simvx.core.scene_io.layout chooses for it. The output round-trips through parse_source(s).dump() == s by construction (parso preserves any text it parses verbatim).

Raises: UnemittableValueError: A value has no source form and no report list was given to collect it.

simvx.core.scene_io.emitter.emit_node_construction(node: simvx.core.node.Node, var_name: str, *, used_types: set[str] | None = None, indent: int = 0) str[source]

Emit the var_name = Type(kwargs...) statement for node.

used_types is mutated in place when supplied so callers can accumulate imports across multiple emissions; the type of node itself is added, plus the types of any complex kwarg values (Vec2/Vec3/Quat).

indent is the column the statement will be written at. A construction that does not fit the line limit from there comes back broken across several lines, laid out as a formatter would (:func:~simvx.core.scene_io.layout.wrap_statement); the first line carries no indent of its own, since the caller is placing it.

simvx.core.scene_io.emitter.iter_runtime_kwargs(node: simvx.core.node.Node, *, used_types: set[str] | None = None, unemittable: set[str] | None = None, derived: set[str] | None = None) list[tuple[str, str]][source]

Return the (kwarg_name, formatted_expr) pairs the emitter would emit for node in greenfield mode.

This is the canonical answer to “which constructor kwargs reflect the non-default state of this node?”: used by the editor’s round-trip save path (simvx.editor.scene_diff) to reconcile a parsed source file against a live runtime tree without duplicating the Property/spatial-default iteration logic.

The returned list includes:

  • name=... when node.name differs from the engine type name.

  • Spatial kwargs (position/rotation/scale) for Node2D / Node3D when they deviate from origin/identity/one.

  • One entry per declared Property whose current value differs from its declared default and is serialisable via :func:emit_value.

A :class:~simvx.core.Texture is written as its constructor call here, once per property that holds it. Writing a shared one into a local instead needs a view of the whole file, which :func:emit_scene has and this per-node view does not.

used_types is mutated in place when supplied so callers can accumulate the names the returned expressions refer to (Vec2/Vec3/Quat, Texture, Path). Ask :func:helper_import_module which module each one comes from: not all of them are simvx.core exports.

unemittable is mutated in place the same way, collecting the name of every Property that holds a NON-default value this emitter cannot write as source. Absence from the returned pairs is otherwise ambiguous – it means either “at its default” or “could not be expressed” – and a caller that edits existing source has to tell those apart before it deletes a line.

derived is the third reason a name can be absent: the value in it is one the engine worked out for this node rather than one the author wrote (:meth:~simvx.core.Node._record_derived), so it is not written into their constructor call. A caller editing existing source must not read that as a stale line either – the author’s own number is what the file should keep.

simvx.core.scene_io.emitter.structural_type_name(node: simvx.core.node.Node) str[source]

Importable type name the emitter writes for node.

Used by the round-trip diff layer to decide whether to add an import for a newly-introduced runtime child.

simvx.core.scene_io.emitter.emit_value(val: Any) str | None[source]

Format val as Python source, or None when it has no source form.

A number is written so that reading it back gives the same number: the fewest digits that parse to exactly the value held, at the precision it is held at (a Vec2 component is a float32), in the notation repr uses for a float that size, so 2.0 is still written 2.0. A string is written with the quotes a formatter would leave it in. :class:Vec2,

Class:

Vec3,

Class:

Quat, :class:~pathlib.Path, :class:~simvx.core.Resource, a file-backed :class:~simvx.core.Texture, lists, tuples, and str-keyed dicts serialise recursively. So do the primitive collision shapes of both dimensions – SphereShape3D, BoxShape3D, CapsuleShape3D, CylinderShape3D, CircleShape2D, RectangleShape2D, CapsuleShape2D and SegmentShape2D – each written as the constructor call that rebuilds its geometry, with the fields the constructor would produce anyway left off.

None is the answer for everything else: :class:Node instances, callables, modules, a texture over pixels held in memory, an array with more than one dimension, a collision shape carrying a point cloud or a mesh, and any container holding one of those – a container is refused whole rather than emitted with a hole in it.

simvx.core.scene_io.emitter.helper_import_module(name: str) str[source]

The module an emitted helper name must be imported from.

Everything the emitter writes by name is a simvx.core export, except the handful listed in :data:_HELPER_MODULESPath is the one, and it comes from :mod:pathlib. Callers that add imports for the names the emitter reports in used_types must ask here rather than assume simvx.core, or they write a file that fails to load on the name they just introduced.

simvx.core.scene_io.emitter.var_name_base(name: str) str[source]

The local a scene file binds name to, before de-duplication.

name is whatever the tree offers – a node’s user-visible name, or the file stem of a texture being hoisted – so it has to be made into an identifier before it can be written. Lowercased, spaces and hyphens turned into underscores, anything else dropped; a leading digit, an empty result, self and every keyword (class, import, return, and the soft ones) take a node_ prefix. return.png is an ordinary name for a back-arrow icon, and return = Texture(...) is not Python: without the prefix the whole save dies in the parser with nothing written and nothing said about which asset caused it.

This is the one place the rule lives. Everything that has to predict, match or reproduce a name a scene file binds calls it rather than restating it: a second copy that drifts turns into a node the diff cannot find and silently re-adds under another name.

simvx.core.scene_io.emitter.expression_describes(expression: str, value: Any) bool[source]

Does a scene file’s expression already build the value a node holds?

The question a save asks about a line it is thinking of rewriting or deleting: does the author’s own text say what the scene says? Comparing the text against this module’s own form does not settle that for a value written as a constructor call whose parameters have defaults, because one such value has several honest spellings – SphereShape3D(), SphereShape3D(0.5) and SphereShape3D(radius=0.5) are one sphere and only the first is what

Func:

_format_shape writes. Those are the collision shapes, and they are the only kind this answers for; any other value answers False whatever its expression says, leaving the caller’s own comparison in charge. A caller comparing text alone does not see through a difference of spelling, so Vec2(0, 0) is not recognised as the Vec2(0.0, 0.0) this module writes for that value.

No file is imported and no shape is built: the expression is read as source, its arguments are matched to the class’s own parameters and read as the literals and vectors they are written as, and the geometry is compared field by field. An expression this cannot read that way answers False as well, which says nothing about what it builds – :func:expression_is_opaque is the question a caller asks to tell the two answers apart.

simvx.core.scene_io.emitter.expression_is_opaque(expression: str, value: Any) bool[source]

Does expression build value’s kind of shape without saying which one?

SphereShape3D(radius=RADIUS) names the class a save would name and then says nothing a save can check: the radius is a value only running the file would produce. Such a line is the author’s own record of the geometry and the only one there is, so a save that rewrote it against the scene, or took it out as stale, would be guessing – :func:expression_describes cannot vouch for it, and cannot deny it either.

False for anything else, including a call this module can read: one that reads back as different geometry is a genuine disagreement, and the scene is what a save carries.

simvx.core.scene_io.emitter.expression_reads_a_shape(expression: str) bool[source]

Does expression build a collision shape with its geometry written out?

No value is needed: the class the call names says which geometry to read, and the arguments are read against that class’s own parameters. So SphereShape3D(radius=0.5) answers True whatever the slot holds now, while SphereShape3D(radius=RADIUS) answers False – it names a shape and then hides which one behind a value only running the file would produce. So do make_shape(), self._shape, and ConvexHullShape3D(points), the last because a shape carrying vertex data has no source form at all.

What it settles, for a save holding a line and a scene that disagree with it: a collider whose KIND was changed in the editor leaves the file spelling one shape class where the scene now holds another. Where both sides read as shapes, that is a disagreement the scene settles, and not a reference to geometry built elsewhere, which a save must leave exactly as it stands.

simvx.core.scene_io.emitter.engine_computed(node: simvx.core.node.Node, name: str) bool[source]

Is node.name still holding exactly what the engine worked out for it?

A control measures its own size and a container places its children, both writing into properties an author also writes into (:meth:~simvx.core.Node._record_derived). Emitting the result would put the engine’s arithmetic into the author’s constructor call, where nothing afterwards could tell it from a number they typed, so

Func:

iter_runtime_kwargs leaves such a value out.

Only “still holds it” counts. A later write of any kind – the author’s, a game’s, the inspector’s – makes the two differ and the value is emitted as usual, so no write path has to clear the record.

Published because the omission has a second half: an inspector showing a value nobody will save has to say so, or the user types into a void.