Tracks every lod_group scene-graph node currently loaded. For each
one, every frame:
Fold each child's raw nD positionBounds into a cached per-entry
local-space :type:BoundingBox, using the current displayDims
to map nD axes onto X/Y/Z, then transform it to world space for the
frustum gate. Eviction uses the same full-geometry box.
When any child publishes optional robust lodBounds, fold them the
same way (falling back per child to positionBounds) for metric
sizing only, so excluded outliers remain visible and resident.
Transform the metric box into world space via
:func:transformBoundingBox and the lod_group's matrixWorld.
Project the 8 corners through the camera and reduce them to the
dimensionless coverage metric, on whichever scale the entry's
selector names — the two branches of evaluateEntry:
'screen-area' (what every derived ladder stamps): the fraction of
the viewport the projected AABB covers by AREA, via
projectBoxAreaFraction. Aspect-free, tops out at 1.0.
'coverage' (legacy): back to pixel coordinates, then the diagonal
of the screen-space AABB divided by FILL_FACTOR × fittedAxisPx
(fittedAxisPx is min(viewport.width, viewport.height) — the
extent calculateCameraDistance actually fits; see the
FILL_FACTOR doc), so 1.0 == the object's projected diagonal has
reached FILL_FACTOR of the fitted axis.
Pick the finest child whose coverage_fraction threshold is
satisfied by that coverage metric, with 10% asymmetric hysteresis on
the downgrade direction to suppress threshold-edge flicker.
If the desired child differs from the current active one, swap
visibility atomically — gated by the never-downgrade display
gate: a fresh aspiration whose additive ladder is still streaming
is not shown while the previously-displayed level looks strictly
better (see shouldHoldPreviousDisplay in lod-display-gate.ts).
The atomic-swap invariant on initial load is realized by
loadLodGroupNode (sequential awaits + visible=false after
attach + a single register() call at the end) — no per-child
ready gate is needed in the registry.
Partition frustum gating (registerPartition / evaluatePartitionEntry):
a kind=partition group's parts are tested every frame against a frustum
padded by PARTITION_FRUSTUM_MARGIN; a part outside it is hidden and
stamped userData.partitionFrustumVisible = false, which the scene
loader's sweep and refinement read to skip its loaders. Because a culled part
misses slice updates, its RE-ENTRY requests a resync of exactly that part's
loaders (deps.requestReprocess(partPaths), coalesced per wrapper across
frames and gated on isUpdateInProgress). The resync re-sweeps under the
UNCHANGED view version — bumping it here would read every lazy fine level
scene-wide as stale and drop all groups to coarse on camera motion.
The bbox infrastructure is shared with the scene-bounds cache and
camera framing — projectBoundsToDisplayDims /
transformBoundingBox live in
scene-manager/clipping/bounds-math.ts and are reused here.
Wiring: the SceneLoader instantiates one registry per scene; the
pipeline hooks evaluatePerFrame into AnimationController
alongside the dynamic-clipping callback. Manual override
(setSelectorMode(path, { lockLevel: i })) bypasses the auto
selector — driven by the layers-panel dropdown.
Per-frame LOD-group selector.
Tracks every
lod_groupscene-graph node currently loaded. For each one, every frame:positionBoundsinto a cached per-entry local-space :type:BoundingBox, using the currentdisplayDimsto map nD axes onto X/Y/Z, then transform it to world space for the frustum gate. Eviction uses the same full-geometry box.lodBounds, fold them the same way (falling back per child topositionBounds) for metric sizing only, so excluded outliers remain visible and resident.transformBoundingBoxand the lod_group'smatrixWorld.selectornames — the two branches ofevaluateEntry:'screen-area'(what every derived ladder stamps): the fraction of the viewport the projected AABB covers by AREA, via projectBoxAreaFraction. Aspect-free, tops out at 1.0.'coverage'(legacy): back to pixel coordinates, then the diagonal of the screen-space AABB divided byFILL_FACTOR × fittedAxisPx(fittedAxisPxismin(viewport.width, viewport.height)— the extentcalculateCameraDistanceactually fits; see theFILL_FACTORdoc), so 1.0 == the object's projected diagonal has reachedFILL_FACTORof the fitted axis.coverage_fractionthreshold is satisfied by that coverage metric, with 10% asymmetric hysteresis on the downgrade direction to suppress threshold-edge flicker.shouldHoldPreviousDisplayinlod-display-gate.ts).The atomic-swap invariant on initial load is realized by
loadLodGroupNode(sequential awaits +visible=falseafter attach + a singleregister()call at the end) — no per-childreadygate is needed in the registry.Partition frustum gating (
registerPartition/evaluatePartitionEntry): akind=partitiongroup's parts are tested every frame against a frustum padded byPARTITION_FRUSTUM_MARGIN; a part outside it is hidden and stampeduserData.partitionFrustumVisible = false, which the scene loader's sweep and refinement read to skip its loaders. Because a culled part misses slice updates, its RE-ENTRY requests a resync of exactly that part's loaders (deps.requestReprocess(partPaths), coalesced per wrapper across frames and gated onisUpdateInProgress). The resync re-sweeps under the UNCHANGED view version — bumping it here would read every lazy fine level scene-wide as stale and drop all groups to coarse on camera motion.The bbox infrastructure is shared with the scene-bounds cache and camera framing —
projectBoundsToDisplayDims/transformBoundingBoxlive inscene-manager/clipping/bounds-math.tsand are reused here.Wiring: the SceneLoader instantiates one registry per scene; the pipeline hooks
evaluatePerFrameintoAnimationControlleralongside the dynamic-clipping callback. Manual override (setSelectorMode(path, { lockLevel: i })) bypasses the auto selector — driven by the layers-panel dropdown.