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_sourceis 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, theadd_childcalls, the class header and thefrom ... 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:
UnemittableValueErrorrather than reaching the file as a flattened, truncated or hollowed-out version of itself. :func:emit_scenetakes areportlist for callers that would rather keep the rest of the save.
Module Contents¶
Functions¶
Emit a complete |
|
Emit the |
|
Return the |
|
Importable type name the emitter writes for |
|
Format |
|
The module an emitted helper |
|
The local a scene file binds |
|
Does a scene file’s |
|
Does |
|
Does |
|
Is |
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:
ValueErrorA 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 areportlist, 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:refusalscarries 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
.pysource file for the live tree rooted atroot.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
reportto 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.namewhen 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, onefrom <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:UnemittableValueErrorinstead.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.layoutchooses for it. The output round-trips throughparse_source(s).dump() == sby construction (parso preserves any text it parses verbatim).Raises: UnemittableValueError: A value has no source form and no
reportlist 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 fornode.used_typesis mutated in place when supplied so callers can accumulate imports across multiple emissions; the type ofnodeitself is added, plus the types of any complex kwarg values (Vec2/Vec3/Quat).indentis 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 fornodein 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=...whennode.namediffers from the engine type name.Spatial kwargs (
position/rotation/scale) forNode2D/Node3Dwhen they deviate from origin/identity/one.One entry per declared
Propertywhose current value differs from its declared default and is serialisable via :func:emit_value.
A :class:
~simvx.core.Textureis 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_scenehas and this per-node view does not.used_typesis 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_modulewhich module each one comes from: not all of them aresimvx.coreexports.unemittableis 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.derivedis 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
valas Python source, orNonewhen 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
Vec2component is a float32), in the notationrepruses for a float that size, so2.0is still written2.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, andstr-keyed dicts serialise recursively. So do the primitive collision shapes of both dimensions –SphereShape3D,BoxShape3D,CapsuleShape3D,CylinderShape3D,CircleShape2D,RectangleShape2D,CapsuleShape2DandSegmentShape2D– each written as the constructor call that rebuilds its geometry, with the fields the constructor would produce anyway left off.
Noneis the answer for everything else: :class:Nodeinstances, 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
namemust be imported from.Everything the emitter writes by name is a
simvx.coreexport, except the handful listed in :data:_HELPER_MODULES–Pathis the one, and it comes from :mod:pathlib. Callers that add imports for the names the emitter reports inused_typesmust ask here rather than assumesimvx.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
nameto, before de-duplication.nameis 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,selfand every keyword (class,import,return, and the soft ones) take anode_prefix.return.pngis an ordinary name for a back-arrow icon, andreturn = 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
expressionalready build thevaluea 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)andSphereShape3D(radius=0.5)are one sphere and only the first is what- Func:
_format_shapewrites. Those are the collision shapes, and they are the only kind this answers for; any other value answersFalsewhatever its expression says, leaving the caller’s own comparison in charge. A caller comparing text alone does not see through a difference of spelling, soVec2(0, 0)is not recognised as theVec2(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
Falseas well, which says nothing about what it builds – :func:expression_is_opaqueis 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
expressionbuildvalue’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_describescannot vouch for it, and cannot deny it either.Falsefor 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
expressionbuild 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)answersTruewhatever the slot holds now, whileSphereShape3D(radius=RADIUS)answersFalse– it names a shape and then hides which one behind a value only running the file would produce. So domake_shape(),self._shape, andConvexHullShape3D(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.namestill 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_kwargsleaves 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.