查看器取景装配失败时释放 renderer 与模型资源
- packages/model3d-viewer/src/scene.ts:模型加载成功后的取景 / ResizeObserver / 渲染循环装配抽成 setupLoadedScene,由外层 try/catch 兜住;任何异常先 teardown 再抛出,避免调用方拿不到 scene 引用时漏掉 WebGL 上下文与模型 GPU 资源
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user