three-cad-viewer 4.3.8 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/{README.md → Readme.md} +7 -5
  2. package/dist/core/picking-controller.d.ts +125 -0
  3. package/dist/core/studio-manager.d.ts +7 -0
  4. package/dist/core/types.d.ts +24 -6
  5. package/dist/core/viewer-state.d.ts +1 -1
  6. package/dist/core/viewer.d.ts +44 -47
  7. package/dist/index.d.ts +5 -5
  8. package/dist/rendering/environment.d.ts +3 -19
  9. package/dist/rendering/highlight.d.ts +209 -0
  10. package/dist/rendering/id-picking.d.ts +464 -0
  11. package/dist/rendering/light-detection.d.ts +3 -3
  12. package/dist/rendering/material-factory.d.ts +11 -7
  13. package/dist/rendering/picked.d.ts +55 -0
  14. package/dist/rendering/studio-composer.d.ts +3 -3
  15. package/dist/rendering/tree-model.d.ts +1 -1
  16. package/dist/scene/clipping.d.ts +53 -0
  17. package/dist/scene/nestedgroup.d.ts +46 -4
  18. package/dist/scene/objectgroup.d.ts +17 -0
  19. package/dist/scene/render-shape.d.ts +3 -19
  20. package/dist/three-cad-viewer.css +54 -20
  21. package/dist/three-cad-viewer.esm.js +4867 -1562
  22. package/dist/three-cad-viewer.esm.js.map +1 -1
  23. package/dist/three-cad-viewer.esm.min.js +3 -3
  24. package/dist/three-cad-viewer.js +4867 -1562
  25. package/dist/three-cad-viewer.min.js +4 -4
  26. package/dist/tools/cad_tools/measure.d.ts +8 -9
  27. package/dist/tools/cad_tools/mesh-measure.d.ts +264 -0
  28. package/dist/tools/cad_tools/select.d.ts +4 -3
  29. package/dist/tools/cad_tools/tools.d.ts +17 -5
  30. package/dist/tools/cad_tools/ui.d.ts +8 -6
  31. package/dist/ui/display.d.ts +25 -2
  32. package/dist/utils/utils.d.ts +1 -1
  33. package/package.json +4 -1
  34. package/dist/rendering/raycast.d.ts +0 -111
