simvx.graphics.render2d.item_builder

What the collection pass is

The pass walks the tree once in the same order the live _draw_recursive visits it (depth-first, parent-before-children, siblings partitioned into below / self / above z-bands, CanvasLayers last by layer, the YSort y-order policy) and, per node, emits one or more Items into an ItemList:

  • seq – a monotonic counter assigned in emission order, so a stable (layer, seq) sort of the produced list reproduces walk order exactly (the walk folds per-sibling-group z into seq; the global sort is not a z lexsort). z is recorded as data only.

  • layer – the CanvasLayer band (world content defaults to 0; a CanvasLayer(layer=N) shifts its whole subtree to band N).

  • zabsolute_z_index of the emitting node (data, never a sort input).

  • clip_scope – the active :class:ClipScopeTable scope, opened/closed around clipped subtrees and the synthesised Control per-child wrap.

  • transform – a per-item LOCAL transform row = the node’s world_transform, not composed with any camera (the view lives in a UBO). Camera-free verts + a local transform row is the keystone that lets a later camera pan touch one UBO row instead of N item rows.

  • pipeline / blend / texture / flags / geometry – taken from the captured draw op.

The transitional op-adapter bridge

Real nodes (Sprite2D, the shape primitives, Text2D’s family) still draw through the immediate-mode Draw2D op API in their on_draw bodies. Rewriting every node to emit Items natively is ongoing work. To stay isolated and make real nodes produce Items today, this builder runs each node’s on_draw against a lightweight recording renderer (:class:_OpRecorder, the _DrawRecorder / DrawLog pattern) that captures the ops a node would draw – with an identity transform so the captured geometry is camera-free and parent-free – then converts each captured op into one Item. The op’s kind maps to a

class:

PipelineKind, its blend to a :class:BlendMode, its tex_id to a texture slot, and its verts/indices are stowed in the :class:GeometryStore behind a handle the Item references. This adapter is the seam native per-node item emission replaces; the surrounding walk/sort/clip/transform machinery stays.

The collection seam: walk a Node tree and emit render Items.

The collection pass of the build-once 2D pipeline: it walks a scene tree and produces an :class:~simvx.graphics.render2d.item_list.ItemList. The

class:

RenderItemCache drives it each frame on the live render path.

Module Contents

Classes

Geometry

Captured vert/index arrays an Item’s geometry handle resolves to.

GeometryStore

A handle-indexed store of captured :class:Geometry.

NodeEntry

The retained per-node record an incremental patch locates items by.

CollectResult

The product of one :meth:ItemBuilder.collect.

ItemBuilder

Walks a Node tree and emits render Items into an :class:ItemList.

Functions

affine_row

Return node’s world_transform as a camera-free LOCAL affine row.

hdr_flag

Resolve a node’s hdr override to its HDR-lane flag (2D-in-HDR).

build_item_list

Collect root into a :class:CollectResult (one-shot convenience).

Data

API

simvx.graphics.render2d.item_builder.__all__

