# Layer Order — Authored Cross-Layer Draw Order > **Status**: IMPLEMENTED on branch `feat/layer-order`. This document specifies an authored > per-layer draw order (`layer_order`), the 2D-layer-stack idea — CSS `z-index` > / Illustrator layer depth — applied to Luxar's overlapping 3D layers. It is > the cheap, authorially honest alternative to per-element cross-node > interleaving or one globally merged draw. It adds no new per-element > machinery: one integer per layer feeds the `renderOrder` assignment that > already exists. --- ## 1. What exists today — and why overlapping layers already look stable `render-order.ts` puts every visible **sorted-mode** mesh on one global integer scale, `renderOrder` 1..M, farthest first, by three rules (full prose at `assignGlobalRenderOrder`): 1. Slots group by partition wrapper; a single leaf is a group of one. 2. Groups order by the **mean view-z** of their members' content centroids. 3. **Containment overrides depth**: when one group's bounding sphere strictly contains another's, the container is forced to draw FIRST (#843), because no single per-mesh integer is correct for an embedded node — the container's centroid sorts nearer for ~half of all camera orientations, and an order-dependent mode drawn container-last multiplies the embedded node's pixels by the container's whole transmittance, ≈ erasing it. Rule 3 is why the two multichannel bioimaging demos do **not** pop today. Measured 2026-09-01 on the hosted build, `volumetric` forced on every layer, 8 camera angles spanning 300°. Converting an anisotropy-corrected neuromast rebuild from lateral-pixel units to microns is a uniform scale, so it leaves the containment edge and `DISTINCT_ORDERINGS = 1` unchanged: | demo | fixed order | radius | contains the next? | | --- | --- | --- | --- | | acto3d heart | `vasculature` @1 | 1130.74 | yes — margin **2.2** | | | `tnni3` @2 | 1126.79 | yes — margin 49.7 | | | `nuclei` @3 | 1064.67 | — | | neuromast | `nuclei` @1 | 555.54 | yes — margin **1.7 %** | | | `membranes` @2 | 546.39 | — | `DISTINCT_ORDERINGS = 1` for both, and the order is exactly by **decreasing radius** — the signature of a containment DAG, whose edges always point large → small. These layers are fits of the same specimen, so they are near-concentric and the largest sphere swallows the rest. **Two problems, both visible in that table.** - **The stability is accidental.** The heart's `vasculature ⊃ tnni3` edge has 2.2 units of margin on a 1130-unit radius — **0.19%**. A refit that nudges either channel's extent breaks the edge and the popping returns, with no authoring change and no signal. (The `CONTAINMENT_EPS = 1e-3` slack does not rescue this: the edge clears the strict test on its own, by 0.19%.) - **The inferred order is not necessarily the intended one.** Container-first is damage limitation — "under-attenuating a marker is the lesser error vs. blinking it out entirely on camera orbit". Nothing lets an author say *vasculature should read through the cardiac tissue*. And a third gap, one rule 3 never addressed: **commutative-mode layers are pinned at `renderOrder` 0** and therefore draw *before* the farthest sorted mesh, always. `additive` additionally carries `depthTest: false` and paints through everything. So `additive`-vs-`volumetric` layering is not merely approximate, it is not expressible at all. --- ## 2. The mechanism: depth bands One authored integer per layer, `layer_order`. Higher = nearer the camera = drawn later = composites on top (§3 D1). `assignGlobalRenderOrder` gains the level as its **primary key**: ``` groups := today's grouping (partition wrapper | single leaf) bands := groups partitioned by effective layer_order for each band, ascending by level: # lower level = farther = drawn first order the band exactly as today # mean view-z, then containment hoist emit renderOrder = nextRank++ per member as today ``` Two properties fall out, and both are load-bearing: - **Unset ≡ level 0.** With nothing authored anywhere, every group lands in a single band, the band loop degenerates to one iteration, and the algorithm is today's algorithm *verbatim* — not a special case, not a guarded fast path. §3 D2's "unset is byte-identical to today" is therefore structural, which is what makes the change safe to land dark and what makes the golden test in §10 meaningful. - **Bands are hard.** Two layers in different bands never interleave, whatever the camera does. That is the entire point: it converts today's accidental stability into a stated contract. The output is still the same `renderOrder` 1..M integer sequence, so nothing downstream changes — no scene-graph change, no new attribute, no per-element work, no shader change. **On three.js `groupOrder`, which outranks `renderOrder`.** Both painter comparators (`three.module.js:8112` `painterSortStable`, `:8142` `reversePainterSortStable`) compare `groupOrder` → `renderOrder` → `z` → `id`, and `groupOrder` is derived from the *innermost* `Group` ancestor's `renderOrder` during `projectObject`. In the standalone app that is 0 everywhere; in the **embed** path `LuxarLayer` deliberately stamps its configured `renderOrder` (default 10) onto **every owned `THREE.Group`** (`src/core/layer/luxar-layer.ts:737-741`), a documented contract to host applications (`LUXAR_LAYER_SPEC.md`, "Draw order and visibility"). Either way the value is *uniform across Luxar's own content*, so per-mesh `renderOrder` remains the discriminator among Luxar layers and bands work unchanged. What follows is a **scope limitation** rather than a problem: `layer_order` orders layers *within* the Luxar subtree only. Ordering Luxar content against a host application's own transparent geometry is the `LuxarLayer` `renderOrder` option's job — a different knob at a different scope. The two compose (host groups pick the band block's position; levels order inside it) and must stay documented as separate. This also means the uniformity above is an invariant worth keeping: if Luxar ever gave two of its own Groups different `renderOrder`, `groupOrder` would start splitting bands underneath us. --- ## 3. Pinned decisions **D1 — Higher = nearer the camera.** `layer_order = 30` draws after (on top of) `layer_order = 10`. This matches CSS `z-index` and Illustrator's bring-to-front, i.e. every 2D layer tool an author has met, and is *inverted* from the renderer's internal "farthest first, lowest `renderOrder` drawn first" direction. The inversion lives in one comparator and is worth it; the alternative optimises for the reader of `render-order.ts` over the author of a scene. **D2 — Unset ≡ 0, and unset MUST stay distinguishable from an authored 0.** `layer_order` gets **no writer-stamped default** — it is absent from both `WRITER_STAMPED_APPEARANCE_DEFAULTS` and `IDENTITY_COMPOSITING_ATTRS`, joining `blending_mode` / `visible` / `join` / `nd_transform` whose "absence on disk is genuine silence". `opacity` is the cautionary precedent — its stamped identity "cannot be distinguished from a deliberate authored identity". Be precise about *why*, because the tempting justification is wrong. A stamped `0` would **not** break the ordering: every group would land in band 0, that is one band, and containment would go on operating exactly as it does for an unauthored scene (D3 — it is a band *difference* that overrides containment, not explicitness). What a stamped default would destroy is the ability to tell the two states APART, and three things depend on that: - the bucket-straddle diagnostic is gated on the order being authored, so it would start firing on scenes that authored nothing; - the Layers panel would show `0` in every field instead of a blank `auto`, so no one could see which layers actually state an order; - any future rule that wants to treat "the author chose 0" differently from "nobody chose" would have no way to, and the distinction cannot be recovered after the fact — every store written in the meantime would already claim it. **D3 — A DIFFERENCE in layer order wins over containment; containment operates only within a band.** Because bands are hard partitions, a containment edge between two different bands cannot be represented at all — the relation is simply dropped. The viewer therefore **warns once per node pair** when a band split breaks a containment relation, naming both paths, so "my embedded marker vanished" is diagnosable rather than mysterious. Containment continues to operate unchanged between groups in the *same* band (including two groups whose orders are both unset, which is every scene today). Note the precise wording, because the obvious paraphrase is wrong: it is the *difference* that overrides containment, not the mere act of authoring. Two layers that share a band are still ordered by containment, and **an authored order equal to a neighbour's has no ordering effect** — including the easy-to-hit case of authoring `0` on one layer while its neighbour states nothing, since unset also resolves to band 0. Stating an order between two layers therefore means giving them *distinct* values. `levelExplicit` is tracked per group but deliberately does not enter the ordering: it gates only the bucket-straddle diagnostic, because a warning about a half-honoured order should not fire for a scene that authored no order at all. **D4 — Applies to every blending mode, commutative included.** A level on an `additive` layer is a no-op against other `additive` layers (addition commutes) but is meaningful against `normal`/`volumetric` ones, which is the §1 third gap. Safe by construction: a layer with no authored level keeps `renderOrder` 0 and today's behaviour exactly. > Assumption flagged for correction: this scope was inferred rather than chosen. > Asked whether commutative modes should be in scope, the answer given was > "**of course, the default, if no depth-level is provided should be the same as > what we have today**" — a constraint (D2) that all three scope options > satisfy. Full scope is taken because it is the only option that both honours > D2 *and* helps the seven demos #1964 moved to `additive`: with an authored > order they could return to `volumetric`, which is what they wanted in the > first place (#880). If the intent was the narrower scope, D4 is the decision > to revisit and §7's collect-loop change is the only code that changes. **D5 — It is a per-LAYER property: a compositing attr, nearest-setter-wins.** `layer_order` joins `COMPOSITING_ATTRS` — the set that "rides on a wrapper Group (where the user thinks of the wrapper as *their layer*) rather than getting copied onto each internal child" — and `AUTHORED_APPEARANCE_ATTRS`, so a structure-only rebuild (`gsplat lod`, `gsplat additive`, …) carries it from source root to output root instead of dropping it (#1600's class of bug). It composes root→leaf **nearest-setter-wins**, like `blending_mode` / `join` / `colormap`, not multiplicatively. **D6 — A level authored strictly inside ANY specialized group is an ERROR.** That is `kind=partition` *or* `kind=lod`, at any depth, which makes the rule statable in one line: **`layer_order` may participate only when authored on a node that is a layer** — a plain group or a top-level leaf — never on the internals of a specialized group. The scene root may carry the attr, but like every rendering attr there it is excluded from composition and ignored. For a partition the reason is severe (§5: it would split the wrapper across bands and destroy the exact Fuchs–Kedem–Naylor part order); for a `kind=lod` group it is that a level is an *alternative*, only one of which renders, so a level on one would be inert — and an attr that writes cleanly and silently does nothing is this codebase's most expensive failure mode. One ancestry check covers both, and covers nested cases (a lod group inside a partition part) with no extra rule. Refused at authoring through *both* doors (the adder kwarg and a post-hoc `node.attrs["layer_order"] = …`), mirroring `reject_lines_only_join` + `reject_lines_only_join_assignment`. The viewer, which must render whatever it is handed, instead warns once and uses one level for the whole order group, keeping the first collected member's — strict write, tolerant read. **D7 — Layers-panel edits are session-only.** Matching every other control in that panel: "Edits made in the panel are viewer-only and not persisted back to the zarr store." --- ## 4. Why this is cheap | | Route B (archived) | Route C (§8.2) | Layer order | | --- | --- | --- | --- | | Per-frame cost | +5.5–6.4 ms at 2M (measured) | one frame of sort lag | **none** | | New per-element machinery | shard meshes, per-shard AABBs, k-way merge | global element storage, per-element uniform indirection | **none** | | Subsystems gaining a permanent invariant | 9 | ~all of them | **0** | | Ordering quality | 84–93% of the error removed | exact | *stability, not correctness* (§5) | | Expressible authorial intent | none | none | **the whole point** | The last two rows are the trade, stated plainly: this does not make interpenetrating layers *correct*. It makes their order **stated, stable and overridable**, which is what the two demos actually need and what neither archived route offered. --- ## 5. Interaction with BSP part order *(The question that shaped this design.)* **They live at different levels of the hierarchy and do not compete.** A `kind=partition` wrapper IS the layer (D5), so the level decides where the *whole partition* sits relative to other layers, while the stored BSP tree decides the internal order of that partition's parts — the exact Fuchs–Kedem–Naylor order, valid for any camera pose including inside the volume (the #565 guarantee). Parts inherit the wrapper's level by nearest-setter-wins, so **a partition is never split across bands**, and the two mechanisms never meet. **That is exactly why a per-part level must be refused (D6, which extends the same refusal to `kind=lod` internals) rather than ignored.** If parts could carry their own levels, the wrapper would split across bands and its parts would interleave *by band* instead of *by the tree* — silently destroying the one ordering guarantee in the system that is exact. An author writing a per-part level has expressed something the renderer cannot honour without discarding a stronger guarantee, so the honest response is a refusal at write time, not a best-effort reinterpretation. A second, quieter reason: `assignGlobalRenderOrder` chooses BSP-rank-vs-view-z **once per group** (`everyMemberRanked`), and that all-or-nothing choice is load-bearing — mixing the two keys makes the comparator non-transitive, and `Array.sort` may then return an order violating both keys. Bands do not disturb this, *because* groups stay whole within a band. A per-part level would reopen it. Two neighbouring structures, for completeness: - **`kind=lod` wrappers** are unaffected: levels are alternatives and only one renders, so a level on the wrapper composes to whichever level is live. - **Nested wrappers** (a partition of LOD groups) are covered by D6's refusal, which closes the whole class rather than requiring a rule about which of two nested levels wins. ### 5.1 The full hierarchy of orders Seven nested mechanisms decide what is drawn when. The level slots in at #3 — above every view-dependent rule, below the two that are not ours to move. | # | Mechanism | Scope | Camera-dependent? | Authorable? | | --- | --- | --- | --- | --- | | 1 | three.js bucket (opaque → transmissive → transparent) | whole scene | no | **no** — §8.1 | | 2 | `groupOrder` (an ancestor `Group`'s `renderOrder`) | subtree | no | unused (always 0) | | 3 | **`layer_order` band** | layer | **no** | **yes — this spec** | | 4 | Containment DAG hoist | within a band | no | indirectly (bounds) | | 5 | Mean view-z of groups | within a band | **yes** | no | | 6 | BSP part rank (exact) or member view-z | within a group | **yes** | no | | 7 | `aSortedIndex` per-element permutation (exact) | within a mesh | **yes** | no | Reading it top-down is the design in one line: **an authored level is the outermost thing we control, and everything view-dependent below it keeps working unchanged, carried along inside whatever band it lands in.** ### 5.2 What a level on a group means Setting a level on a group node moves that group's whole subtree **relative to other bands, as a rigid block — and the view-dynamic orders inside it travel with it intact.** A partition wrapper keeps its exact per-frame BSP part order (#6); every mesh keeps its per-element permutation (#7). The band changes only *where the block sits*, never *how the block is internally ordered*. That is the intended behaviour and the reason bands sit above rules 4–7 rather than among them. Three consequences that are easy to assume wrongly, and are not obvious from the sentence above: - **A band is not an atomic unit.** A level makes a group a block relative to *other bands*, not relative to *other layers sharing its band*. Two leaves in one group at level 20 and an unrelated layer also at level 20 all order among themselves by rules 4–6, so the unrelated layer can interleave *between* the group's leaves. To make a group genuinely indivisible, give it a band of its own. (A plain `group` node is not an order group — `render-order.ts` groups by *partition wrapper or single leaf*, so a plain group's leaves are independent groups that merely happen to share a level.) - **A leaf can escape its group's band.** Nearest-setter-wins (D5) means a level authored on a leaf beats the one on its group ancestor, so that leaf leaves its siblings behind. This is deliberate and consistent with every other compositing attr, and it is *safe* here in a way D6's partition case is not: a plain group carries no exactness guarantee to destroy. But it is the opposite of "the group moves as a unit", so it is a decision (§12.5), not an accident. - **An authored order cannot override the opaque/transparent bucket split.** If an opaque group has an equal or higher order than a transparent group, THREE still draws it first. The renderer warns once for the involved order groups, including the case where one band contains both bucket types. Two smaller cases, checked and benign: - **Negative levels vs. untracked meshes.** `renderOrder` 0 stays reserved for meshes the coordinator does not track; collected slots always receive 1..M regardless of band, so a negative band cannot alias 0 and untracked geometry still draws first. Unchanged from today. - **A level on the scene root** is ignored, like every other rendering attr there. The scene root is a carrier and is excluded from the composition chain. And one free benefit worth naming: because a band is camera-independent, it adds **no re-sort trigger and no per-frame work** — and it makes the LOD case stable that is unstable today, where switching level changes a node's bounds and can therefore flip a containment edge mid-orbit. **The honest limit.** A level is the right tool when the author knows the answer, and the wrong tool when there is not one to know. For two *concave interpenetrating* layers no valid whole-object order exists from every viewpoint — a level pins such a pair to one stated wrong answer instead of a camera-dependent wrong answer. Stable and diagnosable beats flickering, which is why this is worth shipping; it should never be documented as a geometric correctness fix. --- ## 6. Authoring surface (Python) ```python scene.add_gsplats("vasculature", ..., blending_mode="volumetric", layer_order=10) scene.add_gsplats("tissue", ..., blending_mode="volumetric", layer_order=20) scene.add_gsplats("nuclei", ..., blending_mode="volumetric", layer_order=30) ``` - A `layer_order` keyword accepted by `add_points` / `add_lines` / `add_gsplats` / `add_mesh` / `add_group`, plus post-hoc assignment through `node.attrs["layer_order"]`. - Validation (`validation/types.py::validate_layer_order`): a finite Python `int` (a `bool` is refused — `isinstance(True, int)` is the classic hole); any sign. Bounded to the JS safe-integer range (`JS_SAFE_INTEGER_MAX`, 2^53 - 1) — not an arbitrary clamp but the representable domain: the attr crosses to the viewer as a JS `number`, and past that magnitude two orders the author separated can collapse into one band, handing the choice back to the inference this attribute exists to override. - Registered in `COMPOSITING_ATTRS` and `AUTHORED_APPEARANCE_ATTRS`; **absent** from `WRITER_STAMPED_APPEARANCE_DEFAULTS` and `IDENTITY_COMPOSITING_ATTRS` (D2). A present-but-`None` value is rejected rather than treated as absent. - D6's refusal: `reject_layer_order_inside_specialized_group` in `core/group/compositing.py` plus its assignment-door twin in `core/node/node.py::_WriteThroughAttrs`. --- ## 7. Viewer surface | File | Change | | --- | --- | | `data/attrs-composer.ts` | `ComposableAttrs.layer_order?: number`; `EffectiveAttrs.layer_order?: number`; nearest-setter-wins, beside `blending_mode`. Header prose lists the new rule. | | `rendering/node-factory/*` | Stamp the composed level onto `mesh.userData.layerOrder` at node creation, alongside the other composed appearance values. | | `rendering/depth-sort-coordinator/render-order.ts` | `OrderSlot` / `OrderGroup` gain `level` + `levelExplicit`; `assignGlobalRenderOrder` sorts once by `(level, meanZ)`, and `orderGroupsWithContainment` restricts edges to groups in the same band so the existing Kahn pass drains bands in order while reporting harmful dropped containment edges (D3's warning). | | `rendering/depth-sort-coordinator.ts` | The collect loop (`~:1815`) now collects a commutative node **when it carries an explicit level**; other non-order-dependent nodes still reset to `renderOrder = 0` and skip collection. This is the only code D4 touches. | | `ui/layers/layer-controls.ts`, `layer-state.ts`, `layer-apply.ts` | A **Layer order** control per layer row (a small stepper, blank = unset). Session-only (D7). | | `ui/data-loading-monitor/templates/scene-graph.ts` | The live draw-order chip already prints `#renderOrder` + bucket; add the band so an author can see *why* a layer sits where it does. | Deliberately unchanged: the sort worker, the WASM/TS kernels, `element-storage`, picking, LOD, and the commit pipeline. This feature never touches per-element data. --- ## 8. What this does NOT fix 1. **The opaque/transparent bucket split.** three.js renders opaque, then transmissive, then transparent, and `renderOrder` only sorts *within* a bucket. `opaque` is the one blending mode with `transparent: false` (`blending-state.ts:292`), so **no `layer_order` can place an `opaque` mesh in front of a transparent layer.** A level spanning the two buckets is silently partially honoured, which is why the renderer warns once per layer when an authored order conflicts with that bucket order (`warnBucketOrderConflict`, §12.2) rather than leaving it to be discovered. Opaque layers with authored orders also receive positive `renderOrder` values, which can reduce front-to-back early-Z efficiency relative to untracked opaque meshes; this is a performance tradeoff, not a visual one. 2. **Interpenetrating concave layers** — §5's honest limit. 3. **Per-pixel ordering.** Out of reach of any per-element scheme (StopThePop's class of artifact); unchanged deferral. 4. **Within-node ordering**, which is already exact and untouched. --- ## 9. Phases - **Phase 1 — viewer-only, dark.** The composer field, the band partition, the containment-drop warning, the collect-loop change. Nothing authors it yet, so §10's golden test is the whole gate: every scene must produce byte-identical `renderOrder` integers. - **Phase 2 — authoring.** The Python kwarg, validation, the two refusals, the compositing-set registrations, format-spec documentation. - **Phase 3 — UI.** The Layers-panel control and the monitor band readout. - **Phase 4 — the demos.** Return the #1964 demos to `volumetric` with authored levels, and A/B them against their additive versions. This phase is what decides whether the feature earned its place, and it is a judgement on a render, not on a metric. Phases 1–3 are independently landable; Phase 4 is the one that can say no. --- ## 10. Testing and ship gates | Gate | Shape | | --- | --- | | **Unset is byte-identical** | Golden: for every existing test scene and the E2E fixtures, the assigned `renderOrder` integers with no level authored anywhere must equal today's exactly. This is the invariant that makes the change safe; it should fail loudly if the band loop is ever not a no-op on one band. | | Band ordering | `fast-check` property over random (level, view-z, radius) sets: never a member of a lower band after a member of a higher band; within a band, today's order reproduced. | | BSP exactness preserved | The #843 / #565 containment and BSP fixtures must pass unchanged with levels unset, AND with a level authored on the wrapper (which must not perturb internal part order at all). | | D6 refusals | Both doors, both directions: the adder kwarg on a partition part, and `node.attrs["layer_order"] = …` post-hoc. Plus the viewer's warn-and-ignore on a hand-built store carrying an inner level. | | D3 warning fires | A fixture where a containment relation is broken by a band split must emit exactly one warning naming both paths — a "fires-proof" test, not just a no-crash one. | | Commutative scope (D4) | An `additive` layer with an explicit level must receive a positive `renderOrder`; without one it must stay at 0. | | E2E, non-vacuous | A two-layer fixture where compositing order alone decides the dominant channel at the projected overlap: author level A>B, assert the pixel; swap to B>A, assert it inverted. **Fail-first verified** by pinning both levels equal. | | Docs gate | New file must be listed in a `docs/index.rst` toctree or `make check-docs` goes red. | --- ## 11. Risks 1. **D2 is the whole safety argument.** If anything ever stamps a default `layer_order`, rule 3 silently switches off for every store written after that point. The `IDENTITY_COMPOSITING_ATTRS` registration is the thing to guard with a test that asserts *absence*, not presence. 2. **D4 widens the collected set.** Commutative nodes have never received a positive `renderOrder`; an `additive` layer moving out of the "draws first" position can change appearance. Phase 4 converted the neuromast's two layers to depth-sorted `volumetric` blending, so it no longer exercises D4's commutative path. The risk remains for future scenes that mix a levelled commutative layer with untracked `renderOrder = 0` content. Phase 1 is behaviour-preserving only for unauthored scenes. 3. **A level is a promise the renderer cannot always keep** (§8.1's bucket split, §5's interpenetration limit). Documenting where it is partially honoured matters more than the mechanism, because a silently-half-applied ordering is worse than none. 4. **The Layers-panel control invites reordering by dragging**, which this design does not provide (a stepper, not a drag handle). If drag-to-reorder is wanted, it needs a defined mapping from list position to integer levels — §12. --- ## 12. Open questions 1. **Drag-to-reorder in the Layers panel?** A stepper is the minimum. Dragging rows is the familiar 2D-tool gesture, but needs a rule for turning list position into levels (renumber all layers? sparse 10/20/30 with insertion between?) and interacts with D7's session-only edits. 2. ~~**Should an authored order conflicting with the opaque/transparent bucket boundary warn?**~~ **DECIDED — yes, and implemented.** `warnBucketOrderConflict` compares authored opaque and transparent groups and warns once for the involved groups when the requested ordering cannot be honoured. See §8.1 for why the limit exists at all. 3. ~~**Should the demo authoring audit report authored orders?**~~ **DECIDED — the audit rule was restated rather than extended.** `test_overlapping_gsplat_layers_declare_order.py` replaces #1964's all-additive rule with: an overlapping-layer demo must be *either* entirely commutative *or* depth-sorted with an explicit `layer_order` on every layer. Relaxed in one direction (the old rule would now forbid the correct thing), tightened in another (a depth-sorted layer must now carry an order, which the old rule had no way to require). `luxar info` is untouched and remains a genuinely open, separate question. 4. **URL override** (`?layerOrders=path:level,…`) for A/B measurement without re-authoring. Not specified above. 5. **Should a leaf be allowed to escape its group's band?** (§5.2.) Nearest-setter-wins says yes and every other compositing attr agrees, but "set the group's level" then does not guarantee the group stays together. The alternative — a group's level *pins* its whole subtree, refusing or ignoring inner levels — would make groups atomic but breaks the composition rule uniformity that makes these attrs predictable. 6. **Is an atomic-block mode wanted at all?** If "this group must never be interleaved" is a real authoring need (§5.2, first bullet), it is a *different* feature from a level — closer to "render this subtree to its own pass" — and should not be smuggled into this one. --- ## 13. References - `rendering/depth-sort-coordinator/render-order.ts` — the three rules, the containment test, the per-group all-or-nothing rank choice. - `core/group/compositing.py` — `COMPOSITING_ATTRS`, `AUTHORED_APPEARANCE_ATTRS`, `WRITER_STAMPED_APPEARANCE_DEFAULTS`, `IDENTITY_COMPOSITING_ATTRS`. - `data/attrs-composer.ts` — the composition rules. - `ui/layers/README.md` — the layer row model and session-only edits. - PR #843 (containment), #1235 (tightened bound), #880 (demos → volumetric), #1964 (overlapping layers → additive), #1600 (appearance carried through rebuilds).