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

    Variable FILL_FACTORConst

    FILL_FACTOR: 0.5

    Unit anchor for the LEGACY selector: 'coverage' thresholds (older stores, and explicitly authored coverage_fractions=[...] lists — derived ladders now use selector: 'screen-area', whose metric is projectBoxAreaFraction and does not involve this constant): a threshold of 1.0 is satisfied once the group's projected bbox diagonal reaches FILL_FACTOR × fittedAxisPx pixels — i.e. HALF of the fitted screen axis (fittedAxisPx = min(viewport.width, viewport.height)), which a normal full-frame view already exceeds. Coarser children (smaller fractions) step in as the object shrinks below that. Lowering the factor shows finer levels sooner, raising it later.

    Why the fitted axis, not the viewport diagonal (#1410). The previous anchor normalised by hypot(viewport.width, viewport.height), which grows with width regardless of aspect, while calculateCameraDistance fits the VERTICAL fov for aspect ≥ 1 (camera distance has NO aspect dependence there) and the HORIZONTAL fov for aspect < 1. So a wide-but-not-tall canvas grew the denominator without the framing showing any more of the object, and the raw ratio collapsed as the canvas widened — a cube's opening-framing metric fell from 3.43 at 1:1 to 1.31 at 32:9 under the old scheme, and thinner shapes (an in-plane rod, a flat pancake) dropped BELOW the finest threshold entirely on an ultrawide monitor (#1361's blur, returning at wide aspects).

    fittedAxisPx is exactly the extent calculateCameraDistance fits in each regime — height for aspect ≥ 1, width for aspect < 1. The fit uses the larger X/Y extent at the box's nearest face: halfDepth + inPlane/(2·fitRatio·tan(halfFov)) (and divides the second term by aspect in portrait). The near-face distance is therefore proportional to the fitted axis in both regimes, so the projected pixel diagonal divided by fittedAxisPx is EXACTLY invariant across aspect and absolute viewport size for the default centre fit modelled here. Preserving an authored off-centre controls target changes which depth face bounds each side of the projected rectangle. The identity remains exact while the target lies inside the box's screen-plane footprint and at or behind its near face (target.z <= box.max.z). It degrades when either condition is violated: for an 8×8×100 box, a target 20 units off-axis drifts 23.8%, while a centred target 10 units in front of the near face drifts 50.8%. The test matrix verifies the centre-fit identity to 9 decimal digits for a cube, pancake, in-plane rod, UMAP-like box, and a 1×1×100 view-axis rod from 1:4 portrait through 32:9 ultrawide.

    Why 0.5 and not 1.0: at 1.0 the finest level only activates once the object OVERFILLS the fitted axis, reproducing the original #1361 symptom at the default opening framing. Measured opening-framing diagonalPx / fittedAxisPx across all five shapes and seven aspect ratios in the test matrix ranges 0.750 (in-plane rod) – 1.061 (cube, pancake, and view-axis rod). Dividing by 0.5 turns that into a metric of 1.50 – 2.12 — past the finest threshold of 1.0 with 50% headroom in the worst case. The factor remains necessary: at 1.0 the in-plane rod would still open below the finest rung even though the framing itself is now aspect-exact.

    Coupled constant. Python's MAX_COVERAGE_FRACTION (the upper bound on any coverage_fraction, authored or derived — a partition-bound ladder derives exactly this value; see core/group/lod/group.py) stays 4.0, re-expressed as SCREEN_FILL_DIAGONAL_RATIO / FILL_FACTOR rather than 1 / FILL_FACTOR: a screen-filling object's projected diagonal is no longer exactly the fitted axis — that identity only held for the OLD diagonal normalisation, where a screen-filling box's diagonal trivially equals the viewport's own diagonal. Under the fitted-axis normalisation it is instead hypot(aspect, 1) / min(aspect, 1) times the fitted axis — aspect-DEPENDENT, not a constant — measured 1.41 at 1:1, 1.67 at 4:3, 1.80 at 3:2, 1.89 at 16:10, 2.04 at 16:9, 2.57 at a real 21:9 panel (2560×1080), 3.69 at 32:9. SCREEN_FILL_DIAGONAL_RATIO does NOT pin an average across that spread — it is anchored specifically at 16:9, the reference aspect (2.04, rounded to a plain 2), the same reference the FILL_FACTOR value 0.5 above is calibrated at. Away from 16:9 the identity is increasingly approximate: 21:9 alone is ~28% off the round value. So MAX_COVERAGE_FRACTION = 2 / 0.5 = 4.0 is unchanged in VALUE, and the switch point it anchors (reaching metric 4.0) still needs diagonalPx ≈ 2 × fittedAxisPx at 16:9, matching what diagonalPx ≈ viewportDiagonal (metric 4.0 under the OLD scheme) meant there — but away from 16:9 this is a real, accepted behavioural trade, not just a units relabelling. Because a screen-filling object's diagonal is narrower than 2·fittedAxisPx at square-ish aspects and wider at ultrawide ones, a partition-bound ladder's finest level (which derives its anchor from MAX_COVERAGE_FRACTION) now needs a tile to grow LARGER on screen before showing its finest level at square-ish/portrait-ish windows, and SMALLER at ultrawide ones, than it did before #1410 — measured as the ratio of the new required projected diagonal to the old one: ×1.41 at 1:1, ×1.20 at 4:3, ×1.06 at 16:10, ~×1.0 at 16:9 (by construction), ×0.78 at a real 21:9 panel, ×0.54 at 32:9. This is the accepted cost of the fix: the WHOLE-OBJECT ladder (coverage_fractions, anchored at 1.0) is what a normal full-frame opening view hits, and that is exactly where #1410's wide-aspect blur showed up — making that anchor aspect-exact was the goal. A tiled layer's per-tile anchor was already only a heuristic (each tile's own on-screen footprint already varies with camera distance and framing, partition shape, etc.), so it is the right place to absorb the residual aspect dependence rather than the whole-object case. A Python test (test_max_coverage_fraction_matches_the_viewer_fill_factor) reads this file and asserts the MAX_COVERAGE_FRACTION relation holds, and separately that SCREEN_FILL_DIAGONAL_RATIO itself stays close to the 16:9 geometric value it stands for (so the two constants can't silently compensate for each other).

    View-axis depth fix (#1543). A 1×1×100 cloud previously measured only ~0.024 at the default 16:9 framing because the distance was sized from its pure-depth dimension plus a hardcoded 20% margin. The exact near-face fit above raises it to 2.121, so it opens on the finest rung like the other full-scene shapes. The same fit removes the margin: keeping both would count depth twice and pull ordinary 3D scenes unnecessarily far back.

    Known limitation: resize without a re-fit (out of scope here). updateCameraAspect (utils/camera-utils.ts) only updates camera.aspect (perspective) / the horizontal frustum extent (orthographic) on a window resize — it preserves the VERTICAL fov / ortho extent, and the viewer never re-fits the camera distance afterwards. Both screen-space pixel extents of a projected box then depend on viewport HEIGHT alone (not width): with vertical fov and distance unchanged, the horizontal pixel extent is (world extent / (z·tan(vFov/2))) · (height/2) — width cancels out of it algebraically — so narrowing the window width with height held fixed leaves the projected diagonal in pixels completely UNCHANGED while fittedAxisPx (now width, once width < height) keeps shrinking, inflating the metric by height/width. Measured (a 100×100×100 cube, default opening framing, 1600×900 baseline narrowed with height fixed at 900): 1600×900 → 500×900 inflates the metric ×1.80 (the pre-#1410 diagonal normalisation also inflated here, ×1.78 — a wash), but 1600×900 → 200×900 inflates ×4.50 vs only ×1.99 under the old normalisation — because the old denominator (hypot(width, height)) is bounded below by height as width → 0, while the new one (min(width, height)) is not. This is the flip side of the #1410 fix: WIDENING a viewport (the actual #1410 symptom) is now exactly stable (proven above) where it used to decay. NARROWING one is not symmetric: with diagonalPx held fixed, the new metric only overtakes the old one PAST a crossover at width = height² / width0 (506px / aspect ≈ 0.56 for this baseline) — down to that point the new normalisation is actually LESS inflated than the old one (e.g. 1600×900 → 500×900, aspect 0.56: ×1.80 new vs ×1.78 old, already a near-wash), and only below it does narrowing inflate the metric faster than before (1600×900 → 200×900, aspect 0.22: ×4.50 new vs ×1.99 old). Not fixed here: re-fitting the camera on resize is a separate, larger change (it would also move the FRAMING, not just the LOD selection) and out of scope for this normalisation fix.

    The selector normalises the projected diagonal by this to a dimensionless coverage metric, so the same thresholds behave (near-)identically at any aspect ratio and any pixel size of the viewport.

    Exported so tests can pin behaviour against the real constant instead of hard-coding 0.5.