agentic-numerical-control 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -44
- package/dist/engines/cncjs/LICENSES.txt +2593 -0
- package/dist/engines/cncjs/cncjs-headless.cjs +67988 -0
- package/dist/engines/cncjs/manifest.json +195 -0
- package/dist/engines/kiri/LICENSES.txt +1097 -0
- package/dist/engines/kiri/bin/dom-stub.mjs +30 -0
- package/dist/engines/kiri/bin/kiri-main.js +127342 -0
- package/dist/engines/kiri/bin/kiri-pool.js +83963 -0
- package/dist/engines/kiri/bin/kiri-work.js +136718 -0
- package/dist/engines/kiri/bin/runner.mjs +241 -0
- package/dist/engines/kiri/bin/worker-shim.mjs +32 -0
- package/dist/engines/kiri/manifest.json +356 -0
- package/dist/engines/kiri/wasm/manifold.wasm +0 -0
- package/dist/jobs.html +137 -0
- package/dist/machine.html +164 -0
- package/dist/server/analysis.js +356 -0
- package/dist/server/cam.js +592 -0
- package/dist/server/cncjs.js +194 -75
- package/dist/server/history.js +48 -0
- package/dist/server/jobs.js +60 -3
- package/dist/server/machines.js +96 -0
- package/dist/server/program.js +14 -0
- package/dist/server/server.js +577 -52
- package/dist/server/settings-import.js +237 -0
- package/dist/setup.html +156 -0
- package/dist/viewer.html +296 -265
- package/package.json +9 -5
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// ANC's CAM process: runs Kiri:Moto's engine (unchanged upstream code, bundled
|
|
2
|
+
// beside this file) on one job and returns the G-code. server/cam.ts starts
|
|
3
|
+
// it as a separate child process for each job, so CAM work can never block or
|
|
4
|
+
// reach machine control; ANC cancels a job by ending the process.
|
|
5
|
+
//
|
|
6
|
+
// IPC in: { type: "generate", job } (see Job below)
|
|
7
|
+
// { type: "inspect", model, units } (size of a model, no toolpaths)
|
|
8
|
+
// IPC out: { type: "progress", fraction, status }
|
|
9
|
+
// { type: "result", ... } | { type: "error", message }
|
|
10
|
+
// Or from a shell: node runner.mjs job.json out.nc
|
|
11
|
+
//
|
|
12
|
+
// Job: {
|
|
13
|
+
// model: absolute path of an STL file, units: "mm" | "in",
|
|
14
|
+
// stock: { x, y, z } in mm (absolute size),
|
|
15
|
+
// origin: "front-left" | "center" (X/Y zero; Z zero is the top of the stock),
|
|
16
|
+
// device: Kiri CAM device fields (bed size, spindleMax, gcodePre, ...),
|
|
17
|
+
// tools: Kiri tool records, process: Kiri CAM process fields including ops
|
|
18
|
+
// }
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { Worker } from 'node:worker_threads';
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const CWD = process.cwd();
|
|
27
|
+
// Kiri loads its WebAssembly from "../wasm/" relative to the working folder.
|
|
28
|
+
process.chdir(HERE);
|
|
29
|
+
|
|
30
|
+
globalThis.self = globalThis;
|
|
31
|
+
await import('./dom-stub.mjs');
|
|
32
|
+
|
|
33
|
+
const buffers = (transfer) => (Array.isArray(transfer) ? transfer.filter((t) => t instanceof ArrayBuffer) : undefined);
|
|
34
|
+
|
|
35
|
+
// Kiri's client asks self.createWorker() for its worker when it is defined.
|
|
36
|
+
self.createWorker = () => {
|
|
37
|
+
const shim = new URL('./worker-shim.mjs', import.meta.url);
|
|
38
|
+
const worker = new Worker(shim, {
|
|
39
|
+
workerData: {
|
|
40
|
+
script: pathToFileURL(path.join(HERE, 'kiri-work.js')).href,
|
|
41
|
+
pool: pathToFileURL(path.join(HERE, 'kiri-pool.js')).href,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const facade = {
|
|
45
|
+
postMessage: (msg, transfer) => worker.postMessage(msg, buffers(transfer)),
|
|
46
|
+
terminate: () => worker.terminate(),
|
|
47
|
+
onmessage: null,
|
|
48
|
+
};
|
|
49
|
+
worker.on('message', (data) => facade.onmessage?.({ data }));
|
|
50
|
+
worker.on('error', (error) => fail(`CAM worker failed: ${error?.message ?? error}`));
|
|
51
|
+
return facade;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const { newEngine, conf, version } = await import('./kiri-main.js');
|
|
55
|
+
|
|
56
|
+
// For each operation ANC offers: the operation's fields and the CAM process
|
|
57
|
+
// setting each one defaults from (a literal where upstream uses one). Copied
|
|
58
|
+
// from the createPopOp() tables in upstream src/kiri/mode/cam/app/cl-ops.js,
|
|
59
|
+
// which is UI code and can't be imported here. Update with the pin.
|
|
60
|
+
const OP_FIELDS = {
|
|
61
|
+
rough: {
|
|
62
|
+
tool: 'camRoughTool', direction: 'camMillDirection', spindle: 'camRoughSpindle', down: 'camRoughDown', step: 'camRoughOver',
|
|
63
|
+
rate: 'camRoughSpeed', plunge: 'camRoughPlunge', leave: 'camRoughStock', leavez: 'camRoughStockZ', all: 'camRoughAll',
|
|
64
|
+
flats: 'camRoughFlat', inside: 'camRoughIn', omitthru: 'camRoughOmitThru', ov_topz: 0, ov_botz: 0,
|
|
65
|
+
},
|
|
66
|
+
outline: {
|
|
67
|
+
tool: 'camOutlineTool', direction: 'camMillDirection', spindle: 'camOutlineSpindle', step: 'camOutlineOver', steps: 'camOutlineOverCount',
|
|
68
|
+
down: 'camOutlineDown', rate: 'camOutlineSpeed', plunge: 'camOutlinePlunge', dogbones: 'camOutlineDogbone', revbones: 'camOutlineRevbone',
|
|
69
|
+
omitthru: 'camOutlineOmitThru', omitvoid: 'camOutlineOmitVoid', outside: 'camOutlineOut', inside: 'camOutlineIn', wide: 'camOutlineWide',
|
|
70
|
+
ov_topz: 0, ov_botz: 0,
|
|
71
|
+
},
|
|
72
|
+
contour: {
|
|
73
|
+
tool: 'camContourTool', spindle: 'camContourSpindle', step: 'camContourOver', rate: 'camContourSpeed', angle: 'camContourAngle',
|
|
74
|
+
leave: 'camContourLeave', tolerance: 'camTolerance', flatness: 'camFlatness', reduction: 'camContourReduce', bridging: 'camContourBridge',
|
|
75
|
+
bottom: 'camContourBottom', curves: 'camContourCurves', inside: 'camContourIn', clipto: 'camStockClipTo', filter: 'camContourFilter', axis: 'X',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** An operation with every field upstream's app would give it, then ANC's values on top. */
|
|
80
|
+
function fillOp(op) {
|
|
81
|
+
const fields = OP_FIELDS[op.type];
|
|
82
|
+
if (!fields) throw new Error(`Unsupported operation "${op.type}"`);
|
|
83
|
+
const defaults = conf.defaults.cam.p;
|
|
84
|
+
const out = { type: op.type };
|
|
85
|
+
for (const [field, from] of Object.entries(fields)) {
|
|
86
|
+
out[field] = typeof from === 'string' && from.startsWith('cam') ? defaults[from] : from;
|
|
87
|
+
}
|
|
88
|
+
return { ...out, ...op };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function send(msg) {
|
|
92
|
+
if (process.send) process.send(msg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fail(message) {
|
|
96
|
+
if (!process.send) {
|
|
97
|
+
console.error(message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
process.send({ type: 'error', message: String(message) }, () => process.exit(1));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fmtFeed = (f) => String(Number(f.toFixed(4)));
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Kiri links the passes of a pocket with rapids at cutting depth: its prepare
|
|
107
|
+
* step leaves a move inside a pocket's travel boundary as a move. On a real
|
|
108
|
+
* machine a G0 there runs at full speed into uncut stock, so ANC rewrites
|
|
109
|
+
* every rapid that travels or descends below the top of the stock (Z 0) as a
|
|
110
|
+
* G1, at no more than the last cutting feed, and keeps Kiri's own feeds for
|
|
111
|
+
* the lines after it. Returns the G-code and how many moves it changed.
|
|
112
|
+
*/
|
|
113
|
+
export function feedLinksInStock(gcode) {
|
|
114
|
+
const pos = { X: null, Y: null, Z: null };
|
|
115
|
+
let kiriMotion = null; // modal G0-G3 as Kiri wrote it
|
|
116
|
+
let motion = null; // and as ANC sends it
|
|
117
|
+
let kiriF = null;
|
|
118
|
+
let machineF = null;
|
|
119
|
+
let cutF = null;
|
|
120
|
+
let changed = 0;
|
|
121
|
+
const lines = gcode.split('\n').map((line) => {
|
|
122
|
+
const code = line.replace(/[;(].*$/, '');
|
|
123
|
+
const words = [...code.toUpperCase().matchAll(/([A-Z])\s*([-+]?\d*\.?\d+)/g)].map((m) => [m[1], Number(m[2])]);
|
|
124
|
+
const g = words.find(([k, v]) => k === 'G' && v >= 0 && v <= 3);
|
|
125
|
+
const f = words.find(([k]) => k === 'F');
|
|
126
|
+
if (g) kiriMotion = g[1];
|
|
127
|
+
if (f) kiriF = f[1];
|
|
128
|
+
const target = { ...pos };
|
|
129
|
+
for (const [k, v] of words) if (k in target) target[k] = v;
|
|
130
|
+
if (kiriMotion === null || !words.some(([k]) => k in pos)) {
|
|
131
|
+
if (f) machineF = f[1];
|
|
132
|
+
return line;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const inStock = pos.Z !== null && target.Z < 0 && (target.X !== pos.X || target.Y !== pos.Y || target.Z < pos.Z);
|
|
136
|
+
Object.assign(pos, target);
|
|
137
|
+
let want = kiriMotion;
|
|
138
|
+
let wantF = want === 0 ? null : kiriF;
|
|
139
|
+
if (want === 0 && inStock && (cutF ?? kiriF) !== null) {
|
|
140
|
+
want = 1;
|
|
141
|
+
wantF = Math.min(kiriF ?? Infinity, cutF ?? Infinity);
|
|
142
|
+
changed++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let out = code.trimEnd();
|
|
146
|
+
const rest = line.slice(code.trimEnd().length);
|
|
147
|
+
if (g && want !== kiriMotion) out = out.replace(/G0*[0-3](?![\d.])/i, `G${want}`);
|
|
148
|
+
else if (!g && want !== motion) out = `G${want} ${out}`;
|
|
149
|
+
if (wantF !== null && (wantF !== machineF || (f && f[1] !== wantF))) {
|
|
150
|
+
out = out.replace(/\s*F\s*[-+]?\d*\.?\d+/i, '') + (wantF !== machineF ? ` F${fmtFeed(wantF)}` : '');
|
|
151
|
+
}
|
|
152
|
+
machineF = wantF ?? (f ? f[1] : machineF);
|
|
153
|
+
motion = want;
|
|
154
|
+
if (want !== 0) cutF = machineF;
|
|
155
|
+
return out + rest;
|
|
156
|
+
});
|
|
157
|
+
return { gcode: lines.join('\n'), changed };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Reads a model the way Kiri does and reports its size, in the model's own units. */
|
|
161
|
+
export async function inspect(model) {
|
|
162
|
+
const engine = newEngine();
|
|
163
|
+
let triangles = 0;
|
|
164
|
+
engine.setListener((msg) => {
|
|
165
|
+
if (msg?.vertices) triangles = msg.vertices.length / 9;
|
|
166
|
+
});
|
|
167
|
+
engine.setMode('CAM');
|
|
168
|
+
await engine.parse(new Uint8Array(fs.readFileSync(model)).buffer);
|
|
169
|
+
const box = engine.widget.getBoundingBox();
|
|
170
|
+
return {
|
|
171
|
+
engine: 'Kiri:Moto',
|
|
172
|
+
version,
|
|
173
|
+
triangles,
|
|
174
|
+
size: { x: box.max.x - box.min.x, y: box.max.y - box.min.y, z: box.max.z - box.min.z },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function generate(job) {
|
|
179
|
+
const engine = newEngine();
|
|
180
|
+
let lastAlert = null;
|
|
181
|
+
engine.setListener((msg) => {
|
|
182
|
+
const s = msg?.slice ?? msg?.prepare;
|
|
183
|
+
if (s?.update !== undefined) send({ type: 'progress', fraction: s.update, status: s.updateStatus ?? (msg.slice ? 'slicing' : 'preparing') });
|
|
184
|
+
if (msg?.slice?.alert) lastAlert = msg.slice.alert;
|
|
185
|
+
});
|
|
186
|
+
engine.setMode('CAM');
|
|
187
|
+
engine.setDevice(job.device);
|
|
188
|
+
engine.setTools(job.tools);
|
|
189
|
+
|
|
190
|
+
const data = fs.readFileSync(job.model);
|
|
191
|
+
await engine.parse(new Uint8Array(data).buffer);
|
|
192
|
+
if (job.units === 'in') engine.scale(25.4, 25.4, 25.4);
|
|
193
|
+
const box = engine.widget.getBoundingBox();
|
|
194
|
+
const part = { x: box.max.x - box.min.x, y: box.max.y - box.min.y, z: box.max.z - box.min.z };
|
|
195
|
+
const stock = { ...job.stock };
|
|
196
|
+
|
|
197
|
+
// Absolute stock, part's top at the top of the stock ("top" anchor), Z zero
|
|
198
|
+
// at the top of the stock: the same placement Kiri's own app makes in
|
|
199
|
+
// platform.update_stock / update_top_z / update_origin.
|
|
200
|
+
engine.setProcess({ ...job.process, ops: (job.process.ops ?? []).map(fillOp), camStockOffset: false, camZAnchor: 'top', camOriginTop: true, camOriginCenter: job.origin === 'center' });
|
|
201
|
+
engine.setStock(stock);
|
|
202
|
+
const center = { x: (box.max.x + box.min.x) / 2, y: (box.max.y + box.min.y) / 2, z: stock.z / 2 };
|
|
203
|
+
engine.settings.stock.center = center;
|
|
204
|
+
engine.widget.setTopZ(stock.z, true);
|
|
205
|
+
if (job.origin === 'center') engine.setOrigin(center.x, -center.y, stock.z + 0.01);
|
|
206
|
+
else engine.setOrigin(-stock.x / 2 + center.x, stock.y / 2 - center.y, stock.z + 0.01);
|
|
207
|
+
|
|
208
|
+
await engine.slice();
|
|
209
|
+
await engine.prepare();
|
|
210
|
+
const linked = feedLinksInStock(await engine.export());
|
|
211
|
+
return {
|
|
212
|
+
gcode: linked.gcode,
|
|
213
|
+
info: { engine: 'Kiri:Moto', version, part, stock, origin: job.origin, alert: lastAlert, linksAsCuts: linked.changed },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
process.on('uncaughtException', (e) => fail(e?.stack ?? e));
|
|
218
|
+
process.on('unhandledRejection', (e) => fail(e?.stack ?? e));
|
|
219
|
+
|
|
220
|
+
if (process.send) {
|
|
221
|
+
process.on('message', async (msg) => {
|
|
222
|
+
if (msg?.type !== 'generate' && msg?.type !== 'inspect') return;
|
|
223
|
+
try {
|
|
224
|
+
const result = msg.type === 'inspect' ? await inspect(msg.model) : await generate(msg.job);
|
|
225
|
+
// Exit only once the result has been written: a large program takes
|
|
226
|
+
// more than one write to send.
|
|
227
|
+
process.send({ type: 'result', ...result }, () => process.exit(0));
|
|
228
|
+
} catch (e) {
|
|
229
|
+
fail(e?.message ?? e);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
process.on('disconnect', () => process.exit(0));
|
|
233
|
+
send({ type: 'ready' });
|
|
234
|
+
} else if (process.argv[2]) {
|
|
235
|
+
const job = JSON.parse(fs.readFileSync(path.resolve(CWD, process.argv[2]), 'utf-8'));
|
|
236
|
+
const result = await generate(job);
|
|
237
|
+
if (process.argv[3]) fs.writeFileSync(path.resolve(CWD, process.argv[3]), result.gcode);
|
|
238
|
+
else process.stdout.write(result.gcode);
|
|
239
|
+
console.error(JSON.stringify(result.info));
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Runs one of Kiri:Moto's worker bundles (kiri-work.js, kiri-pool.js) in a
|
|
2
|
+
// Node worker_thread, with the Web Worker globals those bundles expect: self,
|
|
3
|
+
// postMessage, onmessage, and Worker for the helper "minion" pool the main
|
|
4
|
+
// worker starts. Upstream code is unchanged; this only adapts the host.
|
|
5
|
+
|
|
6
|
+
import { Worker as NodeWorker, parentPort, workerData } from 'node:worker_threads';
|
|
7
|
+
|
|
8
|
+
const buffers = (transfer) => (Array.isArray(transfer) ? transfer.filter((t) => t instanceof ArrayBuffer) : undefined);
|
|
9
|
+
|
|
10
|
+
globalThis.self = globalThis;
|
|
11
|
+
globalThis.postMessage = (msg, transfer) => parentPort.postMessage(msg, buffers(transfer));
|
|
12
|
+
parentPort.on('message', (data) => {
|
|
13
|
+
const handler = globalThis.onmessage;
|
|
14
|
+
if (handler) handler({ data });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Minions: Kiri asks for new Worker(poolURL); every one runs the pool bundle.
|
|
18
|
+
globalThis.Worker = class {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.worker = new NodeWorker(new URL(import.meta.url), { workerData: { script: workerData.pool, pool: workerData.pool } });
|
|
21
|
+
this.worker.on('message', (data) => this.onmessage?.({ data }));
|
|
22
|
+
this.worker.on('error', (error) => this.onerror?.(Object.assign(error, { preventDefault() {} })));
|
|
23
|
+
}
|
|
24
|
+
postMessage(msg, transfer) {
|
|
25
|
+
this.worker.postMessage(msg, buffers(transfer));
|
|
26
|
+
}
|
|
27
|
+
terminate() {
|
|
28
|
+
this.worker.terminate();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
await import(workerData.script);
|