@file-viewer/renderer-3d 2.2.3 → 2.2.5

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.
package/dist/model.js CHANGED
@@ -3,6 +3,7 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
3
  import { createFileViewerViewStateChange, createFileViewerViewStateChangeEmitter, createFileViewerZoomChangeEmitter, createFileViewerTranslator, registerFileViewerZoomProvider, registerFileViewerViewStateProvider, resolveFileViewerColorScheme, resolveFileViewerLocale, resolveFileViewerModelAssetUrls, resolveFileViewerRuntimeAssetBaseUrl, unregisterFileViewerZoomProvider, unregisterFileViewerViewStateProvider, } from '@file-viewer/core';
4
4
  import { formatGeometryKernelNotice, importOcctGeometryFile, inspectGeometryKernelFile, isGeometryKernelFormat, } from '@file-viewer/geometry-engine';
5
5
  import { buildOcctThreeObject } from './occtModel.js';
6
+ import { syncModelViewport } from './modelViewport.js';
6
7
  const modelStyle = `
7
8
  .model-viewer{display:flex;height:100%;min-height:100%;flex-direction:column;background:#f8fafc;color:#162333}
8
9
  .model-viewer *{box-sizing:border-box}
@@ -199,6 +200,8 @@ export default async function renderModel(buffer, target, type = 'glb', context)
199
200
  let stopSystemThemeListener = null;
200
201
  let activeDarkMode = null;
201
202
  let resizeObserver = null;
203
+ let controlsWithEvents = null;
204
+ let disposed = false;
202
205
  let animationFrame = 0;
203
206
  let activeVersion = 0;
204
207
  let modelMeshCount = 0;
@@ -397,12 +400,13 @@ export default async function renderModel(buffer, target, type = 'glb', context)
397
400
  if (!renderer || !camera) {
398
401
  return;
399
402
  }
400
- const rect = canvas.getBoundingClientRect();
401
- const width = Math.max(1, Math.floor(rect.width));
402
- const height = Math.max(1, Math.floor(rect.height));
403
- renderer.setSize(width, height, false);
404
- camera.aspect = width / height;
405
- camera.updateProjectionMatrix();
403
+ syncModelViewport({
404
+ renderer,
405
+ camera,
406
+ stage,
407
+ canvas,
408
+ devicePixelRatio: target.ownerDocument.defaultView?.devicePixelRatio || 1,
409
+ });
406
410
  };
407
411
  const findThemeHost = () => {
408
412
  let current = root;
@@ -495,7 +499,6 @@ export default async function renderModel(buffer, target, type = 'glb', context)
495
499
  canvas,
496
500
  powerPreference: 'high-performance',
497
501
  });
498
- renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
499
502
  renderer.outputColorSpace = THREE.SRGBColorSpace;
500
503
  }
501
504
  if (!scene) {
@@ -525,6 +528,10 @@ export default async function renderModel(buffer, target, type = 'glb', context)
525
528
  applyModelTheme(initializeTheme);
526
529
  updateHelperVisibility();
527
530
  resize();
531
+ if (!controls) {
532
+ throw new Error('Failed to initialize 3D controls.');
533
+ }
534
+ return controls;
528
535
  };
529
536
  const clearModel = () => {
530
537
  if (modelRoot && scene) {
@@ -919,12 +926,18 @@ export default async function renderModel(buffer, target, type = 'glb', context)
919
926
  errorMessage = normalizeError(reason) || t('model.error.parseFailed', { type: normalizedType.toUpperCase() });
920
927
  updateUi();
921
928
  zoomEmitter.emit();
929
+ throw reason instanceof Error ? reason : new Error(errorMessage);
922
930
  }
923
931
  };
924
932
  const renderFrame = (timestamp) => {
925
933
  if (!renderer || !scene || !camera || !controls) {
926
934
  return;
927
935
  }
936
+ // A framework transition or a repeated File assignment can reset canvas
937
+ // attributes without changing its CSS box. ResizeObserver will not see
938
+ // that backing-buffer downgrade, so validate the cheap size contract on
939
+ // each frame and resize only when the values diverge.
940
+ resize();
928
941
  timer.update(timestamp);
929
942
  const delta = timer.getDelta();
930
943
  controls.autoRotate = autoRotate;
@@ -972,63 +985,75 @@ export default async function renderModel(buffer, target, type = 'glb', context)
972
985
  updateHelperVisibility();
973
986
  emitViewStateChange('view-option-change', 'user');
974
987
  });
975
- updateUi();
976
- ensureScene();
977
- observeModelTheme();
978
- const controlsWithEvents = controls;
979
- controlsWithEvents?.addEventListener('start', onControlsStart);
980
- controlsWithEvents?.addEventListener('end', onControlsEnd);
981
- registerFileViewerZoomProvider(root, {
982
- zoomIn: () => setModelZoom(getModelScale() * MODEL_ZOOM_STEP, 'user', 'zoom-in'),
983
- zoomOut: () => setModelZoom(getModelScale() / MODEL_ZOOM_STEP, 'user', 'zoom-out'),
984
- resetZoom: () => setModelZoom(1, 'user', 'zoom-reset'),
985
- setZoom: scale => setModelZoom(scale, 'api', 'zoom-change'),
986
- fit: applyModelFit,
987
- getState: getModelZoomState,
988
- subscribe: zoomEmitter.subscribe,
989
- });
990
- registerFileViewerViewStateProvider(root, {
991
- getState: getModelViewState,
992
- applyState: applyModelViewState,
993
- fit: applyModelFit,
994
- subscribe: viewStateEmitter.subscribe,
995
- });
996
- resizeObserver = new ResizeObserver(resize);
997
- resizeObserver.observe(canvas);
998
- timer.connect(target.ownerDocument);
999
- renderFrame();
1000
- await loadModel();
988
+ const cleanup = () => {
989
+ if (disposed)
990
+ return;
991
+ disposed = true;
992
+ activeVersion += 1;
993
+ window.cancelAnimationFrame(animationFrame);
994
+ resizeObserver?.disconnect();
995
+ resizeObserver = null;
996
+ themeObserver?.disconnect();
997
+ themeObserver = null;
998
+ stopSystemThemeListener?.();
999
+ stopSystemThemeListener = null;
1000
+ clearModel();
1001
+ controlsWithEvents?.removeEventListener('start', onControlsStart);
1002
+ controlsWithEvents?.removeEventListener('end', onControlsEnd);
1003
+ controlsWithEvents = null;
1004
+ unregisterFileViewerZoomProvider(root);
1005
+ unregisterFileViewerViewStateProvider(root);
1006
+ controls?.dispose();
1007
+ controls = null;
1008
+ if (scene) {
1009
+ disposeObject(scene);
1010
+ }
1011
+ renderer?.dispose();
1012
+ renderer?.renderLists.dispose();
1013
+ renderer = null;
1014
+ timer.dispose();
1015
+ scene = null;
1016
+ camera = null;
1017
+ gridHelper = null;
1018
+ axesHelper = null;
1019
+ hemisphereLight = null;
1020
+ target.replaceChildren();
1021
+ };
1022
+ try {
1023
+ updateUi();
1024
+ controlsWithEvents = ensureScene();
1025
+ observeModelTheme();
1026
+ controlsWithEvents.addEventListener('start', onControlsStart);
1027
+ controlsWithEvents.addEventListener('end', onControlsEnd);
1028
+ registerFileViewerZoomProvider(root, {
1029
+ zoomIn: () => setModelZoom(getModelScale() * MODEL_ZOOM_STEP, 'user', 'zoom-in'),
1030
+ zoomOut: () => setModelZoom(getModelScale() / MODEL_ZOOM_STEP, 'user', 'zoom-out'),
1031
+ resetZoom: () => setModelZoom(1, 'user', 'zoom-reset'),
1032
+ setZoom: scale => setModelZoom(scale, 'api', 'zoom-change'),
1033
+ fit: applyModelFit,
1034
+ getState: getModelZoomState,
1035
+ subscribe: zoomEmitter.subscribe,
1036
+ });
1037
+ registerFileViewerViewStateProvider(root, {
1038
+ getState: getModelViewState,
1039
+ applyState: applyModelViewState,
1040
+ fit: applyModelFit,
1041
+ subscribe: viewStateEmitter.subscribe,
1042
+ });
1043
+ resizeObserver = new ResizeObserver(resize);
1044
+ resizeObserver.observe(stage);
1045
+ timer.connect(target.ownerDocument);
1046
+ renderFrame();
1047
+ await loadModel();
1048
+ }
1049
+ catch (error) {
1050
+ cleanup();
1051
+ throw error;
1052
+ }
1001
1053
  return {
1002
1054
  $el: root,
1003
1055
  unmount() {
1004
- activeVersion += 1;
1005
- window.cancelAnimationFrame(animationFrame);
1006
- resizeObserver?.disconnect();
1007
- resizeObserver = null;
1008
- themeObserver?.disconnect();
1009
- themeObserver = null;
1010
- stopSystemThemeListener?.();
1011
- stopSystemThemeListener = null;
1012
- clearModel();
1013
- controlsWithEvents?.removeEventListener('start', onControlsStart);
1014
- controlsWithEvents?.removeEventListener('end', onControlsEnd);
1015
- unregisterFileViewerZoomProvider(root);
1016
- unregisterFileViewerViewStateProvider(root);
1017
- controls?.dispose();
1018
- controls = null;
1019
- if (scene) {
1020
- disposeObject(scene);
1021
- }
1022
- renderer?.dispose();
1023
- renderer?.renderLists.dispose();
1024
- renderer = null;
1025
- timer.dispose();
1026
- scene = null;
1027
- camera = null;
1028
- gridHelper = null;
1029
- axesHelper = null;
1030
- hemisphereLight = null;
1031
- target.replaceChildren();
1056
+ cleanup();
1032
1057
  },
1033
1058
  };
1034
1059
  }
@@ -0,0 +1,23 @@
1
+ export interface ModelViewportRenderer {
2
+ getPixelRatio(): number;
3
+ setPixelRatio(value: number): void;
4
+ setSize(width: number, height: number, updateStyle: boolean): void;
5
+ }
6
+ export interface ModelViewportCamera {
7
+ aspect: number;
8
+ updateProjectionMatrix(): void;
9
+ }
10
+ export interface SyncModelViewportInput {
11
+ renderer: ModelViewportRenderer;
12
+ camera: ModelViewportCamera;
13
+ stage: HTMLElement;
14
+ canvas: HTMLCanvasElement;
15
+ devicePixelRatio?: number;
16
+ }
17
+ export interface ModelViewportState {
18
+ width: number;
19
+ height: number;
20
+ pixelRatio: number;
21
+ resized: boolean;
22
+ }
23
+ export declare const syncModelViewport: ({ renderer, camera, stage, canvas, devicePixelRatio }: SyncModelViewportInput) => ModelViewportState;
@@ -0,0 +1,39 @@
1
+ const resolveCssSize = (stage, canvas) => {
2
+ return {
3
+ width: Math.max(1, Math.floor(stage.clientWidth ||
4
+ canvas.clientWidth ||
5
+ stage.getBoundingClientRect().width ||
6
+ canvas.getBoundingClientRect().width)),
7
+ height: Math.max(1, Math.floor(stage.clientHeight ||
8
+ canvas.clientHeight ||
9
+ stage.getBoundingClientRect().height ||
10
+ canvas.getBoundingClientRect().height))
11
+ };
12
+ };
13
+ export const syncModelViewport = ({ renderer, camera, stage, canvas, devicePixelRatio = 1 }) => {
14
+ const { width, height } = resolveCssSize(stage, canvas);
15
+ const pixelRatio = Math.min(2, Math.max(1, Number.isFinite(devicePixelRatio) ? devicePixelRatio : 1));
16
+ const pixelRatioChanged = Math.abs(renderer.getPixelRatio() - pixelRatio) > 0.001;
17
+ if (pixelRatioChanged) {
18
+ renderer.setPixelRatio(pixelRatio);
19
+ }
20
+ const expectedWidth = Math.max(1, Math.floor(width * pixelRatio));
21
+ const expectedHeight = Math.max(1, Math.floor(height * pixelRatio));
22
+ const resized = (pixelRatioChanged ||
23
+ canvas.width !== expectedWidth ||
24
+ canvas.height !== expectedHeight);
25
+ if (resized) {
26
+ renderer.setSize(width, height, false);
27
+ }
28
+ const aspect = width / height;
29
+ if (Math.abs(camera.aspect - aspect) > 0.0001) {
30
+ camera.aspect = aspect;
31
+ camera.updateProjectionMatrix();
32
+ }
33
+ return {
34
+ width,
35
+ height,
36
+ pixelRatio,
37
+ resized
38
+ };
39
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-3d",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone 3D model renderer plugin for File Viewer powered by Three.js loaders and OrbitControls.",
@@ -57,8 +57,8 @@
57
57
  "LICENSE"
58
58
  ],
59
59
  "dependencies": {
60
- "@file-viewer/core": "2.2.3",
61
- "@file-viewer/geometry-engine": "2.2.3",
60
+ "@file-viewer/core": "2.2.5",
61
+ "@file-viewer/geometry-engine": "2.2.5",
62
62
  "three": "^0.184.0"
63
63
  },
64
64
  "devDependencies": {