/** * WebGPU pipelines for IfcAnnotation overlays — filled regions and text * labels. Each pipeline is self-contained (owns its own buffers, bind groups, * pipeline state, optional atlas texture) and exposes a `render(pass, * viewProj)` entry point that the caller invokes from inside an existing * RGBA-blended render pass. * * Triangulation: simple ear-clipping for polygon-with-optional-holes, * inlined below to avoid adding a dependency. Good enough for typical IFC * fill regions (rooms, hatched zones) which are usually convex or near-convex. * Pathological concave shapes with many holes may show tessellation glitches — * an `earcut` upgrade is straightforward when it matters. */ import { SymbolicTextAtlas } from './symbolic-text-atlas.js'; export interface SymbolicFillInput { /** Flat ring buffer: [x, z, x, z, …]. Y is taken from `worldY`. */ points: Float32Array; /** Vertex indices marking the start of each hole. Empty = no holes. */ holesOffsets: Uint32Array; worldY: number; /** Straight-alpha RGBA in [0..1]. The shader premultiplies. */ color: [number, number, number, number]; } export interface SymbolicTextInput { worldPos: [number, number, number]; /** Baseline direction (X axis in 3D world space). */ dirX: number; dirZ: number; /** Glyph height in world units. */ height: number; content: string; /** IFC BoxAlignment ("bottom-left", "center", "top-right", …). */ alignment: string; color?: [number, number, number, number]; /** * When true, the shader rebuilds the glyph quad in screen-aligned * (cameraRight, cameraUp) basis so the text always faces the camera. * Used for IfcGridAxis bubble tags — they must stay readable in * top-down/ground views where the authored world-Y up axis collapses. * Defaults to false (authored, in-plane text). */ billboard?: boolean; /** * Per-instance target cap height in screen pixels. 0 / undefined falls * back to the renderer's global default (~14 px). Grid bubble fills + * outlines emit at ~32 px so the bubble stays proportional to the * inscribed tag at every zoom level — without this override they'd * collapse to the same screen size as the tag and the bubble would * disappear behind the character. */ targetPx?: number; } export declare class SymbolicFillPipeline { private readonly device; private readonly format; private readonly sampleCount; private pipeline; private bindGroupLayout; private uniformBuffer; private bindGroup; private vertexBuffer; private vertexCount; constructor(device: GPUDevice, presentationFormat: GPUTextureFormat, sampleCount?: number); private init; /** * Upload a list of fill regions. Each region is triangulated (ear-clipping, * holes-aware) into a single shared vertex buffer. * * Pass an empty array to clear. Triangulation skips degenerate rings * (< 3 vertices) and silently drops any hole that can't be merged into the * outer ring (rare; usually overlapping rings in malformed IFC). */ upload(fills: readonly SymbolicFillInput[]): void; hasGeometry(): boolean; render(pass: GPURenderPassEncoder, viewProj: Float32Array): void; destroy(): void; } export declare class SymbolicTextPipeline { private readonly device; private readonly format; private readonly sampleCount; private readonly atlas; private pipeline; private bindGroupLayout; private uniformBuffer; private cornerBuffer; private instanceBuffer; private atlasTexture; private atlasView; private sampler; private bindGroup; private instanceCount; private uploadedAtlasVersion; constructor(device: GPUDevice, presentationFormat: GPUTextureFormat, sampleCount?: number, atlas?: SymbolicTextAtlas); /** Expose the atlas so the upload pre-warms glyphs before instance encoding. */ getAtlas(): SymbolicTextAtlas; private init; /** Re-upload the atlas to a GPUTexture when its version changes. */ private syncAtlasTexture; private ensureBindGroup; /** * Lay out the given text labels into the atlas, encode an instance buffer, * and upload to the GPU. Pass an empty array to clear. */ upload(texts: readonly SymbolicTextInput[]): void; hasGeometry(): boolean; render(pass: GPURenderPassEncoder, viewProj: Float32Array, viewportPxWidth: number, viewportPxHeight: number, cameraRight: readonly [number, number, number], cameraUp: readonly [number, number, number], targetGlyphPx?: number): void; destroy(): void; } /** * Stitch each hole into the outer ring with a single bridge edge so the * result is a simple polygon ear-clipping can handle. Mirrors mapbox/earcut's * `eliminateHoles` pass: * * 1. For each hole, pick its rightmost (max-x) vertex as the bridge start. * 2. Sort holes by descending bridge-start x so outer holes go in first. * 3. Walk the outer ring for the vertex on the right side of the bridge * that's "visible" from the hole — closest match wins. * 4. Splice the hole (rotated to start at its bridge vertex, and reversed * so its winding opposes the outer ring) into the outer ring at the * anchor, closing both ends back to their starts to form a zero-area * bridge edge. * * This breaks under pathological inputs (overlapping holes, holes outside * the outer ring) but those don't occur in well-formed `IfcAnnotationFillArea` * geometry — the IFC schema requires the outer bound to contain all inner * bounds. */ export type Pt = { x: number; z: number; }; export declare function joinHoles(outer: Pt[], holes: Pt[][]): Pt[]; /** Ear-clipping triangulation of a simple polygon. Returns triangle vertex indices. */ export declare function earClip(ring: ReadonlyArray<{ x: number; z: number; }>): number[][]; //# sourceMappingURL=symbolic-overlay-pipelines.d.ts.map