[‘CollectResult’, ‘Geometry’, ‘GeometryStore’, ‘ItemBuilder’, ‘NodeEntry’, ‘affine_row’, ‘build_item…

simvx.graphics.render2d.item_builder.affine_row(node: Any) simvx.graphics.render2d.item_builder._AffineRow

Return node’s world_transform as a camera-free LOCAL affine row.

The compact (a, b, c, d, tx, ty) row draw2d’s _xf uses (x' = a*x + b*y + tx). Camera-free by construction: world_transform composes only the node’s own + ancestor 2D transforms, never a Camera2D (the view lives in a per-frame UBO), so a camera pan rewrites one UBO row, not N item rows. Nodes without a 2D transform (a plain Node, a CanvasLayer) record identity; a Control is a Node2D and records a real row, whose translation carries its anchor/margin rect origin. Shared by the collection walk and the transform-only patch so both produce identical rows.

class simvx.graphics.render2d.item_builder.Geometry

Bases: typing.NamedTuple

Captured vert/index arrays an Item’s geometry handle resolves to.

The unit of the retained geometry store. verts are 8-float tuples (x, y, u, v, r, g, b, a) in the node’s LOCAL space (camera-free, parent-free); indices is None for line lists. Every handle is freshly appended.

verts: list[tuple]

None

indices: list[int] | None

None

class simvx.graphics.render2d.item_builder.GeometryStore

A handle-indexed store of captured :class:Geometry.

A plain growable list:

Meth:

add appends and returns the handle an Item records in its geometry column; :meth:get resolves it.

Initialization

__slots__

(‘_geoms’,)

__len__() int
add(verts: list[tuple], indices: list[int] | None) int
get(handle: int) simvx.graphics.render2d.item_builder.Geometry
set(handle: int, verts: list[tuple], indices: list[int] | None) None

Overwrite the geometry behind an existing handle (item-level patch).

Lets a render-dirty node re-capture its on_draw and replace its geometry slice in place, keeping the handle stable so the Item column and every consumer keying by handle stay valid (item granularity).

class simvx.graphics.render2d.item_builder.NodeEntry

Bases: typing.NamedTuple

The retained per-node record an incremental patch locates items by.

Built during collection so a later per-item / transform-only update finds the exact rows + transform slot + geometry handles a single node produced, without a full re-walk (item / transform-only granularity).

Attributes

transform_id The node’s row index in :attr:CollectResult.transforms (its LOCAL affine). A transform-only change rewrites just this row. rows The physical :class:ItemList row indices the node’s ops produced. geometry The geometry handles (parallel to rows) the node’s ops produced. A render-dirty change re-captures the node’s on_draw and overwrites these in place. canvas_affine The affine baked into this node’s captured geometry: the enclosing CanvasLayer’s, composed with the Control per-child wrap translation when the node sits under one (None for plain world content). An in-place re-capture re-bakes with this so both survive an item-level patch. screen_space Whether the walk emitted this node inside a screen-pinned scope (a CanvasLayer with follow_viewport=False). The flag comes from the enclosing scope, not from the node’s own draw calls, so an in-place re-capture must re-apply it or the item would fall back under the camera. node The emitting node itself. node_index is keyed by id(node), which answers “is this node retained?” but cannot answer “which nodes are retained?”; a consumer that wants the second question – the freeze detector sampling one clean node per frame – would otherwise have to re-walk the whole tree to find a single node it already has a record for. The reference is dropped by the next collect, and a structural change forces one, so it extends no node’s life beyond a frame.

transform_id: int

None

rows: list[int]

None

geometry: list[int]

None

canvas_affine: simvx.graphics.render2d.item_builder._AffineRow | None

None

screen_space: bool

False

node: Any

None

class simvx.graphics.render2d.item_builder.CollectResult

Bases: typing.NamedTuple

The product of one :meth:ItemBuilder.collect.

Bundles the three artefacts the later sort/batch/submit consume: the SoA

Class:

ItemList, the :class:ClipScopeTable its clip_scope indices point into, the :class:GeometryStore its geometry handles resolve against, and the per-item LOCAL transform rows its transform indices select. node_index maps each emitting node (by id) to its

Class:

NodeEntry, the substrate for in-place per-item / transform-only patching.

items: simvx.graphics.render2d.item_list.ItemList

None

clips: simvx.graphics.render2d.clip_scope.ClipScopeTable

None

geometry: simvx.graphics.render2d.item_builder.GeometryStore

None

transforms: list[simvx.graphics.render2d.item_builder._AffineRow]

None

node_index: dict[int, simvx.graphics.render2d.item_builder.NodeEntry]

None

class simvx.graphics.render2d.item_builder.ItemBuilder

Walks a Node tree and emits render Items into an :class:ItemList.

Reproduces the live _draw_recursive visit order (so a stable (layer, seq) sort of the result equals walk order) while keeping geometry camera-free and recording per-item LOCAL transforms + nested clip scopes. See the module docstring for the op-adapter bridge.

One builder is single-use per :meth:collect; construct a fresh one (or just call :func:build_item_list) per collection.

Initialization

__slots__

(‘_items’, ‘_clips’, ‘_geom’, ‘_transforms’, ‘_seq’, ‘_node_index’, ‘_canvas_affine’, ‘_screen_space…

collect(root: Any, *, layer: int = 0, overlay_draws: tuple = ()) simvx.graphics.render2d.item_builder.CollectResult

Walk root then the open overlay set, returning the :class:CollectResult.

layer is the starting CanvasLayer band (0 = world content). A CanvasLayer child shifts its subtree’s band; nested CanvasLayers are absolute (the layer does not add).

overlay_draws is the ordered (kind, control, entry) sequence from iter_overlay_draws (empty when no overlay is open -> byte-identical to a plain collect(root)). Each overlay is walked in SCREEN SPACE (parity with the immediate path under a panned Camera2D) and into the overlay sort tier, so overlays land strictly on top regardless of CanvasLayer band.

simvx.graphics.render2d.item_builder.hdr_flag(node: Any) simvx.graphics.render2d.item_list.ItemFlags

Resolve a node’s hdr override to its HDR-lane flag (2D-in-HDR).

hdr=True -> :attr:ItemFlags.HDR_OPT_IN (force the HDR lane), hdr=False -> :attr:ItemFlags.HDR_OPT_OUT (force the LDR lane), None/absent ->

Attr:

ItemFlags.NONE (by role: world->HDR, screen->LDR). Read per node and applied to all its items; the submit lane logic (submit.item_in_hdr_lane) combines this with the screen-space role. Resolved fresh in both the build walk and the in-place re-capture patch, so toggling hdr takes effect next frame without a full re-collect.

simvx.graphics.render2d.item_builder.build_item_list(root: Any, *, layer: int = 0) simvx.graphics.render2d.item_builder.CollectResult

Collect root into a :class:CollectResult (one-shot convenience).

Equivalent to ItemBuilder().collect(root, layer=layer). This is the on-demand entry point for tests and the (default-OFF) flagged path; it does not touch the live render path.