three-cad-viewer 4.1.2 → 4.2.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 (58) hide show
  1. package/Readme.md +12 -5
  2. package/dist/camera/camera.d.ts +14 -2
  3. package/dist/core/studio-manager.d.ts +91 -0
  4. package/dist/core/types.d.ts +260 -9
  5. package/dist/core/viewer-state.d.ts +28 -2
  6. package/dist/core/viewer.d.ts +200 -6
  7. package/dist/index.d.ts +7 -2
  8. package/dist/rendering/environment.d.ts +239 -0
  9. package/dist/rendering/light-detection.d.ts +44 -0
  10. package/dist/rendering/material-factory.d.ts +77 -2
  11. package/dist/rendering/material-presets.d.ts +32 -0
  12. package/dist/rendering/room-environment.d.ts +13 -0
  13. package/dist/rendering/studio-composer.d.ts +130 -0
  14. package/dist/rendering/studio-floor.d.ts +53 -0
  15. package/dist/rendering/texture-cache.d.ts +142 -0
  16. package/dist/rendering/triplanar.d.ts +37 -0
  17. package/dist/scene/animation.d.ts +1 -1
  18. package/dist/scene/clipping.d.ts +31 -0
  19. package/dist/scene/nestedgroup.d.ts +64 -27
  20. package/dist/scene/objectgroup.d.ts +47 -0
  21. package/dist/three-cad-viewer.css +339 -29
  22. package/dist/three-cad-viewer.esm.js +27567 -11874
  23. package/dist/three-cad-viewer.esm.js.map +1 -1
  24. package/dist/three-cad-viewer.esm.min.js +10 -4
  25. package/dist/three-cad-viewer.js +27486 -11787
  26. package/dist/three-cad-viewer.min.js +10 -4
  27. package/dist/ui/display.d.ts +147 -0
  28. package/dist/utils/decode-instances.d.ts +60 -0
  29. package/dist/utils/utils.d.ts +10 -0
  30. package/package.json +4 -2
  31. package/src/_version.ts +1 -1
  32. package/src/camera/camera.ts +27 -10
  33. package/src/core/studio-manager.ts +682 -0
  34. package/src/core/types.ts +328 -9
  35. package/src/core/viewer-state.ts +84 -4
  36. package/src/core/viewer.ts +453 -22
  37. package/src/index.ts +25 -1
  38. package/src/rendering/environment.ts +840 -0
  39. package/src/rendering/light-detection.ts +327 -0
  40. package/src/rendering/material-factory.ts +456 -2
  41. package/src/rendering/material-presets.ts +303 -0
  42. package/src/rendering/raycast.ts +2 -2
  43. package/src/rendering/room-environment.ts +192 -0
  44. package/src/rendering/studio-composer.ts +577 -0
  45. package/src/rendering/studio-floor.ts +108 -0
  46. package/src/rendering/texture-cache.ts +1020 -0
  47. package/src/rendering/triplanar.ts +329 -0
  48. package/src/scene/animation.ts +3 -2
  49. package/src/scene/clipping.ts +59 -0
  50. package/src/scene/nestedgroup.ts +399 -0
  51. package/src/scene/objectgroup.ts +186 -11
  52. package/src/scene/orientation.ts +12 -0
  53. package/src/scene/render-shape.ts +55 -21
  54. package/src/types/n8ao.d.ts +28 -0
  55. package/src/ui/display.ts +1032 -27
  56. package/src/ui/index.html +181 -44
  57. package/src/utils/decode-instances.ts +233 -0
  58. package/src/utils/utils.ts +33 -20
