修复喷头拆解视图裁切并验证完整部件取景

This commit is contained in:
xuans 2026-09-18 22:03:51 +08:00
parent 128152ead4
commit b40c21bb53
3 changed files with 87 additions and 15 deletions

View File

@ -65,6 +65,9 @@ try {
[1440, 900], [1440, 900],
[1280, 720], [1280, 720],
[1920, 1080], [1920, 1080],
[2048, 990],
[2560, 1238],
[1366, 768],
[390, 844], [390, 844],
]) { ]) {
await page.setViewportSize({ width, height }); await page.setViewportSize({ width, height });
@ -75,6 +78,7 @@ try {
width: document.documentElement.scrollWidth, width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight, height: document.documentElement.scrollHeight,
scene: rect(document.querySelector("#scene")), scene: rect(document.querySelector("#scene")),
parts: JSON.parse(document.querySelector("#scene").dataset.nozzleBounds),
labels: [...document.querySelectorAll(".nozzle-label")].map(rect), labels: [...document.querySelectorAll(".nozzle-label")].map(rect),
panel: rect(document.querySelector('[data-panel="nozzle"]')), panel: rect(document.querySelector('[data-panel="nozzle"]')),
foot: rect( foot: rect(
@ -83,6 +87,14 @@ try {
}; };
}); });
assert.ok(layout.width <= width); assert.ok(layout.width <= width);
assert.equal(layout.parts.length, 12);
layout.parts.forEach((part, index) => {
assert.ok(part.top >= 20 && part.bottom <= layout.scene.height - 28,
`${width} 部件 ${index + 1} 上下未裁切:${JSON.stringify(part)}`);
const inset = (layout.scene.width < 550 ? 90 : 144) + 8;
assert.ok(part.left >= inset && part.right <= layout.scene.width - inset,
`${width} 部件 ${index + 1} 不被标签遮挡`);
});
if (width >= 1100) { if (width >= 1100) {
assert.ok(layout.height <= height); assert.ok(layout.height <= height);
assert.ok(layout.foot.bottom <= layout.panel.bottom + 1); assert.ok(layout.foot.bottom <= layout.panel.bottom + 1);
@ -111,6 +123,7 @@ try {
height, height,
scene: layout.scene, scene: layout.scene,
labels: layout.labels.length, labels: layout.labels.length,
parts: layout.parts,
}); });
} }
await page.setViewportSize({ width: 1440, height: 900 }); await page.setViewportSize({ width: 1440, height: 900 });

View File

@ -0,0 +1,41 @@
import * as THREE from "three";
export function boxCorners(box) {
return [box.min.x, box.max.x].flatMap((x) =>
[box.min.y, box.max.y].flatMap((y) =>
[box.min.z, box.max.z].map((z) => new THREE.Vector3(x, y, z)),
),
);
}
// 将部件包围盒全部纳入透视视锥,并留出两侧标签与底部说明空间。
export function fitNozzleCamera(camera, controls, boxes, width, height) {
const bounds = boxes.reduce((result, box) => result.union(box), new THREE.Box3());
const center = bounds.getCenter(new THREE.Vector3());
const backward = camera.position.clone().sub(controls.target).normalize();
const right = new THREE.Vector3().crossVectors(camera.up, backward).normalize();
const up = new THREE.Vector3().crossVectors(backward, right).normalize();
const labelWidth = width < 550 ? 90 : 144;
const horizontal = Math.max(0.15, 1 - (2 * (labelWidth + 22)) / width);
const vertical = Math.max(0.2, 1 - 80 / height);
const tangent = Math.tan(THREE.MathUtils.degToRad(camera.getEffectiveFOV() / 2));
let distance = controls.minDistance;
for (const box of boxes) {
for (const point of boxCorners(box)) {
const relative = point.sub(center);
const depth = relative.dot(backward);
distance = Math.max(distance,
depth + Math.abs(relative.dot(right)) / (tangent * camera.aspect * horizontal),
depth + Math.abs(relative.dot(up)) / (tangent * vertical));
}
}
distance *= 1.04;
controls.maxDistance = Math.max(1000, distance * 2);
controls.target.copy(center);
camera.position.copy(center).addScaledVector(backward, distance);
camera.far = Math.max(1600, distance * 4);
camera.updateProjectionMatrix();
camera.lookAt(center);
controls.update();
camera.updateMatrixWorld(true);
}

