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.
@@ -0,0 +1,237 @@
1
+ // Optional import of settings from other CNC software. Read-only discovery
2
+ // looks in a few known places on this computer; import only adds tools to
3
+ // ANC's library, never replaces or removes anything. Credentials, secrets,
4
+ // shell commands and controller firmware settings are never read into ANC.
5
+ //
6
+ // Sources for now:
7
+ // Kiri:Moto tool exports (.km / .b64: base64 JSON with `tools`, or a
8
+ // settings export with `settings.tools`)
9
+ // Fusion tool library exports (.json with `data: [{ type, geometry }]`)
10
+ // CNCjs ~/.cncrc: machine profiles are reported, nothing is imported
11
+ // (ANC reads travel from the controller itself)
12
+ import crypto from "node:crypto";
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { listTools, saveTool } from "./cam.js";
17
+ import { JOBS_DIR } from "./product.js";
18
+ const MAX_BYTES = 5 << 20;
19
+ const IN = 25.4;
20
+ /** Folders searched (one level deep): the usual download and document folders, ANC's jobs folder, Fusion's local tool libraries. */
21
+ function searchFolders() {
22
+ const home = os.homedir();
23
+ const appData = process.env.APPDATA ?? path.join(home, "Library", "Application Support");
24
+ return [
25
+ path.join(home, "Downloads"),
26
+ path.join(home, "Documents"),
27
+ path.join(home, "Desktop"),
28
+ JOBS_DIR,
29
+ path.join(appData, "Autodesk", "CAM360", "libraries", "Local"),
30
+ ];
31
+ }
32
+ const sourceId = (file) => crypto.createHash("sha256").update(path.resolve(file)).digest("hex").slice(0, 10);
33
+ function readSmall(file) {
34
+ try {
35
+ const st = fs.statSync(file);
36
+ return st.isFile() && st.size <= MAX_BYTES ? fs.readFileSync(file, "utf-8") : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ // ------------------------------------------------------------------ Kiri:Moto
43
+ function fromKiri(text, src) {
44
+ let data;
45
+ try {
46
+ const t = text.trim();
47
+ data = JSON.parse(t.startsWith("{") ? t : Buffer.from(t, "base64").toString("utf-8"));
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ const tools = data?.tools ?? data?.settings?.tools;
53
+ if (!Array.isArray(tools)) {
54
+ if (data?.device && data?.code)
55
+ src.notes.push(`Machine profile "${data.device}" is not imported: ANC reads travel and limits from the controller.`);
56
+ return Boolean(data?.device && data?.time);
57
+ }
58
+ for (const t of tools) {
59
+ const label = `${t?.name ?? "unnamed"} (${t?.type ?? "unknown type"})`;
60
+ const type = { endmill: "flat", ballmill: "ball", tapermill: "vbit", drill: "drill" }[t?.type];
61
+ const k = t?.metric === false ? IN : 1;
62
+ const diameter = Number(t?.flute_diam) * k;
63
+ const fluteLength = Number(t?.flute_len) * k;
64
+ if (!type) {
65
+ src.skipped.push(`${label}: ANC has no ${t?.type ?? "such"} tools yet`);
66
+ continue;
67
+ }
68
+ if (!(diameter > 0) || !(fluteLength > 0)) {
69
+ src.skipped.push(`${label}: no diameter or flute length`);
70
+ continue;
71
+ }
72
+ src.tools.push({
73
+ from: label,
74
+ name: String(t.name ?? `${diameter} mm ${type}`).slice(0, 60),
75
+ type,
76
+ diameter: round(diameter),
77
+ fluteLength: round(fluteLength),
78
+ shankDiameter: Number(t.shaft_diam) > 0 ? round(Number(t.shaft_diam) * k) : undefined,
79
+ // Kiri's taper_angle is half the included angle.
80
+ angle: type === "vbit" && Number(t.taper_angle) > 0 ? round(Number(t.taper_angle) * 2) : undefined,
81
+ number: Number.isInteger(t.number) && t.number > 0 ? t.number : undefined,
82
+ });
83
+ }
84
+ return true;
85
+ }
86
+ // --------------------------------------------------------------------- Fusion
87
+ function fromFusion(text, src) {
88
+ let data;
89
+ try {
90
+ data = JSON.parse(text);
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ if (!Array.isArray(data?.data) || !data.data.some((t) => t?.geometry && typeof t?.type === "string"))
96
+ return false;
97
+ for (const t of data.data) {
98
+ if (t?.type === "holder" || t?.type === "probe")
99
+ continue;
100
+ const label = `${t?.description || t?.["product-id"] || "unnamed"} (${t?.type})`;
101
+ const type = { "flat end mill": "flat", "ball end mill": "ball", "chamfer mill": "vbit", "drill": "drill" }[t?.type];
102
+ const g = t?.geometry ?? {};
103
+ const k = /^in/i.test(t?.unit ?? "") ? IN : 1;
104
+ const diameter = Number(g.DC) * k;
105
+ const fluteLength = Number(g.LCF) * k;
106
+ if (!type) {
107
+ src.skipped.push(`${label}: ANC has no ${t?.type} tools yet`);
108
+ continue;
109
+ }
110
+ if (!(diameter > 0) || !(fluteLength > 0)) {
111
+ src.skipped.push(`${label}: no diameter or flute length`);
112
+ continue;
113
+ }
114
+ const number = Number(t?.["post-process"]?.number);
115
+ src.tools.push({
116
+ from: label,
117
+ name: String(t.description || `${round(diameter)} mm ${type}`).slice(0, 60),
118
+ type,
119
+ diameter: round(diameter),
120
+ fluteLength: round(fluteLength),
121
+ shankDiameter: Number(g.SFDM) > 0 ? round(Number(g.SFDM) * k) : undefined,
122
+ // Fusion's taper angle (TA) is half the included angle.
123
+ angle: type === "vbit" && Number(g.TA) > 0 ? round(Number(g.TA) * 2) : undefined,
124
+ number: Number.isInteger(number) && number > 0 ? number : undefined,
125
+ });
126
+ }
127
+ return true;
128
+ }
129
+ // ---------------------------------------------------------------------- CNCjs
130
+ function fromCncrc(text, src) {
131
+ let data;
132
+ try {
133
+ data = JSON.parse(text);
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ const machines = Array.isArray(data?.machines) ? data.machines : [];
139
+ for (const m of machines) {
140
+ const l = m?.limits ?? {};
141
+ const size = ["x", "y", "z"].map((a) => round(Math.abs(Number(l[`${a}max`] ?? 0) - Number(l[`${a}min`] ?? 0))));
142
+ src.notes.push(`Machine profile "${m?.name ?? "unnamed"}" (${size.join(" x ")} mm) is not imported: ANC reads travel from the controller.`);
143
+ }
144
+ const macros = Array.isArray(data?.macros) ? data.macros.length : 0;
145
+ if (macros)
146
+ src.notes.push(`${macros} macros, not imported (ANC has no macros).`);
147
+ const ignored = ["secret", "users", "commands", "events"].filter((k) => data?.[k] !== undefined && (!Array.isArray(data[k]) || data[k].length));
148
+ if (ignored.length)
149
+ src.notes.push(`Never imported: ${ignored.join(", ")} (secrets, sign-ins and shell commands).`);
150
+ return true;
151
+ }
152
+ const round = (n) => Math.round(n * 1000) / 1000;
153
+ // ------------------------------------------------------------------ discovery
154
+ function inspect(file) {
155
+ const text = readSmall(file);
156
+ if (text === null)
157
+ return null;
158
+ let modified = "";
159
+ try {
160
+ modified = fs.statSync(file).mtime.toISOString();
161
+ }
162
+ catch { }
163
+ const source = (app) => ({ id: sourceId(file), app, file, modified, tools: [], skipped: [], notes: [] });
164
+ const ext = path.extname(file).toLowerCase();
165
+ if (path.basename(file) === ".cncrc") {
166
+ const src = source("CNCjs");
167
+ return fromCncrc(text, src) ? src : null;
168
+ }
169
+ if (ext === ".km" || ext === ".b64") {
170
+ const src = source("Kiri:Moto");
171
+ return fromKiri(text, src) ? src : null;
172
+ }
173
+ if (ext === ".json") {
174
+ const src = source("Fusion");
175
+ if (fromFusion(text, src))
176
+ return src;
177
+ const kiri = source("Kiri:Moto");
178
+ return fromKiri(text, kiri) && kiri.tools.length ? kiri : null;
179
+ }
180
+ return null;
181
+ }
182
+ /** Looks for other CNC software's settings in the usual places. Reads only. */
183
+ export function findSettings() {
184
+ const found = [];
185
+ const cncrc = inspect(path.join(os.homedir(), ".cncrc"));
186
+ if (cncrc)
187
+ found.push(cncrc);
188
+ const searched = searchFolders();
189
+ for (const dir of searched) {
190
+ let names = [];
191
+ try {
192
+ names = fs.readdirSync(dir);
193
+ }
194
+ catch {
195
+ continue;
196
+ }
197
+ for (const name of names) {
198
+ if (!/\.(km|b64|json)$/i.test(name))
199
+ continue;
200
+ const src = inspect(path.join(dir, name));
201
+ if (src && (src.tools.length || src.skipped.length || src.notes.length))
202
+ found.push(src);
203
+ }
204
+ }
205
+ return { searched: [path.join(os.homedir(), ".cncrc"), ...searched], found };
206
+ }
207
+ /**
208
+ * Adds a found source's tools to ANC's library (all, or the ones at `pick`,
209
+ * 0-based). Tools already in the library (same type, diameter and name) are
210
+ * left alone; nothing existing is changed.
211
+ */
212
+ export function importTools(id, pick) {
213
+ const src = findSettings().found.find((s) => s.id === id);
214
+ if (!src)
215
+ throw new Error(`No settings source "${id}". Run anc_find_settings to list them.`);
216
+ if (!src.tools.length)
217
+ throw new Error(`${src.file} has no tools ANC can import.`);
218
+ const chosen = pick?.length ? pick.map((i) => src.tools[i]).filter(Boolean) : src.tools;
219
+ if (!chosen.length)
220
+ throw new Error(`None of those tool numbers are in ${src.file} (0 to ${src.tools.length - 1}).`);
221
+ const added = [];
222
+ const alreadyThere = [];
223
+ const numbers = new Set(listTools().map((t) => t.number));
224
+ for (const { from, ...tool } of chosen) {
225
+ const same = listTools().find((t) => t.type === tool.type && Math.abs(t.diameter - tool.diameter) < 0.001 && t.name === tool.name);
226
+ if (same) {
227
+ alreadyThere.push(`${from}: already in the library as tool ${same.id}`);
228
+ continue;
229
+ }
230
+ // Keep the other app's tool number only when it doesn't clash with one ANC already has.
231
+ const number = tool.number !== undefined && !numbers.has(tool.number) ? tool.number : undefined;
232
+ const saved = saveTool({ ...tool, number });
233
+ numbers.add(saved.number);
234
+ added.push(saved);
235
+ }
236
+ return { from: src.file, added, alreadyThere, skipped: src.skipped, tools: listTools() };
237
+ }