Use Z-up machine coordinates in GLB manager
This commit is contained in:
@@ -6,7 +6,7 @@ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
||||
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
|
||||
import { DraggableDialog } from "./DraggableDialog";
|
||||
import { disposeMachineScene, type ImportedMachine } from "./imported-machine";
|
||||
import { buildManagedMachine, cloneMachinePartScene, expectedParentRole, loadMachinePart, MACHINE_PART_ROLES, roleLabel, type MachinePartDraft, type MachinePartRole, type PhysicalZero } from "./machine-manager";
|
||||
import { buildManagedMachine, cloneMachinePartScene, expectedParentRole, loadMachinePart, machineEulerToThreeQuaternion, machineFrameQuaternion, machineToThree, threeQuaternionToMachineEuler, threeToMachine, MACHINE_PART_ROLES, roleLabel, type MachinePartDraft, type MachinePartRole, type PhysicalZero } from "./machine-manager";
|
||||
import type { Vec3 } from "./machine-urdf";
|
||||
|
||||
type Props = { coordinates: "XYZAC" | "XYZBC"; disabled: boolean; onApply: (machine: ImportedMachine) => Promise<void> };
|
||||
@@ -26,33 +26,39 @@ function ManagerPreview({ parts, selectedId, mode, onTransform }: { parts: Machi
|
||||
const light = new THREE.DirectionalLight(0xfff5e5, 4.2); light.position.set(3, 5, 4); light.castShadow = true; scene.add(light);
|
||||
const fill = new THREE.DirectionalLight(0xdcefff, 2.4); fill.position.set(-4, 2, 3); scene.add(fill);
|
||||
const rim = new THREE.DirectionalLight(0xffffff, 1.8); rim.position.set(2, 4, -5); scene.add(rim);
|
||||
const grid = new THREE.GridHelper(4, 40, 0x8eb0b2, 0x466266); scene.add(grid);
|
||||
const grid = new THREE.GridHelper(4, 40, 0x8eb0b2, 0x466266); grid.name = "machine-xy-ground-grid-z-up"; scene.add(grid);
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, .005, 100); camera.position.set(2.4, 1.8, 2.8);
|
||||
const orbit = new OrbitControls(camera, renderer.domElement); orbit.enableDamping = true;
|
||||
const root = new THREE.Group(); root.name = "machine-manager-preview"; scene.add(root);
|
||||
const nodes = new Map<string, THREE.Group>(); const disposable: THREE.Object3D[] = [];
|
||||
for (const part of parts) {
|
||||
const node = new THREE.Group(); node.name = `draft_${part.id}`; node.visible = part.visible; node.position.fromArray(part.translationMm).multiplyScalar(.001); node.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3);
|
||||
const visual = cloneMachinePartScene(part.scene); visual.position.fromArray(part.originMm).multiplyScalar(-.001); node.add(visual); disposable.push(visual);
|
||||
const axes = new THREE.AxesHelper(.12); axes.name = `origin_${part.id}`; axes.visible = part.showOrigin; node.add(axes); root.add(node); nodes.set(part.id, node);
|
||||
const node = new THREE.Group(); node.name = `draft_${part.id}`; node.visible = part.visible; node.position.fromArray(machineToThree(part.translationMm)).multiplyScalar(.001); node.quaternion.copy(machineEulerToThreeQuaternion(part.rotationDeg));
|
||||
const visual = cloneMachinePartScene(part.scene); visual.position.fromArray(machineToThree(part.originMm).map((value) => -value) as Vec3).multiplyScalar(.001); node.add(visual); disposable.push(visual);
|
||||
const axes = new THREE.Group(); axes.name = `origin_${part.id}`; axes.visible = part.showOrigin;
|
||||
const axis = (end: THREE.Vector3, color: number) => { const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), end]); axes.add(new THREE.Line(geometry, new THREE.LineBasicMaterial({ color }))); };
|
||||
axis(new THREE.Vector3(.12, 0, 0), 0xff3333); axis(new THREE.Vector3(0, 0, -.12), 0x33cc66); axis(new THREE.Vector3(0, .12, 0), 0x3388ff); node.add(axes); root.add(node); nodes.set(part.id, node);
|
||||
}
|
||||
const cycle = (child: string, parent: string) => { let cursor: string | null = parent; const seen = new Set([child]); while (cursor) { if (seen.has(cursor)) return true; seen.add(cursor); cursor = parts.find((part) => part.id === cursor)?.parentId ?? null; } return false; };
|
||||
for (const part of parts) if (part.parentId && nodes.has(part.parentId) && !cycle(part.id, part.parentId)) nodes.get(part.parentId)!.add(nodes.get(part.id)!);
|
||||
const bounds = new THREE.Box3().setFromObject(root); const center = bounds.isEmpty() ? new THREE.Vector3() : bounds.getCenter(new THREE.Vector3()); const size = bounds.isEmpty() ? 1 : Math.max(bounds.getSize(new THREE.Vector3()).length(), .2);
|
||||
orbit.target.copy(center); camera.position.copy(center).add(new THREE.Vector3(size, size * .7, size * 1.15)); orbit.update();
|
||||
const transform = new TransformControls(camera, renderer.domElement); transform.setMode(mode); transform.setSpace("local"); scene.add(transform.getHelper());
|
||||
const selected = selectedId ? nodes.get(selectedId) : null; if (selected) transform.attach(selected);
|
||||
const selected = selectedId ? nodes.get(selectedId) : null; const proxy = selected ? new THREE.Group() : null;
|
||||
const frameQuat = machineFrameQuaternion();
|
||||
if (selected && proxy) { proxy.name = "machine-coordinate-transform-proxy"; proxy.position.copy(selected.getWorldPosition(new THREE.Vector3())); proxy.quaternion.copy(frameQuat).multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(...parts.find((part) => part.id === selectedId)!.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3, "XYZ"))); scene.add(proxy); transform.attach(proxy); }
|
||||
transform.addEventListener("dragging-changed", ((event: { value?: boolean }) => { orbit.enabled = !event.value; }) as never);
|
||||
transform.addEventListener("mouseUp", (() => {
|
||||
if (!selectedId || !selected) return;
|
||||
onTransformRef.current(selectedId, selected.position.toArray().map((value) => value * 1000) as Vec3, [THREE.MathUtils.radToDeg(selected.rotation.x), THREE.MathUtils.radToDeg(selected.rotation.y), THREE.MathUtils.radToDeg(selected.rotation.z)]);
|
||||
transform.addEventListener("objectChange", (() => {
|
||||
if (!selectedId || !selected || !proxy) return;
|
||||
const local = selected.parent ? selected.parent.worldToLocal(proxy.position.clone()) : proxy.position.clone(); selected.position.copy(local);
|
||||
const machineRotation = threeQuaternionToMachineEuler(proxy.quaternion); selected.quaternion.copy(machineEulerToThreeQuaternion(machineRotation));
|
||||
onTransformRef.current(selectedId, threeToMachine(selected.position.toArray() as Vec3).map((value) => value * 1000) as Vec3, machineRotation);
|
||||
}) as never);
|
||||
const resize = () => { const width = Math.max(element.clientWidth, 1), height = Math.max(element.clientHeight, 1); camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height, false); };
|
||||
const observer = new ResizeObserver(resize); observer.observe(element); resize(); let frame = 0;
|
||||
const render = () => { orbit.update(); renderer.render(scene, camera); frame = requestAnimationFrame(render); }; render();
|
||||
return () => { cancelAnimationFrame(frame); observer.disconnect(); transform.dispose(); orbit.dispose(); for (const object of disposable) disposeMachineScene(object); environment.dispose(); renderer.dispose(); renderer.domElement.remove(); };
|
||||
return () => { cancelAnimationFrame(frame); observer.disconnect(); transform.dispose(); orbit.dispose(); if (proxy) proxy.removeFromParent(); for (const object of disposable) disposeMachineScene(object); environment.dispose(); renderer.dispose(); renderer.domElement.remove(); };
|
||||
}, [parts, selectedId, mode]);
|
||||
return <div ref={host} className="machine-manager-preview" data-machine-manager-preview data-machine-manager-lighting="studio-pmrem" data-machine-manager-exposure="1.45" data-machine-manager-part-count={parts.length} data-machine-manager-origin-count={parts.filter((part) => part.showOrigin).length} data-transform-mode={mode} aria-label="三维机床装配预览" />;
|
||||
return <div ref={host} className="machine-manager-preview" data-machine-manager-preview data-machine-manager-coordinate-system="machine-z-up" data-machine-manager-axis-map="X:x,Y:-z,Z:y" data-machine-manager-lighting="studio-pmrem" data-machine-manager-exposure="1.45" data-machine-manager-part-count={parts.length} data-machine-manager-origin-count={parts.filter((part) => part.showOrigin).length} data-transform-mode={mode} aria-label="三维机床装配预览,机床 Z 轴朝上" />;
|
||||
}
|
||||
|
||||
export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) {
|
||||
|
||||
@@ -28,6 +28,23 @@ export const MACHINE_PART_ROLES: MachinePartRole[] = ["base", "x", "y", "z", "to
|
||||
export const roleLabel = (role: MachinePartRole, coordinates: string) => ({ base: "床身底座", x: "X 轴部件", y: "Y 轴部件", z: "Z 轴部件", tool: "主轴/刀具", tilt: `${coordinates === "XYZBC" ? "B" : "A"} 轴部件`, c: "C 轴部件", work: "工件/工作台" }[role]);
|
||||
export const expectedParentRole = (role: MachinePartRole): MachinePartRole | null => ({ base: null, x: "base", y: "x", z: "y", tool: "z", tilt: "base", c: "tilt", work: "c" }[role] as MachinePartRole | null);
|
||||
|
||||
// glTF uses Y-up. The editor exposes the machine's right-handed X/Y/Z frame
|
||||
// with Z-up: machine (x, y, z) -> Three.js (x, z, -y).
|
||||
export const machineToThree = (value: Vec3): Vec3 => [value[0], value[2], -value[1]];
|
||||
export const threeToMachine = (value: Vec3): Vec3 => [value[0], -value[2], value[1]];
|
||||
export const machineFrameQuaternion = () => new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().set(1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1));
|
||||
export function machineEulerToThreeQuaternion(rotationDeg: Vec3) {
|
||||
const machine = new THREE.Euler(...rotationDeg.map(THREE.MathUtils.degToRad) as Vec3, "XYZ");
|
||||
const source = new THREE.Matrix4().makeRotationFromEuler(machine);
|
||||
const frame = new THREE.Matrix4().set(1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1);
|
||||
return new THREE.Quaternion().setFromRotationMatrix(frame.clone().multiply(source).multiply(frame.clone().invert()));
|
||||
}
|
||||
export function threeQuaternionToMachineEuler(rotation: THREE.Quaternion) {
|
||||
const frame = machineFrameQuaternion();
|
||||
const machine = frame.clone().invert().multiply(rotation);
|
||||
return new THREE.Euler().setFromQuaternion(machine, "XYZ").toArray().slice(0, 3).map((radians) => THREE.MathUtils.radToDeg(Number(radians))) as Vec3;
|
||||
}
|
||||
|
||||
const finite3 = (value: Vec3, label: string) => {
|
||||
if (value.length !== 3 || value.some((number) => !Number.isFinite(number))) throw new Error(`${label}需要三个有限数值`);
|
||||
};
|
||||
@@ -120,10 +137,10 @@ export async function buildManagedMachine(input: { coordinates: "XYZAC" | "XYZBC
|
||||
for (const role of MACHINE_PART_ROLES) {
|
||||
const part = byRole.get(role)!;
|
||||
const placement = new THREE.Group(); placement.name = `component_${part.id}`; placement.visible = part.visible;
|
||||
placement.position.fromArray(part.translationMm).multiplyScalar(.001);
|
||||
placement.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3);
|
||||
placement.position.fromArray(machineToThree(part.translationMm)).multiplyScalar(.001);
|
||||
placement.quaternion.copy(machineEulerToThreeQuaternion(part.rotationDeg));
|
||||
placement.userData.machinePart = { role, filename: part.filename, hash: part.hash, originMm: [...part.originMm] };
|
||||
const visual = cloneMachinePartScene(part.scene); visual.position.fromArray(part.originMm).multiplyScalar(-.001); placement.add(visual); links[role].add(placement);
|
||||
const visual = cloneMachinePartScene(part.scene); visual.position.fromArray(machineToThree(part.originMm).map((value) => -value) as Vec3).multiplyScalar(.001); placement.add(visual); links[role].add(placement);
|
||||
}
|
||||
links.base.userData.linuxcncMachineGeometry = metadata;
|
||||
const signature = JSON.stringify({ metadata, hashes: MACHINE_PART_ROLES.map((role) => byRole.get(role)!.hash) });
|
||||
|
||||
Reference in New Issue
Block a user