three-cad-viewer 4.3.9 → 5.0.1

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.
@@ -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
+ }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Analyzes equirectangular HDR pixel data to find dominant light sources
5
5
  * (softboxes in studio HDRs, sun in outdoor HDRs). Returns direction,
6
- * intensity, and color for up to 2 lights, used to create shadow-casting
7
- * DirectionalLights in Studio mode.
6
+ * intensity, and color for up to MAX_LIGHTS (currently 1), used to create the
7
+ * shadow-casting DirectionalLight in Studio mode.
8
8
  *
9
9
  * Algorithm: downsample to 128x64 luminance grid → threshold at 10x median
10
10
  * → flood-fill cluster → convert centroids to 3D direction vectors.
@@ -32,7 +32,7 @@ export interface LightDetectionResult {
32
32
  * @param data - Raw pixel data (Uint16Array for HalfFloat, or Float32Array)
33
33
  * @param width - HDR image width in pixels
34
34
  * @param height - HDR image height in pixels
35
- * @returns Detection result with up to 2 lights
35
+ * @returns Detection result with up to MAX_LIGHTS (currently 1) lights
36
36
  */
37
37
  export declare function detectDominantLights(data: Uint16Array | Float32Array, width: number, height: number): LightDetectionResult;
38
38
  /**
@@ -162,7 +162,9 @@ declare class MaterialFactory {
162
162
  *
163
163
  * threejs-materials `properties` uses simplified property names (e.g., "color",
164
164
  * "roughness", "normal") where each entry has an optional `value` (scalar or
165
- * [r,g,b] array in **linear RGB**) and/or `texture` (inline data URI).
165
+ * [r,g,b] color array) and/or `texture` (inline data URI). Color arrays follow
166
+ * a per-key color-space convention: `color` is sRGB, the other color keys are
167
+ * linear (see the COLOR_ARRAY_KEYS handling in the values loop below).
166
168
  *
167
169
  * @param values - Scalar PBR values from threejs-materials
168
170
  * @param textures - Texture map references from threejs-materials
@@ -0,0 +1,55 @@
1
+ import type * as THREE from "three";
2
+ import type { ComponentInfo, TopoType } from "./id-picking.js";
3
+ import type { HighlightController } from "./highlight.js";
4
+ /**
5
+ * A picked component, so the viewer's selection state and the measure/select
6
+ * tools consume ONE interface. {@link IdPicked} wraps a registry `ComponentInfo`
7
+ * + the shader `HighlightController` (the GPU id-pick path) — NO dependency on the
8
+ * exploded scene graph.
9
+ */
10
+ export interface PickedComponent {
11
+ /** Canonical "/"-path sent to the backend (component path, or solid path when
12
+ * {@link fromSolid}). Replaces measure's `getId` + tree-sync name munging. */
13
+ readonly backendId: string;
14
+ /** Leaf name, e.g. "faces_3" — source for select.ts's numeric sub-index. */
15
+ readonly name: string;
16
+ /** Topology type of the resolved component. */
17
+ readonly topo: TopoType;
18
+ /** Solid-selection mode (the topo filter is "solid"). */
19
+ readonly fromSolid: boolean;
20
+ /** World-space hit point, when the backend provides one (else null). */
21
+ readonly point: THREE.Vector3 | null;
22
+ /** Show the hover highlight (`true`) — selection is via {@link toggleSelection}. */
23
+ highlight(asHover: boolean): void;
24
+ /** Remove the hover highlight; `keepSelected` preserves the selected state. */
25
+ unhighlight(keepSelected: boolean): void;
26
+ /** Toggle the persistent selected highlight. */
27
+ toggleSelection(): void;
28
+ /** Clear all highlight (hover + selected) for this component. */
29
+ clearHighlights(): void;
30
+ /** Identity equality (same component / same solid), for hover + isNewObject dedup. */
31
+ equals(other: PickedComponent | null): boolean;
32
+ }
33
+ /**
34
+ * `PickedComponent` over a GPU id-pick result. Drives the shader
35
+ * `HighlightController` by `componentId` (faces of a solid via `*Solid`), with no
36
+ * per-component `ObjectGroup` and no exploded graph.
37
+ */
38
+ export declare class IdPicked implements PickedComponent {
39
+ readonly info: ComponentInfo;
40
+ readonly fromSolid: boolean;
41
+ readonly point: THREE.Vector3 | null;
42
+ private readonly highlightCtl;
43
+ constructor(info: ComponentInfo, fromSolid: boolean, point: THREE.Vector3 | null, highlightCtl: HighlightController);
44
+ /** Whether to act on the whole solid (faces) rather than the single component. */
45
+ private get asSolid();
46
+ get backendId(): string;
47
+ get name(): string;
48
+ get topo(): TopoType;
49
+ highlight(asHover: boolean): void;
50
+ unhighlight(keepSelected: boolean): void;
51
+ toggleSelection(): void;
52
+ clearHighlights(): void;
53
+ private _setSelected;
54
+ equals(other: PickedComponent | null): boolean;
55
+ }
@@ -4,7 +4,7 @@
4
4
  * Wraps the pmndrs EffectComposer to provide:
5
5
  * - Scene rendering (RenderPass)
6
6
  * - Screen-space ambient occlusion (N8AOPostPass)
7
- * - Screen-space shadow mask (BasicShadowMap + KawaseBlurPass)
7
+ * - Screen-space shadow mask (PCFShadowMap + KawaseBlurPass)
8
8
  * - Tone mapping + sRGB output + antialiasing (ToneMappingEffect + SMAAEffect)
9
9
  *
10
10
  * Tone mapping is handled by the postprocessing ToneMappingEffect, which uses
@@ -18,7 +18,7 @@
18
18
  * transparent, and the EffectPass alpha-blends its output onto a pre-cleared
19
19
  * canvas that already has the correct background color.
20
20
  *
21
- * Shadow mask: BasicShadowMap produces sharp shadow boundaries at 4096×4096.
21
+ * Shadow mask: PCFShadowMap produces sharp shadow boundaries at 4096×4096.
22
22
  * A half-resolution ShadowMaterial override pass captures the mask, which is
23
23
  * then blurred via KawaseBlurPass and composited by ShadowMaskEffect before
24
24
  * tone mapping. The floor keeps its own ShadowMaterial reading the shadow map
@@ -2,6 +2,18 @@ import * as THREE from "three";
2
2
  import type { Vector3Tuple } from "three";
3
3
  import { ObjectGroup } from "./nestedgroup.js";
4
4
  import type { Theme, ClipIndex } from "../core/types";
5
+ /**
6
+ * Cap-culling thresholds (see {@link Clipping.cull}). The per-solid stencil + cap
7
+ * meshes are correct but O(N) in draw calls; on a large assembly (~1300 solids ×
8
+ * 3 planes × 3 meshes ≈ 12000 draws/frame) a zoomed-out clip+rotate overruns the
9
+ * GPU watchdog → context loss. Culling bounds the per-frame work:
10
+ * - `CAP_CULL_MIN_PX`: skip a solid's caps when its projected screen radius is
11
+ * below this (sub-pixel solids contribute nothing visible zoomed out).
12
+ * - `CAP_CULL_BUDGET`: hard cap on the number of capped solids (largest-first);
13
+ * the real crash guard for pathologically dense views. Tune on a real GPU.
14
+ */
15
+ export declare const CAP_CULL_MIN_PX = 3;
16
+ export declare const CAP_CULL_BUDGET = 400;
5
17
  /**
6
18
  * A THREE.Plane that maintains a constant relative to a center point.
7
19
  */
@@ -103,6 +115,17 @@ declare class Clipping extends THREE.Group {
103
115
  objectColorCaps: boolean;
104
116
  planeHelpers: PlaneMeshGroup | null;
105
117
  private _planeMeshGroup;
118
+ /** Per-solid stencil/cap units, the unit of screen-size culling. */
119
+ private _capUnits;
120
+ /**
121
+ * Whether {@link cull} last ran with clipping active. Lets the inactive path
122
+ * gate stencils/caps off exactly once, then early-return on later still frames.
123
+ * Starts `true` so the first inactive call performs the initial gate-off (the
124
+ * meshes default to `visible:true`).
125
+ */
126
+ private _cullActive;
127
+ /** Reused buffer for the budget threshold sort (no per-frame alloc). */
128
+ private _radiiScratch;
106
129
  /**
107
130
  * Create a Clipping instance.
108
131
  * @param center - The center point [x, y, z].
@@ -168,6 +191,36 @@ declare class Clipping extends THREE.Group {
168
191
  * @param flag - True to show, false to hide.
169
192
  */
170
193
  setVisible: (flag: boolean) => void;
194
+ /**
195
+ * Bound the per-frame stencil/cap draw work to keep large assemblies from
196
+ * overrunning the GPU watchdog on clip+rotate (see {@link CAP_CULL_MIN_PX}).
197
+ *
198
+ * Toggles each unit's `Object3D.visible` (NOT material.visible), which composes
199
+ * by AND with the existing material-level toggles — `setShapeVisible` (per-solid
200
+ * hide) and `setVisible` (clip-tab on/off) — so a unit renders only when it is
201
+ * un-culled AND its solid is shown AND the clip tab is active. Render order and
202
+ * the per-solid `clearStencil` isolation are untouched (removing a whole solid's
203
+ * units never affects the remaining solids' cap correctness).
204
+ *
205
+ * @param camera - The active camera (ortho or perspective).
206
+ * @param width - Canvas width in CSS px.
207
+ * @param height - Canvas height in CSS px.
208
+ * @param clipActive - `renderer.localClippingEnabled` (clip tab selected).
209
+ */
210
+ cull(camera: THREE.Camera, width: number, height: number, clipActive: boolean): void;
211
+ /**
212
+ * World-space AABB of a solid (local bounding box transformed by the front
213
+ * mesh's world matrix, which folds in GDS z-scale). Returns a shared scratch
214
+ * Box3 (valid only until the next call) or `null` when geometry is missing.
215
+ */
216
+ private _solidWorldBox;
217
+ /**
218
+ * Projected screen radius (px) of a solid's bounding sphere. Projects the world
219
+ * sphere center and a point one world-radius along the camera-right axis, and
220
+ * measures their screen-space separation — correct for both ortho and
221
+ * perspective. Returns `Infinity` (never cull) when geometry is missing.
222
+ */
223
+ private _solidScreenRadius;
171
224
  /**
172
225
  * Save the current clipping state for later restoration.
173
226
  * Captures plane positions, helper visibility, and stencil plane visibility.