Luxar Viewer API Documentation - v2026.9.22
    Preparing search index...

    Class LayerApplyEngine

    Index
    • Parameters

      • path: string

      Returns Object3D<Object3DEventMap> | null

    • Clone-on-first-use for the material at a data-leaf, registering the clone with MaterialManager so camera-dependent uniforms stay current. Non-luxar materials return null.

      Parameters

      • obj: Object3D

      Returns LuxarMaterial | null

    • Resolve every data-leaf affected by changes to a layer at path. Data-node layers map to themselves; group layers fan out to all descendant points/lines/gsplats.

      Parameters

      • path: string

      Returns SceneNode[]

    • Recompose the effective attrs for a single data-leaf by walking the scene-graph ancestry, substituting panel state for every layer=true node in the chain.

      layerPath is the layer whose control was just used. Inside that layer's own subtree — and ONLY when the edited layer actually OWNS a mode (blendingModeExplicit) — the LAYER owns blending_mode: a mode authored on a descendant that is not itself a layer is dropped. blending_mode is nearest-setter-wins and a layer exposes exactly one Blend control, so without this a kind=partition / kind=lod layer whose parts carry their own stamped mode has an inert control — every part shadows the wrapper (the graft_gsplat_node stamping bug, and every scene already written by it). A wrapper that owns NO mode has no control value to impose, so it must NOT suppress its descendants' authored modes — otherwise a plain layer=true group over a mesh authored additive would snap the mesh to its opaque type-default on any non-blend edit (#1275). A nested node that IS a layer keeps its live value: it has its own control. Only blending_mode is affected — the multiplicative attrs still compose and offset still sums, so a part's authored opacity/gamma/κ is preserved.

      identityLayerWindow substitutes the IDENTITY for the edited layer's own display window (intensity/offset) — used by applyComposed for a leaf that renders direct colour while the layer's window is a SCALAR window (a mixed group layer), so the scalar window is never applied as a colour gain. Ancestor/leaf-authored windows still compose.

      precomputedAncestors lets a caller that already walked the chain (both fan-out loops do) hand it over instead of paying for a second walk. collectAncestorNodes resolves each step with a linear children.find, so one fan-out over a P-part wrapper costs ~P²/2 path comparisons — and applyComposed runs on every slider tick.

      Parameters

      • leafPath: string
      • layerPath: string
      • identityLayerWindow: boolean = false
      • OptionalprecomputedAncestors: readonly SceneNode[]

      Returns EffectiveAttrs | null

    • May the composed window for this leaf be re-stated in the leaf's OWN range?

      Two independent questions, both of which must answer yes:

      • Is the relation between the layer and the leaf one that a range change MEANS something across? Only kind=lod.
      • Is the composed window actually STATED in the layer's reference range (LayerInfo.scalarDataRange), so that reading it as a position inside that range is legitimate?

      A LOD level and a partition part are not the same kind of sibling.

      Levels are alternative representations of the WHOLE object, and gsplat LOD merging SUMS amplitudes — a coarse level's amplitude is the same physical signal at a different numeric SCALE. Re-expressing the window per level is exactly the correction that makes every level render one physical value identically, which is #1753's own repro.

      Parts are disjoint spatial subsets of ONE field at the SAME scale, and packages/luxar/src/luxar/io/_compiler/gsplat_assembly.py derives amplitude_data_range = [min, p99.9] per splat set — so two tiles differ purely by CONTENT. Windowing each part on its own range is per-tile auto-contrast: the same physical value renders as a different colour in different tiles and the colormap goes non-monotone, with a visible discontinuity at every BSP seam. On the repo's own tests/fixtures/test_partition_layer.luxar.zarr (layer=True, kind=partition — and layer=True is the default of luxar gsplat convert), part_0 is [0.5000, 0.7455] and part_1 [0.7542, 0.9998]: remapping would ramp black→white across BOTH, so the field would step 0.7455 (white) → 0.7542 (black) at the seam. Saturated-but-monotone is the correct failure. The producers say the same thing twice — core/group/adders/mesh.py::_shared_scalar_window ("a level or a part that stamps its own subset min/max renders the same value as a different colour … which is exactly the discontinuity this helper exists to prevent") and gsplats/lift.py, where beads share the finest node's scalar_data_range rather than a per-segment one.

      So every GROUP node from the edited layer down to (excluding) the leaf must be kind === 'lod'. Consequences, all deliberate: adaptive (a partition of per-tile lod groups) declines outright, because the right reference for a tile's ladder is that TILE's finest level rather than the layer's, and building that is more than #1753 asks for; and a plain group layer over several colormapped leaves (two channels, say) declines too — different physical fields, not one field at two scales.

      An overview tree (an lod group whose coarse cap is a leaf and whose fine branch is a nested partition) is a NO-OP in both branches, and it is worth being precise about why rather than claiming half a win. The tiles decline on the partition, as above. The cap is structurally eligible — but on a measured luxar gsplat lod --recipe overview store the cap and all four parts carry 432 splats each, and deriveScalarRangeFromDescendants breaks that tie with a strict count > bestCount while visiting the cap FIRST, so the cap IS the reference and remapWindowToLeafRange's equality short-circuit returns its window untouched. The fine parts therefore keep rendering on the cap's window (measured: part_2's own [0.00059, 0.19962] on the cap's [0.000116, 0.44551], so its brightest splat lands at LUT 0.45 instead of 1.0). Fixing that needs a per-branch reference, which is the same change adaptive would need.

      Once #1691 / PR #1752 lands (it harmonizes amplitude_data_range across a gsplat structure so siblings SHARE a window) partition parts will carry equal ranges and remapWindowToLeafRange's equality short-circuit would make this a no-op anyway. The kind gate is the safety net until then, and for every store already written.

      composeEffective multiplies intensity and sums offset over the WHOLE ancestry, substituting live panel state for every layer=true node, so several reachable shapes hand back a window in a completely different basis. Remapping one of those does not refine a correct window, it corrupts it — hence a predicate rather than a best-effort. Every arm below is pinned by a test in tests/unit/ui/layers/layer-apply-per-leaf-window.test.ts:

      1. The edited layer is not on this leaf's ancestry (layerDepth < 0). Nothing can be said about the basis, so nothing is done.
      2. The edited layer AUTHORED a gain. intensity/offset are compositing attrs, so add_gsplats_from_file(…, layer=True, intensity=0.5) / luxar gsplat convert --intensity 0.5 stamps them on the kind=lod wrapper itself. walkSceneGraph then seeds displayMin/Max from computeDisplayRange(0.5, 0) = [0, 2] — a window in the normalized-GAIN basis, with no relation to a scalarDataRange of, say, [0, 0.02]. That window was already wrong before this change (100x too wide); remapping it would multiply the error by leafSpan / refSpan on top. Declining leaves the pre-existing behaviour exactly as it was.
      3. A node strictly below the layer is itself TRACKED AS A LAYER. Not "contributes a gain" — a nested layer owns its own window and its own panel row, full stop, and its gain is not evidence either way. A colormapped child whose own range happens to be [0, 1] composes computeUniforms(0, 1) = {1, -0}, indistinguishable from "no window authored", so a gain test passed it and remapped a window that was already correct. deriveScalarRangeFromDescendants is the one derivation in layer-state.ts that does NOT stop at a nested layer, so the outer reference can be a sibling's range: over ch0 ([0, 1], 1e3 splats) and ch1 ([0, 5], 1e4 splats) it is [0, 5], and dragging the wrapper's opacity composed ch0's own correct [0, 1] — which remapping turned into [0, 0.2], 5x too narrow, with nothing re-applying ch0 afterwards. [0, 1] is not exotic: normalized scalars, probabilities, masks, fractions.
      4. A non-layer node strictly below the layer contributes an AUTHORED gain. This arm mirrors resolveColormapWindow's first branch, which says the same thing at load time: a non-identity RAW LEAF gain means the composed gain IS the window and the data range is not consulted.

      Ancestors ABOVE the edited layer are deliberately NOT gated — but they are not folded into the remap either. leafScalarWindow re-expresses the LAYER'S OWN window and re-applies the ancestor gain afterwards, which is what makes the panel match resolveColormapWindow's ancestor-only branch exactly (ancestor intensity = 2, reference [0, 2], leaf [0, 8] → both give [0, 4]) — see the note there for why remapping the COMPOSED window instead only agrees when the two ranges happen to share a relative origin.

      Parameters

      • leafPath: string
      • layerPath: string
      • OptionalprecomputedAncestors: readonly SceneNode[]

      Returns boolean

    • The scalar LUT window for ONE colormap-active leaf: the layer's composed window, re-expressed in that leaf's own data range.

      A layer composes a single window, but a kind=lod layer fans out over LEVELS whose scalars need not share a range — LOD merging sums amplitudes, so a coarsened level carries its own amplitude_data_range for the same physical signal, and the layer's reference range is merely whichever descendant deriveScalarRangeFromDescendants picked. Pushing the composed window verbatim rendered every level on the reference level's window, discarding exactly the per-level differentiation the producer stamped, and made a lazily-created level's window depend on load order (#1753).

      The remap runs only when the layer↔leaf relation is one a range change means something across, AND the composed window really is stated in the reference basis (composedWindowIsInReferenceBasis) — otherwise it would corrupt a window that was already correct, or auto-contrast a spatial partition tile by tile. The remap itself, and the range-shaped cases in which it must not happen, live in the pure remapWindowToLeafRange; the common single-range layer short-circuits there and gets the composed window back bit-exact.

      What gets remapped is the LAYER'S OWN window, not the composed one, and the ancestor gain is re-applied to the result. Only the layer's own window is a position inside layer.scalarDataRange; the composed window is that position already transformed by whatever gain the ancestry above the layer contributes, and reading it as a reference-basis position is a basis error of exactly the kind composedWindowIsInReferenceBasis exists to refuse. It cancels out when the two ranges share a relative origin (ref₀/refSpan === leaf₀/leafSpan — notably when both start at 0) and not otherwise: with ref = [1, 3], leaf = [0, 8] and an ancestor intensity = 2, remapping the composed window gives [-2, 2] where node creation gives [0, 4]. Swapping the layer's contribution reproduces resolveColormapWindow's ancestor-only branch identically for every range pair.

      Only meaningful for a colormap-active material: a direct-colour leaf has no scalar window (its gain/offset are a colour GOG), which is the same condition applyColorAdjustments routes on. That is also why eff here is never the identityLayerWindow composition — that substitution is made only for direct-colour leaves, which never reach this helper.

      Parameters

      Returns { min: number; max: number }

    • Push each composed effective attribute (except colormap, which is per-leaf and doesn't chain through ancestors) to every affected leaf material. Colormap is handled separately because textures don't compose — the nearest ancestor's colormap wins.

      Parameters

      Returns void

    • Push the composed draw order onto the meshes the depth-sort coordinator reads it from.

      Does not go through applyComposed — an order is a cross-node SORT KEY, not a material uniform, so there is no mat.updateX to call and nothing in the shader to refresh. But it DOES compose: the value written to each leaf is composeEffective's, not this layer's raw one.

      That distinction is the bug this replaced. Assigning layer.layerOrder directly to every affected leaf clobbered the order of a nested leaf that is ITSELF a layer with its own authored order — getAffectedDataLeaves returns every data descendant, including nested layers, and nearest-setter-wins says the nested one should win. Composing per leaf restores that, and lets an order on a non-layer intermediate group participate too.

      This is render-only session state. It must not be written into userData.attrs, which is the loaded SceneNode attrs object for lines and gsplats and would make a panel edit look authored on the next composition.

      Parameters

      Returns void

    • Push the mesh shading values (§6.2) to the layer's mesh leaves.

      Deliberately NOT routed through applyComposed, which is what every other control here uses, because these values do not compose along the ancestry: opacity/gamma/intensity multiply and offset sums, so an ancestor's value has to fold into a descendant's, whereas a shade floor is a per-surface appearance choice with no composition rule — multiplying two ambients would mean nothing.

      It still has to FAN OUT like applyComposed does, though. A mesh layer is no longer always a leaf: add_mesh(partition=…) writes a kind=partition wrapper and the panel presents that wrapper as one mesh layer, so layer.path resolves to a THREE.Group with no material of its own. Writing only there left all mesh appearance sliders visible and completely inert on a partitioned surface. A non-mesh leaf needs no extra gate — it simply has no updateAmbient, so the optional chaining below is the type check.

      alphaCutoff also rides to the PICK material, because the pick pass applies the identical cutout (§6.5): a threshold that moved on screen but not in the pick buffer would make a freshly-dissolved region still hoverable.

      Parameters

      Returns void

    • Push a physical layer's live knobs (LayerInfo.physicalKnobs, in material space) onto every material="physical" leaf beneath it. The optional-chained updatePhysicalKnob is the type gate, as for the house knobs above: a house or emissive leaf simply lacks it. A no-op for a layer with no knob record.

      Parameters

      Returns void

    • Colormap applies per-leaf (not composed). For a group-layer we push the selected colormap to every data descendant that accepts one.

      Returns whether the layer now actually renders through a colormap — i.e. at least one leaf accepted the LUT. Clearing a colormap always "takes", so that returns false (no colormap in effect). The caller needs this because the C1 fail-closed guard below can suppress the colormap on every leaf (a group layer over scalar-less points still offers the dropdown): the layer then keeps rendering DIRECT COLOUR, so its display window must stay the direct-colour identity rather than move to a scalar range.

      A leaf whose mesh/material is not in the scene yet (partition parts and LOD levels stream in) is NOT a guard suppression — when no leaf material was reachable at all, the request is taken at face value so the caller keeps the user's pick instead of reverting it mid-load.

      Parameters

      Returns boolean