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

    Data loader interface for Mesh nodes.

    Mirrors DataLoader / LinesDataLoader / GSplatsDataLoader so the scene-loader machinery treats all four kinds uniformly — and so a spatial-index implementation can be swapped in behind this interface later with no caller change (spec §7).

    The viewState parameters are honoured but do not change what is fetched: a mesh is whole-node resident, so both methods return the same complete LoadedMeshData and the view state matters only downstream, where the cull decides which faces are indexed.

    Implements

    Index
    decoder: ArrayDecoder
    rangeLoader: RangeLoader
    zarrStore: Readable
    decodeKTX2?: KTX2TextureDecoder
    handles: MeshArrayHandles | null = null

    Metadata-only handles, opened once by initialize.

    preflight: MeshPreflightResult | null = null

    What Stage 1 established.

    initInFlight: Promise<void> | null = null

    Serializes concurrent calls into initialize onto one metadata open.

    initialize() used to guard only with if (this.handles) return before its awaits — the exact "async initialization race" pattern the repo's own CLAUDE.md calls out. fetch()/load() were the only caller before runPreflight made initialize reachable from a second, independent entry point, so a concurrent runPreflight() + loadMesh() opens every .zarray/.zattrs twice and runs preflightMesh twice. Mirrors the inFlight latch on load: cleared once the attempt settles, and only if it is still OURS — a later call may already have installed a fresh attempt after this one settled — so a transient failure is retried rather than cached forever.

    data: LoadedMeshData | null = null

    The decoded mesh, cached for the loader's lifetime.

    This is the whole point of the whole-node design: one fetch per node, and every later updateView is served from here.

    inFlight: Promise<LoadedMeshData> | null = null

    Serializes concurrent first loads onto one fetch (see load).

    aborter: AbortController = ...

    Aborts the in-flight reads when the loader is torn down.

    The generation token below stops a post-dispose completion from repopulating the cache, but on its own it lets the transfer RUN to completion first — for a near-budget mesh that is up to half a gigabyte fetched and decoded for nothing, concurrently with whatever replaced the node. Aborting the reads stops the spend, not just the publish. Replaced (not just aborted) on dispose so the reuse-after-dispose path starts with a live signal.

    fetchSignal: AbortSignal | null = null

    The signal governing the current fetch, sourced by the shared RangeLoader through its thunk — that loader takes its abort signal from a callback rather than a parameter, so the colour path picks this up without any signature change. Single-flight (inFlight) makes one field sufficient.

    NAMED DIFFERENTLY from its three siblings on purpose. Points, Lines and GSplats each hold _activeSignal and wire the identical setSignalSource(() => this._activeSignal) one line into their constructors, so this looks like a symmetry break — it is a lifetime difference. Their signal is per-UPDATE: set at the top of every updateView and cleared in its finally, live whenever the loader is doing anything. Mesh is whole-node resident, so it fetches ONCE and then serves every later updateView from this.data without any I/O; this field is live only for that single fetch and is null during the scrubs that make up almost all of a session. Calling it "active" would claim the opposite of what holds.

    generation: number = 0

    Bumped by dispose, so a fetch that settles afterwards cannot write its result back into a loader that has been torn down.

    Without it, dispose() during an in-flight load leaves the completion free to repopulate this.data — resurrecting the cache on a dead loader, and for a near-budget mesh pinning up to half a gigabyte that nothing will ever read. Cheap enough that the whole-mesh payload makes it worth having.

    metrics: LoaderMetrics

    Monitor telemetry for this node — see the LoaderMonitor section at the bottom of the class for what a whole-node loader can honestly report.

    events: LoaderEventEmitter = ...

    Monitor listeners, shared implementation with the three sibling facades.

    path: string
    location: Location<Readable>
    • Open metadata handles for every array the attrs declare, then run Stage 1.

      Single-flight: a concurrent second call joins the first attempt rather than opening the same metadata again (see initInFlight).

      Returns Promise<void>

    • Open metadata handles for every array the attrs declare, then run Stage 1.

      Opening is metadata-only — zarr.open(..., { kind: 'array' }) reads .zarray and .zattrs and nothing else — which is what makes it safe to do for all declared arrays, including the string-channel CSR pairs v1 never fetches. The budget has to see them to be a budget.

      Captures generation up front and guards BOTH publishes below with it, mirroring load's own generation guard. Without this, a dispose() racing an in-flight doInitialize() — reachable with no load() in flight at all, since runPreflight calls this too, including during a dataset switch — would still let the metadata open complete and publish this.preflight / this.handles onto a torn-down loader, making the dispose() docstring's "a subsequent loadMesh re-initializes" false in that window: the stale, non-null handles would look already-initialized.

      Returns Promise<void>

    • Run Stage 1 (the metadata-only preflight) eagerly, and return what it established.

      Exists for one caller: MeshProgressiveLoader.assertWithinByteBudget (./mesh-progressive-loader.ts), which needs to charge every level of a reveal ladder against ONE byte budget before any level's chunk data is fetched — each level's own preflight only ever sees its own MESH_DECODE_BUDGET_BYTES ceiling, so nothing else adds them up. This is just initialize (idempotent — a load() that runs later reuses the SAME cached handles/preflight, so calling this first duplicates no request) with its result surfaced instead of stashed on a private field.

      Fetches no chunk data, same as the preflight it wraps.

      Returns Promise<MeshPreflightResult>

    • Serve the mesh, fetching it at most once per loader instance.

      The inFlight latch is not defensive padding. Both entry points below funnel here, and the scene-loader legitimately calls updateView while an initial loadMesh is still in flight (a slice scrub during load). Without the latch each call would start its own full-mesh fetch — the exact duplicate-work the whole-node design exists to avoid.

      One consequence of sharing that fetch: the reads run under the FIRST caller's signal combined with the loader-lifetime one (dispose aborts the latter). A later caller joining an in-flight fetch cannot abort it and will receive the mesh even if its own update was superseded. That is benign here in a way it would not be for the range-query siblings — a superseded caller gets data it no longer needs, never data for the wrong query, because there is only one thing to fetch and it does not depend on the view. The wasted work is bounded by one mesh, once per loader.

      The combined signal reaches EVERY read: the raw faces read directly, the decoder-routed arrays through ArrayDecoder.decode's signal parameter, and the shared colour path through the RangeLoader signal-source thunk wired in the constructor.

      Parameters

      • Optionalsignal: AbortSignal

      Returns Promise<LoadedMeshData>

    • updateView, plus whether the mesh was already in hand.

      Exists for one caller: MeshProgressiveLoader (./mesh-progressive-loader), whose streaming loop asks each level "was that cheap?" to decide whether to keep going this pass or leave the rest to a later one (streaming-policy.ts). The three sibling progressive loaders call the identically named method on their spatial-index sub-loaders, so the ladder loop is the same shape for all four types.

      allResident reads the loader's OWN decode cache rather than the chunk cache the siblings report, because that is where the cost actually is here: a whole-node level either has been fetched and decoded (free to re-serve) or has not (a full network read). Sampled BEFORE the await, so a level that this very call fetches reports false — reporting the post-fetch state would say "resident" for every level and defeat the refine pass's stop rule.

      Parameters

      Returns Promise<{ data: LoadedMeshData; allResident: boolean }>

    • Release the decoded payload after a progressive parent has folded it into its cumulative result. Initialization metadata and handles stay warm, so a later direct load can re-fetch without rebuilding the loader.

      preserveExternalResources is used when the parent's cumulative result is the same single-level object; closing its bitmap would invalidate the transferred payload.

      Parameters

      • preserveExternalResources: boolean = false

      Returns void

    • LoaderMonitor surface (optional, for the data-loading-monitor UI). Mirrors the surface the sibling loaders expose — implementations that don't track metrics may omit these. Both shipped implementations (MeshWholeNodeLoader, MeshProgressiveLoader) provide all four: connectLoaderToMonitor duck-types the complete set, so a partial implementation is silently skipped rather than partially reported.

      Parameters

      Returns void

    • Record how many of this node's triangles the current slice indexes.

      Called by commit-mesh-geometry.ts from the same place it stamps userData.visibleTriangleCount, because that count is produced DOWNSTREAM of the loader: projection decides which faces the index buffer receives, and the loader (which holds the whole mesh either way) cannot know it. The three sibling loaders set visibleElements themselves for the opposite reason — for them the query result IS the visible set.

      Parameters

      • triangles: number

      Returns void

    • Fold one completed fetch into the metrics and emit the monitor load event that feeds the panel's load-rate and bandwidth windows.

      retained is false when the loader was disposed while the fetch was in flight. The cumulative counters still take it — those bytes were really spent — but memoryUsed is a LIVE footprint, and the payload was dropped rather than published, so claiming it would leave the panel reporting resident memory for a torn-down node.

      Parameters

      Returns void

    • Record a failed fetch — EXCEPT a deliberate abort.

      A dataset switch or a dispose during load aborts the in-flight reads (dispose), which is a control path, not a failure: counting it would raise the advisor's error-rate recommendation every time the user switches scenes. Same exclusion the sibling loaders apply through isAbortError.

      Parameters

      • error: unknown

      Returns void

    • Clear all cached state.

      State-clearing rather than terminal, matching the sibling loaders (the points loader's dispose likewise drops its arrays and calls _onceInit.reset()): a subsequent loadMesh re-initializes and re-fetches rather than throwing.

      Returns void