Add draggable GLB hierarchy tree and node origins

This commit is contained in:
2026-09-20 18:53:30 +08:00
parent 53285d04af
commit 72b38b7d43
19 changed files with 150 additions and 38 deletions

View File

@@ -13,7 +13,9 @@ export type MachinePartDraft = MachinePartSource & {
originMm: Vec3;
translationMm: Vec3;
rotationDeg: Vec3;
nodeParents: Record<string, string | null>;
};
export type MachineTreeNode = { path: string; name: string; type: "group" | "mesh"; originMm: Vec3; children: MachineTreeNode[] };
export type PhysicalZero = { x: number; y: number; z: number; a: number; b: number; c: number };
export type ManagedMachineGeometry = {
schema: "linuxcnc.machine-manager/1";
@@ -21,7 +23,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 }>;
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> }>;
};
export const MACHINE_PART_ROLES: MachinePartRole[] = ["base", "x", "y", "z", "tool", "tilt", "c", "work"];
@@ -47,6 +49,40 @@ export function cloneMachinePartScene(source: THREE.Object3D) {
return clone;
}
function scenePathMap(scene: THREE.Object3D) {
const nodes = new Map<string, THREE.Object3D>(), paths = new WeakMap<THREE.Object3D, string>();
const visit = (node: THREE.Object3D, path: string) => { nodes.set(path, node); paths.set(node, path); node.children.forEach((child, index) => visit(child, `${path}/${index}`)); };
visit(scene, "scene"); return { nodes, paths };
}
export function applyMachineNodeParents(scene: THREE.Object3D, overrides: Record<string, string | null>) {
const { nodes } = scenePathMap(scene);
for (const [sourcePath, targetPath] of Object.entries(overrides ?? {})) {
const source = nodes.get(sourcePath), target = targetPath === null ? scene : nodes.get(targetPath);
if (!source || source === scene || !target || source === target) throw new Error("GLB 层级拖放目标无效");
let cursor: THREE.Object3D | null = target;
while (cursor) { if (cursor === source) throw new Error("GLB 层级不能形成循环"); cursor = cursor.parent; }
scene.updateMatrixWorld(true); target.attach(source); scene.updateMatrixWorld(true);
}
return scene;
}
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 ?? {});
const build = (node: THREE.Object3D): MachineTreeNode => ({
path: paths.get(node)!,
name: node.name || (paths.get(node) === "scene" ? part.filename : `${(node as THREE.Mesh).isMesh ? "Mesh" : "Node"} ${paths.get(node)!.split("/").at(-1)}`),
type: (node as THREE.Mesh).isMesh ? "mesh" : "group",
originMm: node.position.toArray().map((value) => value * 1000) as Vec3,
children: node.children.map(build),
});
return build(scene);
}
export async function loadMachinePart(bytes: ArrayBuffer, filename: string): Promise<MachinePartSource> {
if (!bytes.byteLength) throw new Error("GLB 部件为空");
const manager = new THREE.LoadingManager();
@@ -109,7 +145,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 }; }),
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 ?? {}) } }; }),
};
const scene = new THREE.Group(); scene.name = `Managed_${input.coordinates}`;
scene.userData.linuxcncMachine = { schema: "linuxcnc.machine/1", urdf };
@@ -124,7 +160,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 = cloneMachinePartScene(part.scene); visual.position.fromArray(part.originMm).multiplyScalar(-.001); placement.add(visual); links[role].add(placement);
const visual = applyMachineNodeParents(cloneMachinePartScene(part.scene), part.nodeParents ?? {}); visual.position.fromArray(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) });