Apply hierarchy node transforms relative to parents

This commit is contained in:
2026-09-21 09:11:38 +08:00
parent 63f726dc32
commit a472911ad1
19 changed files with 181 additions and 113 deletions

View File

@@ -5,6 +5,7 @@ import type { ImportedMachine } from "./imported-machine";
export type MachinePartRole = "base" | "x" | "y" | "z" | "tool" | "tilt" | "c" | "work";
export type MachinePartSource = { id: string; filename: string; hash: string; scene: THREE.Group };
export type MachineNodeTransform = { translationMm: Vec3; rotationDeg: Vec3 };
export type MachinePartDraft = MachinePartSource & {
role: MachinePartRole | "";
parentId: string | null;
@@ -15,6 +16,7 @@ export type MachinePartDraft = MachinePartSource & {
rotationDeg: Vec3;
nodeParents: Record<string, string | null>;
nodeRoles: Record<string, MachinePartRole | "">;
nodeTransforms: Record<string, MachineNodeTransform>;
};
export type MachineTreeNode = { path: string; name: string; type: "group" | "mesh"; originMm: Vec3; worldOriginMm: Vec3; rotationDeg: Vec3; children: MachineTreeNode[] };
export type PhysicalZero = { x: number; y: number; z: number; a: number; b: number; c: number };
@@ -24,7 +26,7 @@ export type ManagedMachineGeometry = {
cOriginMm: Vec3;
tiltOffsetMm: Vec3;
physicalZero: PhysicalZero;
parts: Array<{ id: string; filename: string; hash: string; role: MachinePartRole; parentId: string | null; originMm: Vec3; translationMm: Vec3; rotationDeg: Vec3; visible: boolean; nodeParents: Record<string, string | null>; nodeRoles: Record<string, MachinePartRole | ""> }>;
parts: Array<{ id: string; filename: string; hash: string; role: MachinePartRole; parentId: string | null; originMm: Vec3; translationMm: Vec3; rotationDeg: Vec3; visible: boolean; nodeParents: Record<string, string | null>; nodeRoles: Record<string, MachinePartRole | "">; nodeTransforms: Record<string, MachineNodeTransform> }>;
};
export const MACHINE_PART_ROLES: MachinePartRole[] = ["base", "x", "y", "z", "tool", "tilt", "c", "work"];
@@ -68,12 +70,46 @@ export function applyMachineNodeParents(scene: THREE.Object3D, overrides: Record
return scene;
}
// Index source paths before reparenting so edits and axis bindings keep their identity.
export function configureMachinePartScene(scene: THREE.Object3D, part: Pick<MachinePartDraft, "nodeParents" | "nodeTransforms">) {
const { nodes, paths } = scenePathMap(scene);
applyMachineNodeParents(scene, part.nodeParents ?? {});
for (const [path, transform] of Object.entries(part.nodeTransforms ?? {})) {
const node = nodes.get(path);
if (!node || node === scene) throw new Error("部件变换节点无效");
finite3(transform.translationMm, "节点相对父级位置");
finite3(transform.rotationDeg, "节点相对父级姿态");
node.position.fromArray(transform.translationMm).multiplyScalar(.001);
node.rotation.set(...transform.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3, "XYZ");
node.updateMatrix();
}
scene.updateMatrixWorld(true);
return { scene, nodes, paths };
}
export function reparentMachineNode(part: MachinePartDraft, sourcePath: string, targetPath: string) {
const { scene, nodes } = configureMachinePartScene(part.scene.clone(true), part);
const source = nodes.get(sourcePath), target = nodes.get(targetPath);
if (!source || source === scene || !target || source === target) throw new Error("GLB 层级拖放目标无效");
for (let cursor: THREE.Object3D | null = target; cursor; cursor = cursor.parent) {
if (cursor === source) throw new Error("GLB 层级不能形成循环");
}
target.attach(source);
// Capture edited local poses after attach: changing parent preserves the current
// assembly pose, and subsequent numeric edits use the new parent's frame.
const nodeTransforms = Object.fromEntries([...nodes].filter(([path]) => path !== "scene").map(([path, node]) => [path, {
translationMm: node.position.toArray().map((value) => value * 1000) as Vec3,
rotationDeg: new THREE.Euler().setFromQuaternion(node.quaternion, "XYZ").toArray().slice(0, 3).map((value) => THREE.MathUtils.radToDeg(Number(value))) as Vec3,
}]));
return { nodeParents: { ...part.nodeParents, [sourcePath]: targetPath === "scene" ? null : targetPath }, nodeTransforms };
}
export function machineSceneNode(scene: THREE.Object3D, path: string) {
return scenePathMap(scene).nodes.get(path) ?? null;
}
export function machinePartTree(part: Pick<MachinePartDraft, "filename" | "scene" | "nodeParents">): MachineTreeNode {
const scene = part.scene.clone(true), { paths } = scenePathMap(scene); applyMachineNodeParents(scene, part.nodeParents ?? {});
export function machinePartTree(part: Pick<MachinePartDraft, "filename" | "scene" | "nodeParents" | "nodeTransforms">): MachineTreeNode {
const { scene, paths } = configureMachinePartScene(part.scene.clone(true), part);
scene.updateMatrixWorld(true);
const build = (node: THREE.Object3D): MachineTreeNode => ({
path: paths.get(node)!,
@@ -106,6 +142,7 @@ export function validateMachineAssembly(coordinates: "XYZAC" | "XYZBC", parts: M
if (!part.role) throw new Error(`${part.filename} 尚未指定机床角色`);
if (byRole.has(part.role)) throw new Error(`${roleLabel(part.role, coordinates)}角色重复`);
finite3(part.originMm, `${part.filename}局部原点`); finite3(part.translationMm, `${part.filename}平移`); finite3(part.rotationDeg, `${part.filename}旋转`);
for (const [path, transform] of Object.entries(part.nodeTransforms ?? {})) { if (path === "scene") throw new Error(`${part.filename} 的 scene 节点不能重复定义部件变换`); finite3(transform.translationMm, `${part.filename}/${path} 相对父级位置`); finite3(transform.rotationDeg, `${part.filename}/${path} 相对父级姿态`); }
byRole.set(part.role, part);
}
for (const role of MACHINE_PART_ROLES) if (!byRole.has(role)) throw new Error(`缺少${roleLabel(role, coordinates)}`);
@@ -149,7 +186,7 @@ export async function buildManagedMachine(input: { coordinates: "XYZAC" | "XYZBC
const structure: MachineStructure = parseMachineUrdf(urdf);
const metadata: ManagedMachineGeometry = {
schema: "linuxcnc.machine-manager/1", coordinates: input.coordinates, cOriginMm: [...input.cOriginMm], tiltOffsetMm: [...input.tiltOffsetMm], physicalZero: { ...input.physicalZero },
parts: MACHINE_PART_ROLES.map((role) => { const part = byRole.get(role)!; return { id: part.id, filename: part.filename, hash: part.hash, role, parentId: part.parentId, originMm: [...part.originMm], translationMm: [...part.translationMm], rotationDeg: [...part.rotationDeg], visible: part.visible, nodeParents: { ...(part.nodeParents ?? {}) }, nodeRoles: { ...(part.nodeRoles ?? {}) } }; }),
parts: MACHINE_PART_ROLES.map((role) => { const part = byRole.get(role)!; return { id: part.id, filename: part.filename, hash: part.hash, role, parentId: part.parentId, originMm: [...part.originMm], translationMm: [...part.translationMm], rotationDeg: [...part.rotationDeg], visible: part.visible, nodeParents: { ...(part.nodeParents ?? {}) }, nodeRoles: { ...(part.nodeRoles ?? {}) }, nodeTransforms: Object.fromEntries(Object.entries(part.nodeTransforms ?? {}).map(([path, transform]) => [path, { translationMm: [...transform.translationMm] as Vec3, rotationDeg: [...transform.rotationDeg] as Vec3 }])) }; }),
};
const scene = new THREE.Group(); scene.name = `Managed_${input.coordinates}`;
scene.userData.linuxcncMachine = { schema: "linuxcnc.machine/1", urdf };
@@ -164,7 +201,7 @@ export async function buildManagedMachine(input: { coordinates: "XYZAC" | "XYZBC
placement.position.fromArray(part.translationMm).multiplyScalar(.001);
placement.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3);
placement.userData.machinePart = { role, filename: part.filename, hash: part.hash, originMm: [...part.originMm] };
const visual = applyMachineNodeParents(cloneMachinePartScene(part.scene), part.nodeParents ?? {}); visual.position.fromArray(part.originMm).multiplyScalar(-.001); placement.add(visual); links[role].add(placement);
const { scene: visual } = configureMachinePartScene(cloneMachinePartScene(part.scene), part); visual.position.add(new THREE.Vector3(...part.originMm).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) });