an.stage.compile#
Compile a top-level Shot (renderer=”cutout”) into a CutoutSceneJSON.
This is the bridge between the renderer-agnostic an.ir types and the
cutout-specific JSON contract that the JS runtime will consume in Phase 2B.
Strategy:
Resolve entities from the project mall: each
AssetRefinshot.entitiesbecomes a sub-tree of the cutout scene (a character with placeholder rect parts when the character store has no sidecar art yet).Flatten authoring actions via
an.ir.compose.flatten— everytween/set/play/composition produces leaf `FlatAction`s with absolute times.Compile each FlatAction to PlacedClipJSON entries on the appropriate track. Tween → a 2-keyframe AnimationClipJSON + a PlacedClipJSON (or, under
step_hz, a grid of step-eased keyframes — an#89). Set → a step channel that HOLDS until the next action on the same target/property. Play → a per-instance clip (__play__{n}) resolved from the target’s descriptor animation (an#7).
The compiler is deterministic and side-effect-free (it doesn’t write to the mall). It reads only.
>>> from an.ir.schema import Meta, SceneIR, Shot
>>> from an.stage.compile import compile_shot
>>> shot = Shot(id="s1", renderer="cutout", duration=2.0)
>>> j = compile_shot(shot, mall={"characters": {}})
>>> j.timeline.duration
2.0
Module Attributes
Every property name the JS runtime's |
|
How many "did you mean" paths an unknown-target message offers. |
|
indexed by the runtime, absent from the tree. |
|
the |
|
The product key under which the follow pass (an#445) leaves the camera keys it resolved from the compiled motion; the camera and parallax passes read them there instead of |
|
The STAGE's own compile passes, in order. |
|
The field kinds |
|
The infix of a container holding a later band of an environment's planes (an#344): |
|
The infix of a wrapper carrying a plane's parallax for the entities placed right after it (an#344): |
|
What a synthetic node's name may contain and an entity id may not (an#344). |
|
phase 0 the backdrop, phase 1 the cast. |
|
The |
|
A |
|
Suffix for the container holding an environment's FOREGROUND planes. |
|
The property space every compiled node lives in ( |
|
at ±π/2 the runtime draws nothing, and a value past it is almost always DEGREES typed where radians were meant (an#314 review). |
Functions
|
|
|
The keys of |
|
|
The stage's passes and every registered one, in run order (stable by name). |
|
|
Compile a single cutout-style |
|
Paths the runtime would index twice (its |
|
|
|
The node name an environment's foreground planes live under. |
|
The built node paths a mistyped |
|
Record a rig with raster parts that a colour-setting pack is applied to. |
|
A |
|
|
|
|
|
|
|
|
|
|
|
Clip-local times at which a stepped tween updates its pose (an#89). |
|
The |
|
One sentence saying |
Classes
|
How a genre's action kind becomes clips in the stage compiler (an#225). |
|
A wrapper the parallax pass gives |
|
What the stage compiler's passes read and write, for one shot (an#247). |
|
The scene being built, as an entity builder sees it (an#247). |
Exceptions
A genre registered a pass (or a builder) the stage already has, without |
|
A shot cannot be compiled to a cutout scene. |
|
A shot compiles, but something in it will not reach the screen. |
- an.stage.compile.AFTER_BANDS_PRODUCT: str = 'after_bands'#
the
AfterBandwrappers the scene pass built.- Type:
state.productskey
- an.stage.compile.AFTER_INFIX: str = '__after_'#
The infix of a wrapper carrying a plane’s parallax for the entities placed right after it (an#344):
<env>__after_<k>,scope="".
- class an.stage.compile.ActionLowering(*args, **kwargs)[source]#
Bases:
ProtocolHow a genre’s action kind becomes clips in the stage compiler (an#225).
Registered as
an.genres.ActionKind.lowering. The stage knows no kind by name beyond its own (tween,set); a genre’s kind supplies:extent_resolver(vocab, *, products):action -> secondsfor a leaf that names no duration (orNone);expand(flat_list, *, vocab, fps, step_hz, default_easing, resolutions, products): the flat list with this kind’s leaves replaced by what they stand for.
productsis the shot’sCompileState.products(an#348): what a genre’s earlier compile pass left for later work, by key (a style’s policy, which decides a walk’s gait and so its length). Read it; the lowering has no other view of the compile state.And, per kind:
view_of(entity_swaps, vocab, *, duration):flat -> view name | None(orNone), for kinds whose clips depend on the view in force;clip(action, *, anim_id, vocab, fps, view): the animation clip of one leaf that survivedexpand.
- class an.stage.compile.AfterBand(wrapper, environment, plane)[source]#
Bases:
objectA wrapper the parallax pass gives
plane’s compensation (an#344).
- an.stage.compile.BAND_INFIX: str = '__band_'#
The infix of a container holding a later band of an environment’s planes (an#344):
<env>__band_<k>,scope=<env>.
- an.stage.compile.CAMERA_KEYS_PRODUCT: str = 'camera_keys'#
The product key under which the follow pass (an#445) leaves the camera keys it resolved from the compiled motion; the camera and parallax passes read them there instead of
camera_keys(shot).
- an.stage.compile.CAMERA_NODE: str = 'root'#
indexed by the runtime, absent from the tree.
- Type:
The runtime’s camera node
- exception an.stage.compile.CompilePassCollision[source]#
Bases:
CutoutCompileErrorA genre registered a pass (or a builder) the stage already has, without
replace=True.
- class an.stage.compile.CompileState(shot, mall, fps, width, height, strict_assets=False, step_hz=None, default_easing=None, style_pack=None, expression_provider=None, textures=<factory>, resolutions=<factory>, overlay_children=<factory>, fonts=<factory>, scene_root=None, vocab=None, animations=<factory>, tracks=<factory>, entity_swaps=<factory>, extra_actions=<factory>, no_lip_sync=frozenset({}), poses=None, view_spans=None, blink_phases=<factory>, gaze_seeds=<factory>, products=<factory>, meta_extensions=<factory>)[source]#
Bases:
objectWhat the stage compiler’s passes read and write, for one shot (an#247).
The stage’s own passes (
STAGE_COMPILE_PASSES) and every pass a genre registers for the"stage"compiler (an.genres.CompilePass) run over one of these, in order;_assemble_document()turns it into the wire document. The fields are the localscompile_shotused to thread by hand, unchanged, so the document is byte-identical.- extra_actions: list#
Actions a pass contributes beside the authored ones, compiled with them by the
actionspass (the cut-outspeechpass’s pulses, an#248).
- exception an.stage.compile.CutoutCompileError[source]#
Bases:
ValueErrorA shot cannot be compiled to a cutout scene. Carries actionable detail.
A
ValueErrorrather than aRuntimeError— unlike the rest of this package’s error tree — because every one of these is “this value is not in the known set”, which is the existing idiom for argument-level rejection. The render-time errors stayRuntimeError: they are failures of the machinery, not of the input.
- exception an.stage.compile.CutoutCompileWarning[source]#
Bases:
UserWarningA shot compiles, but something in it will not reach the screen.
The line between this and
CutoutCompileErroris whether the author could plausibly have meant it. An unknowncamera.moveis always a mistake, so it raises. A speaker with no mouth is usually an off-screen narrator and occasionally a typo — refusing it would break the documented idiom, and passing in silence is what this whole change is against.
- an.stage.compile.DFLT_TARGET_SUGGESTIONS: int = 3#
How many “did you mean” paths an unknown-target message offers.
- an.stage.compile.ENVIRONMENT_ART_PREFIX: str = 'environments/'#
The
assets.texturessrcprefix an environment plate is addressed under.
- an.stage.compile.FOREGROUND_SUFFIX: str = '__front'#
Suffix for the container holding an environment’s FOREGROUND planes.
The split has to produce two sibling containers, and before an#110’s review both were named
entity.id. The runtime’snodeIndexis a flatpath -> containerdict, so the second silently overwrote the first — an authoredset street:xthen moved the foreground half and left the background where it was, with no warning from either side: the compiler’s collision check compares(entity/plane, prop)and never sees(entity, x), and the runtime’s unknown-target throw does not fire because the name IS known, just bound to the wrong one of two. The determinism report’snode_countunder-counted by one per split environment too.
- an.stage.compile.PLANE_EDGE_ON_MARGIN: float = 0.001#
at ±π/2 the runtime draws nothing, and a value past it is almost always DEGREES typed where radians were meant (an#314 review).
- Type:
How close to edge-on a plane may be authored (radians short of ±π/2)
- an.stage.compile.PLANE_FILL_SPAN: float = 4000.0#
A
fillplane with no declared size covers the canvas at any camera scale — defined beside the schema (an.stage.environments.PLANE_FILL_SPAN) so the IR layer’s framing check reads the same number, re-exported here.
- an.stage.compile.RUNTIME_APPLIED_PROPERTIES: frozenset[str] = frozenset({'alpha', 'dash_offset', 'perspective', 'pivot_x', 'pivot_y', 'plane_fade_end', 'plane_fade_start', 'rotation', 'rotation_rad', 'rotation_x', 'scale_x', 'scale_y', 'skew_x', 'skew_y', 'tint_b', 'tint_g', 'tint_r', 'trim_end', 'trim_start', 'x', 'y'})#
Every property name the JS runtime’s
applyPropertySTATIC switch implements — exactly the numeric transform vocabulary (the rest-value SSOT above). This is the Python side of the two-evaluator drift gate:tests/test_loud_discards.pyextracts the runtime’s actual switch cases and asserts exact equality with this set, in both directions. It replacedpose.py’s allow-list when the Python applier was deleted (an#86). Any OTHER property is a swap-set name, applied dynamically through the node’sasset_setsprojection (an#87) —visemeleft the static switch when that landed, which is precisely what makes it a conventional set name rather than control flow.
- an.stage.compile.RUNTIME_FIELD_KINDS: frozenset[str] = frozenset({'angle', 'color', 'discrete', 'number', 'orbit', 'quaternion', 'vector'})#
The field kinds
runtime.jsimplements (itsFIELD_KINDStable; a test pins the two). A declared space using any other kind cannot be drawn by the stage, so the compiler refuses it instead of the browser failing mid-render.
- an.stage.compile.STAGE_COMPILE_PASSES: tuple[CompilePass, ...] = (CompilePass(name='scene', run=<function _scene_pass>, order=100, compiler='stage', builds=None, description='the scene tree, overlay, grain, vocabulary', replace=False), CompilePass(name='counters', run=<function _counters_pass>, order=195, compiler='stage', builds=None, description='counter text blocks: `value` -> a set of strings (an#342)', replace=False), CompilePass(name='actions', run=<function _actions_pass>, order=200, compiler='stage', builds=None, description='authored actions -> clips', replace=False), CompilePass(name='camera_follow', run=<function _camera_follow_pass>, order=550, compiler='stage', builds=None, description="a following camera's keys, from the compiled motion (an#445)", replace=False), CompilePass(name='camera', run=<function _camera_pass>, order=600, compiler='stage', builds=None, description='the camera onto the scene root', replace=False), CompilePass(name='parallax', run=<function _parallax_pass>, order=700, compiler='stage', builds=None, description="planes' parallax", replace=False), CompilePass(name='checks', run=<function _checks_pass>, order=900, compiler='stage', builds=None, description='targets, easings, stand-ins', replace=False))#
The STAGE’s own compile passes, in order. A genre adds passes between them by registering
an.genres.CompilePassobjects for the"stage"compiler; the in-repo cut-out genre registersswap_pose(300),view_spans(310),visemes(400) andface(500), and therigentity builder (an.genres.cutout.CUTOUT_COMPILE_PASSES). Held HERE, not in the genre tables, so nowithout_genres()can take the stage’s own passes away.
- an.stage.compile.STAGE_NODE_SPACE: str = 'stage.node'#
The property space every compiled node lives in (
an.timing.spaces).
- an.stage.compile.STAGE_SCENE_BUILDERS: dict[str, CompilePass] = {'environment': CompilePass(name='environment', run=<function _build_environment_entity>, order=0, compiler='stage', builds='environment', description="an environment's planes (the backdrop)", replace=False), 'prop': CompilePass(name='prop', run=<function _build_prop_entity>, order=1, compiler='stage', builds='prop', description='a prop, a stroked path or a text block', replace=False)}#
phase 0 the backdrop, phase 1 the cast.
- Type:
The stage’s own entity builders
- an.stage.compile.SYNTHETIC_MARK: str = '__'#
What a synthetic node’s name may contain and an entity id may not (an#344).
- class an.stage.compile.SceneBuild(shot, mall, textures, resolutions, style_pack, overlay, fonts, width, height, children=<factory>, in_front=<factory>, reached=<factory>, skipped=<factory>, raster=<factory>)[source]#
Bases:
objectThe scene being built, as an entity builder sees it (an#247).
A builder (
an.genres.CompilePasswithbuildsset) appends its entity’s subtree tochildren(oroverlay, orin_front) and records its textures and resolutions here, exactly as the scene pass did by hand.
- an.stage.compile.after_problems(shot, mall, *, planes_known=True)[source]#
[(entity index, why)]for everystage.afterthe compiler would refuse (an#344): an anchor the shot does not have, the entity itself, a cycle. The one statement bothcompile_shotandan validateread.>>> from an.ir.schema import AssetRef, StagePlacement >>> shot = Shot(id="s", renderer="stage", duration=1.0, entities=[ ... AssetRef(kind="prop", id="a", store="props", ref="a", stage=StagePlacement(after="b")), ... AssetRef(kind="prop", id="b", store="props", ref="b", stage=StagePlacement(after="a"))]) >>> [why.split(":")[0] for _, why in after_problems(shot, {})] ["entity 'a' is placed after 'b'", "entity 'b' is placed after 'a'"]
- an.stage.compile.camera_follow_keys(state)[source]#
The keys of
state.shot’s follow camera, one per frame (an#445).
- an.stage.compile.camera_keys(shot, *, width, height)[source]#
an.ir.camera.camera_keys(), with its refusal typed for this adapter.The resolver itself lives in the IR layer so
an.ir.validatecan call the SAME function — one table, not two reconciled by a test. This wrapper only re-raisesCameraErroras aCutoutCompileError, which is the compiler’s own boundary contract: every failure out ofcompile_shotis one type.
- an.stage.compile.compile_passes_for_stage()[source]#
The stage’s passes and every registered one, in run order (stable by name).
- Return type:
- an.stage.compile.compile_shot(shot, mall=None, *, fps=30, width=1920, height=1080, background='#ffffff', strict_assets=False, step_hz=None, expression_provider=None, style_pack=None, default_easing=None)[source]#
Compile a single cutout-style
Shotto its JS-runtime JSON form.default_easing(an#166) is the scene’smeta.default_easing: the curve of every authored tween that names none (tween > this > the built-in"ease_in_out",resolved_easing()).Noneleaves the document byte-identical to before the knob existed.expression_provider(an#98) is the seam that turns authoredexpressionleaves and dialogue[emotion]sugar into per-axis curves for the face solver;Noneis the genre’s default provider.step_hz(an#89) resamples every authored tween onto a SHOT-wide pose grid of that many updates per second (multiples of1/step_hzon this shot’s clock, shared by every tween in the shot; the grid restarts at a cut), each keyframe step-eased, so the character holds each pose for the frames between grid points — “on twos” at half the frame rate, “on threes” at a third. It is sample-and-hold of the eased curve at the grid instants, not a retiming into holds and fast transitions.None(default) leaves tweens smooth and the compiled document byte-identical to before the knob existed; anything else must satisfy0 < step_hz <= fps— checked HERE as well as byan validate, because a render never runs validate and a non-positive rate used to spinstep_timesforever (an#89 review). Exempt by construction, because they are separate emission sites rather than string-sniffed: the camera (_add_camera_clips— a stepped character under a translating camera slides in screen space, which is why the practice keeps cameras on ones), compiled blinks,playclips, and swap channels (already stepped by format). The value is stamped intometa.step_hz— only when set — so a serialized scene declares its timing policy without moving the contract hash of one that has none.strict_assetsturns a stand-in asset — the placeholder rig drawn for a character whose descriptor is missing, or the default backdrop drawn for an unknown environment ref — from a warning into aCutoutCompileError. Off by default so an asset-less project still renders; on for anything that measures pixels, where a stand-in is a wrong answer wearing a right one’s clothes (an#33).- Return type:
- an.stage.compile.duplicate_paths(root, overlay=())[source]#
Paths the runtime would index twice (its
nodeIndexis later-wins, so the first node would silently stop answering). Over the FINISHED tree, scoped containers included (an#344).>>> duplicate_paths(NodeJSON(name="root", children=[NodeJSON(name="a"), NodeJSON(name="a")])) ['a']
- an.stage.compile.entity_spaces_of(shot)[source]#
{entity id: space}for each entity whose kind declares a space other than the kernel default – what the compiled document records so the default evaluator agrees with validate and compile (an#245).>>> from an.ir.schema import AssetRef, Shot >>> entity_spaces_of(Shot(id="s", entities=[AssetRef(kind="prop", id="p", store="props", ref="p")])) {}
- an.stage.compile.foreground_node_name(entity_id)[source]#
The node name an environment’s foreground planes live under.
- Return type:
>>> foreground_node_name("street") 'street__front'
- an.stage.compile.node_path_suggestions(target, paths, *, n=3)[source]#
The built node paths a mistyped
targetmost plausibly meant.The paths of the SAME entity that end in the same part name — the usual mistake is a missing level (
ned/left_browforned/head/left_brow) — or, when there are none, the closest spellings.>>> built = ["ned", "ned/head", "ned/head/left_brow", "ned/head/mouth", "ned/arm_l"] >>> node_path_suggestions("ned/left_brow", built) ['ned/head/left_brow'] >>> node_path_suggestions("ned/arm_x", built) ['ned/arm_l'] >>> node_path_suggestions("zzz", built) []
- an.stage.compile.note_raster_rig(entity, desc_data, pack, raster)[source]#
Record a rig with raster parts that a colour-setting pack is applied to.
Public (an#338) because a genre that builds a rig calls it before
an.stage.rig.build_rig_subtree(), as the prop builder does.- Return type:
- an.stage.compile.parse_tint(value, *, where)[source]#
A
#rrggbbstring to three multipliers in 0..1.Per-channel in sRGB — on the 8-bit values as written — not linear-light.
tintis a multiply the GPU applies in the space the author read the hex out of, and a fade between two hex values that did not pass through the values between them would surprise whoever wrote them (an#62).
- an.stage.compile.plane_parents(env, entity_id)[source]#
{plane name: the node path its channels must target}.One rule, computed the same way by the builder and by the parallax pass — the alternative is two places deciding which container a plane ended up in, which is the class of drift this wave keeps closing.
>>> from an.stage.environments import EnvironmentDescriptor, Plane >>> env = EnvironmentDescriptor(name="e", planes=[Plane(name="a"), Plane(name="b")], ... characters_after="a")
Since an#343 every container an environment is split into indexes its planes under the environment’s id (
scope), so the answer no longer depends on where the environment was cut:>>> plane_parents(env, "street") {'a': 'street', 'b': 'street'}
- an.stage.compile.retire_front_spelling(shot)[source]#
shotwith every<env>__front/<plane>target spelled<env>/<plane>, and the(old, new)pairs rewritten.The foreground container indexes its planes under the environment’s id since an#343, so the old spelling names nothing; indexing the plane twice would be the an#110 collision. For one cycle the compiler (and
an validate) rewrites it with a warning naming the new spelling; a later issue retires the rewrite.>>> from an.ir.schema import AssetRef, SetAction >>> shot = Shot(id="s", renderer="stage", duration=1.0, ... entities=[AssetRef(kind="environment", id="set", store="environments", ref="r")], ... actions=[SetAction(target="set__front/wall", property="alpha", value=0.5)]) >>> new, pairs = retire_front_spelling(shot) >>> new.actions[0].target, pairs ('set/wall', [('set__front/wall', 'set/wall')])
- an.stage.compile.scene_builders()[source]#
{entity kind: builder}: the stage’s, and every registered one.- Return type:
- an.stage.compile.space_definitions(entity_spaces)[source]#
{space name: definition}for every spaceentity_spacesnames – what the compiled document embeds asmeta.spacessoruntime.jsevaluates each declared entity in its space (an#287).The definition is
to_json()without its prose (a reworded description must not move a contract hash; a space’sversiondoes). A space using a field kind the runtime does not implement is refused here (CutoutCompileError).>>> space_definitions({}) {} >>> space_definitions({"cam": "stage.camera"})["stage.camera"]["fields"][0] {'pattern': 'x', 'spec': {'kind': 'number'}, 'unit': 'px'}
- an.stage.compile.stage_replacements()[source]#
{stage pass or builder: the genre replacing it}– recorded in the compiled document’smeta.extensionswhen non-empty.
- an.stage.compile.step_times(start, duration, step_hz)[source]#
Clip-local times at which a stepped tween updates its pose (an#89).
The grid is SHOT-wide — multiples of
1/step_hzon the shot’s clock, shared by every tween in the shot — not the tween’s own: “on twos” means every character changes pose on the same frames, so a tween starting at 0.033 s updates at the next grid point, not 0.033 s later. (Shots compile independently, so the grid restarts at each cut.) Local 0 (the tween’s start) andduration(where the end value lands) are always present, so a tween shorter than one step is a single step to its end value.step_hzmust be positive: with a non-positive rate the walk below never reachesduration— an infinite loop, not an error — so it is refused.>>> step_times(0.0, 0.3, 10) [0.0, 0.1, 0.2, 0.3] >>> [round(t, 3) for t in step_times(0.05, 0.3, 10)] [0.0, 0.05, 0.15, 0.25, 0.3] >>> step_times(0.0, 0.02, 10) [0.0, 0.02]
- an.stage.compile.style_pack_for(scene_meta, styles_store)[source]#
The
StylePacka scene declares, orNone(an#112).Nonefor every document written before an#112 and for every scene that declares no pack — which is what makes this feature byte-identity-free: the no-pack path is a lookup with a default, not a rewrite.A declared pack that is missing, or is not a
StylePack, RAISES. An art direction the author asked for and did not get is a different picture that renders happily, which is the an#33 failure this package refuses everywhere else.
- an.stage.compile.unknown_target_message(target, paths)[source]#
One sentence saying
targetis not a built node, with suggestions.Shared by the compiler (which raises it) and
an validate(which reports it), so the two say the same thing about the same path.- Return type:
>>> print(unknown_target_message("ned/mouth", ["ned", "ned/head", "ned/head/mouth"])) 'ned/mouth' is not a node of the built scene; did you mean 'ned/head/mouth'? (nodes of 'ned': ['ned', 'ned/head', 'ned/head/mouth'])