/** * Scene graph and mesh management */ import type { Mesh, BatchedMesh, Vec3, PickClipState } from './types.js'; import type { MeshData } from '@ifc-lite/geometry'; import type { RenderPipeline } from './pipeline.js'; import { type BoundingBox, type RaycastHit } from './scene-raycaster.js'; import { type ResidentGpuBytes } from './render-stats.js'; import { type SpatialChunkingConfig } from './chunk-grid.js'; import { type ColdGeometryProvider } from './residency.js'; import type { DecodedInstancedShard } from '@ifc-lite/geometry'; /** * Release the GPU resources owned by a batch / mesh. Every * BatchedMesh and Mesh shares the same {vertex, index, optional * uniform} buffer layout, and forgetting any one of the three is a * GPU memory leak that won't surface until the user spends ten * minutes inside the viewer. Centralised here so callers don't * have to remember the cleanup sequence. * * Accepts the structural shape so it works for both BatchedMesh and * Mesh — they each carry the same buffer trio. */ /** A surface-textured mesh (#961): its own interleaved vertex buffer (with a UV * lane), index buffer, per-mesh uniform buffer, GPU texture + sampler, and a * bindGroup wiring all three. Drawn per-mesh in a dedicated sub-pass. */ export interface TexturedMesh { expressId: number; vertexBuffer: GPUBuffer; indexBuffer: GPUBuffer; indexCount: number; uniformBuffer: GPUBuffer; texture: GPUTexture; sampler: GPUSampler; bindGroup: GPUBindGroup; /** Authored tint (multiplies the sampled texel); white = texture passthrough. */ color: [number, number, number, number]; /** Set when `texture` lives in the shared registry (#1781: one GPU texture * per `IfcImageTexture`, sampled by many meshes) — released by refcount, * never destroyed per-mesh. Undefined for per-mesh #961 blob/pixel uploads. */ sharedTextureKey?: number; } /** * One GPU-uploaded instanced template: unique geometry (slot-0 vertex + index * buffers, 28-byte pos+norm+entityId vertex matching the flat layout) drawn once * per occurrence via a per-instance buffer at slot 1 and * `drawIndexed(indexCount, instanceCount)`. Buffers are Scene-owned, freed in clear(). */ export interface InstancedTemplateGPU { vertexBuffer: GPUBuffer; indexBuffer: GPUBuffer; indexCount: number; instanceBuffer: GPUBuffer; instanceCount: number; /** Union of the occurrences' world AABBs (null when no occurrence has a * finite box — such templates are never culled). Same tuple layout as * BatchedMesh.bounds so the render loop's frustum test is shared. */ bounds: { min: [number, number, number]; max: [number, number, number]; } | null; /** Largest single-occurrence bounding-sphere radius (world units). Upper * bound for the contribution cull: no occurrence can project larger than * this radius at the union box's nearest view depth. */ maxOccRadius: number; /** Occurrences currently selected (highlight flag set). A template with a * selected occurrence is exempt from contribution culling so the highlight * can't vanish while the entity is the user's focus. */ selectedCount: number; } export declare class Scene { private meshes; private batchedMeshes; private buckets; private meshDataBucket; private meshDataMap; private boundingBoxes; private texturedMeshes; /** #1781: GPU textures shared across meshes, keyed by `MeshTextureRef.textureId` * (one `IfcImageTexture` → one upload, sampled by every face set mapping it). * Refcounted: entries die when the last referencing mesh is removed / on clear(). */ private sharedTextures; private texturedDevice?; private instancedTemplates; private instancedVisible; private instancedEntityMap; private instancedTemplateCpu; private instancedDevice?; private instancedSelected; private instancedHidden; private instancedOverridden; private instancedHasTransparent; private readonly instancedVisibilityEpochs; private lastInstancedVisibilityVersion; private instancedVisibilityDirty; private activeBucketKey; private spatialChunking; private gpuBudgetBytes; private residencyFrame; private lastDrawnFrame; private residencyRestoreQueue; private residencyOverBudgetWarned; private coldProvider; private hostBudgetBytes; private coldBuckets; private dirtyBuckets; private coldRestoresInFlight; private hostOverBudgetWarned; private hostEnforceCountdown; private lodBuildsEnabled; private quantizedBatchesEnabled; private finalizeInProgress; private nextSplitId; private nextBatchId; private sharedFrameOrigin; private cachedMaxBufferSize; private static readonly STREAMING_FRAGMENT_MAX_INDICES; private static readonly STREAMING_FRAGMENT_MAX_VERTEX_BYTES; private partialBatchCache; private partialBatchCacheKeys; private partialBatchCacheVersions; private overrideBatches; private colorOverrides; private colorOverrideGeneration; private pendingBatchKeys; private streamingFragments; private meshQueue; private meshQueueReadIndex; private geometryReleased; private ephemeralStreamingMode; /** * Add mesh to scene */ addMesh(mesh: Mesh): void; /** * Get all meshes */ getMeshes(): Mesh[]; /** * Get all batched meshes */ getBatchedMeshes(): BatchedMesh[]; /** The shared local-frame origin all batches relativize against (null until * the first batch is built). Per-mesh highlight/picker VBOs replicate the * batch's exact f32 path against this so they render bit-coincident. */ getSharedFrameOrigin(): [number, number, number] | null; /** * Enable/disable spatial chunk bucketing (issue #1682 phase 2). When set, * colour buckets are additionally partitioned by world grid cell, making * batches spatially compact so per-batch frustum/contribution culling * fires at chunk granularity. Pure reorganization: same triangles, same * shared frame origin, same draw path — only the batch partition changes. * * Set BEFORE geometry loads. Existing buckets keep their keys (keys are * opaque downstream), so flipping mid-model only affects meshes routed * afterwards; the next finalize/recolour re-groups stragglers. */ setSpatialChunking(config: SpatialChunkingConfig | null): void; getSpatialChunking(): SpatialChunkingConfig | null; /** Set (or clear) the GPU residency budget in bytes. */ setGpuResidencyBudget(bytes: number | null): void; getGpuResidencyBudget(): number | null; /** Called once at the start of every Renderer.render() — residency ages * are measured in RENDERED frames, so idle scenes never age out. */ beginResidencyFrame(): void; /** Record that the draw loop drew this batch this frame. */ recordBatchDrawn(batch: BatchedMesh): void; /** * The draw loop wants an evicted batch back on the GPU. Queues its bucket * for a time-budgeted rebuild in processResidencyRestores (driven by the * app's animation loop) — the batch is skipped this frame and pops back in * within a frame or two. */ requestBatchResidency(batch: BatchedMesh): void; hasResidencyRestoreWork(): boolean; /** * Rebuild evicted batches from their buckets' CPU meshData, up to * `budgetMs` per call (same time-slicing philosophy as flushPending). * Returns the number of batches restored. */ processResidencyRestores(device: GPUDevice, pipeline: RenderPipeline, budgetMs?: number): number; /** Wire the cold-storage source (v13 cache chunks). Null disables the tier. */ setColdGeometryProvider(provider: ColdGeometryProvider | null): void; /** * Enable LOD1 builds (issue #1682 phase 5): bucket batches built from now * on (finalize, rebuild, residency restore) get a simplified second index * range when it pays. Set BEFORE geometry loads; streaming fragments and * partial/overlay sub-batches never build LOD. */ setLodBuildsEnabled(enabled: boolean): void; /** * Enable 12-byte lattice-quantized batch vertices (issue #1682 phase 6). * ONLY call after the renderer verified its quantized pipeline variants * exist (see Renderer.enableQuantizedBatches) — quantized buffers are * undrawable without them. Applies to batches built from now on; every * createBatchedMesh output (buckets, fragments, partial + override * batches) quantizes onto the SAME 2^-10 lattice, so depth-equal overlay * matching and cross-batch coincidence are preserved bit-exactly. Batches * whose extent exceeds the u16 lattice range fall back to f32 silently. */ setQuantizedBatches(enabled: boolean): void; /** * Whether THIS mesh's source batch renders quantized — drives the * hydrated-mesh lattice snap in createMeshFromData (a mesh whose batch * fell back to f32, e.g. >64m extent, must NOT snap). Falls back to the * global flag when the mesh isn't bucketed (mid-stream hydration). */ isMeshQuantized(meshData: MeshData): boolean; /** Set (or clear) the HOST budget in bytes for bucket CPU geometry. */ setHostResidencyBudget(bytes: number | null): void; /** CPU bytes held by bucket meshData (positions + normals + indices). */ getResidentCpuBytes(): number; /** A bucket whose content diverged from what the cache entry holds * (recolour / move / removal) must never be cold-evicted: restoring it * from disk would resurrect the pre-edit geometry. */ private markBucketDirty; /** * Demote warm buckets (GPU-evicted, CPU kept) to cold (shell only) until * bucket CPU bytes fit the host budget. Same LRU policy as the GPU tier. * Eligibility is strict: pristine, non-overflow ("#N" sub-buckets are * excluded — their piece membership cannot be re-derived unambiguously), * GPU-evicted, provider present. Cold eviction removes the bucket's meshes * from meshDataMap/meshDataBucket too — that is what actually frees the * typed arrays. */ private enforceHostBudget; /** * Restore EVERY cold bucket to warm (used before the cold provider goes * away, e.g. a federated add invalidates the entry-backed provider while * primary chunks are cold — without this they would be stranded shells). * Resolves when all in-flight restores settle; failures are logged by the * per-bucket restore path and leave those buckets cold. */ drainColdTier(): Promise; /** Kick off the async disk restore for a cold bucket the draw loop wants. * On completion the bucket is warm again and re-queued for GPU rebuild. */ private startColdRestore; /** * Synchronously rebuild EVERY evicted bucket batch (no time budget) — * for one-shot capture renders (IDS/clash/BCF snapshots) whose isolation * options may reveal batches that aged out under the budget. The live * view never needs this: visible batches are never evicted. The budget * pass re-evicts unused batches after the usual idle age. * Returns the number of batches restored. */ restoreAllEvicted(device: GPUDevice, pipeline: RenderPipeline): number; /** * Evict least-recently-drawn bucket batches until the resident set fits * the budget. Called after each frame's submit; destroying just-submitted * buffers is safe (WebGPU defers destruction past in-flight work). Never * evicts a batch drawn this frame — a visible set larger than the budget * renders correctly and stays over budget (warned once). */ enforceGpuBudget(): void; /** Destroy + drop cached partial sub-batches derived from `batch` (their * sourceBatchKeys embed the batch id, so they are stale once it is * evicted/replaced). */ private dropPartialCacheForBatch; /** Destroy + drop EVERY cached partial sub-batch. The clones built during * hide/isolate are deliberately excluded from the GPU residency budget and * are otherwise only freed on clear()/finalize/evict — never when filtering * ends. The render loop calls this on the transition back to fully-visible so * the ~model-sized clone VRAM is not pinned until the next model reload. Uses * the same destroy-then-clear idiom as clear(); safe to call between frames * because the previous frame is already submitted (WebGPU defers the free * past in-flight work). */ dropAllPartialCaches(): void; /** Free the hydrated (pick / selection-highlight) individual meshes that are * no longer selected, destroying their GPU buffers and dropping them from * `this.meshes`. A mesh is kept iff its expressId is in `keep` AND it * matches `keepModelIndex` (undefined = any model) — the same predicate the * render loop uses to draw selection highlights, so disposal is its exact * complement. The model scoping matters for federation: models can share * express ids, and an id-only check would strand the OTHER model's hydrated * mesh resident and drawing when selection moves across models. Only meshes * flagged `hydrated` are touched — authored geometry added via addMesh() * and batch geometry are left untouched. Returns how many were freed. */ disposeHydratedMeshesExcept(keep: ReadonlySet, keepModelIndex?: number): number; /** * Bucket BASE key for a mesh: colour key, prefixed with the mesh's grid * cell when spatial chunking is on. EVERY bucket-key derivation * (streaming append, fragment grouping, finalize re-group, recolour move, * partial-batch piece filter) must go through this so a mesh always * resolves to the same bucket. `color` overrides the mesh's own colour for * recolour routing. */ private bucketBaseKey; /** * Store MeshData for lazy GPU buffer creation (used for selection highlighting) * This avoids creating 2x GPU buffers during streaming * Accumulates multiple mesh pieces per expressId (elements can have multiple geometry pieces) */ addMeshData(meshData: MeshData): void; /** * Get MeshData by expressId (for lazy buffer creation) * Returns merged MeshData if element has multiple pieces with same color, * or first piece if colors differ (to preserve correct per-piece colors) * @param expressId - The expressId to look up * @param modelIndex - Optional modelIndex to filter by (for multi-model support) */ getMeshData(expressId: number, modelIndex?: number): MeshData | undefined; /** * Check if MeshData exists for an expressId * @param expressId - The expressId to look up * @param modelIndex - Optional modelIndex to filter by (for multi-model support) */ /** * Extract only the vertices/triangles belonging to `targetId` from a * color-merged MeshData that contains many entities. Returns a new * lightweight MeshData suitable for selection highlighting. */ private extractEntityFromMergedMesh; hasMeshData(expressId: number, modelIndex?: number): boolean; /** * Get all MeshData pieces for an expressId (without merging). * Optionally filter by modelIndex for multi-model safety. */ /** * Iterate every CPU-side `MeshData` the scene holds — every piece * for every expressId across every model. Used by the BIM ↔ scan * deviation BVH builder which needs world-space triangle positions * regardless of which IFC ingest path they came from. * * Deduplicates by `MeshData` identity: a colour-merged batch is * stored under every contributor's expressId, and visiting it * multiple times would double-count its triangles in the BVH. */ forEachMeshData(visit: (md: MeshData) => void): void; getMeshDataPieces(expressId: number, modelIndex?: number): MeshData[] | undefined; /** * Generate color key for grouping meshes. * Quantizes RGBA to 10-bit per channel and packs into a compact string. * Avoids floating-point template literal overhead of the old approach. */ private colorKey; /** * Append meshes to color batches incrementally * Merges new meshes into existing color groups or creates new ones * * STREAMING OPTIMIZATION: During streaming, creates lightweight "fragment" * batches from ONLY the new meshes instead of re-merging all accumulated * data. This reduces streaming from O(N²) to O(N). Call finalizeStreaming() * when streaming completes to do one O(N) full merge. */ appendToBatches(meshDataArray: MeshData[], device: GPUDevice, pipeline: RenderPipeline, isStreaming?: boolean): void; /** * Rebuild all pending batches (call this after streaming completes) * * Each bucket key already maps to data that fits within the GPU buffer * limit (enforced at accumulation time by resolveActiveBucket), so no * splitting is needed here — just create one batch per key. */ rebuildPendingBatches(device: GPUDevice, pipeline: RenderPipeline): void; /** * Check if there are pending batch rebuilds */ hasPendingBatches(): boolean; /** * Remove every mesh registered for `expressId` from the scene. * Affected buckets are marked for rebuild on the next call to * `rebuildPendingBatches`, so the GPU drops them on the next * frame. Returns `true` when at least one mesh was removed. * * Used by the viewer's authoring actions (split, delete) to * make tombstoned IFC entities disappear from the rendered * scene — the previous v1 workaround was to hide them via * `hiddenIds`, but that left the mesh in GPU memory and inside * raycast bounds. This is the proper removal path. * * Notes: * - For color-merged meshes (the `entityIds` per-vertex case) * a single MeshData often hosts many entities. We do NOT * drop the whole mesh in that case — that would also remove * the other entities — but we DO clear the bbox + meshDataMap * for the requested expressId, so picking and selection stop * finding the removed entity. Re-rendering the merged mesh * unchanged is the right behaviour because color-merged * batches are an optimisation: the geometry is still real; * the IFC tombstone just means we ignore it for queries. */ removeMeshesForEntity(expressId: number): boolean; /** * Tombstone a GPU-instanced entity on delete/split: set its HIDDEN flag on the * GPU (both render + pick shaders discard it) before forgetting its occurrence * locations, then drop all instanced state for it. The occurrence's buffer slots * are not reclaimed (delete/split is rare) — hiding them is sufficient and never * touches other entities' slots. Returns true if the id was instanced. (#1238) */ private removeInstancedEntity; /** * Bulk variant of `removeMeshesForEntity`. Avoids re-marking the * same bucket key once per entity in the common "split N walls" * batch. Returns the number of entities that had at least one * dedicated mesh removed. */ removeMeshesForEntities(expressIds: Iterable): number; /** * Translate every mesh for `expressId` by `delta` in renderer * world frame (Y-up). Modifies `positions` in place and marks * the affected bucket(s) for re-batch on the next call to * `rebuildPendingBatches`. * * Bounding boxes are cleared for the entity so the next bounds * query recomputes from the new positions; raycast bounds will * therefore lag by exactly one query, which is acceptable for * the drag-end → fresh-pick interaction the gizmo drives. * * Returns `true` when at least one mesh was modified. Used by * the viewer's `translateEntity` action to keep the rendered * mesh in sync with the IFC coords mutation. * * Color-merged meshes (shared by many entities via per-vertex * `entityIds`) cannot be translated for a single entity without * walking the entityIds array vertex by vertex; this helper * skips them and returns `false` so the caller can fall back * to a full reload if needed. */ translateMeshesForEntity(expressId: number, delta: [number, number, number]): boolean; /** * Translate every flat (non-instanced) mesh for `expressId` by `delta`. See * {@link translateMeshesForEntity} for the full contract; this is the flat half. */ /** * Mark a mesh's bucket for rebuild after its positions were mutated in * place (move/rotate), migrating it to a new bucket when spatial chunking * is on and the mesh crossed a grid-cell boundary. Without the migration * the mesh would keep its stale cell key, so the partial-batch piece * filter (which re-derives keys from CURRENT positions) would silently * drop it under hide/isolate. Same move mechanics as updateMeshColors. */ private rebucketMovedMesh; private translateFlatMeshesForEntity; /** * Translate every GPU-instanced occurrence of `expressId` by `delta` in the * renderer world frame. Instanced occurrences live in the per-template instance * buffers (NOT meshDataMap), so the flat translate path can't reach them — this * is what keeps repeated geometry (e.g. windows / mullions emitted via * IfcMappedItem) lifting with its storey in Exploded mode (#1289). * * Mutates BOTH halves so every consumer stays consistent: * - the CPU instance record (so getInstancedMeshDataPieces / bounds / raycast * / measure / section / export see the new position), and * - the GPU instance buffer (so the occurrence renders at the new position). * The cached world AABB is shifted by the same delta — every occurrence of the * entity moves identically, so a recompute is unnecessary. * * Returns `true` when at least one occurrence moved. No-op (returns false) for * a non-instanced id or a zero delta. */ translateInstancedEntity(expressId: number, delta: [number, number, number]): boolean; /** Recompute an instanced entity's cached world AABB by unioning the transformed * template AABB across its occurrences (using their CURRENT matrices). Used after * a translate so bounds reflect the moved geometry. No-op for a non-instanced id. */ private recomputeInstancedBounds; /** Drop the per-entity selection-highlight meshes for `expressId` (frozen * copies in `this.meshes`) + free their GPU buffers, so the highlight is * rebuilt from the entity's current geometry on the next render. Used after a * translate or removal, which mutate the underlying geometry but don't touch * these standalone highlight meshes. */ private evictHighlightMeshes; /** Bulk variant of `translateMeshesForEntity`. */ translateMeshesForEntities(updates: Map): number; /** * Rotate every flat mesh for `expressId` by `angleRad` about the renderer * vertical (+Y) axis through `pivot` (renderer world, Y-up). This is the Y-up * image of an IFC yaw about the storey-up Z axis. Modifies `positions` and * `normals` in place and marks the affected bucket(s) for re-batch. * * Positions may live in a per-element local frame (`MeshData.origin`, world = * origin + position), so the pivot is folded into each mesh's local frame * before rotating; normals are direction vectors and rotate as-is. * * Same colour-merge caveat as `translateFlatMeshesForEntity` (skips meshes * whose vertices belong to more than this entity). GPU-instanced occurrences * are not rotated (the collab edit path only rotates flat/authored meshes). * Returns true when a mesh was modified. */ rotateMeshesForEntity(expressId: number, angleRad: number, pivot: [number, number, number]): boolean; /** Bulk variant of `rotateMeshesForEntity`. */ rotateMeshesForEntities(updates: Map): number; /** * Queue meshes for deferred GPU upload. * Instant (no GPU work) — safe to call from React effects. * The animation loop calls flushPending() each frame to drain the queue. */ queueMeshes(meshes: MeshData[]): void; /** True if the mesh queue has pending work. */ hasQueuedMeshes(): boolean; /** True while un-finalised streaming fragments are still being drawn. An * element appended during streaming (e.g. an authored IfcSpace) renders as * such a fragment AND accumulates in its colour bucket; once the bucket is * re-batched (e.g. by a move) the fragment becomes a stale duplicate, so the * caller should `finalizeStreaming` to merge fragments away. */ hasStreamingFragments(): boolean; /** True while a finalize rebuild (sync or time-sliced) is mid-flight — * the fragment list is already cleared then, so settle-sensitive callers * must check BOTH this and hasStreamingFragments(). */ isFinalizeInProgress(): boolean; /** True when streaming runs in ephemeral mode (huge files) — fragments render * directly from GPU and geometry is NOT retained for re-batch, so callers * must NOT finalize (there's nothing to rebuild the batches from). */ isEphemeralStreaming(): boolean; setEphemeralStreamingMode(enabled: boolean): void; /** * Drain the mesh queue with a per-frame time budget. * Processes queued meshes through appendToBatches in streaming mode * (creates lightweight fragment batches for immediate rendering). * * @returns true if any meshes were processed (caller should render) */ flushPending(device: GPUDevice, pipeline: RenderPipeline): boolean; /** * Create lightweight fragment batches from a single streaming batch. * Fragments are grouped by color and added to batchedMeshes for immediate * rendering, but tracked separately for cleanup in finalizeStreaming(). */ private createStreamingFragments; private splitMeshForStreaming; /** * Finalize streaming: destroy temporary fragment batches and do one full * O(N) merge of all accumulated mesh data into proper batches. * Call this when streaming completes instead of rebuildPendingBatches(). * * IMPORTANT: During streaming, external code (applyColorUpdatesToMeshes) * may mutate meshData.color in-place for deferred style/material colors. * This means the bucket keys (computed at insertion time from the ORIGINAL * color) no longer match the meshes' current colors. We must re-group all * meshData by their CURRENT color to produce correct batches. */ finalizeStreaming(device: GPUDevice, pipeline: RenderPipeline): void; private finalizeStreamingInner; /** * Time-sliced version of finalizeStreaming. * Re-groups mesh data and rebuilds GPU batches in small chunks, * yielding to the event loop between chunks so orbit/pan stays responsive. * Streaming fragments continue rendering until each new batch replaces them. * * @param device GPU device * @param pipeline Render pipeline * @param budgetMs Max milliseconds per chunk (default 8 — half a 60fps frame) * @returns Promise that resolves when all batches are rebuilt */ finalizeStreamingAsync(device: GPUDevice, pipeline: RenderPipeline, budgetMs?: number): Promise; finishEphemeralStreaming(): void; /** * Release JS-side mesh geometry data (positions, normals, indices) after * GPU batches have been built. This frees the ~1.9GB of typed arrays that * duplicate data already resident in GPU vertex/index buffers. * * After calling this method: * - Bounding boxes are precomputed and cached for all entities * - meshDataMap and bucket meshData arrays are cleared (typed arrays become GC-eligible) * - Color updates (updateMeshColors) are no longer available * - Partial batch creation and color overlays are no longer available * - CPU raycasting falls back to bounding-box-only (no triangle intersection) * - Selection highlighting must use GPU picking instead of CPU mesh reconstruction * * Call this after finalizeStreaming() when all color updates have been applied. */ releaseGeometryData(): void; /** * Whether JS geometry data has been released (GPU-resident mode). */ isGeometryDataReleased(): boolean; /** * Update colors for existing meshes and rebuild affected batches * Call this when deferred color parsing completes * * OPTIMIZATION: Uses meshDataBucket reverse-map for O(1) batch lookup * instead of O(N) indexOf scan per mesh. Critical for bulk IDS validation updates. */ updateMeshColors(updates: Map, device: GPUDevice, pipeline: RenderPipeline): void; /** * Create a new batched mesh from mesh data array. * @param bucketKey - Optional unique key for this batch. When omitted the * base color key is used (fine for overlay / partial batches that don't * participate in the main buckets map). */ private createBatchedMesh; /** * Merge multiple mesh geometries into single vertex/index buffers. * Delegates to the extracted mergeGeometry() utility. */ private mergeGeometry; /** * Get the effective max buffer size for this GPU device, with a safety margin. */ private getMaxBufferSize; /** * Split a meshDataArray into chunks that fit within GPU buffer limits. * Delegates to the extracted splitMeshDataForBufferLimit() utility. */ private splitMeshDataForBufferLimit; /** * Resolve which bucket a mesh should be added to. * If the active bucket for this color would overflow the GPU buffer limit, * a new sub-bucket is created with a suffixed key (e.g. "500|500|500|1000#1"). * Returns the bucket key to use (may be the base key or a suffixed key). */ private resolveActiveBucket; /** * Extract the base color key from a bucket key (strips "#N" suffix if present). */ private baseColorKey; /** * Get or create a partial batch for a subset of visible elements from a batch * * PERFORMANCE FIX: Instead of creating 10,000+ individual meshes for partially visible batches, * this creates a single sub-batch containing only the visible elements. * The sub-batch is cached and reused until visibility changes. * * @param colorKey - The color key of the original batch (unique per bucket, used as cache key) * @param visibleIds - Set of visible expressIds from this batch * @param device - GPU device for buffer creation * @param pipeline - Rendering pipeline * @returns BatchedMesh containing only visible elements, or undefined if no visible elements */ getOrCreatePartialBatch(sourceBatchKey: string, colorKey: string, visibleIds: Set, device: GPUDevice, pipeline: RenderPipeline, visibilityEpoch?: number): BatchedMesh | undefined; /** * Set color overrides for lens coloring. * Builds overlay batches grouped by override color. * Original batches are NEVER modified — clearing is instant. * * Applies the same buffer-size splitting as regular batches to prevent * GPU buffer overflow on large models. */ setColorOverrides(overrides: Map, device: GPUDevice, pipeline: RenderPipeline): void; /** * Clear all color overrides — instant, no batch rebuild needed. */ clearColorOverrides(): void; /** Monotonic counter that changes whenever the colour-override set changes. * The render loop folds it into the partial sub-batch cache epoch so the * per-frame fast path stays correct across override changes. */ getColorOverrideGeneration(): number; /** Get overlay batches for rendering */ getOverrideBatches(): BatchedMesh[]; /** Check if color overrides are active */ hasColorOverrides(): boolean; /** * Get the active expressId → RGBA override map, or null if none. * * Used by the renderer to promote overridden meshes/batches to the opaque * pipeline so the overlay paint pass (depthCompare 'equal') finds matching * depth. Without this, an override on an entity that defaults to the * transparent pipeline (IfcSpace, IfcOpeningElement, glass, …) silently * fails to paint — the transparent pipeline doesn't write depth, so the * equality test rejects every fragment. * * Returns a `ReadonlyMap` view: the renderer holds the only writeable * reference (via `setColorOverrides`) so routing decisions stay in sync * with the overlay batches we built from the same data. */ getColorOverrides(): ReadonlyMap | null; /** Destroy GPU resources for overlay batches */ private destroyOverrideBatches; /** * Clear scene */ /** Textured meshes (#961) for the renderer's dedicated textured sub-pass. */ getTexturedMeshes(): readonly TexturedMesh[]; /** * GPU bytes currently held by the scene's mesh collections (issue #1682 * observability). Sums actual `GPUBuffer.size` values across colour batches * (streaming fragments are members of `batchedMeshes`, so they are counted * exactly once), cached partial sub-batches, hydrated individual meshes, * textured meshes (plus a 4 B/texel texture estimate) and instanced * templates. Instanced templates are counted even while hidden in the Types * view: hiding does not free their buffers. O(collections) walk with no GPU * calls, intended for on-demand telemetry, not per-frame use. */ getResidentGpuBytes(): ResidentGpuBytes; /** * Toggle the instanced draw pass. Instanced geometry is class-0 occurrences * (the Model view); hide it in the Types view mode, where the flat path shows * the class-1/2 type library instead. Buffers stay uploaded — just not drawn — * so toggling back is free. */ setInstancedVisible(visible: boolean): void; /** GPU-instancing templates for the renderer's instanced draw pass. Empty when * hidden (Types view mode) so the draw loop skips it. */ getInstancedTemplates(): readonly InstancedTemplateGPU[]; /** * Decode a per-batch IFNS instancing shard (`processGeometryBatchInstanced`) * and upload its templates for GPU-instanced drawing. Each unique geometry * becomes a slot-0 vertex buffer (28-byte pos+norm+entityId, matching the flat * layout — the per-vertex entityId is a 0 placeholder; vs_instanced reads the * per-occurrence id) + index buffer, plus a slot-1 per-instance buffer (mat4 + * entityId + rgba) already composed in the WebGL Y-up frame * (`prepareInstancedRender` folds the Z-up→Y-up swap). No-op on a non-shard or * empty payload. Idempotent only in the sense of "append" — call clear() to * reset on a new model. * * Takes an ALREADY-DECODED shard (the worker/main layer that receives the raw * IFNS bytes owns `decodeInstancedShard`), so this module keeps only a * type-only dependency on @ifc-lite/geometry and the Scene stays focused on * GPU upload. */ addInstancedShard(device: GPUDevice, shard: DecodedInstancedShard): void; /** Transform a template's local AABB by an occurrence's column-major mat4 (read * from the packed instance record at `matOffset`) and union the world box into * boundingBoxes[eid]. Returns the occurrence's world box so the caller can also * fold it into the template's cull metadata. */ private unionInstancedWorldAabb; /** True if `expressId` is a GPU-instanced occurrence (lives only in the instanced * shard, not the flat meshDataMap). CPU consumers use this to decide whether to * fall back to the instanced accessors below. */ isInstancedEntity(expressId: number): boolean; /** All instanced occurrence express_ids (for CPU consumers that enumerate geometry, * e.g. the raycast-engine and exporters). */ getInstancedEntityIds(): IterableIterator; /** Number of distinct GPU-instanced entities. O(1) — for size heuristics * (e.g. the orbit-pivot raycast skip) that must not miss instanced-heavy * models where the flat mesh/batch census reads deceptively small. */ getInstancedEntityCount(): number; /** Materialize EVERY instanced occurrence as world-space MeshData. Transient + not * retained — for one-shot full-geometry consumers (glTF / IFC5 export) that must * include the instanced occurrences absent from geometryResult.meshes. Returns [] * when no instanced data is loaded or after geometry release (templates freed). */ getAllInstancedMeshData(): MeshData[]; /** World-space AABB for an instanced occurrence (union over its occurrences), * or null if not instanced. Populated at upload time, so this is O(1). */ getInstancedEntityBounds(expressId: number): BoundingBox | null; /** Lazily materialize per-occurrence world-space MeshData for an instanced entity * (template geometry × each occurrence's matrix). NOT retained — built on demand * for CPU consumers that need triangles (exact raycast / measure / section-face / * export). Returns undefined if the id is not instanced. */ getInstancedMeshDataPieces(expressId: number): MeshData[] | undefined; /** * Per-instance SELECTION: highlight the occurrences of `expressIds` by setting * their flag byte (bit 0) and clearing the previously-selected ones. The shader * (vs_instanced -> fs_main) applies the blue highlight per occurrence, so no * re-draw is needed. No-op until a shard has been uploaded. */ setInstancedSelection(expressIds: ReadonlySet): void; /** Keep each template's selectedCount in sync with selection flips so the * render loop can exempt templates with selected occurrences from * contribution culling (the highlight must not vanish on the user's focus). */ private bumpTemplateSelectedCount; /** * Per-instance VISIBILITY (hide / isolate): set the hidden flag bit on occurrences * that should not render, mirroring the flat path's hiddenIds/isolatedIds filter. * The shader discards hidden occurrences in BOTH the render and pick passes, so they * neither draw nor are pickable. `isolatedIds != null` means "show only these"; any * occurrence not in the set is hidden. Diffed so an unchanged visibility set is a * no-op (no writeBuffer). No-op until a shard has been uploaded. */ setInstancedVisibility(hiddenIds: ReadonlySet | null | undefined, isolatedIds: ReadonlySet | null | undefined): void; /** * Per-instance COLOUR OVERRIDE (lens / IDS / compare / 4D): patch the colour * bytes of the affected occurrences in place; occurrences dropped from the map * are restored to their original colour. Pass null/empty to clear all. */ setInstancedColorOverrides(overrides: ReadonlyMap | null): void; /** True when an active colour override made some instanced occurrence translucent, * so the renderer should run the transparent instanced sub-pass. */ hasTransparentInstances(): boolean; /** Write the combined flag lane (selected | hidden) for every occurrence of `eid`. * Folding both bits here means selection and visibility updates never clobber each * other (they share the one u32 flags lane at INSTANCE_FLAGS_OFFSET). */ private writeInstanceFlags; private writeInstanceColor; private restoreInstanceColor; /** * Build a textured mesh (#961): interleave position+normal+entityId+uv into one * vertex buffer, upload the decoded RGBA8 texture, create a sampler honouring * the IFC RepeatS/RepeatT wrap, and wire a bindGroup (uniform+texture+sampler). * The per-frame uniform (viewProj/section/flags + colour tint) is written by * the renderer each frame, mirroring how colour batches are driven. */ /** True when the mesh can render through the textured pipeline: UVs plus * either a Rust-decoded image (#961) or a viewer-resolved ImageBitmap for * an external `IfcImageTexture` reference (#1781). A `textureRef` whose * image was NOT resolved (missing zip sibling) renders as ordinary * flat-colour geometry instead. */ private static hasRenderableTexture; /** * Interleave a textured mesh's vertices into the stride-36 layout * `[px,py,pz, nx,ny,nz, entityId(u32), u,v]`. Shared by initial upload and * the translate re-upload so the two can't drift. Returns null when the mesh * has no texture/uvs/geometry. */ private interleaveTexturedVertices; private createTexturedMesh; /** Release a textured mesh's GPU texture: shared (#1781) entries decrement * the registry refcount and die with their LAST reference; per-mesh (#961) * uploads are destroyed outright. */ private releaseTexturedMeshTexture; clear(): void; /** * Calculate bounding box from actual mesh vertex data */ getBounds(): { min: { x: number; y: number; z: number; }; max: { x: number; y: number; z: number; }; } | null; /** * Get all expressIds that have mesh data (for CPU raycasting). * After geometry release, returns expressIds from the cached bounding boxes. */ getAllMeshDataExpressIds(): number[]; /** * Get or compute bounding box for an entity from its mesh vertex data. * Results are cached per expressId for subsequent calls. * @param expressId - The expressId (globalId) to look up * @returns Bounding box with min/max corners, or null if no mesh data exists */ getEntityBoundingBox(expressId: number): BoundingBox | null; /** * Local (pre-placement, object-space) AABB for an entity (issue #1474) — the * element's true, un-rotated extent, unlike {@link getEntityBoundingBox}'s * world-space (axis-aligned-to-world) box. Y-up metres, same frame as * `positions`. O(1): no vertex scan, reads `MeshData.localBounds` captured * by the geometry pipeline. * * Unions `localBounds` across all of the entity's mesh pieces — safe with * no reconciliation, since every piece of one element is already expressed * in the same local frame (see `MeshData.localBounds` docs). For a * GPU-instanced entity, unions the local box of every occurrence's * template — one `expressId` can hold multiple occurrence records backed * by DIFFERENT templates (e.g. a mapped-item assembly whose sub-items * split across materials), not just repeats of one template, mirroring the * flat-path union above. * * Returns `null` for a container/assembly with no mesh (e.g. * `IfcElementAssembly`), or when not captured (older cached geometry). */ getEntityLocalBounds(expressId: number): { min: [number, number, number]; max: [number, number, number]; } | null; /** * The resolved local→world placement transform for an entity (issue * #1474): row-major 4×4 (16 numbers), Y-up metres — pairs with * {@link getEntityLocalBounds} to reconstruct the element's true oriented * world box (an OBB), unlike {@link getEntityBoundingBox}'s pre-unioned * world-axis-aligned box. * * A flat entity's mesh pieces all share one placement (one * `IfcLocalPlacement` per element) — returns the first piece that carries * one. For a GPU-instanced entity, reads the FIRST occurrence record's * transform (from the packed instance buffer, column-major, transposed * here so the public contract is row-major regardless of path). * * KNOWN LIMITATION: unlike {@link getEntityLocalBounds} (safe to union), * a transform can't be meaningfully aggregated across multiple occurrence * records — an entity whose shape is internally composed of several * independently-placed mapped sub-items (e.g. a railing with repeated * baluster geometry) has genuinely DIFFERENT per-occurrence transforms * under one `expressId`. This returns one representative transform, not * necessarily the "whole entity's" placement, for such cases. * * Returns `null` for a container/assembly with no mesh, or when not * captured (older cached geometry, or the instancing template was released). * * Returns `Float64Array`, NOT `Float32Array`: `localToWorld` carries the * placement's translation in the *original* (pre-RTC) coordinate frame, * which for a building-scale/georeferenced model can be tens of thousands * of metres from the origin — f32 there loses sub-millimetre precision * (the exact fan-collapse failure mode `MeshData.origin` exists to avoid * for `positions`). The flat path's source data is already f64 * (`piece.localToWorld` round-trips from Rust's `[f64; 16]`); the * instanced path's source (the GPU instance buffer) is genuinely f32, so * widening it here is lossless but doesn't recover precision already lost * upstream in that path. */ getEntityTransform(expressId: number): Float64Array | null; /** * CPU raycast against all mesh data. * Returns expressId and modelIndex of closest hit, or null. * Delegates to extracted raycaster utilities. */ raycast(rayOrigin: Vec3, rayDir: Vec3, hiddenIds?: Set, isolatedIds?: Set | null, clip?: PickClipState | null): RaycastHit | null; } //# sourceMappingURL=scene.d.ts.map