@@ -0,0 +1,209 @@
1
+ import * as THREE from "three";
2
+ import type { ComponentRegistry } from "./id-picking.js";
3
+ /**
4
+ * Shader-based component highlight (compact graph).
5
+ *
6
+ * Design:
7
+ * - Per-component highlight state lives in ONE `R8UI` data texture indexed by
8
+ * `componentId` (the attribute already on the compact geometry). The texture is
9
+ * shared by every compact visual material via `onBeforeCompile`, so a state
10
+ * write is reflected by all materials with no recompile.
11
+ * - State is BIT FLAGS ({@link HighlightFlag}); SELECTED wins over HOVER.
12
+ * - `selectSolid` sets the flag for every registry component sharing a `solidPath`.
13
+ *
14
+ * Driven by the live event loop via `IdPicker.pickAt → registry → controller`.
15
+ */
16
+ /** Highlight color for a selected component (was `ObjectGroup.HIGHLIGHT_COLOR_SELECTED`). */
17
+ export declare const HIGHLIGHT_COLOR_SELECTED = 5480675;
18
+ /** Highlight color for a hovered, not-selected component (was `HIGHLIGHT_COLOR_HOVER`). */
19
+ export declare const HIGHLIGHT_COLOR_HOVER = 9026019;
20
+ /**
21
+ * Per-topo FOCUS BASE sizes (was `ObjectGroup.vertexFocusSize` / `edgeFocusWidth`).
22
+ * These are injected PER-MATERIAL by the `patch*Material` methods (a `#define` /
23
+ * material-local uniform), NOT via the shared {@link HighlightUniforms} — edges
24
+ * (5) and vertices (8) need different values, and the hover-vs-selected `−2` delta
25
+ * (`objectgroup.ts:285-298 widen()`) is resolved in-shader from the {@link
26
+ * HighlightFlag} bits: `HOVER → base`, `SELECTED && !HOVER → base − 2`, else the
27
+ * material's authored size.
28
+ */
29
+ export declare const VERTEX_FOCUS_SIZE = 6;
30
+ export declare const EDGE_FOCUS_WIDTH = 5;
31
+ /**
32
+ * Per-component highlight state, stored as bit flags in one texel of the state
33
+ * texture. SELECTED takes precedence over HOVER when both are set, so hovering an
34
+ * already-selected component keeps the selected color (matches the old
35
+ * `_getHighlightColor` / `unhighlight(true)`).
36
+ */
37
+ export declare const HighlightFlag: {
38
+ readonly NONE: 0;
39
+ readonly SELECTED: number;
40
+ readonly HOVER: number;
41
+ };
42
+ export type HighlightFlagValue = (typeof HighlightFlag)[keyof typeof HighlightFlag];
43
+ /**
44
+ * Width of the highlight-state data texture in texels. Height grows with the
45
+ * component count: `height = ceil((maxId + 1) / WIDTH)`. A component's texel is at
46
+ * `(id % WIDTH, floor(id / WIDTH))` — the same mapping the shader recomputes.
47
+ */
48
+ export declare const HIGHLIGHT_STATE_TEXTURE_WIDTH = 2048;
49
+ /** Uniform: the shared `usampler2D` highlight-state texture (R8UI). */
50
+ export declare const U_HIGHLIGHT_STATE = "uHighlightState";
51
+ /** Uniform: texture width, for `id -> ivec2` texel coordinates. */
52
+ export declare const U_HIGHLIGHT_TEX_WIDTH = "uHighlightTexWidth";
53
+ /** Uniform: selected color (linear RGB vec3). */
54
+ export declare const U_HIGHLIGHT_SELECTED_COLOR = "uHighlightSelectedColor";
55
+ /** Uniform: hover color (linear RGB vec3). */
56
+ export declare const U_HIGHLIGHT_HOVER_COLOR = "uHighlightHoverColor";
57
+ /**
58
+ * The shared `THREE.IUniform` set bound into every patched compact material. One
59
+ * instance per {@link HighlightController}; the same object is referenced by all
60
+ * materials so a single write updates them together. Property names MUST match the
61
+ * `U_HIGHLIGHT_*` GLSL identifier constants above.
62
+ *
63
+ * Intentionally TOPO-AGNOSTIC: focus sizes are per-topo and the original (authored)
64
+ * size is per-material, so those are injected by `patch*Material` per material —
65
+ * NOT here. See {@link VERTEX_FOCUS_SIZE} / {@link EDGE_FOCUS_WIDTH}.
66
+ */
67
+ export interface HighlightUniforms {
68
+ uHighlightState: {
69
+ value: THREE.DataTexture | null;
70
+ };
71
+ uHighlightTexWidth: {
72
+ value: number;
73
+ };
74
+ uHighlightSelectedColor: {
75
+ value: THREE.Color;
76
+ };
77
+ uHighlightHoverColor: {
78
+ value: THREE.Color;
79
+ };
80
+ }
81
+ /**
82
+ * Minimal material surface the patch methods need. Declared structurally because
83
+ * three's examples-jsm `LineMaterial` has a `vertexColors: string | boolean` field
84
+ * that is not assignable to the nominal `THREE.Material` type.
85
+ */
86
+ type PatchableMaterial = Pick<THREE.Material, "onBeforeCompile" | "customProgramCacheKey" | "needsUpdate" | "userData">;
87
+ /**
88
+ * Owns the per-component highlight state texture and patches compact visual
89
+ * materials to read it. Created by the compact `NestedGroup` alongside its
90
+ * `ComponentRegistry`.
91
+ *
92
+ * Lifecycle: construct → `patch*Material` on each compact face/edge/vertex visual
93
+ * material as it is built → `resize(registry.maxId)` once all components are
94
+ * registered → `setHover` / `setSelected` / `selectSolid` / `clear` to drive
95
+ * highlight → `dispose`.
96
+ */
97
+ export declare class HighlightController {
98
+ /** The registry whose ids index the state texture. */
99
+ readonly registry: ComponentRegistry;
100
+ /** Shared uniforms bound into every patched material. */
101
+ readonly uniforms: HighlightUniforms;
102
+ /** Backing R8UI state texture (one byte = {@link HighlightFlag} bits per id). */
103
+ private texture;
104
+ /** CPU-side mirror of the texture data (length = `capacity`). */
105
+ private data;
106
+ /** Number of texels currently allocated (= width * height ≥ maxId + 1). */
107
+ private capacity;
108
+ /** Ids currently carrying the HOVER flag (one component, or a whole solid). */
109
+ private hoverIds;
110
+ /** Cheap identity of the current hover target so a repeat is a no-op. */
111
+ private hoverKey;
112
+ /**
113
+ * @param registry - the compact group's component registry; sizes the texture
114
+ * and resolves `solidPath` for {@link selectSolid}.
115
+ */
116
+ constructor(registry: ComponentRegistry);
117
+ /**
118
+ * Allocate an R8UI data texture (+ CPU mirror) holding at least `texelCount`
119
+ * texels. `NearestFilter`, no mips — the shader reads exact integer flags via
120
+ * `texelFetch`. This is the FROZEN texture format.
121
+ */
122
+ private _allocate;
123
+ /** The shared state texture (re-created by {@link resize}). */
124
+ get stateTexture(): THREE.DataTexture;
125
+ /**
126
+ * Set or clear `bit` on a component's texel and flag the texture for re-upload
127
+ * on change. The linear data index equals the id (row-major, width-W texels), so
128
+ * texel `(id % W, floor(id / W))` is `data[id]`. Ignores background (0) and ids
129
+ * past the current capacity (caller must {@link resize} first).
130
+ */
131
+ private _setBit;
132
+ /**
133
+ * Move the HOVER flag onto exactly the given `ids` (clearing it from the
134
+ * previously hovered set). `key` is a cheap identity so a repeat target is a
135
+ * no-op — without it, re-hovering the same target every mouse-move would toggle
136
+ * the bits off→on and re-upload the texture each frame. Does not touch SELECTED.
137
+ */
138
+ private _applyHover;
139
+ /**
140
+ * Move the HOVER flag onto a single component `id` (clearing the previous hover),
141
+ * or clear hover entirely when `id` is `null`/background.
142
+ */
143
+ setHover(id: number | null): void;
144
+ /**
145
+ * HOVER a whole solid (its FACES only — see {@link selectSolid}), or clear hover
146
+ * when `null`. Mirrors {@link selectSolid} for the transient hover state.
147
+ */
148
+ setHoverSolid(solidPath: string | null): void;
149
+ /** Set or clear the SELECTED flag for a single component id. */
150
+ setSelected(id: number, flag: boolean): void;
151
+ /** Whether a component currently carries the SELECTED flag. */
152
+ isSelected(id: number): boolean;
153
+ /**
154
+ * Whether a solid is selected — every one of its faces carries SELECTED (false if it
155
+ * has no faces). Used for solid toggle, so a solid that is only partially selected
156
+ * (e.g. one face previously single-selected) is treated as not-selected and a click
157
+ * selects the whole solid rather than clearing it.
158
+ */
159
+ isSolidSelected(solidPath: string): boolean;
160
+ /**
161
+ * Set or clear SELECTED for a whole solid. Flags only the solid's **faces**
162
+ * (`topo === "face"`) — the body tints while edges keep their colour and corners
163
+ * stay hidden. Iterates {@link ComponentRegistry.entries}.
164
+ */
165
+ selectSolid(solidPath: string, flag: boolean): void;
166
+ /** Clear all highlight state (hover + selection) for every component. */
167
+ clear(): void;
168
+ /**
169
+ * Grow the state texture to hold at least `maxId + 1` texels, preserving existing
170
+ * state, and re-bind the new texture object into {@link uniforms}. No-op when the
171
+ * current capacity already suffices.
172
+ */
173
+ resize(maxId: number): void;
174
+ /**
175
+ * Shared `onBeforeCompile` installer: binds the shared uniforms, prepends the
176
+ * common vertex/fragment headers, forwards the component id, then runs the
177
+ * topo-specific `customize` (color override / widening). Idempotent per material.
178
+ */
179
+ private _install;
180
+ /**
181
+ * Install `onBeforeCompile` on a face (`MeshStandardMaterial`) visual material.
182
+ * Overwrites `diffuseColor.rgb` with the highlight color BEFORE lighting (after
183
+ * the base color/map is applied) so a highlighted face is lit exactly as the old
184
+ * `material.color` swap was — selected wins over hover.
185
+ */
186
+ patchFaceMaterial(material: PatchableMaterial): void;
187
+ /**
188
+ * Install `onBeforeCompile` on an edge (`LineMaterial`) visual material (Option A).
189
+ * Widens the screen-space half-width for flagged segments by patching the stock
190
+ * LineMaterial expansion (`offset *= linewidth;`, screen-space branch) and recolors
191
+ * via the shared fragment override. `vertexColors` axes/trihedron carry no
192
+ * registry id (state 0) so they stay inert.
193
+ */
194
+ patchEdgeMaterial(material: PatchableMaterial): void;
195
+ /**
196
+ * Install `onBeforeCompile` on a vertex (`PointsMaterial`) visual material.
197
+ * Flagged points widen to the focus size and recolor. When `cullUnhighlighted`
198
+ * (the solid highlight-Points cloud, invisible until selected), non-flagged points
199
+ * are culled — `gl_PointSize = 0` AND a fragment `discard` (the discard is the
200
+ * real guard; size-0 rasterization is driver-defined). Standalone visible vertices
201
+ * (default) keep their authored `size` and color when unflagged.
202
+ */
203
+ patchVertexMaterial(material: PatchableMaterial, options?: {
204
+ cullUnhighlighted?: boolean;
205
+ }): void;
206
+ /** Dispose the state texture and release tracking. */
207
+ dispose(): void;
208
+ }
209
+ export {};
@@ -0,0 +1,464 @@
1
+ import * as THREE from "three";
2
+ import type { Camera } from "../camera/camera.js";
3
+ /**
4
+ * Topology type of a pickable component. Mirrors the values of `TopoFilter`
5
+ * below (minus the `none` sentinel).
6
+ */
7
+ export type TopoType = "face" | "edge" | "vertex" | "solid";
8
+ /**
9
+ * Filter types for topology-based picking. `none` is the "no filter" sentinel
10
+ * (all topos eligible).
11
+ */
12
+ export declare const TopoFilter: {
13
+ none: null;
14
+ vertex: "vertex";
15
+ edge: "edge";
16
+ face: "face";
17
+ solid: "solid";
18
+ };
19
+ export type TopoFilterType = (typeof TopoFilter)[keyof typeof TopoFilter];
20
+ /** Reserved component id meaning "nothing under the cursor". */
21
+ export declare const BACKGROUND_ID = 0;
22
+ /**
23
+ * Name of the per-vertex integer buffer attribute carrying the component id on
24
+ * every pickable geometry (faces, edges and vertices). The geometry attaches a
25
+ * `Uint32BufferAttribute` under this name; the pick pass reads it as an **integer**
26
+ * attribute (`gpuType = THREE.IntType`, GLSL3 `in uint`) so ids stay exact past
27
+ * 2^24. The geometry side must set `gpuType = THREE.IntType` on the attribute for
28
+ * the pick shader to read it.
29
+ */
30
+ export declare const COMPONENT_ID_ATTRIBUTE = "componentId";
31
+ /**
32
+ * three.js camera/object layers reserved for the pick passes.
33
+ *
34
+ * A pickable object lives on visual layer 0 (so the main camera draws it) **and**
35
+ * its topo's pick layer. The pick pass sets the (reused) main camera to a single
36
+ * pick layer so `overrideMaterial` only touches one topo at a time — a mesh shader
37
+ * can't render lines/points. `obj_vertices` points live on `VERTEX` *only* (off the
38
+ * visual pass).
39
+ */
40
+ export declare const PICK_LAYER: {
41
+ readonly FACE: 1;
42
+ readonly EDGE: 2;
43
+ readonly VERTEX: 3;
44
+ };
45
+ export type PickLayer = (typeof PICK_LAYER)[keyof typeof PICK_LAYER];
46
+ /**
47
+ * Attach the per-vertex `componentId` integer attribute to a pickable geometry: the
48
+ * canonical way to bind {@link COMPONENT_ID_ATTRIBUTE} so the GLSL3 pick shader reads
49
+ * it as `in uint` (`gpuType = IntType` — without it three uploads floats and the bits
50
+ * are wrong). Use `instanced` for fat-line (per-segment) edge geometry.
51
+ */
52
+ export declare function applyComponentIds(geometry: THREE.BufferGeometry, componentId: Uint32Array, instanced?: boolean): void;
53
+ /**
54
+ * Add `object` to its topo's pick layer additively — it stays on visual layer 0 (the
55
+ * main render pass) AND becomes pickable. For face/edge geometry and visible vertices.
56
+ */
57
+ export declare function enablePickLayer(object: THREE.Object3D, topo: "face" | "edge" | "vertex"): void;
58
+ /**
59
+ * Put `object` on its topo's pick layer ONLY (off visual layer 0), so the main camera
60
+ * never draws it — for the pick-only `obj_vertices` Points cloud.
61
+ */
62
+ export declare function setPickLayerExclusive(object: THREE.Object3D, topo: "face" | "edge" | "vertex"): void;
63
+ /**
64
+ * Metadata for one pickable component (face, edge, vertex, or solid). The id-buffer
65
+ * pass reads back an id, and this is what it resolves to.
66
+ */
67
+ export interface ComponentInfo {
68
+ /** Unique id within the current model; encoded into the pick buffer. Never 0. */
69
+ id: number;
70
+ /** Full path, e.g. "/Assembly/Part1/faces/faces_3". */
71
+ path: string;
72
+ /** Leaf name, e.g. "faces_3". */
73
+ name: string;
74
+ /** Topology type. */
75
+ topo: TopoType;
76
+ /** Subtype of the owning shape (e.g. "solid"), or null. */
77
+ subtype: string | null;
78
+ /**
79
+ * Path of the owning solid when this component is a face/edge/vertex *inside* a
80
+ * solid — pick-only, no tree node. `null` for standalone faces/edges/vertices
81
+ * that are tree leaves in their own right.
82
+ */
83
+ solidPath: string | null;
84
+ }
85
+ /**
86
+ * Map a dropdown topo filter to the picker's `TopoType[]`. `none`/`[null]` (the
87
+ * "no filter" sentinel) → `undefined` (all topos eligible).
88
+ */
89
+ export declare function pickerTopoFilter(filter: TopoFilterType[]): TopoType[] | undefined;
90
+ /**
91
+ * Tree-leaf path that owns a picked component: the owning solid's path for a
92
+ * sub-component, else the component's own path with the topo suffix stripped. This
93
+ * is the leaf the double-click pick and the visibility gate operate on.
94
+ */
95
+ export declare function leafPath(info: ComponentInfo): string;
96
+ /**
97
+ * Signature of the live clip state, to detect actual clip changes for the picker
98
+ * (used by the render loop's dirty cadence).
99
+ */
100
+ export declare function clipSignature(planes: THREE.Plane[] | null, intersection: boolean): string;
101
+ /**
102
+ * Flat `id -> ComponentInfo` registry. Replaces the `NestedGroup.groups[id]`
103
+ * explosion for pick lookups. Ids are allocated monotonically starting at 1;
104
+ * id 0 ({@link BACKGROUND_ID}) means "nothing".
105
+ */
106
+ export declare class ComponentRegistry {
107
+ private byId;
108
+ private nextId;
109
+ constructor();
110
+ /**
111
+ * Register a component and return its freshly allocated id. The returned id is
112
+ * also written onto the stored record's `id` field.
113
+ */
114
+ register(info: Omit<ComponentInfo, "id">): number;
115
+ /** Look up a component by id; `undefined` for unknown ids or background (0). */
116
+ get(id: number): ComponentInfo | undefined;
117
+ /**
118
+ * Iterate every registered component in registration (id-ascending) order.
119
+ * Used by the highlight controller to resolve solid selection (all components
120
+ * sharing a `solidPath`) without a scene-graph walk.
121
+ */
122
+ entries(): IterableIterator<ComponentInfo>;
123
+ /**
124
+ * Largest allocated id so far (0 when empty). Ids are allocated contiguously from
125
+ * 1 and NEVER recycled ({@link removeByPathPrefix} deletes records but keeps
126
+ * `nextId`), so this is the high-water mark used as the upper bound for sizing the
127
+ * highlight-state texture; it is `>=` {@link size} (equal until the first removal).
128
+ */
129
+ get maxId(): number;
130
+ /** Number of registered components. */
131
+ get size(): number;
132
+ /**
133
+ * Drop every component whose path is `prefix` or lies under it (`prefix + "/"`),
134
+ * for {@link Viewer.removePart}. Does NOT recycle ids — surviving components keep
135
+ * their ids (and thus their highlight-state texel), and {@link maxId} stays put.
136
+ */
137
+ removeByPathPrefix(prefix: string): void;
138
+ /** Drop all entries and reset id allocation. */
139
+ clear(): void;
140
+ }
141
+ /**
142
+ * Pack a component id into an RGBA8 byte tuple (full 32-bit range, low byte in R).
143
+ * The pick fragment shader must write the same layout. Inverse of {@link unpackId}.
144
+ */
145
+ export declare function packId(id: number): [number, number, number, number];
146
+ /** Decode an RGBA8 tuple (as written by {@link packId}) back into a component id. */
147
+ export declare function unpackId(r: number, g: number, b: number, a: number): number;
148
+ /**
149
+ * Build the per-vertex face component-id attribute for a solid/standalone-face
150
+ * node's EXISTING indexed tessellation, registering one face component per face
151
+ * under `{path}/faces/faces_{i}` in array (backend-enumeration) order.
152
+ *
153
+ * No de-indexing: vertices are pooled per face, so each vertex maps to exactly one
154
+ * face. `collisions` counts vertices referenced by more than one face — it must be
155
+ * 0 (a non-zero value means cross-face sharing, i.e. de-indexing would be required).
156
+ *
157
+ * @param vertexCount - number of vertices in the position pool (`positions.length / 3`)
158
+ * @param triangles - flat index array (with `trianglesPerFace`) OR nested
159
+ * `number[][]` (one face's flat index list per entry)
160
+ * @param trianglesPerFace - per-face triangle counts when `triangles` is flat;
161
+ * `undefined` for the nested format
162
+ */
163
+ export declare function buildFaceComponentIds(vertexCount: number, triangles: Uint32Array | number[] | number[][], trianglesPerFace: Uint32Array | number[] | undefined, path: string, subtype: string | null, registry: ComponentRegistry): {
164
+ componentId: Uint32Array;
165
+ collisions: number;
166
+ };
167
+ /**
168
+ * Build the per-segment (instanced) edge component-id attribute, registering one
169
+ * edge component per edge under `{path}/edges/edges_{i}` in array order. Produces
170
+ * one id per line segment (one `LineSegments2` instance).
171
+ *
172
+ * @param edges - flat segment endpoints (with `segmentsPerEdge`) OR nested
173
+ * `number[][]` (one edge's flat points per entry, 6 floats per segment)
174
+ * @param segmentsPerEdge - per-edge segment counts when `edges` is flat;
175
+ * `undefined` for the nested format
176
+ */
177
+ export declare function buildEdgeComponentIds(edges: Float32Array | number[] | number[][], segmentsPerEdge: Uint32Array | number[] | undefined, path: string, subtype: string | null, registry: ComponentRegistry): {
178
+ componentId: Uint32Array;
179
+ };
180
+ /**
181
+ * Build the per-point vertex component-id attribute for a node's `obj_vertices`
182
+ * (the real B-rep corners — the *selectable* vertices, NOT the triangle-mesh
183
+ * vertices), registering one vertex component per point under
184
+ * `{path}/vertices/vertices_{i}` in array (backend `get_vertices`) order.
185
+ *
186
+ * @param objVertices - flat xyz triples (3 floats per vertex)
187
+ */
188
+ export declare function buildVertexComponentIds(objVertices: Float32Array | number[], path: string, subtype: string | null, registry: ComponentRegistry): {
189
+ componentId: Uint32Array;
190
+ };
191
+ /**
192
+ * Result of a successful pick: the component id read from the id buffer, its
193
+ * registry metadata, and the world-space hit point.
194
+ */
195
+ export interface PickResult {
196
+ /** Picked component id (never {@link BACKGROUND_ID}). */
197
+ id: number;
198
+ /** Registry metadata for the picked component. */
199
+ info: ComponentInfo;
200
+ /**
201
+ * World-space hit point, sourced from the position-MRT attachment. `null` when
202
+ * the RGBA32F position attachment is unsupported (id-only pick).
203
+ */
204
+ point: THREE.Vector3 | null;
205
+ }
206
+ /** Options for {@link IdPicker.pickAt}. */
207
+ export interface PickAtOptions {
208
+ /**
209
+ * Restrict which topo types may be resolved (mirrors the existing
210
+ * `topoFilter`). `undefined`/empty = all (priority vertex>edge>face applies).
211
+ * `"solid"` makes faces eligible (caller maps the face to its `solidPath`).
212
+ */
213
+ topoFilter?: TopoType[];
214
+ /**
215
+ * Odd N for the N×N readback window (touch radius for thin edges / tiny
216
+ * vertices). Defaults to {@link IdPicker}'s window size (5).
217
+ */
218
+ windowSize?: number;
219
+ }
220
+ /** Options for the per-topo pick materials. */
221
+ export interface PickMaterialOptions {
222
+ /** Live clipping planes the pick pass must honor (so clipped geometry is not picked). */
223
+ clippingPlanes?: THREE.Plane[] | null;
224
+ /** Intersection clipping (mirror the visual material's `clipIntersection`). */
225
+ clipIntersection?: boolean;
226
+ /**
227
+ * Forward depth bias in **view-space world units** (a positive nudge toward the
228
+ * camera) so a corner's higher-priority topo wins the shared-depth pick test over
229
+ * its coincident lower-priority topo. Applied to `mvPosition.z` BEFORE projection —
230
+ * i.e. a constant world distance, independent of the camera's near/far spread.
231
+ * (An earlier build biased a constant in NDC z, which under an orthographic camera
232
+ * on a large scene became a multi-mm world window that let geometry behind an
233
+ * occluding face get picked "through" it.) Set per-scene from the bounding radius.
234
+ */
235
+ depthBias?: number;
236
+ }
237
+ /**
238
+ * View-space depth-bias factors (× scene bounding radius) for the edge and vertex
239
+ * pick passes. Face = 0. Kept monotonic with the vertex > edge > face hover priority
240
+ * so a corner's vertex wins over its edges and its edges over their faces. The factor
241
+ * is a fraction of the bounding radius: large enough to clear 24-bit depth-buffer
242
+ * quantization (≈ (far−near)/2^24 with far/near ∝ radius), small enough that the
243
+ * world window stays sub-mm on typical models so geometry a real distance behind an
244
+ * occluder is NOT picked through it.
245
+ */
246
+ export declare const EDGE_DEPTH_BIAS_FACTOR = 0.0001;
247
+ export declare const VERTEX_DEPTH_BIAS_FACTOR = 0.0002;
248
+ /**
249
+ * Pick size (framebuffer px in the half-res target) for vertex pick points. Kept
250
+ * SMALL: the N×N readback window provides the "touch radius" by reading neighbours,
251
+ * so fat rendering is unnecessary and would only overwrite neighbouring face pixels
252
+ * in the shared id buffer (hurting faces-only picks near corners). A few px keeps
253
+ * the corner reliably present without eating faces.
254
+ */
255
+ export declare const PICK_POINT_SIZE = 3;
256
+ /**
257
+ * Build the **face** pick override-material: a GLSL3 `ShaderMaterial` that reads the
258
+ * integer {@link COMPONENT_ID_ATTRIBUTE} and writes `packId(componentId)` +
259
+ * world position to the MRT pick target. Honors clipping (planes + intersection).
260
+ */
261
+ export declare function createFacePickMaterial(options?: PickMaterialOptions): THREE.ShaderMaterial;
262
+ /**
263
+ * Build the **vertex** pick override-material for an `obj_vertices` `THREE.Points`
264
+ * cloud: a GLSL3 `ShaderMaterial` that draws each point **fat** (`gl_PointSize` =
265
+ * {@link PICK_POINT_SIZE}, a touch radius independent of the visual point size),
266
+ * reads the integer `componentId`, and writes id + world position to the MRT. A
267
+ * tiny forward depth bias lets a corner vertex win the depth test over its
268
+ * coincident face (so it can be picked), while occluded vertices stay hidden.
269
+ * Honors clipping.
270
+ */
271
+ export declare function createVertexPickMaterial(options?: PickMaterialOptions): THREE.ShaderMaterial;
272
+ /**
273
+ * Pick width (px in the half-res target) for edge pick lines. Kept THIN: edges run
274
+ * along face boundaries, so a fat band would overwrite face pixels on both sides and
275
+ * wreck faces-only picks. The N×N readback window supplies the touch radius instead.
276
+ */
277
+ export declare const PICK_EDGE_WIDTH = 2;
278
+ /**
279
+ * Build the **edge** pick override-material for a `LineSegments2` /
280
+ * `LineSegmentsGeometry` fat line: a GLSL3 `ShaderMaterial` that ports three
281
+ * `LineMaterial`'s screen-space instanced fat-line expansion (instanceStart/End +
282
+ * the `position` quad template, `linewidth`/`resolution` uniforms), reads the
283
+ * **instanced** integer `componentId`, and writes id + world position to the MRT.
284
+ * Drawn fat ({@link PICK_EDGE_WIDTH}) for a touch radius independent of the visual
285
+ * line width. `resolution` MUST be set to the pick target size by the caller.
286
+ * Honors clipping. (World-units + dash + color paths are dropped — pick only.)
287
+ */
288
+ export declare function createEdgePickMaterial(options?: PickMaterialOptions): THREE.ShaderMaterial;
289
+ /** MRT attachment index of the packed-id texture. */
290
+ export declare const PICK_ID_ATTACHMENT = 0;
291
+ /** MRT attachment index of the world-position texture. */
292
+ export declare const PICK_POS_ATTACHMENT = 1;
293
+ /**
294
+ * Create the MRT pick render target: a single `WebGLRenderTarget` with two color
295
+ * attachments —
296
+ * - {@link PICK_ID_ATTACHMENT} (0): RGBA8/UnsignedByte packed `componentId`
297
+ * (`packId`, low byte in R), and
298
+ * - {@link PICK_POS_ATTACHMENT} (1): RGBA32F world-space hit position,
299
+ * `xyz` = point, `w = 1` where a fragment was written (0 = background).
300
+ *
301
+ * `NearestFilter` on both — interpolated ids/positions are meaningless. A depth
302
+ * buffer keeps the nearest fragment per pixel; no depth *texture* is read.
303
+ * `NoColorSpace` on the id texture stops any sRGB transform corrupting the bytes.
304
+ *
305
+ * Plain factory: the caller ({@link IdPicker.pickAt}) owns sizing/resize/disposal.
306
+ * `width`/`height` are already the pick-buffer pixel size (caller applies half-DPR).
307
+ *
308
+ * @param withPosition - allocate the RGBA32F world-position attachment. Requires
309
+ * `EXT_color_buffer_float` (probed by {@link IdPicker}); when `false` the target
310
+ * carries the id attachment only and `pickAt` returns `point = null` (the fat
311
+ * `out` at location 1 in the pick shaders is simply dropped — writing to an
312
+ * unbound draw buffer is well-defined and discarded in WebGL2).
313
+ */
314
+ export declare function createPickTargets(width: number, height: number, withPosition?: boolean): THREE.WebGLRenderTarget;
315
+ /**
316
+ * GPU id-based picker: cursor → component id → registry → backend path, plus the
317
+ * world-space hit point.
318
+ *
319
+ * Lifecycle the host (viewer) drives:
320
+ * - {@link attach} once the scene + camera exist (re-attach after `clear()`),
321
+ * - {@link setSize} on canvas resize, {@link setClippingPlanes} on clip change,
322
+ * - {@link setDirty} whenever the view/geometry changes (camera move, geometry
323
+ * rebuild) so the pick buffer is re-rendered lazily on the next {@link pickAt},
324
+ * - {@link pickAt} on hover/click to resolve the component under the cursor.
325
+ *
326
+ * Per-topo passes (FACE, then fat EDGE lines, then fat VERTEX points) accumulate
327
+ * into one MRT target; `pickAt` reads an N×N window and resolves **vertex > edge >
328
+ * face** priority via the registry's topo, honoring `topoFilter`, then reads the
329
+ * position attachment → `point`.
330
+ */
331
+ export declare class IdPicker {
332
+ readonly registry: ComponentRegistry;
333
+ /**
334
+ * Default N for the N×N readback window — the pick "touch radius" applied to the
335
+ * (thin) geometry. 3 is odd, so the window is symmetric about the cursor pixel
336
+ * (half = 1 → cursor ±1 on each axis); an even size is asymmetric. Tuned
337
+ * interactively (pick-explore.html): 2 and 3 feel identical, much larger (e.g. 7)
338
+ * pulls edge/vertex priority too far off the actual line/point; 1 is too unforgiving.
339
+ */
340
+ windowSize: number;
341
+ private renderer;
342
+ /** Render source — the scene + camera the pick pass renders. */
343
+ private scene;
344
+ private camera;
345
+ /** Live clipping planes mirrored onto the pick materials (`null` = clipping off). */
346
+ private clippingPlanes;
347
+ /** Canvas size in CSS px (the pick target is sized at half-DPR internally). */
348
+ private width;
349
+ private height;
350
+ /** Intersection clipping mode mirrored onto the pick materials. */
351
+ private clipIntersection;
352
+ /**
353
+ * Scene bounding radius, used to scale the edge/vertex view-space depth bias to a
354
+ * small constant world distance (see {@link setSceneRadius}). 0 until the host sets
355
+ * it — with 0, coincident corner topo would z-fight and be unpickable, so the host
356
+ * MUST call {@link setSceneRadius} once the model bounds are known.
357
+ */
358
+ private sceneRadius;
359
+ /** Whether the pick buffer must be re-rendered before the next read. */
360
+ private dirty;
361
+ /** MRT pick target (id + world-position attachments). Allocated lazily in `pickAt`. */
362
+ private pickTarget;
363
+ /** Per-topo pick override-materials. Allocated lazily in `pickAt`. */
364
+ private faceMaterial;
365
+ private edgeMaterial;
366
+ private vertexMaterial;
367
+ /** Clip-plane count the pick materials were last compiled for (program key). */
368
+ private compiledPlaneCount;
369
+ /** Intersection mode the pick materials were last compiled for (program key). */
370
+ private compiledIntersection;
371
+ /** Scratch to save/restore the renderer clear color across a pick pass. */
372
+ private readonly savedClearColor;
373
+ /** Scratch to save/restore the renderer viewport across a pick pass. */
374
+ private readonly savedViewport;
375
+ /** Scratch readback buffer for the position attachment (one RGBA32F pixel). */
376
+ private readonly posPixel;
377
+ /** Scratch readback buffer for the N×N id window (grown as needed). */
378
+ private idWindow;
379
+ /**
380
+ * Whether the GPU can render to a float color attachment (`EXT_color_buffer_float`,
381
+ * probed at construction). When `false`, the pick target carries the id attachment
382
+ * only and {@link pickAt} returns `point = null` — id picking still works, just
383
+ * without the world-space hit point (degrades hover coords / double-click pivot to
384
+ * the bbox-center fallback). Universal in real WebGL2; the probe guards exotic
385
+ * contexts.
386
+ */
387
+ readonly positionSupported: boolean;
388
+ constructor(renderer: THREE.WebGLRenderer, registry: ComponentRegistry);
389
+ /**
390
+ * Probe `EXT_color_buffer_float` — the capability needed to *render into* the
391
+ * RGBA32F world-position attachment. Getting the extension is the canonical
392
+ * feature test (no side effects) and is WebGL2-only: it does not exist on WebGL1
393
+ * (whose float-color extension is `WEBGL_color_buffer_float`), so a non-null result
394
+ * already implies a WebGL2 context. Near-universal on WebGL2 but spec-optional.
395
+ */
396
+ private static _probeFloatColor;
397
+ /**
398
+ * Point the picker at the scene + camera it renders for picking. Call once
399
+ * the scene exists and again after the scene/camera are recreated (`clear()`).
400
+ */
401
+ attach(scene: THREE.Scene, camera: Camera): void;
402
+ /**
403
+ * Mark the pick buffer stale so the next {@link pickAt} re-renders it. Call on any
404
+ * view change (camera move/zoom, geometry rebuild) — NOT on mouse-move.
405
+ */
406
+ setDirty(): void;
407
+ /**
408
+ * Set the scene bounding radius so the edge/vertex pick passes bias depth by a
409
+ * small constant WORLD distance (radius × {@link EDGE_DEPTH_BIAS_FACTOR} /
410
+ * {@link VERTEX_DEPTH_BIAS_FACTOR}) rather than a constant in NDC z. The world
411
+ * window is then independent of the camera near/far spread — the fix for geometry
412
+ * behind an occluding face getting picked "through" it under an orthographic camera
413
+ * on a large scene. Call whenever the model bounds change (initial build, add/remove
414
+ * part). Updates existing materials in place and seeds lazily-created ones.
415
+ */
416
+ setSceneRadius(radius: number): void;
417
+ /** View-space edge depth bias (world units) for the current scene radius. */
418
+ private _edgeDepthBias;
419
+ /** View-space vertex depth bias (world units) for the current scene radius. */
420
+ private _vertexDepthBias;
421
+ /** Resize the pick render target to match the canvas (CSS px; half-DPR internally). */
422
+ setSize(width: number, height: number): void;
423
+ /**
424
+ * Update the clipping the pick materials must honor — planes (`null` when the
425
+ * viewer's clipping is off) and intersection mode (mirror the visual material's
426
+ * `clipIntersection`). Recompiles the materials only when the plane *count* or the
427
+ * intersection *mode* changes (three keys the program on `NUM_CLIPPING_PLANES` /
428
+ * `UNION_CLIPPING_PLANES`); a value-only change just re-renders. Always
429
+ * re-renders next {@link pickAt}.
430
+ */
431
+ setClippingPlanes(planes: THREE.Plane[] | null, intersection?: boolean): void;
432
+ /**
433
+ * Resolve the component under the cursor, with its world-space hit point.
434
+ *
435
+ * @param x - canvas-relative x in CSS px (`getBoundingClientRect`)
436
+ * @param y - canvas-relative y in CSS px (top-left origin; WebGL Y-flip is internal)
437
+ * @returns the picked component (`point` = world hit position), or `null` for
438
+ * background / no hit.
439
+ *
440
+ * Re-renders the MRT pick target if {@link dirty} (per-topo passes on the pick
441
+ * layers, honoring clipping), then reads an N×N window around the cursor pixel
442
+ * (canvas px → ×dpr×0.5 → Y-flip) and resolves **vertex > edge > face** priority
443
+ * via the registry's topo (nearest-to-center tie-break), restricted to
444
+ * `options.topoFilter`. The position attachment at the chosen pixel → `point`.
445
+ */
446
+ pickAt(x: number, y: number, options?: PickAtOptions): PickResult | null;
447
+ /** Lazily allocate / resize the MRT target and the per-topo pick materials. */
448
+ private _ensureResources;
449
+ /**
450
+ * Render the per-topo pick passes into the MRT target, reusing the main camera.
451
+ * Clears once, then accumulates **FACE → EDGE → VERTEX** (each drawn fat
452
+ * and later, so higher-priority topos overwrite at coincident pixels). All
453
+ * renderer state touched here is saved and restored.
454
+ */
455
+ private _renderPickBuffer;
456
+ /** One pick pass: render only `layer`'s objects with `material` (no clear).
457
+ * Clipping planes are kept in sync solely by {@link setClippingPlanes} (and the
458
+ * lazy material creation in {@link _ensureResources}); re-assigning them here per
459
+ * pass would mutate `clippingPlanes` without `needsUpdate`, risking a stale
460
+ * `NUM_CLIPPING_PLANES` if the count ever changed off that path. */
461
+ private _pass;
462
+ /** Release GPU resources (pick render target + materials). */
463
+ dispose(): void;
464
+ }