View File

@ -1,5 +1,6 @@
import * as THREE from "three"; import * as THREE from "three";
import "./Nozzle.css"; import "./Nozzle.css";
import { boxCorners, fitNozzleCamera } from "./NozzleFraming.js";
const modules = [ const modules = [
{ {
@ -116,6 +117,10 @@ export class NozzleScene {
return group; return group;
}); });
this.build(); this.build();
this.partBounds = this.parts.map((part) => {
const box = new THREE.Box3().setFromObject(part);
return box.translate(part.position.clone().negate());
});
this.overlay = document.createElement("div"); this.overlay = document.createElement("div");
this.overlay.className = "nozzle-annotations"; this.overlay.className = "nozzle-annotations";
this.overlay.hidden = true; this.overlay.hidden = true;
@ -287,30 +292,33 @@ export class NozzleScene {
} }
focus() { focus() {
const { camera, controls } = this.owner;
camera.position.copy(controls.target).add(new THREE.Vector3(0.32, 0.42, 1).multiplyScalar(340));
this.fit();
}
boxes() {
this.group.updateMatrixWorld(true);
return this.parts.map((part, index) => this.partBounds[index].clone().applyMatrix4(part.matrixWorld));
}
fit() {
const { camera, controls, container } = this.owner; const { camera, controls, container } = this.owner;
const narrow = container.clientWidth < 550; if (!container.clientWidth || !container.clientHeight) return;
const available = Math.max( fitNozzleCamera(camera, controls, this.boxes(), container.clientWidth, container.clientHeight);
0.3,
(container.clientWidth - (narrow ? 180 : 300)) / container.clientWidth,
);
const distance = Math.max(340, 170 / (camera.aspect * available * 0.63));
controls.target.set(0, 22, 0);
camera.position.copy(
new THREE.Vector3(0.32, 0.42, 1)
.normalize()
.multiplyScalar(distance)
.add(controls.target),
);
controls.update();
} }
update(disassembled) { update(disassembled) {
this.amount += ((disassembled ? 1 : 0) - this.amount) * 0.09; const target = disassembled ? 1 : 0;
const changing = Math.abs(target - this.amount) > 0.0001;
this.amount = changing ? this.amount + (target - this.amount) * 0.09 : target;
this.parts.forEach((part, i) => { this.parts.forEach((part, i) => {
part.position part.position
.fromArray(modules[i].base) .fromArray(modules[i].base)
.addScaledVector(new THREE.Vector3(...modules[i].offset), this.amount); .addScaledVector(new THREE.Vector3(...modules[i].offset), this.amount);
}); });
if (changing || this.lastDisassembled !== disassembled) this.fit();
this.lastDisassembled = disassembled;
this.owner.container.dataset.nozzleExplode = this.amount.toFixed(3); this.owner.container.dataset.nozzleExplode = this.amount.toFixed(3);
this.annotate(); this.annotate();
} }
@ -328,6 +336,16 @@ export class NozzleScene {
labelWidth = narrow ? 90 : 144; labelWidth = narrow ? 90 : 144;
const slots = [[], []]; const slots = [[], []];
this.group.updateMatrixWorld(true); this.group.updateMatrixWorld(true);
camera.updateMatrixWorld(true);
this.owner.container.dataset.nozzleBounds = JSON.stringify(this.boxes().map((box) => {
const points = boxCorners(box).map((point) => point.project(camera));
return {
left: Math.min(...points.map((point) => (point.x + 1) * w / 2)),
right: Math.max(...points.map((point) => (point.x + 1) * w / 2)),
top: Math.min(...points.map((point) => (1 - point.y) * h / 2)),
bottom: Math.max(...points.map((point) => (1 - point.y) * h / 2)),
};
}));
this.parts.forEach((part, i) => { this.parts.forEach((part, i) => {
const p = part.getWorldPosition(new THREE.Vector3()).project(camera); const p = part.getWorldPosition(new THREE.Vector3()).project(camera);
slots[i % 2].push({ i, x: ((p.x + 1) * w) / 2, y: ((1 - p.y) * h) / 2 }); slots[i % 2].push({ i, x: ((p.x + 1) * w) / 2, y: ((1 - p.y) * h) / 2 });