容器尺寸变化时只重算相机取景,不再重新采一遍顶点
- packages/model3d-viewer/framing:取景求解器把每个采样点投影后的「上下向 / 左右向 / 深度」存成样本,resolve 可以带上新的宽高比重算;顶点投影与宽高比无关,比例变化时只在这份样本上重取最大值,距离算法与取值口径不变(含非有限顶点作废整次取景的旧行为) - packages/model3d-viewer/scene:顶点采样改成加载后只做一次,computeFraming 只调用 resolve(aspect);拖拽面板连续触发 ResizeObserver 时不再重复 updateMatrixWorld 与逐顶点读取(单网格上限 20 万点) - framing.test:补两条用例——带新比例重算与全新求解器结果一致、非有限顶点回落到与尺度相关的默认距离
This commit is contained in:
@@ -203,6 +203,48 @@ describe('createModel3dViewerFitSolver', () => {
|
||||
expect(Number.isFinite(fit)).toBe(true);
|
||||
expect(fit).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('带上新比例可复用同一份顶点样本重算取景距离', () => {
|
||||
const rod: Point[] = [
|
||||
[-1, -1, -1],
|
||||
[1, 1, 1],
|
||||
];
|
||||
const solver = createModel3dViewerFitSolver({
|
||||
center: computeModel3dViewerBoxCenter(CUBE),
|
||||
aspect: 2,
|
||||
fallbackRadius: computeModel3dViewerBoxRadius(CUBE),
|
||||
});
|
||||
for (const [x, y, z] of rod) {
|
||||
solver.add(x, y, z);
|
||||
}
|
||||
|
||||
const wide = solver.resolve(2);
|
||||
const narrow = solver.resolve(0.5);
|
||||
|
||||
expect(wide).toBeCloseTo(solveForPoints(CUBE, rod, 2), 6);
|
||||
expect(narrow).toBeCloseTo(solveForPoints(CUBE, rod, 0.5), 6);
|
||||
expect(narrow).toBeGreaterThan(wide);
|
||||
// 复用样本:再问一次同一个比例,结果不变。
|
||||
expect(solver.resolve(2)).toBe(wide);
|
||||
});
|
||||
|
||||
it('顶点里出现非有限值时整次取景回落到与尺度相关的默认距离', () => {
|
||||
const solver = createModel3dViewerFitSolver({
|
||||
center: computeModel3dViewerBoxCenter(CUBE),
|
||||
aspect: 1,
|
||||
fallbackRadius: computeModel3dViewerBoxRadius(CUBE),
|
||||
});
|
||||
solver.add(Number.NaN, 0, 0);
|
||||
solver.add(1, 1, 1);
|
||||
// 没有可用顶点时的同一口径:退化取景距离只由包围盒尺度决定。
|
||||
const withoutSamples = createModel3dViewerFitSolver({
|
||||
center: computeModel3dViewerBoxCenter(CUBE),
|
||||
aspect: 1,
|
||||
fallbackRadius: computeModel3dViewerBoxRadius(CUBE),
|
||||
}).resolve(1);
|
||||
|
||||
expect(solver.resolve(1)).toBeCloseTo(withoutSamples, 6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeModel3dViewerFraming', () => {
|
||||
|
||||
@@ -52,7 +52,12 @@ export type Model3dViewerFitSolverInput = {
|
||||
|
||||
export type Model3dViewerFitSolver = {
|
||||
add(x: number, y: number, z: number): void;
|
||||
resolve(): number;
|
||||
/**
|
||||
* 取景距离。`aspect` 省略时用创建时的比例;带上新比例就能在**不重新采顶点**的前提下重算 ——
|
||||
* 顶点投影出来的三个量(上下向、左右向、深度)与容器宽高比无关,只有左右向的视野上限随比例
|
||||
* 变化,所以容器变窄变宽只需要在这一份样本上重取一次最大值。
|
||||
*/
|
||||
resolve(aspect?: number): number;
|
||||
};
|
||||
|
||||
export type Model3dViewerFraming = {
|
||||
@@ -186,26 +191,67 @@ export function createModel3dViewerFitSolver(
|
||||
|
||||
const halfFovY = (fovDegrees * Math.PI) / 360;
|
||||
const limitY = fitFraction * Math.tan(halfFovY);
|
||||
const limitX = limitY * aspect;
|
||||
|
||||
let required = 0;
|
||||
const fallbackDistance = fallbackRadius / (fitFraction * Math.tan(halfFovY));
|
||||
|
||||
// 采样点按「上下向投影绝对值 / 左右向投影绝对值 / 深度」三个浮点数存下来,比例变化时直接在
|
||||
// 这份样本上重算,不必再走一遍 updateMatrixWorld + 逐顶点读取(单个网格上限 20 万点)。
|
||||
let sampleCount = 0;
|
||||
let samples = new Float32Array(3 * 4096);
|
||||
// 非法顶点照旧要作废整次取景:旧实现里 NaN 会把累加出的最大值污染成 NaN,这里保持同一口径。
|
||||
let hasInvalidSample = false;
|
||||
|
||||
function resolveForAspect(value: number): number {
|
||||
if (hasInvalidSample) {
|
||||
return fallbackDistance;
|
||||
}
|
||||
const limitX = limitY * normalizePositive(value, FALLBACK_ASPECT);
|
||||
let required = 0;
|
||||
for (let index = 0; index < sampleCount; index += 1) {
|
||||
const base = index * 3;
|
||||
const upAbs = samples[base] ?? 0;
|
||||
const rightAbs = samples[base + 1] ?? 0;
|
||||
const depth = samples[base + 2] ?? 0;
|
||||
const byUp = upAbs / limitY - depth;
|
||||
if (byUp > required) {
|
||||
required = byUp;
|
||||
}
|
||||
const byRight = rightAbs / limitX - depth;
|
||||
if (byRight > required) {
|
||||
required = byRight;
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(required) || required <= 0) {
|
||||
return fallbackDistance;
|
||||
}
|
||||
return Math.max(required * FIT_SAFETY, fallbackRadius);
|
||||
}
|
||||
|
||||
return {
|
||||
add(x: number, y: number, z: number) {
|
||||
if (sampleCount * 3 + 3 > samples.length) {
|
||||
const grown = new Float32Array(samples.length * 2);
|
||||
grown.set(samples);
|
||||
samples = grown;
|
||||
}
|
||||
const corner = subtract({ x, y, z }, center);
|
||||
const depth = dot(corner, forward);
|
||||
required = Math.max(
|
||||
required,
|
||||
Math.abs(dot(corner, up)) / limitY - depth,
|
||||
Math.abs(dot(corner, right)) / limitX - depth,
|
||||
);
|
||||
},
|
||||
resolve() {
|
||||
if (!Number.isFinite(required) || required <= 0) {
|
||||
return fallbackDistance;
|
||||
const upAbs = Math.abs(dot(corner, up));
|
||||
const rightAbs = Math.abs(dot(corner, right));
|
||||
if (
|
||||
!Number.isFinite(upAbs) ||
|
||||
!Number.isFinite(rightAbs) ||
|
||||
!Number.isFinite(depth)
|
||||
) {
|
||||
hasInvalidSample = true;
|
||||
}
|
||||
return Math.max(required * FIT_SAFETY, fallbackRadius);
|
||||
const base = sampleCount * 3;
|
||||
samples[base] = upAbs;
|
||||
samples[base + 1] = rightAbs;
|
||||
samples[base + 2] = depth;
|
||||
sampleCount += 1;
|
||||
},
|
||||
resolve(value?: number) {
|
||||
return resolveForAspect(value ?? aspect);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -285,27 +285,30 @@ export async function createModel3dViewerScene(
|
||||
const bounds = new THREE.Box3().setFromObject(model3d);
|
||||
const viewport = measureViewport();
|
||||
|
||||
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: viewport.aspect,
|
||||
fovDegrees: MODEL3D_VIEWER_CAMERA_FOV_DEGREES,
|
||||
fallbackRadius: computeModel3dViewerBoxRadius(fitBox),
|
||||
});
|
||||
// 顶点采样只做一次:投影结果与容器宽高比无关,容器尺寸变化只是在同一份样本上重取最大值,
|
||||
// 不再重复 updateMatrixWorld + 逐顶点读取(拖拽面板时 ResizeObserver 会连续回调)。
|
||||
feedModel3dViewerFitSolver(model3d, (x, y, z) => fitSolver.add(x, y, z));
|
||||
|
||||
/**
|
||||
* 按给定宽高比重算取景。容器在隐藏面板里挂载时首帧量不到尺寸,只能先按方形兜底,
|
||||
* 等容器真正有尺寸后必须用真实比例重算,否则模型会按方形取景、被裁掉或显得过小。
|
||||
*/
|
||||
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(),
|
||||
distance: fitSolver.resolve(aspect),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -406,7 +409,7 @@ export async function createModel3dViewerScene(
|
||||
}
|
||||
|
||||
// 首帧按创建时的比例(容器量不到尺寸时是方形兜底)套一次取景;真实比例由下面的
|
||||
// ResizeObserver 首轮回调重算,避免在这里重复一次顶点采样。
|
||||
// ResizeObserver 首轮回调重算,重算只走缓存样本,不再重复顶点采样。
|
||||
applyFraming(framing, false);
|
||||
applyViewport();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user