141 lines
12 KiB
JavaScript
141 lines
12 KiB
JavaScript
import { mkdir, writeFile } from 'node:fs/promises';
|
|
import { createHash } from 'node:crypto';
|
|
import * as THREE from '../web/react/node_modules/three/build/three.module.js';
|
|
import { GLTFExporter } from '../web/react/node_modules/three/examples/jsm/exporters/GLTFExporter.js';
|
|
// GLTFExporter uses the browser FileReader interface for embedded binary buffers.
|
|
globalThis.FileReader = class {
|
|
readAsArrayBuffer(blob) { blob.arrayBuffer().then((result) => { this.result = result; this.onloadend?.(); }); }
|
|
readAsDataURL(blob) { blob.arrayBuffer().then((result) => { this.result = `data:${blob.type};base64,${Buffer.from(result).toString('base64')}`; this.onloadend?.(); }); }
|
|
};
|
|
const output = new URL('../web/react/public/machines/', import.meta.url);
|
|
await mkdir(output, { recursive: true });
|
|
const manifest = { schema: 'linuxcnc.machine.examples/1', generator: 'scripts/generate_glb_machines.mjs', provenance: 'Original procedural demonstration machines; no third-party mesh assets', license: 'CC0-1.0', machines: [] };
|
|
for (const variant of ['standard', 'gantry']) for (const type of ['ac', 'bc']) {
|
|
const gantry = variant === 'gantry';
|
|
const profile = gantry ? 'trt-xyz-gantry/1' : 'trt-xyz-head/1';
|
|
const toolLink = gantry ? 'z_slide' : 'spindle_head';
|
|
const stem = gantry ? `demo-${type}-gantry` : `demo-${type}-xyz-head`;
|
|
const tiltAxis = type[0].toUpperCase();
|
|
// Non-zero fixtures intentionally exercise rotation points and skewed cradle offsets.
|
|
const p = type === 'ac' ? [.024, -.012, .008] : [-.018, .014, .012];
|
|
const d = type === 'ac' ? [0, .018, -.045] : [.022, 0, -.040];
|
|
const t = p.map((v, i) => v + d[i]);
|
|
const radians = (degrees) => degrees * Math.PI / 180;
|
|
const joints = [
|
|
{ name: 'x_joint', parent: 'base', child: 'x_slide', type: 'prismatic', xyz: [0, 0, 0], axis: [1, 0, 0], c: 'X', limits: [-.2, .2, .02] },
|
|
{ name: 'y_joint', parent: 'x_slide', child: 'y_slide', type: 'prismatic', xyz: [0, 0, 0], axis: [0, 1, 0], c: 'Y', limits: [-.1, .1, .02] },
|
|
{ name: 'z_joint', parent: 'y_slide', child: 'z_slide', type: 'prismatic', xyz: [0, 0, 0], axis: [0, 0, 1], c: 'Z', limits: [-.12, .12, .02] },
|
|
{ name: 'spindle_mount', parent: 'z_slide', child: 'spindle_head', type: 'fixed', xyz: [0, 0, 0] },
|
|
{ name: 'tilt_joint', parent: 'base', child: 'tilt_table', type: 'revolute', xyz: t, axis: type === 'ac' ? [1, 0, 0] : [0, 1, 0], c: tiltAxis, limits: [radians(type === 'ac' ? -100 : -36000), radians(type === 'ac' ? 50 : 36000), radians(30)] },
|
|
{ name: 'c_joint', parent: 'tilt_table', child: 'c_table', type: 'revolute', xyz: d.map((v) => -v), axis: [0, 0, 1], c: 'C', limits: [radians(-36000), radians(36000), radians(30)] },
|
|
{ name: 'work_mount', parent: 'c_table', child: 'work_frame', type: 'fixed', xyz: p.map((v) => -v) },
|
|
];
|
|
if (gantry) joints.splice(joints.findIndex((j) => j.name === 'spindle_mount'), 1);
|
|
const links = ['base', ...joints.map((j) => j.child)];
|
|
const urdf = `<?xml version="1.0"?>\n<robot name="demo_${type}_${gantry ? 'gantry' : 'xyz_head'}" xmlns:cnc="urn:linuxcnc:web:machine:1">\n <cnc:machine profile="${profile}" base="base" tool="${toolLink}" work="work_frame"/>\n${links.map((n) => ` <link name="${n}"/>`).join('\n')}\n${joints.map((j) => ` <joint name="${j.name}" type="${j.type}">\n <parent link="${j.parent}"/><child link="${j.child}"/>\n <origin xyz="${j.xyz.join(' ')}" rpy="0 0 0"/>${j.c ? `\n <axis xyz="${j.axis.join(' ')}"/><cnc:axis coordinate="${j.c}"/>\n <limit lower="${j.limits[0]}" upper="${j.limits[1]}" velocity="${j.limits[2]}" effort="1000"/>` : ''}\n </joint>`).join('\n')}\n</robot>\n`;
|
|
const scene = new THREE.Scene(); scene.name = `CNC_${type.toUpperCase()}${gantry ? "_GANTRY" : ""}`;
|
|
scene.userData.linuxcncMachine = { schema: 'linuxcnc.machine/1', urdf };
|
|
scene.userData.assetLicense = 'CC0-1.0';
|
|
const frame = new THREE.Group(); frame.name = 'urdf_frame'; frame.rotation.x = -Math.PI / 2; scene.add(frame);
|
|
const nodes = Object.fromEntries(links.map((n) => { const g = new THREE.Group(); g.name = n; g.userData.link = n; return [n, g]; }));
|
|
frame.add(nodes.base);
|
|
for (const j of joints) { nodes[j.child].position.fromArray(j.xyz); nodes[j.parent].add(nodes[j.child]); }
|
|
const materials = Object.fromEntries(Object.entries({ enamel: [0xd8e4e3, .25, .32], teal: [0x167f83, .4, .27], dark: [0x253640, .35, .43], steel: [0x8a9faa, .85, .23], polished: [0xdce8ee, .9, .16], gold: [0xe4b34e, .7, .25], black: [0x122026, .2, .62] }).map(([n, [color, metalness, roughness]]) => [n, new THREE.MeshStandardMaterial({ color, metalness, roughness })]));
|
|
let counter = 0;
|
|
function mesh(parent, label, shape, position, material) { const m = new THREE.Mesh(shape, materials[material]); m.name = `${label}_${++counter}`; m.position.fromArray(position); m.castShadow = true; m.receiveShadow = true; parent.add(m); return m; }
|
|
const box = (parent, label, size, position, mat = 'enamel') => mesh(parent, label, new THREE.BoxGeometry(...size), position, mat);
|
|
const cyl = (parent, label, radius, length, position, mat = 'steel', axis = 'z') => { const m = mesh(parent, label, new THREE.CylinderGeometry(radius, radius, length, 48), position, mat); if (axis === 'z') m.rotation.x = Math.PI / 2; if (axis === 'x') m.rotation.z = Math.PI / 2; return m; };
|
|
const base = nodes.base;
|
|
box(base, 'bed', [.60, .52, .14], [0, .03, -.25]);
|
|
box(base, 'plinth', [.63, .55, .045], [0, .03, -.337], 'dark');
|
|
box(base, 'chip_tray', [.60, .46, .025], [0, -.015, -.169], 'steel');
|
|
for (const x of [-.275, .275]) box(base, 'tray_lip', [.018, .46, .04], [x, -.015, -.14], 'teal');
|
|
for (const x of [-.25, .25]) for (const y of [-.18, .22]) cyl(base, 'levelling_foot', .031, .025, [x, y, -.37], 'black');
|
|
if (gantry) {
|
|
// Four fixed posts carry X rails. The complete bridge belongs to X,
|
|
// its cross-carriage to Y, and the ram/spindle to Z.
|
|
box(base, 'gantry_foundation', [.88, .78, .045], [0, 0, -.355], 'dark');
|
|
for (const x of [-.38, .38]) for (const y of [-.32, .32]) {
|
|
box(base, 'gantry_post', [.055, .055, .82], [x, y, .05]);
|
|
box(base, 'post_foot', [.09, .09, .02], [x, y, -.35], 'teal');
|
|
}
|
|
const beam = (parent, label, start, end, thickness, mat) => {
|
|
const a = new THREE.Vector3(...start), b = new THREE.Vector3(...end);
|
|
const bar = box(parent, label, [thickness, thickness, a.distanceTo(b)], a.clone().add(b).multiplyScalar(.5).toArray(), mat);
|
|
bar.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), b.sub(a).normalize());
|
|
};
|
|
for (const y of [-.32, .32]) {
|
|
box(base, 'gantry_longitudinal_beam', [.82, .06, .065], [0, y, .475], 'teal');
|
|
box(base, 'gantry_x_rail', [.78, .018, .012], [0, y, .517], 'polished');
|
|
for (let x = -.3; x < .31; x += .1) cyl(base, 'gantry_rail_bolt', .003, .004, [x, y, .526], 'black');
|
|
for (const sign of [-1, 1]) beam(base, 'corner_brace', [sign * .38, y, .30], [sign * .22, y, .45], .02, 'steel');
|
|
box(nodes.x_slide, 'gantry_x_bearing', [.12, .05, .025], [0, y, .54], 'dark');
|
|
}
|
|
for (const x of [-.38, .38]) {
|
|
box(base, 'fixed_end_tie', [.04, .69, .045], [x, 0, .46], 'steel');
|
|
}
|
|
for (const z of [.55, .65]) box(nodes.x_slide, 'moving_bridge_chord', [.06, .73, .035], [0, 0, z], 'teal');
|
|
for (let i = 0; i < 6; i++) {
|
|
const y = -.33 + i * .11;
|
|
beam(nodes.x_slide, 'bridge_diagonal', [0, y, i % 2 ? .65 : .55], [0, y + .11, i % 2 ? .55 : .65], .018, 'enamel');
|
|
}
|
|
for (const x of [-.043, .043]) box(nodes.x_slide, 'gantry_y_rail', [.014, .65, .016], [x, 0, .53], 'polished');
|
|
box(nodes.y_slide, 'gantry_y_carriage', [.13, .12, .035], [0, 0, .50], 'dark');
|
|
box(nodes.y_slide, 'gantry_ram_guide', [.145, .085, .14], [0, .036, .40], 'enamel');
|
|
for (const x of [-.045, .045]) box(nodes.z_slide, 'gantry_z_rail', [.012, .015, .29], [x, .065, .35], 'steel');
|
|
box(nodes.z_slide, 'gantry_z_ram', [.09, .07, .25], [0, .02, .365], 'teal');
|
|
} else {
|
|
for (const x of [-.245, .245]) {
|
|
box(base, 'portal_column', [.09, .12, .62], [x, .19, .11]);
|
|
box(base, 'column_inlay', [.095, .008, .48], [x, .126, .12], 'teal');
|
|
}
|
|
box(base, 'portal_crossbeam', [.58, .115, .10], [0, .19, .40]);
|
|
for (const z of [.365, .427]) {
|
|
box(base, 'x_rail', [.49, .013, .017], [0, .124, z], 'steel');
|
|
for (let x = -.22; x < .23; x += .055) cyl(base, 'rail_bolt', .003, .004, [x, .115, z], 'black', 'y');
|
|
}
|
|
box(nodes.x_slide, 'x_carriage', [.15, .035, .12], [0, .10, .394], 'teal');
|
|
for (const x of [-.045, .045]) box(nodes.x_slide, 'y_rail', [.014, .285, .016], [x, -.02, .365], 'steel');
|
|
box(nodes.y_slide, 'y_saddle', [.15, .13, .065], [0, 0, .327], 'dark');
|
|
for (const x of [-.055, .055]) box(nodes.y_slide, 'z_rail', [.014, .012, .23], [x, .055, .205], 'steel');
|
|
box(nodes.z_slide, 'z_carriage', [.135, .035, .18], [0, .055, .19], 'teal');
|
|
}
|
|
box(nodes[toolLink], 'head_casting', [.12, .13, .15], [0, -.018, .175]);
|
|
box(nodes[toolLink], 'head_face', [.108, .007, .10], [0, -.087, .185], 'teal');
|
|
cyl(nodes[toolLink], 'spindle_motor', .039, .095, [0, 0, .295], 'dark');
|
|
cyl(nodes[toolLink], 'spindle_housing', .037, .083, [0, 0, .088], 'steel');
|
|
cyl(nodes[toolLink], 'collet', .017, .03, [0, 0, .032], 'dark');
|
|
cyl(nodes[toolLink], 'master_tool', .004, .017, [0, 0, .0085], 'gold');
|
|
// AC bearings along X; BC bearings along Y. Both visual and joint axes agree.
|
|
for (const side of [-1, 1]) {
|
|
const pos = [...t]; pos[type === 'ac' ? 0 : 1] += side * .122;
|
|
const support = [...pos]; support[2] = -.111;
|
|
box(base, 'trunnion_pedestal', type === 'ac' ? [.055, .11, .116] : [.11, .055, .116], support);
|
|
cyl(base, 'tilt_bearing', .05, .06, pos, 'teal', type === 'ac' ? 'x' : 'y');
|
|
const shaft = [0, 0, 0]; shaft[type === 'ac' ? 0 : 1] = side * .084;
|
|
cyl(nodes.tilt_table, 'tilt_shaft', .022, .072, shaft, 'steel', type === 'ac' ? 'x' : 'y');
|
|
const arm = [...shaft]; arm[2] = -.025;
|
|
box(nodes.tilt_table, 'cradle_arm', type === 'ac' ? [.017, .10, .074] : [.10, .017, .074], arm, 'teal');
|
|
}
|
|
box(nodes.tilt_table, 'cradle', [.17, .16, .018], [0, 0, -.056], 'teal');
|
|
const rotaryHousing = new THREE.Group(); rotaryHousing.position.fromArray(d.map((v) => -v)); nodes.tilt_table.add(rotaryHousing);
|
|
cyl(rotaryHousing, 'c_drive', .075, .072, [0, 0, -.038], 'dark');
|
|
cyl(nodes.c_table, 'c_platter', .080, .019, [0, 0, -.0095], 'steel');
|
|
cyl(nodes.c_table, 'table_surface', .077, .001, [0, 0, .0005], 'polished');
|
|
for (let n = 0; n < 8; n++) {
|
|
const angle = n * Math.PI / 4;
|
|
cyl(nodes.c_table, 'fixture_hole', .003, .001, [.057 * Math.cos(angle), .057 * Math.sin(angle), .0012], 'black');
|
|
}
|
|
for (const y of [-.035, 0, .035]) box(nodes.c_table, 'table_t_slot', [.125, .003, .001], [0, y, .0015], 'black');
|
|
box(nodes.c_table, 'c_index', [.008, .014, .004], [.073, 0, .002], 'gold');
|
|
const glb = await new GLTFExporter().parseAsync(scene, { binary: true, trs: true, onlyVisible: false });
|
|
const filename = `${stem}.glb`;
|
|
await writeFile(new URL(filename, output), Buffer.from(glb));
|
|
await writeFile(new URL(`${stem}.urdf`, output), urdf);
|
|
manifest.machines.push({ id: gantry ? `${type}-gantry` : type, label: `${type.toUpperCase()} 示例${gantry ? '桁架型' : ''}`, profile, filename, urdf: `${stem}.urdf`, bytes: glb.byteLength, sha256: createHash('sha256').update(Buffer.from(glb)).digest('hex'), coordinates: `XYZ${tiltAxis}C`, rotation_point_mm: p.map((v) => v * 1000), offsets_mm: d.map((v) => v * 1000), links, meshes: counter });
|
|
scene.traverse((obj) => { obj.geometry?.dispose(); }); Object.values(materials).forEach((m) => m.dispose());
|
|
}
|
|
await writeFile(new URL('manifest.json', output), JSON.stringify(manifest, null, 2) + '\n');
|
|
await writeFile(new URL('LICENSE.txt', output), 'Original procedural AC/BC machine demonstration assets generated for linuxcncWebWASM.\nDedicated to the public domain under CC0 1.0 Universal.\nhttps://creativecommons.org/publicdomain/zero/1.0/\nNo manufacturer affiliation or dimensional calibration is implied.\n');
|
|
console.log(JSON.stringify(manifest, null, 2));
|