RUNE-1014: Story Layer Sprite Tree Proposed
Summary
LayerSprite groups an ordered child sprite tree in local coordinates. StoryScene.RootLayer uses the same runtime primitive as nested layers while remaining an implicit authoring scope. Layer opacity is corrected isolated group opacity, and non-root layers may opt into the planar Layer 3D contract defined by RUNE-1017. RootLayer is now the compatibility world root beneath an internal dual-branch CompositionRoot.
YAML Shape
- create@stage:
layer:
- rect@hero: null
width: 120
height: 80
- rect: null
width: 16
height: 16Child @name values are layer-local. Named children are addressed with qualified paths such as stage.hero.opacity; anonymous children render and affect ordering/bounds but cannot be referenced later or reordered directly.
Runtime Semantics
- Child-list order is painter order.
- Child coordinates are layer-local.
- Layer
x/yis a top-left position;LayerSpritehas noalign. - Layers clip only when
clipis set. ContentBoundsintentionally remains structural/model-space child bounds; presentation-aware capture, damage, and projection use native snapshot local/projected bounds.- Scrolling a clipped region requires a fixed clipped outer layer and a moving inner layer.
- Group shadows remain unsupported because a layer has no independent primitive shape.
Root Composition and Compatibility
StoryScene.RootLayer MUST remain the public top-level world LayerSprite. Existing StoryScene.AddSprite(name, sprite) and direct RootLayer mutation MUST mean world space. RootLayer.AttachRender MUST remain a world-only identity-root-transform compatibility path.
Bake/export MUST instead render through:
CompositionRoot (internal)
RootLayer (world: Camera -> World -> Host)
ScreenLayer (internal: Host only, drawn last)
final host-mapped logical-canvas clipScreenLayer MUST NOT be exposed as a second freely mutable public Layer API. Root lookup and StoryScene.Sprites MUST union named children from both layers and reject ambiguity/duplicates. Attach, detach, disposal, video discovery, and resource traversal MUST visit both content roots.
RootLayer itself retains the root Layer 3D prohibition. Ordinary child layers MAY activate planar 3D in world or screen. Internal compositor/viewport nodes MUST NOT activate Layer 3D.
Camera, World, Host, and Clip Order
The render engine post-multiplies row-vector command matrices. A world point MUST use:
P * Camera * World * HostA screen point MUST use P * Host. Let C be logical-canvas center, K camera position, Z camera zoom, Rc camera rotation, O world origin, S world scale, Rw world rotation, and T world translation:
cameraResult = C + rotate(P - K, -Rc) * Z
worldResult = O + rotate(cameraResult - O, Rw) * S + TCompositionRoot MUST emit this exact sequence:
- Apply Host, push
(0,0,canvasWidth,canvasHeight)as a device-space clip, then restore the transform while retaining the clip. - Emit Camera as
translate(-K),rotate(-Rc),scale(Z),translate(C). - Emit World as
translate(-O),rotate(Rw),scale(S),translate(O + T). - Emit Host (output scale, then output-pixel offset) and draw RootLayer.
- Restore, apply Host only, draw ScreenLayer, then pop the shared clip.
The final clip MUST depend only on the host-mapped logical canvas, never camera/world state. World content MUST remain unclipped before its transforms so off-canvas content may enter view. Neither world nor screen may draw into letterbox/pillarbox regions.
Increasing camera.x moves world content left; increasing world.x moves it right. Positive camera rotation rotates the viewed world oppositely, while positive world rotation follows the direct clockwise Sprite convention. Camera and world scales multiply. Screen content ignores both stages and is structurally above world; cross-space reorder is invalid.
An inactive layer records ordinary translation, scale, and rotation commands. Once a 3D-specific property is authored, position, scale, and X/Y/Z rotation move into snapshot compositing metadata and the complete subtree is flattened as one plane. Activation is permanent for that instance; see RUNE-1017.
Group Opacity Migration
LayerSprite always records presentation opacity as animatable child-snapshot group_opacity. It does not emit CmdSetOpacity over descendants.
- opacity one replays directly;
- opacity below one isolates the ordinary subtree once;
- active 3D applies opacity once to the projected effect output;
- nested layer opacities isolate at each child boundary and multiply naturally.
StoryScene.RootLayerisolates root snapshot group opacity once around the complete world root.
The corrected behavior intentionally changes overlapping semi-transparent layer children: overlap inside the layer is composed before layer opacity is applied.
Tree Mutation
V1 root actions create, destroy, morph, and reorder root children. Nested topology is initialized by the layer: child list and has no insert/remove/reparent/rename actions, but named same-parent children may be reordered.
reorder moves one or more subjects in YAML list order. before: other and after: other place subjects adjacent to a same-parent sibling; before: null means first and after: null means last. Cross-parent reorder, duplicate/empty subjects, or an anchor included in the moved set is invalid. Root reorder cannot cross world/screen because the internal roots are distinct parents; null anchors remain within the subjects' own space.
Morph and Lifecycle
Layer morph recursively matches compatible same-name children. Matching children seed presentation values and morph descriptor-backed properties; unmatched, incompatible, and anonymous children use the parent replacement/fade behavior.
Active Layer 3D state can transfer only from layer to layer. Morph applies inherited targets before renaming the original, so validation failures leave public scene identity unchanged. If later installation or assignment fails, the replacement is removed and the original name is restored without an orphan internal sprite. Morphing an active 3D layer to a non-layer is rejected before replacement.
RenderSprite.AttachRender is transactional. A validation or resource failure calls OnRenderDetached to unwind partial child/clip resources, releases the native node and callback handle, clears attachment state, and permits retry. Detach/reattach preserves the layer model, child tree, group opacity, and persistent 3D activation.
Bake attaches CompositionRoot once; it owns RootLayer, ScreenLayer, and transform presentation for one render lifetime. Each LayerSprite still owns/render its child RuneNodes. Action-boundary snapshots are committed bottom-up before the export/preview composition snapshot.
Dependencies
- RUNE-305 defines isolated child-boundary opacity and projected compositing.
- RUNE-1010 defines sprite ownership/reuse constraints.
- RUNE-1011 defines export snapshot playback.
- RUNE-1017 defines Layer 3D properties, activation, validation, and morph behavior.
Test Strategy
- Verify local naming, qualified lookup, painter order, and same-parent reorder.
- Verify layer clips and content bounds remain local to the layer.
- Verify opacity applies once to overlapping children for inactive and projected layers.
- Verify nested layer opacities multiply once per child boundary.
- Verify failed attachment and morph restore detached/original state without leaked child resources or internal replacement identities.