@@ -99,10 +99,20 @@ class ObjectGroup extends THREE.Group {
99
99
  vertexFocusSize: number;
100
100
  edgeFocusWidth: number;
101
101
  shapeGeometry?: THREE.BufferGeometry | null;
102
+ /** Material tag from shapes data, used for Studio mode material lookup */
103
+ materialTag: string;
102
104
  minZ?: number;
103
105
  height?: number;
104
106
  private _zebra: ZebraTool | null;
105
107
 
108
+ // Studio mode state
109
+ private _cadFrontMaterial: ColoredMaterial | null;
110
+ private _cadBackMaterial: ColoredMaterial | null;
111
+ private _cadOriginalColor: THREE.Color | null;
112
+ private _cadOriginalBackColor: THREE.Color | null;
113
+ private _isStudioMode: boolean;
114
+ private _cadEdgesVisible: boolean | null;
115
+
106
116
  /**
107
117
  * Create an ObjectGroup for managing a CAD object's visual representation.
108
118
  * @param opacity - Default opacity value (0.0 to 1.0).
@@ -145,6 +155,15 @@ class ObjectGroup extends THREE.Group {
145
155
  this.edgeFocusWidth = 5; // Size of the edges when highlighted
146
156
 
147
157
  this._zebra = null; // Lazy-initialized zebra tool
158
+ this.materialTag = "";
159
+
160
+ // Studio mode state
161
+ this._cadFrontMaterial = null;
162
+ this._cadBackMaterial = null;
163
+ this._cadOriginalColor = null;
164
+ this._cadOriginalBackColor = null;
165
+ this._isStudioMode = false;
166
+ this._cadEdgesVisible = null;
148
167
  }
149
168
 
150
169
  /**
@@ -157,6 +176,13 @@ class ObjectGroup extends THREE.Group {
157
176
  return this._zebra;
158
177
  }
159
178
 
179
+ /**
180
+ * Whether this ObjectGroup is currently in Studio mode.
181
+ */
182
+ get isStudioMode(): boolean {
183
+ return this._isStudioMode;
184
+ }
185
+
160
186
  /**
161
187
  * Dispose of all resources and clean up memory.
162
188
  * Releases geometry, materials, children, and zebra tool.
@@ -175,6 +201,13 @@ class ObjectGroup extends THREE.Group {
175
201
  this._zebra.dispose();
176
202
  this._zebra = null;
177
203
  }
204
+ // Release studio material references (do NOT dispose -- NestedGroup owns them)
205
+ this._cadFrontMaterial = null;
206
+ this._cadBackMaterial = null;
207
+ this._cadOriginalColor = null;
208
+ this._cadOriginalBackColor = null;
209
+ this._isStudioMode = false;
210
+ this._cadEdgesVisible = null;
178
211
  }
179
212
 
180
213
  /**
@@ -427,7 +460,11 @@ class ObjectGroup extends THREE.Group {
427
460
  setBlackEdges(flag: boolean): void {
428
461
  if (this.edgeMaterial && !this.edgeMaterial.vertexColors) {
429
462
  const color = flag ? 0x000000 : this.edge_color;
430
- this.originalColor = new THREE.Color(color);
463
+ // Only update originalColor for edge-only objects (no face mesh).
464
+ // For face+edge objects, originalColor tracks the face color.
465
+ if (!this.front) {
466
+ this.originalColor = new THREE.Color(color);
467
+ }
431
468
  this.edgeMaterial.color = new THREE.Color(color);
432
469
  this.edgeMaterial.needsUpdate = true;
433
470
  }
@@ -469,7 +506,12 @@ class ObjectGroup extends THREE.Group {
469
506
  */
470
507
  setShapeVisible(flag: boolean): void {
471
508
  if (this.front) {
472
- this.front.material.visible = flag;
509
+ if (this._isStudioMode) {
510
+ // Studio materials are shared — use mesh.visible for per-object visibility
511
+ this.front.visible = flag;
512
+ } else {
513
+ this.front.material.visible = flag;
514
+ }
473
515
  }
474
516
  for (const clippingGroup of this.clipping.values()) {
475
517
  const child0 = clippingGroup.children[0];
@@ -488,7 +530,11 @@ class ObjectGroup extends THREE.Group {
488
530
  }
489
531
  }
490
532
  if (this.back && this.renderback) {
491
- this.back.material.visible = flag;
533
+ if (this._isStudioMode) {
534
+ this.back.visible = flag;
535
+ } else {
536
+ this.back.material.visible = flag;
537
+ }
492
538
  }
493
539
  }
494
540
 
@@ -508,12 +554,17 @@ class ObjectGroup extends THREE.Group {
508
554
  * Set visibility of back faces.
509
555
  */
510
556
  setBackVisible(flag: boolean): void {
511
- if (
512
- this.back &&
513
- this.front &&
514
- this.front.material.visible
515
- ) {
516
- this.back.material.visible = this.renderback || flag;
557
+ if (this.back && this.front) {
558
+ const frontVisible = this._isStudioMode
559
+ ? this.front.visible
560
+ : this.front.material.visible;
561
+ if (frontVisible) {
562
+ if (this._isStudioMode) {
563
+ this.back.visible = this.renderback || flag;
564
+ } else {
565
+ this.back.material.visible = this.renderback || flag;
566
+ }
567
+ }
517
568
  }
518
569
  }
519
570
 
@@ -522,10 +573,13 @@ class ObjectGroup extends THREE.Group {
522
573
  */
523
574
  getVisibility(): boolean {
524
575
  if (this.front) {
576
+ const frontVisible = this._isStudioMode
577
+ ? this.front.visible
578
+ : this.front.material.visible;
525
579
  if (this.edgeMaterial) {
526
- return this.front.material.visible || this.edgeMaterial.visible;
580
+ return frontVisible || this.edgeMaterial.visible;
527
581
  } else {
528
- return this.front.material.visible;
582
+ return frontVisible;
529
583
  }
530
584
  } else if (this.edgeMaterial) {
531
585
  return this.edgeMaterial.visible;
@@ -677,6 +731,127 @@ class ObjectGroup extends THREE.Group {
677
731
  setZebraMappingMode(value: ZebraMappingMode): void {
678
732
  this.zebra.setMappingMode(value);
679
733
  }
734
+
735
+ // ===========================================================================
736
+ // Studio Mode
737
+ // ===========================================================================
738
+
739
+ /**
740
+ * Enter Studio mode: swap CAD materials for pre-built Studio materials.
741
+ *
742
+ * The caller (NestedGroup) is responsible for resolving material tags and
743
+ * building MeshPhysicalMaterial instances via MaterialFactory. ObjectGroup
744
+ * just receives the finished materials and performs the swap.
745
+ *
746
+ * On first call, saves the current CAD material references so they can be
747
+ * restored by `leaveStudioMode()`. Copies the `material.visible` flag from
748
+ * CAD to Studio material to preserve tree-view hide/show state. Updates
749
+ * `originalColor` / `originalBackColor` so highlight/unhighlight works
750
+ * correctly in Studio mode.
751
+ *
752
+ * @param studioFront - Studio material for front face, or null if this object has no front mesh
753
+ * @param studioBack - Studio material for back face, or null if back face should not be swapped
754
+ */
755
+ enterStudioMode(
756
+ studioFront: THREE.MeshPhysicalMaterial | null,
757
+ studioBack: THREE.MeshPhysicalMaterial | null,
758
+ ): void {
759
+ if (this._isStudioMode) return;
760
+
761
+ // --- Save CAD state ---
762
+ if (this.front) {
763
+ this._cadFrontMaterial = this.front.material;
764
+ }
765
+ if (this.back) {
766
+ this._cadBackMaterial = this.back.material;
767
+ }
768
+ // Save original colors used by highlight/unhighlight
769
+ this._cadOriginalColor = this.originalColor ? this.originalColor.clone() : null;
770
+ this._cadOriginalBackColor = this.originalBackColor ? this.originalBackColor.clone() : null;
771
+
772
+ // Save edge visibility state
773
+ this._cadEdgesVisible = this.edgeMaterial ? this.edgeMaterial.visible : null;
774
+
775
+ // --- Swap front material ---
776
+ if (this.front && studioFront) {
777
+ // Transfer per-object visibility to mesh.visible (NOT material.visible)
778
+ // because studio materials are shared across objects via cache.
779
+ // Writing to a shared material's .visible would affect all users.
780
+ this.front.visible = this.front.material.visible;
781
+ this.front.material = studioFront;
782
+ // Update originalColor to studio material's color for correct highlight
783
+ this.originalColor = studioFront.color;
784
+ }
785
+
786
+ // --- Swap back material ---
787
+ if (this.back && studioBack && this.renderback) {
788
+ // Same: per-object visibility via mesh.visible, not shared material
789
+ this.back.visible = this.back.material.visible;
790
+ this.back.material = studioBack;
791
+ // Update originalBackColor for correct highlight on back face
792
+ this.originalBackColor = studioBack.color;
793
+ }
794
+
795
+ this._isStudioMode = true;
796
+ }
797
+
798
+ /**
799
+ * Leave Studio mode: restore CAD materials.
800
+ *
801
+ * Copies `material.visible` from Studio back to CAD material to preserve
802
+ * any visibility changes made while in Studio mode (e.g., tree-view toggle).
803
+ * Restores `originalColor` / `originalBackColor` to CAD material colors.
804
+ * Restores edge visibility to the state saved when entering Studio mode.
805
+ */
806
+ leaveStudioMode(): void {
807
+ if (!this._isStudioMode) return;
808
+
809
+ // --- Restore front material ---
810
+ if (this.front && this._cadFrontMaterial) {
811
+ // Copy visibility from mesh.visible back to CAD material
812
+ // (studio mode uses mesh.visible for per-object visibility)
813
+ this._cadFrontMaterial.visible = this.front.visible;
814
+ this.front.material = this._cadFrontMaterial;
815
+ this.front.visible = true; // Reset mesh visibility
816
+ }
817
+
818
+ // --- Restore back material ---
819
+ if (this.back && this._cadBackMaterial && this.renderback) {
820
+ // Copy visibility from mesh.visible back to CAD material
821
+ this._cadBackMaterial.visible = this.back.visible;
822
+ this.back.material = this._cadBackMaterial;
823
+ this.back.visible = true; // Reset mesh visibility
824
+ }
825
+
826
+ // --- Restore original colors for highlight ---
827
+ if (this._cadOriginalColor) {
828
+ this.originalColor = this._cadOriginalColor.clone();
829
+ }
830
+ if (this._cadOriginalBackColor) {
831
+ this.originalBackColor = this._cadOriginalBackColor.clone();
832
+ }
833
+
834
+ // --- Restore edge visibility ---
835
+ if (this.edgeMaterial && this._cadEdgesVisible !== null) {
836
+ this.edgeMaterial.visible = this._cadEdgesVisible;
837
+ }
838
+
839
+ this._isStudioMode = false;
840
+ }
841
+
842
+ /**
843
+ * Toggle edge visibility while in Studio mode.
844
+ *
845
+ * Only affects edges (not vertices). Should only be called while in
846
+ * Studio mode; the saved CAD edge visibility is not affected.
847
+ *
848
+ * @param visible - Whether edges should be visible
849
+ */
850
+ setStudioShowEdges(visible: boolean): void {
851
+ if (this.edgeMaterial) {
852
+ this.edgeMaterial.visible = visible;
853
+ }
854
+ }
680
855
  }
681
856
 
682
857
  /**
@@ -191,8 +191,20 @@ class OrientationMarker {
191
191
  */
192
192
  render(renderer: THREE.WebGLRenderer): void {
193
193
  if (this.ready && this.scene && this.camera) {
194
+ // Rendering the corner marker mutates renderer state (viewport/scissor/scissor test).
195
+ // Preserve and restore those values so downstream renders are not clipped or offset.
196
+ const prevViewport = renderer.getViewport(new THREE.Vector4());
197
+ const prevScissor = renderer.getScissor(new THREE.Vector4());
198
+ const prevScissorTest = renderer.getScissorTest();
199
+
200
+ // Draw marker in its small corner viewport.
194
201
  renderer.setViewport(0, 0, this.width, this.height);
195
202
  renderer.render(this.scene, this.camera);
203
+
204
+ // Restore previous state for the main scene / shared render pipeline.
205
+ renderer.setViewport(prevViewport);
206
+ renderer.setScissor(prevScissor);
207
+ renderer.setScissorTest(prevScissorTest);
196
208
  }
197
209
  }
198
210
 
@@ -167,6 +167,7 @@ class ShapeRenderer {
167
167
  }
168
168
  const vertices = shape.vertices;
169
169
  const normals = shape.normals;
170
+ const uvs = shape.uvs instanceof Float32Array ? shape.uvs : null;
170
171
  // Determine format and validate
171
172
  let current = 0;
172
173
  if (hasTrianglesPerFace(shape)) {
@@ -186,6 +187,7 @@ class ShapeRenderer {
186
187
 
187
188
  const vecs = new Float32Array(triangles.length * 3);
188
189
  const norms = new Float32Array(triangles.length * 3);
190
+ const uvArr = uvs ? new Float32Array(triangles.length * 2) : null;
189
191
  for (let i = 0; i < triangles.length; i++) {
190
192
  const s = triangles[i];
191
193
  vecs[3 * i] = vertices[3 * s];
@@ -194,6 +196,22 @@ class ShapeRenderer {
194
196
  norms[3 * i] = normals[3 * s];
195
197
  norms[3 * i + 1] = normals[3 * s + 1];
196
198
  norms[3 * i + 2] = normals[3 * s + 2];
199
+ if (uvs && uvArr) {
200
+ uvArr[2 * i] = uvs[2 * s];
201
+ uvArr[2 * i + 1] = uvs[2 * s + 1];
202
+ }
203
+ }
204
+ const newShapeObj: Shape = {
205
+ triangles: [...Array(triangles.length).keys()],
206
+ vertices: Array.from(vecs),
207
+ normals: Array.from(norms),
208
+ edges: [],
209
+ obj_vertices: [],
210
+ edge_types: [],
211
+ face_types: [shape.face_types[j]],
212
+ };
213
+ if (uvArr) {
214
+ newShapeObj.uvs = Array.from(uvArr);
197
215
  }
198
216
  const new_shape: Shapes = {
199
217
  version: 2,
@@ -206,23 +224,18 @@ class ShapeRenderer {
206
224
  type: "shapes",
207
225
  color: part.color,
208
226
  alpha: part.alpha,
209
- renderback: true,
227
+ renderback: part.subtype !== "solid",
210
228
  state: [1, 3],
211
229
  accuracy: part.accuracy,
212
230
  bb: null,
213
- shape: {
214
- triangles: [...Array(triangles.length).keys()],
215
- vertices: Array.from(vecs),
216
- normals: Array.from(norms),
217
- edges: [],
218
- obj_vertices: [],
219
- edge_types: [],
220
- face_types: [shape.face_types[j]],
221
- },
231
+ shape: newShapeObj,
222
232
  };
223
233
  if (part.texture) {
224
234
  new_shape.texture = part.texture;
225
235
  }
236
+ if (part.material) {
237
+ new_shape.material = part.material;
238
+ }
226
239
  new_shape.geomtype = shape.face_types[j];
227
240
  new_shape.subtype = part.subtype;
228
241
  new_shape.exploded = true;
@@ -241,6 +254,7 @@ class ShapeRenderer {
241
254
 
242
255
  const vecs = new Float32Array(triangles.length * 3);
243
256
  const norms = new Float32Array(triangles.length * 3);
257
+ const uvArr = uvs ? new Float32Array(triangles.length * 2) : null;
244
258
  for (let i = 0; i < triangles.length; i++) {
245
259
  const s = triangles[i];
246
260
  vecs[3 * i] = vertices[3 * s];
@@ -249,6 +263,22 @@ class ShapeRenderer {
249
263
  norms[3 * i] = normals[3 * s];
250
264
  norms[3 * i + 1] = normals[3 * s + 1];
251
265
  norms[3 * i + 2] = normals[3 * s + 2];
266
+ if (uvs && uvArr) {
267
+ uvArr[2 * i] = uvs[2 * s];
268
+ uvArr[2 * i + 1] = uvs[2 * s + 1];
269
+ }
270
+ }
271
+ const newShapeObj2: Shape = {
272
+ triangles: [...Array(triangles.length).keys()],
273
+ vertices: Array.from(vecs),
274
+ normals: Array.from(norms),
275
+ edges: [],
276
+ obj_vertices: [],
277
+ edge_types: [],
278
+ face_types: [shape.face_types[j]],
279
+ };
280
+ if (uvArr) {
281
+ newShapeObj2.uvs = Array.from(uvArr);
252
282
  }
253
283
  const new_shape: Shapes = {
254
284
  version: 2,
@@ -261,23 +291,18 @@ class ShapeRenderer {
261
291
  type: "shapes",
262
292
  color: part.color,
263
293
  alpha: part.alpha,
264
- renderback: true,
294
+ renderback: part.subtype !== "solid",
265
295
  state: [1, 3],
266
296
  accuracy: part.accuracy,
267
297
  bb: null,
268
- shape: {
269
- triangles: [...Array(triangles.length).keys()],
270
- vertices: Array.from(vecs),
271
- normals: Array.from(norms),
272
- edges: [],
273
- obj_vertices: [],
274
- edge_types: [],
275
- face_types: [shape.face_types[j]],
276
- },
298
+ shape: newShapeObj2,
277
299
  };
278
300
  if (part.texture) {
279
301
  new_shape.texture = part.texture;
280
302
  }
303
+ if (part.material) {
304
+ new_shape.material = part.material;
305
+ }
281
306
  new_shape.geomtype = shape.face_types[j];
282
307
  new_shape.subtype = part.subtype;
283
308
  new_shape.exploded = true;
@@ -468,6 +493,7 @@ class ShapeRenderer {
468
493
  edge_types: number[] | Uint8Array | Uint32Array;
469
494
  triangles_per_face?: number[] | Uint32Array;
470
495
  segments_per_edge?: number[] | Uint32Array;
496
+ uvs?: number[] | Float32Array;
471
497
  }
472
498
 
473
499
  // Shape interface matches MutableShape - we cast to allow reassignment
@@ -490,9 +516,12 @@ class ShapeRenderer {
490
516
  // Only flatten if it's actually nested (no segments_per_edge means nested format)
491
517
  if (s.edges != null && !(s.edges instanceof Float32Array)) {
492
518
  if (s.segments_per_edge !== undefined) {
493
- // Binary format with flat edges - convert directly
494
519
  if (!Array.isArray(s.edges[0])) {
520
+ // Flat number[] — convert directly
495
521
  s.edges = new Float32Array(s.edges as number[]);
522
+ } else {
523
+ // Nested number[][] with segments_per_edge — flatten then convert
524
+ s.edges = new Float32Array(flatten(s.edges as number[][], 1));
496
525
  }
497
526
  }
498
527
  // If no segments_per_edge, leave as number[][] for _decompose
@@ -522,6 +551,11 @@ class ShapeRenderer {
522
551
  s.obj_vertices = new Float32Array(s.obj_vertices as number[]);
523
552
  }
524
553
 
554
+ // uvs: flat number[] -> Float32Array (2 floats per vertex)
555
+ if (s.uvs != null && !(s.uvs instanceof Float32Array)) {
556
+ s.uvs = new Float32Array(s.uvs as number[]);
557
+ }
558
+
525
559
  // face_types: number[] -> Uint32Array
526
560
  if (s.face_types != null && !(s.face_types instanceof Uint32Array)) {
527
561
  s.face_types = new Uint32Array(s.face_types);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Minimal type declarations for n8ao.
3
+ *
4
+ * n8ao does not ship TypeScript types. This provides just enough
5
+ * typing to suppress TS7016 during builds while keeping the actual
6
+ * N8AOPostPass usage typed as `any` (the API is untyped upstream).
7
+ */
8
+ declare module "n8ao" {
9
+ import type { Scene, Camera } from "three";
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ export class N8AOPostPass {
13
+ enabled: boolean;
14
+ camera: Camera;
15
+ configuration: {
16
+ aoRadius: number;
17
+ distanceFalloff: number;
18
+ intensity: number;
19
+ halfRes: boolean;
20
+ depthAwareUpsampling: boolean;
21
+ gammaCorrection: boolean;
22
+ [key: string]: unknown;
23
+ };
24
+ constructor(scene: Scene, camera: Camera, width: number, height: number);
25
+ setQualityMode(mode: "Low" | "Medium" | "High" | "Ultra"): void;
26
+ setSize(width: number, height: number): void;
27
+ }
28
+ }