Add mesh to scene with per-mesh GPU resources for unique colors
Add multiple meshes to the scene (convenience method for streaming)
Array of MeshData to add
OptionalisStreaming: boolean
If true, throttles batch rebuilding for better streaming performance
Append additional point clouds without clearing existing ones.
key makes the chunk removable on its own via removePointCloudChunk (one COPC node, #6869).
Optionalkey: stringStreaming entry: open an empty asset that will receive chunks via
appendPointCloudChunk. Call endPointCloudStream when no more
chunks will arrive (currently a no-op but kept for symmetry).
Capture the next submitted frame's bounded color pixels, not compositor state.
Clear all caches (call when geometry changes)
Drop all point cloud GPU resources.
Clear the caps of every cutting plane beyond the first whose id is not in keep (#7545).
Optionalkeep: readonly string[]Clear the 2D section overlay: the primary plane's cap, or with planeId
the cap of that cutting plane beyond the first (#7545).
OptionalplaneId: stringCompute BIM ↔ scan deviation for every loaded point cloud asset.
Delegates to the DeviationComputer collaborator, which owns the
compute pipeline and the BVH-reuse fingerprint; see
deviation/deviation-computer.ts for the full contract.
Optionalopts: DeviationComputeOptionsConsume the render request flag. Returns true (and resets the flag) if a render was requested since the last call. Used by the animation loop to decide whether to render.
Guarded entry point (#4885) for the unguarded body below.
Destroy the renderer and release all GPU resources.
Cleans up scene buffers, render pipeline textures, picking resources, post-processing buffers, section-plane renderers, and snap caches. After calling this method the renderer is no longer usable. Safe to call multiple times (idempotent).
An init() still in flight is invalidated too. It is parked on
await device.init(...), and without the generation bump below it resumes
after this returns, allocates a complete replacement GPU stack that nothing
references, and re-publishes ready — resolving whenReady() waiters
against a renderer the host has already torn down (#2465). The bump is what
initOnce() re-checks after its await, and what makes markReady() refuse
the stale completion.
This is why the teardown itself lives in teardown(): initOnce() runs it
on the PREVIOUS init's objects as part of its own re-init, and routing that
through here would have every init invalidate its own generation, leaving
whenReady() pending forever.
Anyone parked in whenReady() is FAILED rather than left pending. Nothing
after this call can make the wait true — the invalidation above is exactly
what stops the in-flight init from publishing readiness, and a host that
remounts builds a new Renderer rather than re-initialising this one — so
leaving the promise unsettled suspends the caller's async frame for the
lifetime of the page. apps/viewer's point-cloud drop is one of those
frames: it captured this instance before the teardown, and would stop
mid-load with the spinner still up and no error to report. See
whenReady() for the rejection contract.
Probe the quantized pipeline variants and, when all exist, enable 12-byte quantized batch vertices (issue #1682 phase 6). Returns whether quantization is active — on failure (e.g. a fragile backend rejecting pipeline creation) batches stay on the f32 path.
Ensure all meshes have GPU resources (call after adding meshes if pipeline wasn't ready)
Fit camera to view all loaded geometry
Vendor/architecture identity of the GPU adapter, snapshotted during
init() (issue #2624 device-loss telemetry), or null when the runtime
does not expose GPUAdapter.info. Safe to call after a device loss:
the snapshot is plain strings copied at init, not a live GPU object.
Get the canvas element
Get diagnostic info for mobile debugging
Statistics of the last COMPLETED render() call (draw calls issued,
batches drawn / frustum-culled / contribution-culled), or null before
the first frame. Pair with getScene().getResidentGpuBytes() for the
load-complete telemetry snapshot (issue #1682 observability).
Get the GPU device (returns null if not initialized, or if the device has been lost).
The device-lost check is load-bearing, not defensive tidiness. A lost
device is NOT torn down: WebGPUDevice.destroy() is the only thing that
nulls the handle and it is never called for an involuntary loss (TDR /
GPU-process crash / driver reset), so isInitialized() stays true and
this used to keep handing out a zombie GPUDevice. Every caller then
called createBuffer() on it, which throws — bypassing both render()'s
own deviceLost guard and the onDeviceLost listeners entirely.
Returning null instead routes into the if (!device) return check that
every call site already has, so a lost device degrades to "stop
uploading" rather than an uncaught throw.
DELIBERATELY NOT NARROWED, unlike getScene(): the one external
GPUDevice member measured is queue; every other call site
hands the device back to a SceneContents method. See isDeviceLost() /
onDeviceLost() for the recovery contract.
OptionalpointCloudHandle: { id: number }Get render pipeline (for batching). DELIBERATELY NOT NARROWED, unlike
getScene(): the measurement found ZERO external
RenderPipeline members — all 12 call sites pass the handle straight
back into a SceneContents upload method typed for the real class.
Total number of point cloud assets currently uploaded.
Exact float64 placement copy for CPU math; the f32 transform rounds map-grid origins (#6894).
Total number of points across all point cloud assets.
MEASURED external surface, not the 4400-line Scene: widening SceneContents is a published-API decision, not a detail — see scene-contents.ts.
Every section plane's world frame (#7545), for the UI's tags and handle: the last frame's bounds and rotation, and planes (the caller's newest) when given.
Optionalplanes: Pick<SectionFrameInput, "primary" | "extras">Whether the last rendered frame clipped surfaces (section, terrain or box).
Check if 2D section overlay has geometry to render
Initialize renderer.
Safe to call on an already-initialised instance: the previous GPU objects
are released first. The comment below advertises a destroy() + init()
re-init flow, and the obvious device-loss auto-recovery is to call
init() on the live instance — which, without this, silently orphaned
two render pipelines, the picker, the post passes, the point-cloud and
deviation pipelines and the overlay layer's glyph atlas, per
recovery (#2448). Making the method self-safe is cheaper than trusting
every future caller to remember.
Concurrent calls are SERIALISED, not coalesced: the second waits for the
first to settle and then runs in full. Without that, pipeline — which
only ever marks a COMPLETED init — is still null while the first call is
awaiting device.init(), so both calls sail past the guard above and both
allocate a full set of GPU objects, orphaning the first. Queueing turns
the concurrent case into the sequential one the guard already handles,
rather than adding a second, differently-shaped rule.
Invalidate BVH cache (call when geometry changes)
True once the GPU device has been lost for a non-intentional reason.
Check if renderer is fully initialized and ready to use.
ready is part of the test, not decoration: between init() being called
and its queued body running, the device and pipeline still belong to the
PREVIOUS init and are about to be destroyed, so the other two conditions
alone would report a renderer that is on its way out as usable.
So is the device-loss check. A lost device is never torn down —
WebGPUDevice.destroy() is the only thing that nulls the handle and an
involuntary loss (driver reset / VRAM exhaustion / GPU-process crash)
never calls it — so isInitialized() stays true, the pipeline stays
non-null, and ready stays set from the init that completed before the
loss. All three conditions therefore still hold while render() is a
no-op and getGPUDevice() returns null: the renderer would report itself
usable through this third door alone. Unlike the two revocations above
this one needs no generation scoping — an init() clears ready
synchronously, so a latch left standing until the queued body clears it
cannot make this method spuriously false in the meantime.
Load geometry from GeometryResult or MeshData array This is the main entry point for loading IFC geometry into the renderer
Either a GeometryResult from geometry.process() or an array of MeshData
Subscribe to non-intentional GPU device loss (driver reset / VRAM
exhaustion — NOT an intentional destroy()). Fired at most once per
device. After it fires, render() is a no-op and the renderer reports
itself un-ready (isReady() false, whenReady() rejecting with
RendererDeviceLostError) until it is re-initialised, so the typical
response is to dispose this renderer and reload the model. Returns an
unsubscribe function.
Camera and model state live on the CPU (JS) and survive device loss, so a reload restores the model at its current orientation — the loss is a GPU event, not a data loss.
Subscribe to the renderer having degraded frame after frame without
recovering (issue #2417). Distinct from onDeviceLost: the device is
still alive by every signal available, which is exactly why render()
refuses to latch on these throws — but the user is looking at a viewport
that has stopped updating, and until this callback existed nothing said
so. Fired at most once per renderer, once PERSISTENT_DEGRADATION_FRAMES
frames have degraded CONSECUTIVELY — any frame that completes resets the
run, so a session that failed occasionally and recovered every time never
reports. Returns an unsubscribe function.
No replay for a late subscriber, unlike onDeviceLost — a loss can latch
during init(), before any subscriber can exist, but a degraded frame
cannot: renderFrame() returns early while pipeline is null, so the
count only moves once the host is driving frames, which is strictly after
init() resolved and the host subscribed.
Check whether a render has been requested without clearing the flag. Used by the animation loop to test the dirty flag before committing to render (e.g. when throttling may skip the frame).
Pick object at screen coordinates Respects visibility filtering so users can only select visible elements Returns PickResult with expressId and modelIndex for multi-model support
Note: x, y are CSS pixel coordinates relative to the canvas element. These are scaled internally to match the actual canvas pixel dimensions.
Resolves null once the device is gone — see pickPathAlive().
Optionaloptions: PickOptionsGPU-based rectangle pick. Drag-select returns the set of
expressIds touched by any pixel inside [x0,y0]..[x1,y1]
(CSS pixels, canvas-relative). Both meshes and point clouds
participate.
See PickingManager.pickRect for the visibility-filter +
limitation notes.
Resolves an empty set once the device is gone — see pickPathAlive().
Optionaloptions: PickOptionsStage one new owner; borrowed mesh buffers must remain immutable until disposal.
Exact visible-surface raycast in CSS canvas coordinates.
Optionaloptions: PickOptions & { snapOptions?: Partial<SnapOptions> }Raycast with magnetic edge snapping behavior This provides the "stick and slide along edges" experience
Note: x, y are CSS pixel coordinates relative to the canvas element. These are scaled internally to match the actual canvas pixel dimensions.
Optionaloptions: PickOptions & { snapOptions?: Partial<SnapOptions> }Read per-scan-asset signed-distance statistics after a completed run.
Read every computed point's signed distance (4 B/point) for summary statistics (#6872).
Bind a streamed point cloud to its federation identity after upload: the post-offset globalId picking and deviation readback report, and, when given, the model index they attribute the asset to (#6887). Takes effect on the next render — no GPU buffer rewrite needed.
OptionalmodelIndex: numberFree the chunks appended under key; returns the points removed (0 for a stale handle).
Draw a frame through the shared submission path, preserving the void API.
Optionaloptions: RenderOptionsDraw one frame; true only when it submits without a contained synchronous failure (#6709).
Never throws, so callers never need to guard this call to keep their animation loop alive.
What a throw MEANS depends on its type (isDeviceLossThrow), and since
issue #2417 that holds for the WHOLE frame — both this catch and the
encode region's own, which share containFrameThrow:
DOMException is the device reporting its own death synchronously
(Safari 26.5, issue #2229). It latches the same deviceLost state the
async device.lost promise would: later frames become quiet skips and
onDeviceLost listeners fire exactly once.RangeError from a buffer the host cannot allocate,
say) costs only this frame: the swap-chain config is invalidated so
the next frame reconfigures, a frame is re-requested within a bounded
budget, the failure is counted in getDiagnostics(), and rendering
carries on. Once enough such frames have degraded without recovering,
onPersistentRenderDegradation fires once.Frame setup and encoding through submit share containFrameThrow
(#2417); the encode catch also balances the validation error scope.
Submission is synchronous, distinct from GPU completion/validation.
Optionaloptions: RenderOptionsRequest a render on the next animation frame. Safe to call many times per frame — only one render will happen.
Resize canvas
Draw the focused clash's CONTACT geometry as 3D line segments — the real
shared-face polygon outlines / intersection lines, not the AABB box.
vertices is a flat line-list (x,y,z per endpoint, 2 endpoints per
segment) in world frame. Omit color to follow the overlay theme's
clashOverlap, as for setClashOverlapBox. Pass null to clear. Shares
the clash-box line buffer, so only one of this / setClashOverlapBox is
shown at a time.
Draw the focused clash's TRUE INTERSECTION VOLUME — the actual overlap
mesh from clashIntersectionSolid — as an opaque solid, so the clash
reads as a shape rather than a wireframe box or contact line (the
BIMcollab Zoom / Solibri presentation). Pass null to clear. Independent
of setClashOverlapBox / setClashContactLines: the caller decides
which one is current for a given clash (solid when the kernel resolved
one, box/lines as the fallback when it didn't). Omit color to follow
the overlay theme's clashOverlap, as for setClashOverlapBox.
Show (or clear) the clash-overlap box: the wireframe AABB of a focused
clash, drawn in color so the overlap region reads as a distinct third
colour next to the two glowing clash elements (#1277). Omit color to
draw it in the overlay theme's clashOverlap and keep following it
across setOverlayTheme calls (#5490). Pass null to clear.
min/max are world-space corners (clash works in world frame).
Toggle Eye-Dome Lighting and tune its strength.
EDL adds depth perception to point clouds (and meshes) via screen- space depth gradient — silhouette pixels get a soft black halo. Cheap: ~9 texture taps per pixel. Only runs when point clouds are loaded.
Set one standalone 3D line overlay, or clear it by passing null.
vertices is a flat world-space line-list — [x1,y1,z1, x2,y2,z2, …],
one segment per six floats. The vertices are already lifted to world
space, so these overlays draw whether or not a section plane is active.
A trailing partial segment is dropped rather than rejecting the array.
An empty array clears too, but null skips building the pipelines.
Every channel is an independent buffer with its own visibility, so
setting one leaves the other three untouched. All four share the colour
set by Renderer.setOverlayTheme's overlayLine field; label colour is per-text via
SymbolicTextInput.color on uploadAnnotationTexts3D.
The channels differ in exactly one way — whether they grow the scene
bounds. annotation (#653) and alignment DO: a file holding only them
has no IfcProduct meshes to frame, so Home / fit-to-view would have
nothing to aim at and the near/far range would clip the lines away.
grid (IfcGridAxis, #967) and dxf (the DXF reference layer, #2043) do
NOT: they are reference layers that routinely extend past the model
envelope, so growing the bounds would reframe the camera whenever one
was ticked on. The rule is "does this content DEFINE the model's
extent", NOT "is it behind a visibility toggle" — annotations sit behind
ifcAnnotationsVisible too.
Cap the drawing buffer's device-pixel ratio, trading sharpness for fill on HiDPI screens; see computeDrawingBufferSize.
Absolute workspace yaw about the render-frame pivot; GPU-instanced only (#4890). Skips invalidation on a no-op (#4890 review).
Absolute workspace translation in renderer Y-up metres (#4226).
Set the one colour vocabulary every overlay draws in (#5484): the selection
highlight, the section-plane preview (one accent for every axis), every
overlay line channel (section-cut outline, IfcAnnotation, alignment,
IfcGrid, DXF / LandXML) and the clash pair / overlap tints. Call this on
theme change and once after init(); the underlying GPU uniforms are
written only here, never per frame. Supersedes setOverlayLineColor
(deleted — the app never called it with a themed colour). See
OverlayTheme for which fields want linear-light RGBA and which want
sRGB-direct RGBA. The matching label colour is per-text via
SymbolicTextInput.color on uploadAnnotationTexts3D.
Apply rendering options (color mode, fixed override, point size).
Replace all loaded point clouds with assets.
Phase 0 entry point — single-chunk inline assets from IFCx
(pcd::base64, points::array, points::base64). Future phases
accept streaming sources via a different overload.
Set/clear a streamed cloud's column-major model matrix (16 floats) for IfcMapConversion alignment (#1804). Applied on the next frame's uniform write without rewriting vertex buffers.
Toggle one resident streamed scan without releasing its GPU buffers.
Opt in the currently selected authored curves for exact magnetic picking.
Upload filled IfcAnnotation regions for the symbolic overlay (issue #653). Pass an empty array to clear.
Upload IfcAnnotation text labels for the symbolic overlay (issue #653). Pass an empty array to clear.
Upload 2D section drawing data for 3D overlay rendering.
Cardinal-axis call site: pass axis + position percentage and the
upload computes the plane offset along the cardinal axis using the
model bounds (or sectionRange override). 2D points are then lifted
to 3D via the cardinal-axis swap.
Custom-plane call site (issue #243): pass customPlane = { origin, tangent, bitangent }. The 2D points are lifted via the explicit
basis, exactly inverting the projection SectionCutter applied when
generating the polygons. Without this the cap silhouette lands off
the actual cutting plane (the bug PR #581 hid by suppressing the
cap entirely for non-cardinal planes).
OptionalsectionRange: { max?: number; min?: number }Optionalflipped: booleanOptionalcustomPlane: {OptionalplaneId: stringResolves once init() has finished and the GPU device + point-cloud
renderer are usable. Callers that may run before init completes — e.g.
dropping a point cloud immediately after the viewport mounts, before
the async WebGPU init resolves — should await renderer.whenReady()
before beginPointCloudStream, which otherwise throws
"Renderer not initialized".
REJECTS (with an Error whose name is RendererDestroyedError) if
destroy() runs while the caller is waiting, or if it already ran and no
init() has been started since. It never resolves against a destroyed
renderer — that is what this method exists to prevent — so the only
alternative would be a promise that never settles, which suspends the
caller's async frame permanently and takes everything the frame captured
with it. The viewer reaches that state on an ordinary path: Viewport
builds a NEW Renderer per mount and destroys the old one in its effect
cleanup, so a destroy() there is FINAL for the instance a consumer
captured — a point-cloud drop that straddles a layout swap or a
StrictMode remount would otherwise hang mid-load, with no error, forever.
Callers should handle the rejection as "the target went away", not as a
load failure.
It also REJECTS (RendererDeviceLostError) while the GPU device is lost.
The device is what this method promises, and a lost one cannot serve the
call the caller is waiting to make — getGPUDevice() returns null, so
beginPointCloudStream throws "Renderer not initialized" the moment the
wait resolves. The third outcome is the one destroy() already ruled out:
parking a waiter that only a host-initiated init() could ever settle,
and that the viewer's usual response to a loss (drop this renderer, build
a new one) guarantees will never come. Unlike the destroyed case this is
NOT final — a later init() on the same instance re-arms the wait
synchronously, so "retry after re-init" is a contract callers can act on,
which is why the two rejections carry different names.
Main renderer class