import * as THREE from "three"; import type { Camera } from "../camera/camera.js"; /** * Topology type of a pickable component. Mirrors the values of `TopoFilter` * below (minus the `none` sentinel). */ export type TopoType = "face" | "edge" | "vertex" | "solid"; /** * Filter types for topology-based picking. `none` is the "no filter" sentinel * (all topos eligible). */ export declare const TopoFilter: { none: null; vertex: "vertex"; edge: "edge"; face: "face"; solid: "solid"; }; export type TopoFilterType = (typeof TopoFilter)[keyof typeof TopoFilter]; /** Reserved component id meaning "nothing under the cursor". */ export declare const BACKGROUND_ID = 0; /** * Name of the per-vertex integer buffer attribute carrying the component id on * every pickable geometry (faces, edges and vertices). The geometry attaches a * `Uint32BufferAttribute` under this name; the pick pass reads it as an **integer** * attribute (`gpuType = THREE.IntType`, GLSL3 `in uint`) so ids stay exact past * 2^24. The geometry side must set `gpuType = THREE.IntType` on the attribute for * the pick shader to read it. Between the stages the id travels as two float * varyings (16-bit halves), deliberately NOT as a `flat uint` — see the note in * highlight.ts: a `flat` varying costs ~25x the scene's buffer bytes in graphics * memory under WebKit's ANGLE-on-Metal backend. */ export declare const COMPONENT_ID_ATTRIBUTE = "componentId"; /** * three.js camera/object layers reserved for the pick passes. * * A pickable object lives on visual layer 0 (so the main camera draws it) **and** * its topo's pick layer. The pick pass sets the (reused) main camera to a single * pick layer so `overrideMaterial` only touches one topo at a time — a mesh shader * can't render lines/points. `obj_vertices` points live on `VERTEX` *only* (off the * visual pass). */ export declare const PICK_LAYER: { readonly FACE: 1; readonly EDGE: 2; readonly VERTEX: 3; }; export type PickLayer = (typeof PICK_LAYER)[keyof typeof PICK_LAYER]; /** * Attach the per-vertex `componentId` integer attribute to a pickable geometry: the * canonical way to bind {@link COMPONENT_ID_ATTRIBUTE} so the GLSL3 pick shader reads * it as `in uint` (`gpuType = IntType` — without it three uploads floats and the bits * are wrong). Use `instanced` for fat-line (per-segment) edge geometry. */ export declare function applyComponentIds(geometry: THREE.BufferGeometry, componentId: Uint32Array, instanced?: boolean): void; /** * Add `object` to its topo's pick layer additively — it stays on visual layer 0 (the * main render pass) AND becomes pickable. For face/edge geometry and visible vertices. */ export declare function enablePickLayer(object: THREE.Object3D, topo: "face" | "edge" | "vertex"): void; /** * Put `object` on its topo's pick layer ONLY (off visual layer 0), so the main camera * never draws it — for the pick-only `obj_vertices` Points cloud. */ export declare function setPickLayerExclusive(object: THREE.Object3D, topo: "face" | "edge" | "vertex"): void; /** * Metadata for one pickable component (face, edge, vertex, or solid). The id-buffer * pass reads back an id, and this is what it resolves to. */ export interface ComponentInfo { /** Unique id within the current model; encoded into the pick buffer. Never 0. */ id: number; /** Full path, e.g. "/Assembly/Part1/faces/faces_3". */ path: string; /** Leaf name, e.g. "faces_3". */ name: string; /** Topology type. */ topo: TopoType; /** Subtype of the owning shape (e.g. "solid"), or null. */ subtype: string | null; /** * Path of the owning solid when this component is a face/edge/vertex *inside* a * solid — pick-only, no tree node. `null` for standalone faces/edges/vertices * that are tree leaves in their own right. */ solidPath: string | null; } /** * Map a dropdown topo filter to the picker's `TopoType[]`. `none`/`[null]` (the * "no filter" sentinel) → `undefined` (all topos eligible). */ export declare function pickerTopoFilter(filter: TopoFilterType[]): TopoType[] | undefined; /** * Tree-leaf path that owns a picked component: the owning solid's path for a * sub-component, else the component's own path with the topo suffix stripped. This * is the leaf the double-click pick and the visibility gate operate on. */ export declare function leafPath(info: ComponentInfo): string; /** * Signature of the live clip state, to detect actual clip changes for the picker * (used by the render loop's dirty cadence). */ export declare function clipSignature(planes: THREE.Plane[] | null, intersection: boolean): string; /** * Flat `id -> ComponentInfo` registry. Replaces the `NestedGroup.groups[id]` * explosion for pick lookups. Ids are allocated monotonically starting at 1; * id 0 ({@link BACKGROUND_ID}) means "nothing". */ export declare class ComponentRegistry { private byId; private nextId; constructor(); /** * Register a component and return its freshly allocated id. The returned id is * also written onto the stored record's `id` field. */ register(info: Omit): number; /** Look up a component by id; `undefined` for unknown ids or background (0). */ get(id: number): ComponentInfo | undefined; /** * Iterate every registered component in registration (id-ascending) order. * Used by the highlight controller to resolve solid selection (all components * sharing a `solidPath`) without a scene-graph walk. */ entries(): IterableIterator; /** * Largest allocated id so far (0 when empty). Ids are allocated contiguously from * 1 and NEVER recycled ({@link removeByPathPrefix} deletes records but keeps * `nextId`), so this is the high-water mark used as the upper bound for sizing the * highlight-state texture; it is `>=` {@link size} (equal until the first removal). */ get maxId(): number; /** Number of registered components. */ get size(): number; /** * Drop every component whose path is `prefix` or lies under it (`prefix + "/"`), * for {@link Viewer.removePart}. Does NOT recycle ids — surviving components keep * their ids (and thus their highlight-state texel), and {@link maxId} stays put. */ removeByPathPrefix(prefix: string): void; /** Drop all entries and reset id allocation. */ clear(): void; } /** * Pack a component id into an RGBA8 byte tuple (full 32-bit range, low byte in R). * The pick fragment shader must write the same layout. Inverse of {@link unpackId}. */ export declare function packId(id: number): [number, number, number, number]; /** Decode an RGBA8 tuple (as written by {@link packId}) back into a component id. */ export declare function unpackId(r: number, g: number, b: number, a: number): number; /** * Build the per-vertex face component-id attribute for a solid/standalone-face * node's EXISTING indexed tessellation, registering one face component per face * under `{path}/faces/faces_{i}` in array (backend-enumeration) order. * * No de-indexing: vertices are pooled per face, so each vertex maps to exactly one * face. `collisions` counts vertices referenced by more than one face — it must be * 0 (a non-zero value means cross-face sharing, i.e. de-indexing would be required). * * @param vertexCount - number of vertices in the position pool (`positions.length / 3`) * @param triangles - flat index array (with `trianglesPerFace`) OR nested * `number[][]` (one face's flat index list per entry) * @param trianglesPerFace - per-face triangle counts when `triangles` is flat; * `undefined` for the nested format */ export declare function buildFaceComponentIds(vertexCount: number, triangles: Uint32Array | number[] | number[][], trianglesPerFace: Uint32Array | number[] | undefined, path: string, subtype: string | null, registry: ComponentRegistry): { componentId: Uint32Array; collisions: number; }; /** * Build the per-segment (instanced) edge component-id attribute, registering one * edge component per edge under `{path}/edges/edges_{i}` in array order. Produces * one id per line segment (one `LineSegments2` instance). * * @param edges - flat segment endpoints (with `segmentsPerEdge`) OR nested * `number[][]` (one edge's flat points per entry, 6 floats per segment) * @param segmentsPerEdge - per-edge segment counts when `edges` is flat; * `undefined` for the nested format */ export declare function buildEdgeComponentIds(edges: Float32Array | number[] | number[][], segmentsPerEdge: Uint32Array | number[] | undefined, path: string, subtype: string | null, registry: ComponentRegistry): { componentId: Uint32Array; }; /** * Build the per-point vertex component-id attribute for a node's `obj_vertices` * (the real B-rep corners — the *selectable* vertices, NOT the triangle-mesh * vertices), registering one vertex component per point under * `{path}/vertices/vertices_{i}` in array (backend `get_vertices`) order. * * @param objVertices - flat xyz triples (3 floats per vertex) */ export declare function buildVertexComponentIds(objVertices: Float32Array | number[], path: string, subtype: string | null, registry: ComponentRegistry): { componentId: Uint32Array; }; /** * Result of a successful pick: the component id read from the id buffer, its * registry metadata, and the world-space hit point. */ export interface PickResult { /** Picked component id (never {@link BACKGROUND_ID}). */ id: number; /** Registry metadata for the picked component. */ info: ComponentInfo; /** * World-space hit point, sourced from the position-MRT attachment. `null` when * the RGBA32F position attachment is unsupported (id-only pick). */ point: THREE.Vector3 | null; } /** Options for {@link IdPicker.pickAt}. */ export interface PickAtOptions { /** * Restrict which topo types may be resolved (mirrors the existing * `topoFilter`). `undefined`/empty = all (priority vertex>edge>face applies). * `"solid"` makes faces eligible (caller maps the face to its `solidPath`). */ topoFilter?: TopoType[]; /** * Odd N for the N×N readback window (touch radius for thin edges / tiny * vertices). Defaults to {@link IdPicker}'s window size (5). */ windowSize?: number; } /** Options for the per-topo pick materials. */ export interface PickMaterialOptions { /** Live clipping planes the pick pass must honor (so clipped geometry is not picked). */ clippingPlanes?: THREE.Plane[] | null; /** Intersection clipping (mirror the visual material's `clipIntersection`). */ clipIntersection?: boolean; /** * Forward depth bias in **view-space world units** (a positive nudge toward the * camera) so a corner's higher-priority topo wins the shared-depth pick test over * its coincident lower-priority topo. Applied to `mvPosition.z` BEFORE projection — * i.e. a constant world distance, independent of the camera's near/far spread. * (An earlier build biased a constant in NDC z, which under an orthographic camera * on a large scene became a multi-mm world window that let geometry behind an * occluding face get picked "through" it.) Set per-scene from the bounding radius. */ depthBias?: number; } /** * View-space depth-bias factors (× scene bounding radius) for the edge and vertex * pick passes. Face = 0. Kept monotonic with the vertex > edge > face hover priority * so a corner's vertex wins over its edges and its edges over their faces. The factor * is a fraction of the bounding radius: large enough to clear 24-bit depth-buffer * quantization (≈ (far−near)/2^24 with far/near ∝ radius), small enough that the * world window stays sub-mm on typical models so geometry a real distance behind an * occluder is NOT picked through it. */ export declare const EDGE_DEPTH_BIAS_FACTOR = 0.0001; export declare const VERTEX_DEPTH_BIAS_FACTOR = 0.0002; /** * Pick size (framebuffer px in the half-res target) for vertex pick points. Kept * SMALL: the N×N readback window provides the "touch radius" by reading neighbours, * so fat rendering is unnecessary and would only overwrite neighbouring face pixels * in the shared id buffer (hurting faces-only picks near corners). A few px keeps * the corner reliably present without eating faces. */ export declare const PICK_POINT_SIZE = 3; /** * Build the **face** pick override-material: a GLSL3 `ShaderMaterial` that reads the * integer {@link COMPONENT_ID_ATTRIBUTE} and writes `packId(componentId)` + * world position to the MRT pick target. Honors clipping (planes + intersection). */ export declare function createFacePickMaterial(options?: PickMaterialOptions): THREE.ShaderMaterial; /** * Build the **vertex** pick override-material for an `obj_vertices` `THREE.Points` * cloud: a GLSL3 `ShaderMaterial` that draws each point **fat** (`gl_PointSize` = * {@link PICK_POINT_SIZE}, a touch radius independent of the visual point size), * reads the integer `componentId`, and writes id + world position to the MRT. A * tiny forward depth bias lets a corner vertex win the depth test over its * coincident face (so it can be picked), while occluded vertices stay hidden. * Honors clipping. */ export declare function createVertexPickMaterial(options?: PickMaterialOptions): THREE.ShaderMaterial; /** * Pick width (px in the half-res target) for edge pick lines. Kept THIN: edges run * along face boundaries, so a fat band would overwrite face pixels on both sides and * wreck faces-only picks. The N×N readback window supplies the touch radius instead. */ export declare const PICK_EDGE_WIDTH = 2; /** * Build the **edge** pick override-material for a `LineSegments2` / * `LineSegmentsGeometry` fat line: a GLSL3 `ShaderMaterial` that ports three * `LineMaterial`'s screen-space instanced fat-line expansion (instanceStart/End + * the `position` quad template, `linewidth`/`resolution` uniforms), reads the * **instanced** integer `componentId`, and writes id + world position to the MRT. * Drawn fat ({@link PICK_EDGE_WIDTH}) for a touch radius independent of the visual * line width. `resolution` MUST be set to the pick target size by the caller. * Honors clipping. (World-units + dash + color paths are dropped — pick only.) */ export declare function createEdgePickMaterial(options?: PickMaterialOptions): THREE.ShaderMaterial; /** MRT attachment index of the packed-id texture. */ export declare const PICK_ID_ATTACHMENT = 0; /** MRT attachment index of the world-position texture. */ export declare const PICK_POS_ATTACHMENT = 1; /** * Create the MRT pick render target: a single `WebGLRenderTarget` with two color * attachments — * - {@link PICK_ID_ATTACHMENT} (0): RGBA8/UnsignedByte packed `componentId` * (`packId`, low byte in R), and * - {@link PICK_POS_ATTACHMENT} (1): RGBA32F world-space hit position, * `xyz` = point, `w = 1` where a fragment was written (0 = background). * * `NearestFilter` on both — interpolated ids/positions are meaningless. A depth * buffer keeps the nearest fragment per pixel; no depth *texture* is read. * `NoColorSpace` on the id texture stops any sRGB transform corrupting the bytes. * * Plain factory: the caller ({@link IdPicker.pickAt}) owns sizing/resize/disposal. * `width`/`height` are already the pick-buffer pixel size (caller applies half-DPR). * * @param withPosition - allocate the RGBA32F world-position attachment. Requires * `EXT_color_buffer_float` (probed by {@link IdPicker}); when `false` the target * carries the id attachment only and `pickAt` returns `point = null` (the fat * `out` at location 1 in the pick shaders is simply dropped — writing to an * unbound draw buffer is well-defined and discarded in WebGL2). */ export declare function createPickTargets(width: number, height: number, withPosition?: boolean): THREE.WebGLRenderTarget; /** * GPU id-based picker: cursor → component id → registry → backend path, plus the * world-space hit point. * * Lifecycle the host (viewer) drives: * - {@link attach} once the scene + camera exist (re-attach after `clear()`), * - {@link setSize} on canvas resize, {@link setClippingPlanes} on clip change, * - {@link setDirty} whenever the view/geometry changes (camera move, geometry * rebuild) so the pick buffer is re-rendered lazily on the next {@link pickAt}, * - {@link pickAt} on hover/click to resolve the component under the cursor. * * Per-topo passes (FACE, then fat EDGE lines, then fat VERTEX points) accumulate * into one MRT target; `pickAt` reads an N×N window and resolves **vertex > edge > * face** priority via the registry's topo, honoring `topoFilter`, then reads the * position attachment → `point`. */ export declare class IdPicker { readonly registry: ComponentRegistry; /** * Default N for the N×N readback window — the pick "touch radius" applied to the * (thin) geometry. 3 is odd, so the window is symmetric about the cursor pixel * (half = 1 → cursor ±1 on each axis); an even size is asymmetric. Tuned * interactively (pick-explore.html): 2 and 3 feel identical, much larger (e.g. 7) * pulls edge/vertex priority too far off the actual line/point; 1 is too unforgiving. */ windowSize: number; private renderer; /** Render source — the scene + camera the pick pass renders. */ private scene; private camera; /** Live clipping planes mirrored onto the pick materials (`null` = clipping off). */ private clippingPlanes; /** Canvas size in CSS px (the pick target is sized at half-DPR internally). */ private width; private height; /** Intersection clipping mode mirrored onto the pick materials. */ private clipIntersection; /** * Scene bounding radius, used to scale the edge/vertex view-space depth bias to a * small constant world distance (see {@link setSceneRadius}). 0 until the host sets * it — with 0, coincident corner topo would z-fight and be unpickable, so the host * MUST call {@link setSceneRadius} once the model bounds are known. */ private sceneRadius; /** Whether the pick buffer must be re-rendered before the next read. */ private dirty; /** MRT pick target (id + world-position attachments). Allocated lazily in `pickAt`. */ private pickTarget; /** Per-topo pick override-materials. Allocated lazily in `pickAt`. */ private faceMaterial; private edgeMaterial; private vertexMaterial; /** Clip-plane count the pick materials were last compiled for (program key). */ private compiledPlaneCount; /** Intersection mode the pick materials were last compiled for (program key). */ private compiledIntersection; /** Scratch to save/restore the renderer clear color across a pick pass. */ private readonly savedClearColor; /** Scratch to save/restore the renderer viewport across a pick pass. */ private readonly savedViewport; /** Scratch readback buffer for the position attachment (one RGBA32F pixel). */ private readonly posPixel; /** Scratch readback buffer for the N×N id window (grown as needed). */ private idWindow; /** * Whether the GPU can render to a float color attachment (`EXT_color_buffer_float`, * probed at construction). When `false`, the pick target carries the id attachment * only and {@link pickAt} returns `point = null` — id picking still works, just * without the world-space hit point (degrades hover coords / double-click pivot to * the bbox-center fallback). Universal in real WebGL2; the probe guards exotic * contexts. */ readonly positionSupported: boolean; constructor(renderer: THREE.WebGLRenderer, registry: ComponentRegistry); /** * Probe `EXT_color_buffer_float` — the capability needed to *render into* the * RGBA32F world-position attachment. Getting the extension is the canonical * feature test (no side effects) and is WebGL2-only: it does not exist on WebGL1 * (whose float-color extension is `WEBGL_color_buffer_float`), so a non-null result * already implies a WebGL2 context. Near-universal on WebGL2 but spec-optional. */ private static _probeFloatColor; /** * Point the picker at the scene + camera it renders for picking. Call once * the scene exists and again after the scene/camera are recreated (`clear()`). */ attach(scene: THREE.Scene, camera: Camera): void; /** * Mark the pick buffer stale so the next {@link pickAt} re-renders it. Call on any * view change (camera move/zoom, geometry rebuild) — NOT on mouse-move. */ setDirty(): void; /** * Set the scene bounding radius so the edge/vertex pick passes bias depth by a * small constant WORLD distance (radius × {@link EDGE_DEPTH_BIAS_FACTOR} / * {@link VERTEX_DEPTH_BIAS_FACTOR}) rather than a constant in NDC z. The world * window is then independent of the camera near/far spread — the fix for geometry * behind an occluding face getting picked "through" it under an orthographic camera * on a large scene. Call whenever the model bounds change (initial build, add/remove * part). Updates existing materials in place and seeds lazily-created ones. */ setSceneRadius(radius: number): void; /** View-space edge depth bias (world units) for the current scene radius. */ private _edgeDepthBias; /** View-space vertex depth bias (world units) for the current scene radius. */ private _vertexDepthBias; /** Resize the pick render target to match the canvas (CSS px; half-DPR internally). */ setSize(width: number, height: number): void; /** * Update the clipping the pick materials must honor — planes (`null` when the * viewer's clipping is off) and intersection mode (mirror the visual material's * `clipIntersection`). Recompiles the materials only when the plane *count* or the * intersection *mode* changes (three keys the program on `NUM_CLIPPING_PLANES` / * `UNION_CLIPPING_PLANES`); a value-only change just re-renders. Always * re-renders next {@link pickAt}. */ setClippingPlanes(planes: THREE.Plane[] | null, intersection?: boolean): void; /** * Resolve the component under the cursor, with its world-space hit point. * * @param x - canvas-relative x in CSS px (`getBoundingClientRect`) * @param y - canvas-relative y in CSS px (top-left origin; WebGL Y-flip is internal) * @returns the picked component (`point` = world hit position), or `null` for * background / no hit. * * Re-renders the MRT pick target if {@link dirty} (per-topo passes on the pick * layers, honoring clipping), then reads an N×N window around the cursor pixel * (canvas px → ×dpr×0.5 → Y-flip) and resolves **vertex > edge > face** priority * via the registry's topo (nearest-to-center tie-break), restricted to * `options.topoFilter`. The position attachment at the chosen pixel → `point`. */ pickAt(x: number, y: number, options?: PickAtOptions): PickResult | null; /** Lazily allocate / resize the MRT target and the per-topo pick materials. */ private _ensureResources; /** * Render the per-topo pick passes into the MRT target, reusing the main camera. * Clears once, then accumulates **FACE → EDGE → VERTEX** (each drawn fat * and later, so higher-priority topos overwrite at coincident pixels). All * renderer state touched here is saved and restored. */ private _renderPickBuffer; /** One pick pass: render only `layer`'s objects with `material` (no clear). * Clipping planes are kept in sync solely by {@link setClippingPlanes} (and the * lazy material creation in {@link _ensureResources}); re-assigning them here per * pass would mutate `clippingPlanes` without `needsUpdate`, risking a stale * `NUM_CLIPPING_PLANES` if the count ever changed off that path. */ private _pass; /** Release GPU resources (pick render target + materials). */ dispose(): void; }