@file-viewer/renderer-3d
Advanced tools
| export interface ModelViewportRenderer { | ||
| getPixelRatio(): number; | ||
| setPixelRatio(value: number): void; | ||
| setSize(width: number, height: number, updateStyle: boolean): void; | ||
| } | ||
| export interface ModelViewportCamera { | ||
| aspect: number; | ||
| updateProjectionMatrix(): void; | ||
| } | ||
| export interface SyncModelViewportInput { | ||
| renderer: ModelViewportRenderer; | ||
| camera: ModelViewportCamera; | ||
| stage: HTMLElement; | ||
| canvas: HTMLCanvasElement; | ||
| devicePixelRatio?: number; | ||
| } | ||
| export interface ModelViewportState { | ||
| width: number; | ||
| height: number; | ||
| pixelRatio: number; | ||
| resized: boolean; | ||
| } | ||
| export declare const syncModelViewport: ({ renderer, camera, stage, canvas, devicePixelRatio }: SyncModelViewportInput) => ModelViewportState; |
| const resolveCssSize = (stage, canvas) => { | ||
| return { | ||
| width: Math.max(1, Math.floor(stage.clientWidth || | ||
| canvas.clientWidth || | ||
| stage.getBoundingClientRect().width || | ||
| canvas.getBoundingClientRect().width)), | ||
| height: Math.max(1, Math.floor(stage.clientHeight || | ||
| canvas.clientHeight || | ||
| stage.getBoundingClientRect().height || | ||
| canvas.getBoundingClientRect().height)) | ||
| }; | ||
| }; | ||
| export const syncModelViewport = ({ renderer, camera, stage, canvas, devicePixelRatio = 1 }) => { | ||
| const { width, height } = resolveCssSize(stage, canvas); | ||
| const pixelRatio = Math.min(2, Math.max(1, Number.isFinite(devicePixelRatio) ? devicePixelRatio : 1)); | ||
| const pixelRatioChanged = Math.abs(renderer.getPixelRatio() - pixelRatio) > 0.001; | ||
| if (pixelRatioChanged) { | ||
| renderer.setPixelRatio(pixelRatio); | ||
| } | ||
| const expectedWidth = Math.max(1, Math.floor(width * pixelRatio)); | ||
| const expectedHeight = Math.max(1, Math.floor(height * pixelRatio)); | ||
| const resized = (pixelRatioChanged || | ||
| canvas.width !== expectedWidth || | ||
| canvas.height !== expectedHeight); | ||
| if (resized) { | ||
| renderer.setSize(width, height, false); | ||
| } | ||
| const aspect = width / height; | ||
| if (Math.abs(camera.aspect - aspect) > 0.0001) { | ||
| camera.aspect = aspect; | ||
| camera.updateProjectionMatrix(); | ||
| } | ||
| return { | ||
| width, | ||
| height, | ||
| pixelRatio, | ||
| resized | ||
| }; | ||
| }; |
+86
-61
@@ -6,2 +6,3 @@ import * as THREE from 'three'; | ||
| import { buildOcctThreeObject } from './occtModel.js'; | ||
| import { syncModelViewport } from './modelViewport.js'; | ||
| const modelStyle = ` | ||
@@ -203,2 +204,4 @@ .model-viewer{display:flex;height:100%;min-height:100%;flex-direction:column;background:#f8fafc;color:#162333} | ||
| let resizeObserver = null; | ||
| let controlsWithEvents = null; | ||
| let disposed = false; | ||
| let animationFrame = 0; | ||
@@ -402,8 +405,9 @@ let activeVersion = 0; | ||
| } | ||
| const rect = canvas.getBoundingClientRect(); | ||
| const width = Math.max(1, Math.floor(rect.width)); | ||
| const height = Math.max(1, Math.floor(rect.height)); | ||
| renderer.setSize(width, height, false); | ||
| camera.aspect = width / height; | ||
| camera.updateProjectionMatrix(); | ||
| syncModelViewport({ | ||
| renderer, | ||
| camera, | ||
| stage, | ||
| canvas, | ||
| devicePixelRatio: target.ownerDocument.defaultView?.devicePixelRatio || 1, | ||
| }); | ||
| }; | ||
@@ -501,3 +505,2 @@ const findThemeHost = () => { | ||
| }); | ||
| renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); | ||
| renderer.outputColorSpace = THREE.SRGBColorSpace; | ||
@@ -532,2 +535,6 @@ } | ||
| resize(); | ||
| if (!controls) { | ||
| throw new Error('Failed to initialize 3D controls.'); | ||
| } | ||
| return controls; | ||
| }; | ||
@@ -927,2 +934,3 @@ const clearModel = () => { | ||
| zoomEmitter.emit(); | ||
| throw reason instanceof Error ? reason : new Error(errorMessage); | ||
| } | ||
@@ -934,2 +942,7 @@ }; | ||
| } | ||
| // A framework transition or a repeated File assignment can reset canvas | ||
| // attributes without changing its CSS box. ResizeObserver will not see | ||
| // that backing-buffer downgrade, so validate the cheap size contract on | ||
| // each frame and resize only when the values diverge. | ||
| resize(); | ||
| timer.update(timestamp); | ||
@@ -982,61 +995,73 @@ const delta = timer.getDelta(); | ||
| }); | ||
| updateUi(); | ||
| ensureScene(); | ||
| observeModelTheme(); | ||
| const controlsWithEvents = controls; | ||
| controlsWithEvents?.addEventListener('start', onControlsStart); | ||
| controlsWithEvents?.addEventListener('end', onControlsEnd); | ||
| registerFileViewerZoomProvider(root, { | ||
| zoomIn: () => setModelZoom(getModelScale() * MODEL_ZOOM_STEP, 'user', 'zoom-in'), | ||
| zoomOut: () => setModelZoom(getModelScale() / MODEL_ZOOM_STEP, 'user', 'zoom-out'), | ||
| resetZoom: () => setModelZoom(1, 'user', 'zoom-reset'), | ||
| setZoom: scale => setModelZoom(scale, 'api', 'zoom-change'), | ||
| fit: applyModelFit, | ||
| getState: getModelZoomState, | ||
| subscribe: zoomEmitter.subscribe, | ||
| }); | ||
| registerFileViewerViewStateProvider(root, { | ||
| getState: getModelViewState, | ||
| applyState: applyModelViewState, | ||
| fit: applyModelFit, | ||
| subscribe: viewStateEmitter.subscribe, | ||
| }); | ||
| resizeObserver = new ResizeObserver(resize); | ||
| resizeObserver.observe(canvas); | ||
| timer.connect(target.ownerDocument); | ||
| renderFrame(); | ||
| await loadModel(); | ||
| const cleanup = () => { | ||
| if (disposed) | ||
| return; | ||
| disposed = true; | ||
| activeVersion += 1; | ||
| window.cancelAnimationFrame(animationFrame); | ||
| resizeObserver?.disconnect(); | ||
| resizeObserver = null; | ||
| themeObserver?.disconnect(); | ||
| themeObserver = null; | ||
| stopSystemThemeListener?.(); | ||
| stopSystemThemeListener = null; | ||
| clearModel(); | ||
| controlsWithEvents?.removeEventListener('start', onControlsStart); | ||
| controlsWithEvents?.removeEventListener('end', onControlsEnd); | ||
| controlsWithEvents = null; | ||
| unregisterFileViewerZoomProvider(root); | ||
| unregisterFileViewerViewStateProvider(root); | ||
| controls?.dispose(); | ||
| controls = null; | ||
| if (scene) { | ||
| disposeObject(scene); | ||
| } | ||
| renderer?.dispose(); | ||
| renderer?.renderLists.dispose(); | ||
| renderer = null; | ||
| timer.dispose(); | ||
| scene = null; | ||
| camera = null; | ||
| gridHelper = null; | ||
| axesHelper = null; | ||
| hemisphereLight = null; | ||
| target.replaceChildren(); | ||
| }; | ||
| try { | ||
| updateUi(); | ||
| controlsWithEvents = ensureScene(); | ||
| observeModelTheme(); | ||
| controlsWithEvents.addEventListener('start', onControlsStart); | ||
| controlsWithEvents.addEventListener('end', onControlsEnd); | ||
| registerFileViewerZoomProvider(root, { | ||
| zoomIn: () => setModelZoom(getModelScale() * MODEL_ZOOM_STEP, 'user', 'zoom-in'), | ||
| zoomOut: () => setModelZoom(getModelScale() / MODEL_ZOOM_STEP, 'user', 'zoom-out'), | ||
| resetZoom: () => setModelZoom(1, 'user', 'zoom-reset'), | ||
| setZoom: scale => setModelZoom(scale, 'api', 'zoom-change'), | ||
| fit: applyModelFit, | ||
| getState: getModelZoomState, | ||
| subscribe: zoomEmitter.subscribe, | ||
| }); | ||
| registerFileViewerViewStateProvider(root, { | ||
| getState: getModelViewState, | ||
| applyState: applyModelViewState, | ||
| fit: applyModelFit, | ||
| subscribe: viewStateEmitter.subscribe, | ||
| }); | ||
| resizeObserver = new ResizeObserver(resize); | ||
| resizeObserver.observe(stage); | ||
| timer.connect(target.ownerDocument); | ||
| renderFrame(); | ||
| await loadModel(); | ||
| } | ||
| catch (error) { | ||
| cleanup(); | ||
| throw error; | ||
| } | ||
| return { | ||
| $el: root, | ||
| unmount() { | ||
| activeVersion += 1; | ||
| window.cancelAnimationFrame(animationFrame); | ||
| resizeObserver?.disconnect(); | ||
| resizeObserver = null; | ||
| themeObserver?.disconnect(); | ||
| themeObserver = null; | ||
| stopSystemThemeListener?.(); | ||
| stopSystemThemeListener = null; | ||
| clearModel(); | ||
| controlsWithEvents?.removeEventListener('start', onControlsStart); | ||
| controlsWithEvents?.removeEventListener('end', onControlsEnd); | ||
| unregisterFileViewerZoomProvider(root); | ||
| unregisterFileViewerViewStateProvider(root); | ||
| controls?.dispose(); | ||
| controls = null; | ||
| if (scene) { | ||
| disposeObject(scene); | ||
| } | ||
| renderer?.dispose(); | ||
| renderer?.renderLists.dispose(); | ||
| renderer = null; | ||
| timer.dispose(); | ||
| scene = null; | ||
| camera = null; | ||
| gridHelper = null; | ||
| axesHelper = null; | ||
| hemisphereLight = null; | ||
| target.replaceChildren(); | ||
| cleanup(); | ||
| }, | ||
| }; | ||
| } |
+3
-3
| { | ||
| "name": "@file-viewer/renderer-3d", | ||
| "version": "2.2.3", | ||
| "version": "2.2.4", | ||
| "private": false, | ||
@@ -60,4 +60,4 @@ "type": "module", | ||
| "dependencies": { | ||
| "@file-viewer/core": "2.2.3", | ||
| "@file-viewer/geometry-engine": "2.2.3", | ||
| "@file-viewer/core": "2.2.4", | ||
| "@file-viewer/geometry-engine": "2.2.4", | ||
| "three": "^0.184.0" | ||
@@ -64,0 +64,0 @@ }, |
70808
4.31%12
20%1346
6.91%+ Added
+ Added
- Removed
- Removed
Updated