From 0c0819249db40bcc06db16c6b7629f1b64996046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 24 Sep 2026 17:23:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9F=A5=E7=9C=8B=E5=99=A8=E5=8F=96=E6=99=AF?= =?UTF-8?q?=E8=A3=85=E9=85=8D=E5=A4=B1=E8=B4=A5=E6=97=B6=E9=87=8A=E6=94=BE?= =?UTF-8?q?=20renderer=20=E4=B8=8E=E6=A8=A1=E5=9E=8B=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packages/model3d-viewer/src/scene.ts:模型加载成功后的取景 / ResizeObserver / 渲染循环装配抽成 setupLoadedScene,由外层 try/catch 兜住;任何异常先 teardown 再抛出,避免调用方拿不到 scene 引用时漏掉 WebGL 上下文与模型 GPU 资源 --- packages/model3d-viewer/src/scene.ts | 303 ++++++++++++++------------- 1 file changed, 160 insertions(+), 143 deletions(-) diff --git a/packages/model3d-viewer/src/scene.ts b/packages/model3d-viewer/src/scene.ts index 60ea056b8..5d9ad026a 100644 --- a/packages/model3d-viewer/src/scene.ts +++ b/packages/model3d-viewer/src/scene.ts @@ -277,162 +277,179 @@ export async function createModel3dViewerScene( abortIfNeeded(); - const bounds = new THREE.Box3().setFromObject(model3d); - const viewport = measureViewport(); - /** - * 按给定宽高比重算取景。容器在隐藏面板里挂载时首帧量不到尺寸,只能先按方形兜底, - * 等容器真正有尺寸后必须用真实比例重算,否则模型会按方形取景、被裁掉或显得过小。 + * 模型已加载,但取景 / ResizeObserver / 渲染循环还没装好:这一段里的任何异常都必须先 + * 释放 renderer 与模型资源再抛出,否则调用方拿不到 scene 引用,WebGL 上下文与 GPU 资源就漏了。 */ - function computeFraming(aspect: number) { - const fitBox = { - min: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z }, - max: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }, - }; - const fitSolver = createModel3dViewerFitSolver({ - center: computeModel3dViewerBoxCenter(fitBox), - aspect, - fovDegrees: MODEL3D_VIEWER_CAMERA_FOV_DEGREES, - fallbackRadius: computeModel3dViewerBoxRadius(fitBox), - }); - feedModel3dViewerFitSolver(model3d, (x, y, z) => fitSolver.add(x, y, z)); - return computeModel3dViewerFraming({ - box: fitBox, - aspect, - fovDegrees: MODEL3D_VIEWER_CAMERA_FOV_DEGREES, - distance: fitSolver.resolve(), - }); - } + function setupLoadedScene(): Model3dViewerScene { + const bounds = new THREE.Box3().setFromObject(model3d); + const viewport = measureViewport(); - let framing = computeFraming(viewport.aspect); - let framedAspect = viewport.aspect; - let hasFramedViewport = viewport.width > 0 && viewport.height > 0; - - function measureViewport(): { - width: number; - height: number; - aspect: number; - } { - const rect = container.getBoundingClientRect(); - const width = Math.max(Math.round(rect.width), 0); - const height = Math.max(Math.round(rect.height), 0); - return { - width, - height, - aspect: width > 0 && height > 0 ? width / height : 1, - }; - } - - function applyViewport() { - const current = measureViewport(); - if (current.width === 0 || current.height === 0 || !renderer) { - return; + /** + * 按给定宽高比重算取景。容器在隐藏面板里挂载时首帧量不到尺寸,只能先按方形兜底, + * 等容器真正有尺寸后必须用真实比例重算,否则模型会按方形取景、被裁掉或显得过小。 + */ + function computeFraming(aspect: number) { + const fitBox = { + min: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z }, + max: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }, + }; + const fitSolver = createModel3dViewerFitSolver({ + center: computeModel3dViewerBoxCenter(fitBox), + aspect, + fovDegrees: MODEL3D_VIEWER_CAMERA_FOV_DEGREES, + fallbackRadius: computeModel3dViewerBoxRadius(fitBox), + }); + feedModel3dViewerFitSolver(model3d, (x, y, z) => fitSolver.add(x, y, z)); + return computeModel3dViewerFraming({ + box: fitBox, + aspect, + fovDegrees: MODEL3D_VIEWER_CAMERA_FOV_DEGREES, + distance: fitSolver.resolve(), + }); } - renderer.setSize(current.width, current.height, false); - camera.aspect = current.aspect; - camera.updateProjectionMatrix(); - } - /** - * 把取景套到相机与轨道控制器上。`preserveCamera` 时保留用户当前的观察方向与相对 - * 缩放,只把相机沿原方向搬到新取景距离上;否则回到取景复位视角。 - */ - function applyFraming(next: Model3dViewerFraming, preserveCamera: boolean) { - const position = preserveCamera - ? resolveModel3dViewerReframedCameraPosition({ - previous: framing, - next, - cameraPosition: { - x: camera.position.x, - y: camera.position.y, - z: camera.position.z, - }, - }) - : { - x: next.center.x + next.resetDirection.x * next.distance, - y: next.center.y + next.resetDirection.y * next.distance, - z: next.center.z + next.resetDirection.z * next.distance, - }; - camera.near = next.near; - camera.far = next.far; - camera.updateProjectionMatrix(); - orbitControls.minDistance = next.minDistance; - orbitControls.maxDistance = next.maxDistance; - orbitControls.target.set(next.center.x, next.center.y, next.center.z); - camera.position.set(position.x, position.y, position.z); - orbitControls.update(); - framing = next; - } + let framing = computeFraming(viewport.aspect); + let framedAspect = viewport.aspect; + let hasFramedViewport = viewport.width > 0 && viewport.height > 0; - /** - * 宽高比确实变化时按新比例重算取景并套用:比例没变(同比例缩放容器)就保留现有取景, - * 不打断用户当前视角。返回是否重算了取景。 - */ - function reframeIfAspectChanged(aspect: number, preserveCamera: boolean) { - if ( - hasFramedViewport && - Math.abs(aspect - framedAspect) <= framedAspect * FRAMING_ASPECT_TOLERANCE - ) { - return false; + function measureViewport(): { + width: number; + height: number; + aspect: number; + } { + const rect = container.getBoundingClientRect(); + const width = Math.max(Math.round(rect.width), 0); + const height = Math.max(Math.round(rect.height), 0); + return { + width, + height, + aspect: width > 0 && height > 0 ? width / height : 1, + }; } - applyFraming(computeFraming(aspect), preserveCamera); - framedAspect = aspect; - hasFramedViewport = true; - return true; - } - /** - * 复位视角:按容器当前宽高比重算取景,不复用可能过期的 `framing` —— 容器变窄后旧取景的 - * 距离与缩放上下限都不再贴合,跟着旧取景复位会把模型裁掉或放得过小。 - */ - function resetView() { - const current = measureViewport(); - const measurable = current.width > 0 && current.height > 0; - // 容器量不到尺寸(隐藏面板、首帧)时只能按现有取景复位,等量到尺寸再重算。 - applyFraming(measurable ? computeFraming(current.aspect) : framing, false); - if (measurable) { - framedAspect = current.aspect; - hasFramedViewport = true; - } - } - - // 首帧按创建时的比例(容器量不到尺寸时是方形兜底)套一次取景;真实比例由下面的 - // ResizeObserver 首轮回调重算,避免在这里重复一次顶点采样。 - applyFraming(framing, false); - applyViewport(); - - if (typeof ResizeObserver !== 'undefined') { - resizeObserver = new ResizeObserver(() => { - applyViewport(); - if (disposed) { - return; - } + function applyViewport() { const current = measureViewport(); - if (current.width === 0 || current.height === 0) { + if (current.width === 0 || current.height === 0 || !renderer) { return; } - // 首次拿到非零尺寸:按真实比例重算取景并复位,用户此后的缩放不再被覆盖。 - // 此后只有宽高比确实变了(可缩放的面板)才重算,且保留用户当前的朝向与相对缩放。 - reframeIfAspectChanged(current.aspect, hasFramedViewport); - }); - resizeObserver.observe(container); - } + renderer.setSize(current.width, current.height, false); + camera.aspect = current.aspect; + camera.updateProjectionMatrix(); + } - function renderFrame() { - if (disposed || !renderer || !scene) { - return; + /** + * 把取景套到相机与轨道控制器上。`preserveCamera` 时保留用户当前的观察方向与相对 + * 缩放,只把相机沿原方向搬到新取景距离上;否则回到取景复位视角。 + */ + function applyFraming(next: Model3dViewerFraming, preserveCamera: boolean) { + const position = preserveCamera + ? resolveModel3dViewerReframedCameraPosition({ + previous: framing, + next, + cameraPosition: { + x: camera.position.x, + y: camera.position.y, + z: camera.position.z, + }, + }) + : { + x: next.center.x + next.resetDirection.x * next.distance, + y: next.center.y + next.resetDirection.y * next.distance, + z: next.center.z + next.resetDirection.z * next.distance, + }; + camera.near = next.near; + camera.far = next.far; + camera.updateProjectionMatrix(); + orbitControls.minDistance = next.minDistance; + orbitControls.maxDistance = next.maxDistance; + orbitControls.target.set(next.center.x, next.center.y, next.center.z); + camera.position.set(position.x, position.y, position.z); + orbitControls.update(); + framing = next; + } + + /** + * 宽高比确实变化时按新比例重算取景并套用:比例没变(同比例缩放容器)就保留现有取景, + * 不打断用户当前视角。返回是否重算了取景。 + */ + function reframeIfAspectChanged(aspect: number, preserveCamera: boolean) { + if ( + hasFramedViewport && + Math.abs(aspect - framedAspect) <= + framedAspect * FRAMING_ASPECT_TOLERANCE + ) { + return false; + } + applyFraming(computeFraming(aspect), preserveCamera); + framedAspect = aspect; + hasFramedViewport = true; + return true; + } + + /** + * 复位视角:按容器当前宽高比重算取景,不复用可能过期的 `framing` —— 容器变窄后旧取景的 + * 距离与缩放上下限都不再贴合,跟着旧取景复位会把模型裁掉或放得过小。 + */ + function resetView() { + const current = measureViewport(); + const measurable = current.width > 0 && current.height > 0; + // 容器量不到尺寸(隐藏面板、首帧)时只能按现有取景复位,等量到尺寸再重算。 + applyFraming( + measurable ? computeFraming(current.aspect) : framing, + false, + ); + if (measurable) { + framedAspect = current.aspect; + hasFramedViewport = true; + } + } + + // 首帧按创建时的比例(容器量不到尺寸时是方形兜底)套一次取景;真实比例由下面的 + // ResizeObserver 首轮回调重算,避免在这里重复一次顶点采样。 + applyFraming(framing, false); + applyViewport(); + + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(() => { + applyViewport(); + if (disposed) { + return; + } + const current = measureViewport(); + if (current.width === 0 || current.height === 0) { + return; + } + // 首次拿到非零尺寸:按真实比例重算取景并复位,用户此后的缩放不再被覆盖。 + // 此后只有宽高比确实变了(可缩放的面板)才重算,且保留用户当前的朝向与相对缩放。 + reframeIfAspectChanged(current.aspect, hasFramedViewport); + }); + resizeObserver.observe(container); + } + + function renderFrame() { + if (disposed || !renderer || !scene) { + return; + } + frameHandle = window.requestAnimationFrame(renderFrame); + orbitControls.update(); + renderer.render(scene, camera); } frameHandle = window.requestAnimationFrame(renderFrame); - orbitControls.update(); - renderer.render(scene, camera); - } - frameHandle = window.requestAnimationFrame(renderFrame); - return { - resetView, - setAutoRotate(enabled: boolean) { - orbitControls.autoRotate = enabled; - }, - dispose: teardown, - }; + return { + resetView, + setAutoRotate(enabled: boolean) { + orbitControls.autoRotate = enabled; + }, + dispose: teardown, + }; + } + + try { + return setupLoadedScene(); + } catch (error) { + teardown(); + throw error; + } }