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
@@ -40,14 +40,19 @@ import { BoundingBox, BoxHelper } from "../scene/bbox.js";
40
40
  import { Tools, type ToolResponse } from "../tools/cad_tools/tools.js";
41
41
  import { version } from "../_version.js";
42
42
  import { PickedObject, Raycaster, TopoFilter } from "../rendering/raycast.js";
43
+ import { StudioManager } from "./studio-manager.js";
43
44
  import { ViewerState } from "./viewer-state.js";
44
45
  import { logger } from "../utils/logger.js";
46
+ import { isInstancedFormat, decodeInstancedFormat, decodeInlineBuffers } from "../utils/decode-instances.js";
45
47
  import type { Display } from "../ui/display.js";
46
48
  import type { Vector3Tuple, QuaternionTuple } from "three";
47
49
  import {
48
50
  CollapseState,
49
51
  type ZebraColorScheme,
50
52
  type ZebraMappingMode,
53
+ type StudioToneMapping,
54
+ type StudioTextureMapping,
55
+ type StudioBackground,
51
56
  type NotificationCallback,
52
57
  type RenderOptions,
53
58
  type ViewerOptions,
@@ -205,6 +210,8 @@ interface DisplayOptionsInternal {
205
210
  zebraTool?: boolean;
206
211
  glass?: boolean;
207
212
  tools?: boolean;
213
+ canvas?: HTMLCanvasElement;
214
+ gl?: WebGLRenderingContext | WebGL2RenderingContext;
208
215
  keymap?: KeymapConfig;
209
216
  [key: string]: unknown;
210
217
  }
@@ -289,6 +296,8 @@ class Viewer {
289
296
  // Always available (set in constructor)
290
297
  display!: Display;
291
298
  renderer!: THREE.WebGLRenderer;
299
+ private _externalGl: boolean;
300
+ onAfterRender: (() => void) | null;
292
301
  mouse!: THREE.Vector2;
293
302
  cadTools!: Tools;
294
303
  animation!: Animation;
@@ -352,6 +361,11 @@ class Viewer {
352
361
  // Raycaster
353
362
  raycaster: Raycaster | null;
354
363
 
364
+ // Studio mode orchestration (owns composer, floor, shadow lights, env manager)
365
+ private _studioManager!: StudioManager;
366
+
367
+ /** Environment manager — proxied from StudioManager for display.ts access. */
368
+ get envManager() { return this._studioManager.envManager; }
355
369
  // Z-scale
356
370
  zScale!: number;
357
371
 
@@ -402,6 +416,7 @@ class Viewer {
402
416
  this.notifyCallback = notifyCallback;
403
417
  this.pinAsPngCallback = pinAsPngCallback;
404
418
  this.updateMarker = updateMarker;
419
+ this.onAfterRender = null;
405
420
 
406
421
  this.hasAnimationLoop = false;
407
422
 
@@ -445,17 +460,28 @@ class Viewer {
445
460
 
446
461
  this.mouse = new THREE.Vector2();
447
462
 
448
- // setup renderer
449
- this.renderer = new THREE.WebGLRenderer({
463
+ // setup renderer — support externally provided canvas and/or WebGL context
464
+ const rendererParams: THREE.WebGLRendererParameters = {
450
465
  alpha: true,
451
466
  antialias: true,
452
467
  stencil: true,
453
- });
468
+ };
469
+ if (options.canvas) {
470
+ rendererParams.canvas = options.canvas;
471
+ }
472
+ if (options.gl) {
473
+ rendererParams.context = options.gl;
474
+ }
475
+ this._externalGl = !!(options.canvas || options.gl);
476
+ this.renderer = new THREE.WebGLRenderer(rendererParams);
477
+ this.renderer.outputColorSpace = THREE.SRGBColorSpace;
454
478
  this.renderer.setPixelRatio(window.devicePixelRatio);
455
479
  this.renderer.setSize(this.state.get("cadWidth"), this.state.get("height"));
456
480
  this.renderer.setClearColor(0xffffff, 0);
457
481
  this.renderer.autoClear = false;
458
482
 
483
+ // Create studio manager (env, floor, composer created lazily inside)
484
+
459
485
  this.lastNotification = {};
460
486
  this.lastBbox = null;
461
487
 
@@ -493,9 +519,31 @@ class Viewer {
493
519
 
494
520
  this.display.setupUI(this, this.renderer.domElement);
495
521
 
522
+ // Create studio manager (owns env, floor, composer, shadows, subscriptions)
523
+ this._studioManager = new StudioManager({
524
+ renderer: this.renderer,
525
+ state: this.state,
526
+ isRendered: () => this._rendered !== null,
527
+ getScene: () => this.rendered.scene,
528
+ getCamera: () => this.rendered.camera,
529
+ getAmbientLight: () => this.rendered.ambientLight,
530
+ getDirectLight: () => this.rendered.directLight,
531
+ getNestedGroup: () => this.rendered.nestedGroup,
532
+ getClipping: () => this.rendered.clipping,
533
+ getBbox: () => this.bbox,
534
+ getLastBboxId: () => this.lastBbox?.id ?? null,
535
+ setAxes: (flag, notify) => this.setAxes(flag, notify),
536
+ setGrids: (grids, notify) => this.setGrids(grids, notify),
537
+ setOrtho: (flag, notify) => this.setOrtho(flag, notify),
538
+ update: (updateMarker, notify) => this.update(updateMarker, notify),
539
+ dispatchEvent: (event) => this.display.container.dispatchEvent(event),
540
+ onSelectionChanged: (id) => this.display.onSelectionChanged(id),
541
+ });
542
+
496
543
  console.debug("three-cad-viewer: WebGL Renderer created");
497
544
  }
498
545
 
546
+
499
547
  /**
500
548
  * Return three-cad-viewer version as semver string.
501
549
  * @returns semver version
@@ -805,7 +853,14 @@ class Viewer {
805
853
  update = (updateMarker: boolean, notify: boolean = true): void => {
806
854
  if (!this.ready) return;
807
855
 
808
- this.renderer.clear();
856
+ if (this._externalGl) {
857
+ this.renderer.resetState();
858
+ }
859
+ // When the composer is active, its RenderPass handles clearing;
860
+ // skip manual clear to avoid double-clear artifacts.
861
+ if (!this._studioManager.hasComposer) {
862
+ this.renderer.clear();
863
+ }
809
864
 
810
865
  if (
811
866
  this.raycaster &&
@@ -823,7 +878,19 @@ class Viewer {
823
878
  this.state.get("cadWidth"),
824
879
  this.state.get("height"),
825
880
  );
826
- this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
881
+
882
+ // Env background: render HDRI to 2D render target (fixed-FOV bgCamera)
883
+ if (this._studioManager.isEnvBackgroundActive) {
884
+ this._studioManager.updateEnvBackground(this.renderer, this.rendered.camera.getCamera());
885
+ }
886
+
887
+ // Render: use composer pipeline when available (AO + tone mapping + SMAA),
888
+ // otherwise fall back to direct renderer.render().
889
+ if (this._studioManager.hasComposer) {
890
+ this._studioManager.render();
891
+ } else {
892
+ this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
893
+ }
827
894
  this.cadTools.update();
828
895
 
829
896
  this.rendered.directLight.position.copy(this.rendered.camera.getCamera().position);
@@ -860,6 +927,16 @@ class Viewer {
860
927
  },
861
928
  notify,
862
929
  );
930
+
931
+ // In shared/external WebGL mode, clean up renderer state before external
932
+ // renderers/hooks (overlays, etc.) draw on the same context.
933
+ if (this._externalGl) {
934
+ this.renderer.resetState();
935
+ }
936
+
937
+ if (this.onAfterRender) {
938
+ this.onAfterRender();
939
+ }
863
940
  };
864
941
 
865
942
  /**
@@ -917,11 +994,13 @@ class Viewer {
917
994
  dispose(): void {
918
995
  this.clear();
919
996
 
997
+ // dispose studio resources (composer, floor, env, shadows — must be before renderer)
998
+ this._studioManager.dispose();
920
999
  // dispose renderer
921
1000
  this.renderer.renderLists.dispose();
922
1001
  this.renderer.dispose();
923
- // forceContextLoss may not exist in test mocks
924
- if (typeof this.renderer.forceContextLoss === "function") {
1002
+ // Skip context loss for externally provided WebGL contexts
1003
+ if (!this._externalGl && typeof this.renderer.forceContextLoss === "function") {
925
1004
  this.renderer.forceContextLoss();
926
1005
  }
927
1006
  console.debug("three-cad-viewer: WebGL context disposed");
@@ -984,7 +1063,11 @@ class Viewer {
984
1063
  this.state.set("zscaleActive", false);
985
1064
  }
986
1065
 
987
- // Reset to tree tab for next render
1066
+ // Reset to tree tab for next render.
1067
+ // IMPORTANT: This fires the activeTab subscription synchronously,
1068
+ // which calls switchToTab("tree", oldTab). If oldTab was "studio",
1069
+ // leaveStudioMode() runs here, while _rendered and scene are still valid.
1070
+ // Do NOT move this after deepDispose(scene).
988
1071
  this.state.set("activeTab", "tree");
989
1072
 
990
1073
  // clear render canvas
@@ -1002,6 +1085,8 @@ class Viewer {
1002
1085
 
1003
1086
  // dispose all rendered state objects
1004
1087
  deepDispose(this._rendered.scene);
1088
+
1089
+ // Studio lights were children of the scene and have been disposed by
1005
1090
  deepDispose(this._rendered.gridHelper);
1006
1091
  deepDispose(this._rendered.clipping);
1007
1092
  deepDispose(this._rendered.camera);
@@ -1133,6 +1218,9 @@ class Viewer {
1133
1218
  if (group.front) {
1134
1219
  return "#" + group.front.material.color.getHexString();
1135
1220
  }
1221
+ if (group.originalColor) {
1222
+ return "#" + group.originalColor.getHexString();
1223
+ }
1136
1224
  }
1137
1225
  return null;
1138
1226
  };
@@ -1289,7 +1377,7 @@ class Viewer {
1289
1377
 
1290
1378
  /**
1291
1379
  * Set the active sidebar tab.
1292
- * @param tabName - Tab name: "tree", "clip", "material", or "zebra"
1380
+ * @param tabName - Tab name: "tree", "clip", "material", "zebra", or "studio"
1293
1381
  * @param notify - whether to send notification or not.
1294
1382
  */
1295
1383
  setActiveTab(tabName: ActiveTab, notify: boolean = true): void {
@@ -1341,10 +1429,26 @@ class Viewer {
1341
1429
  renderOptions: RenderOptions,
1342
1430
  viewerOptions: ViewerOptions,
1343
1431
  ): void {
1432
+ // Decode instanced/compressed format if detected
1433
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1434
+ if (isInstancedFormat(shapes as any)) {
1435
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1436
+ shapes = decodeInstancedFormat(shapes as any);
1437
+ }
1438
+ // Decode any remaining inline base64 buffers (e.g., edge/vertex-only objects)
1439
+ decodeInlineBuffers(shapes);
1344
1440
  this.shapes = shapes;
1345
1441
  this.renderOptions = renderOptions;
1346
1442
  this.setViewerDefaults(viewerOptions);
1347
1443
 
1444
+ // Backward compat: studioOptions on shapes root is deprecated
1445
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1446
+ if ((shapes as any).studioOptions) {
1447
+ logger.warn("shapes.studioOptions is deprecated — pass studio settings in viewerOptions instead");
1448
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1449
+ this.state.updateStudioState((shapes as any).studioOptions);
1450
+ }
1451
+
1348
1452
  this.animation.cleanBackup();
1349
1453
 
1350
1454
  const timer = new Timer("viewer", this.state.get("timeit"));
@@ -1435,8 +1539,12 @@ class Viewer {
1435
1539
  cadWidth: this.state.get("cadWidth"),
1436
1540
  height: this.state.get("height"),
1437
1541
  maxAnisotropy: this.renderer.capabilities.getMaxAnisotropy(),
1438
- tickValueElement: this.display.tickValueElement,
1439
- tickInfoElement: this.display.tickInfoElement,
1542
+ ...(this.state.get("tools")
1543
+ ? {
1544
+ tickValueElement: this.display.tickValueElement,
1545
+ tickInfoElement: this.display.tickInfoElement,
1546
+ }
1547
+ : {}),
1440
1548
  getCamera: () => this._rendered?.camera.getCamera() ?? null,
1441
1549
  getAxes0: () => this.state?.get("axes0") ?? false,
1442
1550
  });
@@ -1485,6 +1593,9 @@ class Viewer {
1485
1593
 
1486
1594
  scene.add(clipping);
1487
1595
 
1596
+ // Add studio floor group to scene (hidden by default, shown in enterStudioMode)
1597
+ scene.add(this._studioManager.floor.group);
1598
+
1488
1599
  // Theme is already resolved ("light" or "dark") by ViewerState constructor
1489
1600
  const theme = this.state.get("theme");
1490
1601
 
@@ -1589,6 +1700,11 @@ class Viewer {
1589
1700
 
1590
1701
  this.ready = true;
1591
1702
 
1703
+ if (!this.state.get("tools")) {
1704
+ this.display.showToolsPanel(false);
1705
+ this.rendered.orientationMarker.setVisible(false);
1706
+ }
1707
+
1592
1708
  // Apply clip settings AFTER ready=true (clip setters check this.ready)
1593
1709
  // Set normals first (if provided), passing slider values to avoid reset to gridSize/2
1594
1710
  this.setClipNormal(0, viewerOptions.clipNormal0 ?? null, clipSlider0, true);
@@ -1748,6 +1864,10 @@ class Viewer {
1748
1864
  this.rendered.camera.switchCamera(flag);
1749
1865
  this.rendered.controls.setCamera(this.rendered.camera.getCamera());
1750
1866
 
1867
+ // Update composer camera after the actual swap (not in the ortho
1868
+ // subscriber, which fires before the camera switches)
1869
+ this._studioManager.setCamera(this.rendered.camera.getCamera());
1870
+
1751
1871
  this.rendered.gridHelper.scaleLabels();
1752
1872
  this.rendered.gridHelper.update(this.rendered.camera.getZoom(), true);
1753
1873
 
@@ -1994,7 +2114,7 @@ class Viewer {
1994
2114
  name: string,
1995
2115
  meta: boolean,
1996
2116
  shift: boolean,
1997
- _alt: boolean,
2117
+ alt: boolean,
1998
2118
  point: THREE.Vector3 | null,
1999
2119
  nodeType: string | null = "leaf",
2000
2120
  tree: boolean = false,
@@ -2036,7 +2156,8 @@ class Viewer {
2036
2156
  this.removeLastBbox();
2037
2157
  if (tree) {
2038
2158
  this.rendered.treeview.hideAll();
2039
- this.setState(id, [1, 1], nodeType ?? "leaf");
2159
+ const showEdges = this._studioManager.isActive ? 0 : 1;
2160
+ this.setState(id, [1, showEdges], nodeType ?? "leaf");
2040
2161
  } else {
2041
2162
  const center = boundingBox.center();
2042
2163
  this.setCameraTarget(point ?? new THREE.Vector3(...center));
@@ -2045,18 +2166,27 @@ class Viewer {
2045
2166
  } else if (shift) {
2046
2167
  this.removeLastBbox();
2047
2168
  this.rendered.treeview.hideAll();
2048
- this.setState(id, [1, 1], nodeType ?? "leaf");
2169
+ const showEdges = this._studioManager.isActive ? 0 : 1;
2170
+ this.setState(id, [1, showEdges], nodeType ?? "leaf");
2049
2171
  const center = boundingBox.center();
2050
2172
  this.setCameraTarget(new THREE.Vector3(...center));
2051
2173
  this.display.showCenterInfo(center);
2052
2174
  } else if (meta) {
2053
2175
  this.setState(id, [0, 0], nodeType ?? "leaf");
2176
+ } else if (alt) {
2177
+ // same as else branch to make typscript happy
2178
+ this.display.showBoundingBoxInfo(path, name, boundingBox);
2179
+ this.setBoundingBox(id);
2180
+ this.rendered.treeview.openPath(id);
2054
2181
  } else {
2055
2182
  this.display.showBoundingBoxInfo(path, name, boundingBox);
2056
2183
  this.setBoundingBox(id);
2057
2184
  this.rendered.treeview.openPath(id);
2058
2185
  }
2059
2186
  }
2187
+ if (this._studioManager.isActive) {
2188
+ this.display.onSelectionChanged(this.lastBbox?.id ?? null);
2189
+ }
2060
2190
  this.update(true);
2061
2191
  };
2062
2192
 
@@ -2088,7 +2218,7 @@ class Viewer {
2088
2218
  this.state.get("height"),
2089
2219
  this.bb_max / 30,
2090
2220
  this.rendered.scene.children[0],
2091
- () => {},
2221
+ () => { },
2092
2222
  );
2093
2223
  raycaster.init();
2094
2224
  raycaster.onPointerMove(e);
@@ -2121,9 +2251,9 @@ class Viewer {
2121
2251
  boundingBox:
2122
2252
  shapesFormat === "GDS"
2123
2253
  ? new THREE.Box3(
2124
- point.clone().subScalar(10),
2125
- point.clone().addScalar(10),
2126
- )
2254
+ point.clone().subScalar(10),
2255
+ point.clone().addScalar(10),
2256
+ )
2127
2257
  : nearestMesh.geometry.boundingBox,
2128
2258
  boundingSphere:
2129
2259
  shapesFormat === "GDS"
@@ -2672,6 +2802,18 @@ class Viewer {
2672
2802
  this.update(this.updateMarker);
2673
2803
  };
2674
2804
 
2805
+ /**
2806
+ * Resets zebra tool settings to defaults: count=9, opacity=1, direction=0,
2807
+ * colorScheme=blackwhite, mappingMode=reflection.
2808
+ */
2809
+ resetZebra = (): void => {
2810
+ this.setZebraCount(9);
2811
+ this.setZebraOpacity(1.0);
2812
+ this.setZebraDirection(0);
2813
+ this.setZebraColorScheme("blackwhite");
2814
+ this.setZebraMappingMode("reflection");
2815
+ };
2816
+
2675
2817
  /**
2676
2818
  * Gets the current stripe count value.
2677
2819
  * @returns The stripe count (2-50).
@@ -2712,6 +2854,253 @@ class Viewer {
2712
2854
  return this.state.get("zebraMappingMode");
2713
2855
  };
2714
2856
 
2857
+ // ---------------------------------------------------------------------------
2858
+ // Getters & Setters: Studio Mode
2859
+ // ---------------------------------------------------------------------------
2860
+
2861
+ /**
2862
+ * Sets the studio environment preset.
2863
+ * @param value - The environment name ("studio", "neutral", "outdoor", "none", or custom HDR URL).
2864
+ * @param notify - Whether to notify about the changes.
2865
+ * @public
2866
+ */
2867
+ setStudioEnvironment = (value: string, notify: boolean = true): void => {
2868
+ this.state.set("studioEnvironment", value, notify);
2869
+ };
2870
+
2871
+ /**
2872
+ * Sets the studio environment intensity.
2873
+ * @param value - The environment intensity (0-3).
2874
+ * @param notify - Whether to notify about the changes.
2875
+ * @public
2876
+ */
2877
+ setStudioEnvIntensity = (value: number, notify: boolean = true): void => {
2878
+ value = Math.max(0, Math.min(3, value));
2879
+ this.state.set("studioEnvIntensity", value, notify);
2880
+ };
2881
+
2882
+ /**
2883
+ * Sets the background mode for Studio mode.
2884
+ * @param value - The background mode ("grey", "white", "gradient", "environment", or "transparent").
2885
+ * @param notify - Whether to notify about the changes.
2886
+ * @public
2887
+ */
2888
+ setStudioBackground = (value: StudioBackground, notify: boolean = true): void => {
2889
+ this.state.set("studioBackground", value, notify);
2890
+ };
2891
+
2892
+ /**
2893
+ * Sets the tone mapping mode for Studio mode.
2894
+ * @param value - The tone mapping mode ("neutral", "ACES", or "none").
2895
+ * @param notify - Whether to notify about the changes.
2896
+ * @public
2897
+ */
2898
+ setStudioToneMapping = (value: StudioToneMapping, notify: boolean = true): void => {
2899
+ this.state.set("studioToneMapping", value, notify);
2900
+ };
2901
+
2902
+ /**
2903
+ * Sets the exposure value for Studio mode.
2904
+ * @param value - The exposure value (0-2).
2905
+ * @param notify - Whether to notify about the changes.
2906
+ * @public
2907
+ */
2908
+ setStudioExposure = (value: number, notify: boolean = true): void => {
2909
+ value = Math.max(0, Math.min(2, value));
2910
+ this.state.set("studioExposure", value, notify);
2911
+ };
2912
+
2913
+ /**
2914
+ * Sets whether 4K environment maps are used (default: 2K).
2915
+ * @param value - True for 4K, false for 2K.
2916
+ * @param notify - Whether to notify about the changes.
2917
+ * @public
2918
+ */
2919
+ setStudio4kEnvMaps = (value: boolean, notify: boolean = true): void => {
2920
+ this.state.set("studio4kEnvMaps", value, notify);
2921
+ };
2922
+
2923
+ /**
2924
+ * Gets whether 4K environment maps are enabled.
2925
+ * @returns True for 4K, false for 2K.
2926
+ * @public
2927
+ */
2928
+ getStudio4kEnvMaps = (): boolean => {
2929
+ return this.state.get("studio4kEnvMaps");
2930
+ };
2931
+
2932
+ /**
2933
+ * Sets the environment rotation for Studio mode.
2934
+ * @param value - The rotation in degrees (0-360).
2935
+ * @param notify - Whether to notify about the changes.
2936
+ * @public
2937
+ */
2938
+ setStudioEnvRotation = (value: number, notify: boolean = true): void => {
2939
+ this.state.set("studioEnvRotation", value, notify);
2940
+ };
2941
+
2942
+ /**
2943
+ * Gets the current environment rotation for Studio mode.
2944
+ * @returns The rotation in degrees (0-360).
2945
+ * @public
2946
+ */
2947
+ getStudioEnvRotation = (): number => {
2948
+ return this.state.get("studioEnvRotation");
2949
+ };
2950
+
2951
+ /**
2952
+ * Sets the texture mapping mode for Studio mode.
2953
+ * @param value - The texture mapping mode ("triplanar" or "parametric").
2954
+ * @param notify - Whether to notify about the changes.
2955
+ * @public
2956
+ */
2957
+ setStudioTextureMapping = (value: StudioTextureMapping, notify: boolean = true): void => {
2958
+ this.state.set("studioTextureMapping", value, notify);
2959
+ };
2960
+
2961
+ /**
2962
+ * Gets the current texture mapping mode for Studio mode.
2963
+ * @returns The texture mapping mode ("triplanar" or "parametric").
2964
+ * @public
2965
+ */
2966
+ getStudioTextureMapping = (): StudioTextureMapping => {
2967
+ return this.state.get("studioTextureMapping");
2968
+ };
2969
+
2970
+ /**
2971
+ * Gets the current studio environment preset.
2972
+ * @returns The environment name ("studio", "neutral", "outdoor", "none", or custom HDR URL).
2973
+ * @public
2974
+ */
2975
+ getStudioEnvironment = (): string => {
2976
+ return this.state.get("studioEnvironment");
2977
+ };
2978
+
2979
+ /**
2980
+ * Gets the current studio environment intensity.
2981
+ * @returns The environment intensity (0-3).
2982
+ * @public
2983
+ */
2984
+ getStudioEnvIntensity = (): number => {
2985
+ return this.state.get("studioEnvIntensity");
2986
+ };
2987
+
2988
+ /**
2989
+ * Gets the current background mode for Studio mode.
2990
+ * @returns The background mode ("grey", "white", "gradient", "environment", or "transparent").
2991
+ * @public
2992
+ */
2993
+ getStudioBackground = (): StudioBackground => {
2994
+ return this.state.get("studioBackground");
2995
+ };
2996
+
2997
+ /**
2998
+ * Gets the current tone mapping mode for Studio mode.
2999
+ * @returns The tone mapping mode ("neutral", "ACES", or "none").
3000
+ * @public
3001
+ */
3002
+ getStudioToneMapping = (): StudioToneMapping => {
3003
+ return this.state.get("studioToneMapping");
3004
+ };
3005
+
3006
+ /**
3007
+ * Gets the current exposure value for Studio mode.
3008
+ * @returns The exposure value (0-3).
3009
+ * @public
3010
+ */
3011
+ getStudioExposure = (): number => {
3012
+ return this.state.get("studioExposure");
3013
+ };
3014
+
3015
+ /**
3016
+ * Sets the shadow intensity in Studio mode.
3017
+ * A value of 0 disables shadows; values > 0 enable them at that darkness.
3018
+ * @param value - The shadow intensity (0-1).
3019
+ * @param notify - Whether to notify about the changes.
3020
+ * @public
3021
+ */
3022
+ setStudioShadowIntensity = (value: number, notify: boolean = true): void => {
3023
+ value = Math.max(0, Math.min(1, value));
3024
+ this.state.set("studioShadowIntensity", value, notify);
3025
+ };
3026
+
3027
+ /**
3028
+ * Gets the current shadow intensity in Studio mode.
3029
+ * @returns The shadow intensity (0-1). 0 means shadows are off.
3030
+ * @public
3031
+ */
3032
+ getStudioShadowIntensity = (): number => {
3033
+ return this.state.get("studioShadowIntensity");
3034
+ };
3035
+
3036
+ /**
3037
+ * Sets the shadow softness in Studio mode.
3038
+ * Controls PCSS penumbra width (virtual light source size).
3039
+ * @param value - The shadow softness (0-1).
3040
+ * @param notify - Whether to notify about the changes.
3041
+ * @public
3042
+ */
3043
+ setStudioShadowSoftness = (value: number, notify: boolean = true): void => {
3044
+ value = Math.max(0, Math.min(1, value));
3045
+ this.state.set("studioShadowSoftness", value, notify);
3046
+ };
3047
+
3048
+ /**
3049
+ * Gets the current shadow softness in Studio mode.
3050
+ * @returns The shadow softness (0-1).
3051
+ * @public
3052
+ */
3053
+ getStudioShadowSoftness = (): number => {
3054
+ return this.state.get("studioShadowSoftness");
3055
+ };
3056
+
3057
+ /**
3058
+ * Sets the ambient occlusion intensity in Studio mode.
3059
+ * A value of 0 disables AO; values > 0 enable it at that intensity.
3060
+ * @param value - The AO intensity (0-3.0).
3061
+ * @param notify - Whether to notify about the changes.
3062
+ * @public
3063
+ */
3064
+ setStudioAOIntensity = (value: number, notify: boolean = true): void => {
3065
+ this.state.set("studioAOIntensity", value, notify);
3066
+ };
3067
+
3068
+ /**
3069
+ * Gets the current ambient occlusion intensity in Studio mode.
3070
+ * @returns The AO intensity value (0.5-3.0).
3071
+ * @public
3072
+ */
3073
+ getStudioAOIntensity = (): number => {
3074
+ return this.state.get("studioAOIntensity");
3075
+ };
3076
+
3077
+ /**
3078
+ * Returns whether Studio mode is currently active.
3079
+ * @returns True if Studio mode is active and the viewer has rendered content.
3080
+ * @public
3081
+ */
3082
+ get isStudioActive(): boolean {
3083
+ return this._studioManager.isActive;
3084
+ }
3085
+
3086
+ /**
3087
+ * Get the ObjectGroup and path for the currently selected object in Studio mode.
3088
+ * Returns null if nothing is selected, Studio mode is inactive, or the
3089
+ * selection is a CompoundGroup (assembly node) rather than a leaf object.
3090
+ */
3091
+ getSelectedObjectGroup(): { object: ObjectGroup; path: string } | null {
3092
+ return this._studioManager.getSelectedObjectGroup();
3093
+ }
3094
+
3095
+ /** Enter Studio mode. Called by display.ts switchToTab(). @internal */
3096
+ enterStudioMode = () => this._studioManager.enterStudioMode();
3097
+
3098
+ /** Leave Studio mode. Called by display.ts switchToTab(). @internal */
3099
+ leaveStudioMode = () => this._studioManager.leaveStudioMode();
3100
+
3101
+ /** Reset Studio settings to defaults. @public */
3102
+ resetStudio = () => this._studioManager.resetStudio();
3103
+
2715
3104
  // ---------------------------------------------------------------------------
2716
3105
  // Camera State Getters & Setters
2717
3106
  // ---------------------------------------------------------------------------
@@ -3927,6 +4316,21 @@ class Viewer {
3927
4316
  }
3928
4317
  };
3929
4318
 
4319
+ /**
4320
+ * Resets clip planes to default normals and slider positions.
4321
+ * Normals reset to -X, -Y, -Z; sliders to gridSize/2; checkboxes unchecked.
4322
+ */
4323
+ resetClip = (): void => {
4324
+ if (!this.ready) return;
4325
+ const mid = this.gridSize / 2;
4326
+ this.setClipNormal(0, [-1, 0, 0], mid, true);
4327
+ this.setClipNormal(1, [0, -1, 0], mid, true);
4328
+ this.setClipNormal(2, [0, 0, -1], mid, true);
4329
+ this.setClipIntersection(false, true);
4330
+ this.setClipObjectColorCaps(false, true);
4331
+ this.setClipPlaneHelpers(false, true);
4332
+ };
4333
+
3930
4334
  // ---------------------------------------------------------------------------
3931
4335
  // Image Export
3932
4336
  // ---------------------------------------------------------------------------
@@ -3982,7 +4386,14 @@ class Viewer {
3982
4386
  this.state.get("cadWidth"),
3983
4387
  this.state.get("height"),
3984
4388
  );
3985
- this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
4389
+ if (this._studioManager.isEnvBackgroundActive) {
4390
+ this._studioManager.updateEnvBackground(this.renderer, this.rendered.camera.getCamera());
4391
+ }
4392
+ if (this._studioManager.hasComposer) {
4393
+ this._studioManager.render();
4394
+ } else {
4395
+ this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
4396
+ }
3986
4397
  },
3987
4398
  onComplete: () => {
3988
4399
  // Restore animation loop to original state
@@ -4226,6 +4637,9 @@ class Viewer {
4226
4637
  this.rendered.camera.changeDimensions(this.bb_radius, cadWidth, height);
4227
4638
  this.controls.handleResize();
4228
4639
 
4640
+ // Resize the post-processing composer (render targets must match viewport)
4641
+ this._studioManager.setSize(cadWidth, height);
4642
+
4229
4643
  // update the this
4230
4644
  this.update(true);
4231
4645
 
@@ -4307,14 +4721,31 @@ class Viewer {
4307
4721
  };
4308
4722
 
4309
4723
  /**
4310
- * Show/hide the info panel.
4311
- * @param flag - whether to show the info panel
4724
+ * Collapse or expand the info panel in glass mode.
4725
+ * @param flag - true to show, false to collapse
4312
4726
  * @public
4313
4727
  */
4314
- showInfo = (flag: boolean): void => {
4728
+ showInfoPanel = (flag: boolean): void => {
4315
4729
  this.display.showInfo(flag);
4316
4730
  };
4317
4731
 
4732
+ /**
4733
+ * @deprecated Use showInfoPanel() instead.
4734
+ */
4735
+ showInfo = (flag: boolean): void => {
4736
+ console.warn("showInfo() is deprecated, use showInfoPanel() instead.");
4737
+ this.showInfoPanel(flag);
4738
+ };
4739
+
4740
+ /**
4741
+ * Collapse or expand the tools panel (tabs + content) in glass mode.
4742
+ * @param flag - true to show, false to collapse
4743
+ * @public
4744
+ */
4745
+ showToolsPanel = (flag: boolean): void => {
4746
+ this.display.showToolsPanel(flag);
4747
+ };
4748
+
4318
4749
  /**
4319
4750
  * Show/hide the pinning button.
4320
4751
  * @param flag - whether to show the pinning button