@toolpath/plan 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2951 @@
1
+ // src/coverage.ts
2
+ var COVERAGE_KINDS = [
3
+ "pinned",
4
+ "ignored",
5
+ "open",
6
+ "machined",
7
+ "notMachined"
8
+ ];
9
+ var COVERAGE_LABELS = {
10
+ pinned: "Pinned",
11
+ ignored: "Ignored",
12
+ open: "Open",
13
+ machined: "Machined",
14
+ notMachined: "Not machined"
15
+ };
16
+ var facesByRegion = (coverage) => new Map(
17
+ COVERAGE_KINDS.flatMap(
18
+ (kind) => (coverage?.faces[kind] ?? []).map((face) => [face.region, { face, kind }])
19
+ )
20
+ );
21
+ var tagKey = (tag) => tag.toLowerCase();
22
+ var tagSet = (tags) => new Set([...tags].map(tagKey));
23
+ var planTags = (setups) => tagSet(setups.flatMap((setup) => setup.actions.flatMap((action) => action.featureTags ?? [])));
24
+ var areaOf = (faces) => faces.reduce((sum, face) => sum + face.area, 0);
25
+ var classifyFaces = (input) => {
26
+ const { regions, features, ignored = [] } = input;
27
+ const owners = ownersByRegion(features);
28
+ const ignoredSet = new Set(ignored);
29
+ const faces = {
30
+ pinned: [],
31
+ ignored: [],
32
+ open: [],
33
+ machined: [],
34
+ notMachined: []
35
+ };
36
+ for (const region of regions) {
37
+ const face = { region: region.idx, area: region.area, owners: owners.get(region.idx) ?? [] };
38
+ faces[kindOf(face, ignoredSet, input)].push(face);
39
+ }
40
+ for (const kind of COVERAGE_KINDS) {
41
+ faces[kind].sort((a, b) => b.area - a.area);
42
+ }
43
+ return { total: areaOf(regions), faces };
44
+ };
45
+ var ownersByRegion = (features) => {
46
+ const owners = /* @__PURE__ */ new Map();
47
+ for (const feature of features) {
48
+ for (const region of feature.regionIdxs ?? []) {
49
+ owners.set(region, [...owners.get(region) ?? [], feature.tag]);
50
+ }
51
+ }
52
+ return owners;
53
+ };
54
+ var kindOf = (face, ignored, input) => {
55
+ if (ignored.has(face.region)) {
56
+ return "ignored";
57
+ }
58
+ const ownedBy = (tags) => face.owners.some((tag) => tags.has(tag.toLowerCase()));
59
+ if (input.pinned !== void 0) {
60
+ return ownedBy(input.pinned) ? "pinned" : "open";
61
+ }
62
+ return ownedBy(input.machined) ? "machined" : "notMachined";
63
+ };
64
+ var RANKS = [
65
+ ["blind_hole", "through_hole", "filleted_blind_hole"],
66
+ ["pocket", "open_pocket", "boss", "filleted_boss", "undercut_tslot", "undercut_dovetail", "sink"],
67
+ ["chamfer", "inner_fillet", "outer_fillet", "contour_surface", "slanted_face"],
68
+ ["wall", "face"],
69
+ ["profile"]
70
+ ];
71
+ var RANK_OF = new Map(RANKS.flatMap((types, rank) => types.map((type) => [type, rank])));
72
+ var rankOf = (type) => RANK_OF.get(type) ?? 2.5;
73
+ var isFallback = (type) => type === "profile";
74
+ var groupFaces = (faces, features) => {
75
+ const byTag = new Map(features.map((feature) => [feature.tag.toLowerCase(), feature]));
76
+ const { groups, taken } = gatherUnderFeatures(facesOfFeatures(faces, byTag), byTag);
77
+ const alone = faces.filter((face) => !taken.has(face.region)).map((face) => ({ tag: null, faces: [face], area: face.area }));
78
+ return [...groups, ...alone].sort(
79
+ (a, b) => Number(a.tag === null) - Number(b.tag === null) || b.area - a.area
80
+ );
81
+ };
82
+ var facesOfFeatures = (faces, byTag) => {
83
+ const includes = /* @__PURE__ */ new Map();
84
+ for (const face of faces) {
85
+ const known = [...new Set(face.owners)].filter((tag) => byTag.has(tag.toLowerCase()));
86
+ for (const tag of known) {
87
+ includes.set(tag, [...includes.get(tag) ?? [], face]);
88
+ }
89
+ }
90
+ return includes;
91
+ };
92
+ var gatherUnderFeatures = (includes, byTag) => {
93
+ const left = new Map([...includes].map(([tag, all]) => [tag, all.length]));
94
+ const before = gathersBefore(byTag, left);
95
+ const taken = /* @__PURE__ */ new Set();
96
+ const groups = [];
97
+ let best = nextToGather(left, before);
98
+ while (best !== null) {
99
+ const gathered = includes.get(best).filter((face) => !taken.has(face.region));
100
+ markGathered(gathered, taken, left);
101
+ groups.push({ tag: best, faces: gathered, area: areaOf(gathered) });
102
+ best = nextToGather(left, before);
103
+ }
104
+ return { groups, taken };
105
+ };
106
+ var gathersBefore = (byTag, left) => (a, b) => {
107
+ const [fa, fb] = [byTag.get(a.toLowerCase()), byTag.get(b.toLowerCase())];
108
+ return (Number(isFallback(fa.featureType)) - Number(isFallback(fb.featureType)) || left.get(b) - left.get(a) || rankOf(fa.featureType) - rankOf(fb.featureType) || (fa.regionIdxs?.length ?? 0) - (fb.regionIdxs?.length ?? 0) || a.localeCompare(b)) < 0;
109
+ };
110
+ var nextToGather = (left, before) => {
111
+ let best = null;
112
+ for (const [tag, count] of left) {
113
+ if (count > 0 && (best === null || before(tag, best))) {
114
+ best = tag;
115
+ }
116
+ }
117
+ return best;
118
+ };
119
+ var markGathered = (gathered, taken, left) => {
120
+ for (const face of gathered) {
121
+ taken.add(face.region);
122
+ for (const tag of new Set(face.owners)) {
123
+ countDown(left, tag);
124
+ }
125
+ }
126
+ };
127
+ var countDown = (left, tag) => {
128
+ const count = left.get(tag);
129
+ if (count !== void 0) {
130
+ left.set(tag, count - 1);
131
+ }
132
+ };
133
+
134
+ // src/refusals.ts
135
+ var MAX_DEPTH = 6;
136
+ var GONE = "That folder is gone.";
137
+ var TOO_DEEP = `Folders sit at most ${MAX_DEPTH} below their setup.`;
138
+
139
+ // src/plan-draft.ts
140
+ var MAX_SETUPS = 64;
141
+ var MAX_NAME = 100;
142
+ var MAX_FEATURES = 5e3;
143
+ var alreadyCut = (frame) => ({
144
+ refused: `${frame.name} is cut from that direction already.`
145
+ });
146
+ var sameDirection = (a, b) => a.x === b.x && a.y === b.y && a.z === b.z;
147
+ var isIndexed = (setup) => setup.process === "3+1" || setup.process === "3+2";
148
+ var placedFolders = (setups) => {
149
+ const placed = [];
150
+ const walk = (folder, setup, parent, depth) => {
151
+ const here = placedAt(folder, setup, parent, depth);
152
+ placed.push(here);
153
+ for (const child of folder.folders ?? []) {
154
+ walk(child, setup, here, depth + 1);
155
+ }
156
+ };
157
+ for (const setup of setups) {
158
+ walk(setup, setup, null, 0);
159
+ }
160
+ return placed;
161
+ };
162
+ var placedAt = (folder, setup, parent, depth) => {
163
+ const toolOrientation = parent !== null && isIndexed(setup) && folder.direction !== void 0;
164
+ return {
165
+ folder,
166
+ setup,
167
+ parent: parent?.folder ?? null,
168
+ depth,
169
+ direction: parent === null ? setup.direction : toolOrientation ? folder.direction : parent.direction,
170
+ frameId: parent === null || toolOrientation ? folder.id : parent.frameId,
171
+ toolOrientation
172
+ };
173
+ };
174
+ var findPlaced = (setups, id) => placedFolders(setups).find((placed) => placed.folder.id === id);
175
+ var framesOf = (setups) => {
176
+ const placed = placedFolders(setups);
177
+ return placed.filter((each) => each.frameId === each.folder.id).map((frame) => ({
178
+ id: frame.folder.id,
179
+ name: frame.folder.name,
180
+ holdId: frame.setup.id,
181
+ direction: frame.direction,
182
+ features: [
183
+ ...new Set(
184
+ placed.filter((each) => each.frameId === frame.folder.id).flatMap((each) => each.folder.features)
185
+ )
186
+ ]
187
+ }));
188
+ };
189
+ var heightOf = (folder) => 1 + Math.max(0, ...(folder.folders ?? []).map(heightOf));
190
+ var idsIn = (folder) => [
191
+ folder.id,
192
+ ...(folder.folders ?? []).flatMap(idsIn)
193
+ ];
194
+ var updateFolder = (setups, id, change) => {
195
+ const visit = (folder) => {
196
+ const next = folder.id === id ? change(folder) : folder;
197
+ const children = (next.folders ?? []).map(visit);
198
+ return children.some((child, index) => child !== next.folders[index]) ? { ...next, folders: children } : next;
199
+ };
200
+ return setups.map((setup) => visit(setup));
201
+ };
202
+ var pruneFolder = (setups, id) => {
203
+ const prune = (folder) => folder.folders.some((child) => child.id === id) ? { ...folder, folders: folder.folders.filter((child) => child.id !== id).map(prune) } : { ...folder, folders: folder.folders.map(prune) };
204
+ return setups.filter((setup) => setup.id !== id).map((setup) => prune(setup));
205
+ };
206
+ var removeFolder = (setups, id) => removeFolders(setups, [id]);
207
+ var removeFolders = (setups, ids) => {
208
+ const parents = [...new Set(ids.flatMap((id) => findPlaced(setups, id)?.parent?.id ?? []))];
209
+ const pruned = ids.reduce((all, id) => pruneFolder(all, id), [...setups]);
210
+ return parents.reduce((all, parentId) => liftLoneFolder(all, parentId), pruned);
211
+ };
212
+ var following = (folder) => {
213
+ const { direction: _d, process: _p, ...rest } = folder;
214
+ return rest;
215
+ };
216
+ var tiltsBelow = (folder) => folder.folders.flatMap((child) => [
217
+ ...child.direction ? [child.direction] : [],
218
+ ...tiltsBelow(child)
219
+ ]);
220
+ var liftedFrom = (here, top) => {
221
+ if (here.folders.length !== 1 || here.features.length > 0) {
222
+ return null;
223
+ }
224
+ const [only] = here.folders;
225
+ const { direction: ownTilt, ...kept } = only;
226
+ if (top) {
227
+ return liftedToTop(here, kept, ownTilt);
228
+ }
229
+ const tilt = ownTilt ?? here.direction;
230
+ return { ...kept, ...tilt ? { direction: tilt } : {} };
231
+ };
232
+ var liftedToTop = (setup, kept, ownTilt) => {
233
+ if (ownTilt) {
234
+ const tilts = tiltsBelow(kept);
235
+ return {
236
+ ...kept,
237
+ direction: ownTilt,
238
+ ...tilts.length > 0 ? { process: indexedFor(ownTilt, tilts) } : {}
239
+ };
240
+ }
241
+ return {
242
+ ...kept,
243
+ direction: setup.direction,
244
+ ...setup.process && tiltsBelow(kept).length > 0 ? { process: setup.process } : {}
245
+ };
246
+ };
247
+ var liftLoneFolder = (setups, id) => {
248
+ const placed = findPlaced(setups, id);
249
+ const next = placed ? liftedFrom(placed.folder, placed.depth === 0) : null;
250
+ if (!placed || !next) {
251
+ return [...setups];
252
+ }
253
+ if (!placed.parent) {
254
+ return setups.map((setup) => setup.id === id ? next : setup);
255
+ }
256
+ return replaceChild(setups, placed.parent.id, id, next);
257
+ };
258
+ var replaceChild = (setups, parentId, childId, next) => updateFolder(setups, parentId, (folder) => ({
259
+ ...folder,
260
+ folders: folder.folders.map((child) => child.id === childId ? next : child)
261
+ }));
262
+ var namesIn = (setups) => placedFolders(setups).map((placed) => placed.folder.name);
263
+ var FOLDER_NUMBER = /^Folder (\d+)$/;
264
+ var nextFolderName = (names) => `Folder ${Math.max(
265
+ 0,
266
+ ...names.map(
267
+ (each) => Number(FOLDER_NUMBER.exec(typeof each === "string" ? each : each.name)?.[1] ?? 0)
268
+ )
269
+ ) + 1}`;
270
+ var addFolder = (setups, parentId, id, workId = `${id}:work`) => {
271
+ const parent = findPlaced(setups, parentId);
272
+ if (!parent || parent.depth >= MAX_DEPTH) {
273
+ return [...setups];
274
+ }
275
+ const name = nextFolderName(namesIn(setups));
276
+ const added = { id, name, features: [], folders: [] };
277
+ const split = parent.folder.folders.length === 0;
278
+ return updateFolder(
279
+ setups,
280
+ parentId,
281
+ (folder) => split ? {
282
+ ...folder,
283
+ features: [],
284
+ folders: [
285
+ { id: workId, name: folder.name, features: folder.features, folders: [] },
286
+ added
287
+ ]
288
+ } : { ...folder, folders: [...folder.folders, added] }
289
+ );
290
+ };
291
+ var renameFolder = (setups, id, name) => updateFolder(setups, id, (folder) => ({
292
+ ...folder,
293
+ name: name.trim().slice(0, MAX_NAME) || folder.name
294
+ }));
295
+ var setSetupProcess = (setups, setupId, process) => {
296
+ if (process === void 0 && placedFolders(setups).some((placed) => placed.setup.id === setupId && placed.toolOrientation)) {
297
+ return { refused: "Move or delete its tool orientations first." };
298
+ }
299
+ return setups.map((setup) => {
300
+ if (setup.id !== setupId) {
301
+ return setup;
302
+ }
303
+ const { process: _was, ...rest } = setup;
304
+ return process ? { ...rest, process } : rest;
305
+ });
306
+ };
307
+ var setToolOrientation = (setups, folderId, direction) => {
308
+ const placed = findPlaced(setups, folderId);
309
+ if (!placed || placed.depth === 0) {
310
+ return { refused: "Only a folder inside a setup can be a tool orientation." };
311
+ }
312
+ if (direction && !isIndexed(placed.setup)) {
313
+ return { refused: "Only a 3+1 or 3+2 setup has tool orientations." };
314
+ }
315
+ const taken = direction ? framesOf(setups).find(
316
+ (frame) => frame.id !== folderId && sameDirection(frame.direction, direction)
317
+ ) : void 0;
318
+ if (taken) {
319
+ return alreadyCut(taken);
320
+ }
321
+ return updateFolder(
322
+ setups,
323
+ folderId,
324
+ (folder) => direction ? { ...following(folder), direction } : following(folder)
325
+ );
326
+ };
327
+ var withFolders = (setups) => {
328
+ const fill = (folder) => ({
329
+ ...folder,
330
+ features: folder.features ?? [],
331
+ folders: (folder.folders ?? []).map(fill)
332
+ });
333
+ return setups.map((setup) => fill(setup));
334
+ };
335
+ var isDirection = (value) => {
336
+ if (typeof value !== "object" || value === null) {
337
+ return false;
338
+ }
339
+ const { x, y, z } = value;
340
+ return [x, y, z].every((n) => typeof n === "number" && Number.isFinite(n));
341
+ };
342
+ var copyDirection = ({ x, y, z }) => ({ x, y, z });
343
+ var parseDraftSetups = (value) => {
344
+ if (!Array.isArray(value)) {
345
+ return "The plan has no list of setups.";
346
+ }
347
+ if (value.length > MAX_SETUPS) {
348
+ return `A plan can state at most ${MAX_SETUPS} setups.`;
349
+ }
350
+ const seen = { ids: /* @__PURE__ */ new Set(), tags: /* @__PURE__ */ new Set(), names: [] };
351
+ const setups = parseEach(value, (entry) => parseSetup(entry, seen));
352
+ if (typeof setups === "string") {
353
+ return setups;
354
+ }
355
+ if (anyDirectionTwice(setups)) {
356
+ return "Each direction can be a setup only once.";
357
+ }
358
+ return setups;
359
+ };
360
+ var parseEach = (entries, parse) => {
361
+ const parsed = [];
362
+ for (const entry of entries) {
363
+ const each = parse(entry);
364
+ if (typeof each === "string") {
365
+ return each;
366
+ }
367
+ parsed.push(each);
368
+ }
369
+ return parsed;
370
+ };
371
+ var parseSetup = (entry, seen) => {
372
+ const { direction, process } = entry ?? {};
373
+ if (!isDirection(direction)) {
374
+ return "A setup needs a direction.";
375
+ }
376
+ if (process !== void 0 && process !== "3+1" && process !== "3+2") {
377
+ return "A setup is held 3 axis, 3+1 or 3+2.";
378
+ }
379
+ const parsed = parseFolder(entry, 0, seen);
380
+ if (typeof parsed === "string") {
381
+ return parsed;
382
+ }
383
+ return {
384
+ ...parsed,
385
+ direction: copyDirection(direction),
386
+ ...process ? { process } : {}
387
+ };
388
+ };
389
+ var parseFolder = (entry, depth, seen) => {
390
+ const { id, name, direction, features, folders } = entry ?? {};
391
+ if (typeof id !== "string" || !id || seen.ids.has(id) || typeof name !== "string") {
392
+ return "A folder needs an id of its own and a name.";
393
+ }
394
+ if (depth > MAX_DEPTH) {
395
+ return `A folder can sit at most ${MAX_DEPTH} below its setup.`;
396
+ }
397
+ if (direction !== void 0 && !isDirection(direction)) {
398
+ return "A folder\u2019s direction is three numbers.";
399
+ }
400
+ const own = claimFeatures(features, folders, seen);
401
+ if (typeof own === "string") {
402
+ return own;
403
+ }
404
+ seen.ids.add(id);
405
+ const children = parseEach(
406
+ Array.isArray(folders) ? folders : [],
407
+ (child) => parseFolder(child, depth + 1, seen)
408
+ );
409
+ if (typeof children === "string") {
410
+ return children;
411
+ }
412
+ const folder = {
413
+ id,
414
+ name: name.trim().slice(0, MAX_NAME) || nextFolderName(seen.names),
415
+ features: own,
416
+ folders: children,
417
+ ...direction ? { direction: copyDirection(direction) } : {}
418
+ };
419
+ seen.names.push(folder.name);
420
+ return folder;
421
+ };
422
+ var FEATURE_TAG = /^[0-9a-f]{1,16}$/;
423
+ var claimFeatures = (features, folders, seen) => {
424
+ const pinned2 = features ?? [];
425
+ if (!isTagList(pinned2)) {
426
+ return "A folder\u2019s features are feature tags.";
427
+ }
428
+ const own = [...new Set(pinned2)];
429
+ if (own.length > 0 && Array.isArray(folders) && folders.length > 0) {
430
+ return "A folder holds features or folders, never both.";
431
+ }
432
+ if (own.some((tag) => seen.tags.has(tag))) {
433
+ return "A feature can be pinned in one folder only.";
434
+ }
435
+ own.forEach((tag) => seen.tags.add(tag));
436
+ return own;
437
+ };
438
+ var isTagList = (value) => Array.isArray(value) && value.length <= MAX_FEATURES && value.every((tag) => typeof tag === "string" && FEATURE_TAG.test(tag));
439
+ var anyDirectionTwice = (setups) => {
440
+ const frames = framesOf(setups);
441
+ return frames.some(
442
+ (frame, index) => frames.findIndex((other) => sameDirection(other.direction, frame.direction)) !== index
443
+ );
444
+ };
445
+ var addSetup = (setups, direction, id) => framesOf(setups).some((frame) => sameDirection(frame.direction, direction)) || setups.length >= MAX_SETUPS ? [...setups] : [
446
+ ...setups,
447
+ {
448
+ id,
449
+ name: nextFolderName(namesIn(setups)),
450
+ direction,
451
+ features: [],
452
+ folders: []
453
+ }
454
+ ];
455
+ var statedFrom = (planned, newId, pins) => {
456
+ const stating = { setups: [], newId, pins, pinned: /* @__PURE__ */ new Set() };
457
+ for (const setup of planned) {
458
+ const stated = stateSetup(stating, setup);
459
+ if (stated) {
460
+ stating.setups.push(stated);
461
+ }
462
+ }
463
+ return stating.setups;
464
+ };
465
+ var stateSetup = (stating, setup) => {
466
+ const [first, ...rest] = setup.orientations.map((orientation) => orientation.direction);
467
+ if (!first || frameFacing(stating.setups, first) || stating.setups.length >= MAX_SETUPS) {
468
+ return null;
469
+ }
470
+ const tilts = tiltsToState(stating.setups, first, rest);
471
+ const passedOver = rest.filter((tilt) => !tilts.some((each) => sameDirection(each, tilt)));
472
+ const claimed = claimsOf(setup);
473
+ const notMachined = notMachinedIn(setup);
474
+ const take = (tags) => takePins(stating, notMachined, tags);
475
+ const tilted = tilts.map((tilt) => [tilt, take(claimedFrom(claimed, tilt))]);
476
+ const own = take([
477
+ ...[first, ...passedOver].flatMap((direction) => claimedFrom(claimed, direction)),
478
+ ...unlistedRows(setup, claimed)
479
+ ]);
480
+ return statedFrame(stating, first, own, tilted);
481
+ };
482
+ var tiltsToState = (setups, first, rest) => rest.filter(
483
+ (tilt, index) => !frameFacing(setups, tilt) && !sameDirection(tilt, first) && rest.findIndex((each) => sameDirection(each, tilt)) === index
484
+ );
485
+ var notMachinedIn = (setup) => new Set(
486
+ (setup.rows ?? []).filter((row) => row.mark === "notMachined").map((row) => row.tag.toLowerCase())
487
+ );
488
+ var takePins = (stating, notMachined, tags) => {
489
+ if (!stating.pins) {
490
+ return [];
491
+ }
492
+ const taken = [];
493
+ for (const tag of tags) {
494
+ const key = tag.toLowerCase();
495
+ if (!notMachined.has(key) && !stating.pinned.has(key)) {
496
+ stating.pinned.add(key);
497
+ taken.push(tag);
498
+ }
499
+ }
500
+ return taken;
501
+ };
502
+ var claimsOf = (setup) => {
503
+ const claimed = /* @__PURE__ */ new Map();
504
+ const listed = setup.orientations.flatMap(
505
+ (orientation) => (orientation.features ?? []).map(({ tag }) => ({ tag, direction: orientation.direction }))
506
+ );
507
+ for (const claim of listed) {
508
+ if (!claimed.has(claim.tag.toLowerCase())) {
509
+ claimed.set(claim.tag.toLowerCase(), claim);
510
+ }
511
+ }
512
+ return claimed;
513
+ };
514
+ var claimedFrom = (claimed, direction) => [...claimed.values()].filter((claim) => sameDirection(claim.direction, direction)).map((claim) => claim.tag);
515
+ var unlistedRows = (setup, claimed) => (setup.rows ?? []).filter((row) => !claimed.has(row.tag.toLowerCase())).map((row) => row.tag);
516
+ var statedFrame = (stating, direction, own, tilted) => {
517
+ const newFolder = folderMaker(stating);
518
+ if (tilted.length === 0) {
519
+ return { ...newFolder(own), direction };
520
+ }
521
+ const frame = newFolder([]);
522
+ const followers = tilted.length === 1 || own.length > 0 ? [newFolder(own)] : [];
523
+ const tiltFolders = tilted.map(([tilt, features]) => ({
524
+ ...newFolder(features),
525
+ direction: tilt
526
+ }));
527
+ return {
528
+ ...frame,
529
+ direction,
530
+ process: indexedFor(
531
+ direction,
532
+ tilted.map(([tilt]) => tilt)
533
+ ),
534
+ folders: [...followers, ...tiltFolders]
535
+ };
536
+ };
537
+ var folderMaker = (stating) => {
538
+ const names = namesIn(stating.setups);
539
+ return (features) => {
540
+ const folder = {
541
+ id: stating.newId(),
542
+ name: nextFolderName(names),
543
+ features,
544
+ folders: []
545
+ };
546
+ names.push(folder.name);
547
+ return folder;
548
+ };
549
+ };
550
+ var foldersFromPlan = (planned, newId) => statedFrom(planned, newId, false);
551
+ var planFromResults = (planned, newId) => statedFrom(planned, newId, true);
552
+ var moveSetup = (setups, id, by) => {
553
+ const from = setups.findIndex((setup) => setup.id === id);
554
+ if (from < 0) {
555
+ return [...setups];
556
+ }
557
+ const to = Math.max(0, Math.min(setups.length - 1, from + by));
558
+ const next = [...setups];
559
+ const [moved] = next.splice(from, 1);
560
+ next.splice(to, 0, moved);
561
+ return next;
562
+ };
563
+ var moveWithin = (setups, id, by) => {
564
+ const parent = findPlaced(setups, id)?.parent;
565
+ if (!parent) {
566
+ return moveSetup(setups, id, by);
567
+ }
568
+ const reorder = (folder) => {
569
+ if (folder.id !== parent.id) {
570
+ return { ...folder, folders: folder.folders.map(reorder) };
571
+ }
572
+ const from = folder.folders.findIndex((child) => child.id === id);
573
+ const to = Math.max(0, Math.min(folder.folders.length - 1, from + by));
574
+ const folders = [...folder.folders];
575
+ const [moved] = folders.splice(from, 1);
576
+ folders.splice(to, 0, moved);
577
+ return { ...folder, folders };
578
+ };
579
+ return setups.map((setup) => reorder(setup));
580
+ };
581
+ var indexedFor = (setupDirection, tilts) => {
582
+ const z = setupDirection;
583
+ const axes = tilts.map((t) => ({ x: z.y * t.z - z.z * t.y, y: z.z * t.x - z.x * t.z, z: z.x * t.y - z.y * t.x })).map((a) => ({ a, length: Math.hypot(a.x, a.y, a.z) })).filter(({ length }) => length > 1e-9).map(({ a, length }) => ({ x: a.x / length, y: a.y / length, z: a.z / length }));
584
+ const [first] = axes;
585
+ return !first || axes.every((a) => Math.abs(a.x * first.x + a.y * first.y + a.z * first.z) > 1 - 1e-6) ? "3+1" : "3+2";
586
+ };
587
+ var pinnedIn = (setups, tag) => {
588
+ const key = tagKey(tag);
589
+ return placedFolders(setups).find(
590
+ (placed) => placed.folder.features.some((each) => tagKey(each) === key)
591
+ )?.folder.id ?? null;
592
+ };
593
+ var pinHomes = (setups) => {
594
+ const homes = /* @__PURE__ */ new Map();
595
+ for (const { folder } of placedFolders(setups)) {
596
+ for (const tag of folder.features) {
597
+ if (!homes.has(tagKey(tag))) {
598
+ homes.set(tagKey(tag), folder.id);
599
+ }
600
+ }
601
+ }
602
+ return homes;
603
+ };
604
+ var pinOf = (setups, tag) => {
605
+ const key = tag.toLowerCase();
606
+ for (const { folder } of placedFolders(setups)) {
607
+ const pin = folder.features.find((each) => each.toLowerCase() === key);
608
+ if (pin) {
609
+ return { folderId: folder.id, tag: pin };
610
+ }
611
+ }
612
+ return null;
613
+ };
614
+ var pinInto = (setups, folderId, tag, newId = `${folderId}:${tag}`) => {
615
+ const was = pinnedIn(setups, tag);
616
+ const unpinned = was ? unpinFeature(setups, was, tag) : [...setups];
617
+ if (was === folderId) {
618
+ return { setups: unpinned, setupId: folderId, added: false, unpinned: true };
619
+ }
620
+ const holdsFolders = (findPlaced(setups, folderId)?.folder.folders.length ?? 0) > 0;
621
+ const name = nextFolderName(namesIn(setups));
622
+ return {
623
+ setups: updateFolder(
624
+ unpinned,
625
+ folderId,
626
+ (folder) => holdsFolders ? {
627
+ ...folder,
628
+ folders: [...folder.folders, { id: newId, name, features: [tag], folders: [] }]
629
+ } : { ...folder, features: [...folder.features, tag] }
630
+ ),
631
+ setupId: holdsFolders ? newId : folderId,
632
+ added: false,
633
+ unpinned: false
634
+ };
635
+ };
636
+ var pinTargetOf = (setups, direction) => {
637
+ const frame = frameFacing(setups, direction);
638
+ if (!frame) {
639
+ return null;
640
+ }
641
+ const placed = placedFolders(setups);
642
+ const leaf = placed.find((each) => each.frameId === frame.id && each.folder.folders.length === 0);
643
+ return leaf?.folder.id ?? frame.id;
644
+ };
645
+ var frameFacing = (setups, direction) => framesOf(setups).find((each) => sameDirection(each.direction, direction));
646
+ var pinFrom = (setups, direction, tag, newId) => {
647
+ const frame = frameFacing(setups, direction);
648
+ if (frame) {
649
+ const was = pinnedIn(setups, tag);
650
+ const inFrame = was !== null && placedFolders(setups).some((p) => p.folder.id === was && p.frameId === frame.id);
651
+ return pinInto(setups, inFrame ? was : pinTargetOf(setups, direction), tag, newId);
652
+ }
653
+ const next = addSetup(setups, direction, newId);
654
+ if (next.length === setups.length) {
655
+ return { refused: `A plan holds at most ${MAX_SETUPS} setups.` };
656
+ }
657
+ return {
658
+ setups: next.map((setup) => setup.id === newId ? { ...setup, features: [tag] } : setup),
659
+ setupId: newId,
660
+ added: true,
661
+ unpinned: false
662
+ };
663
+ };
664
+ var unpinFeature = (setups, folderId, tag) => updateFolder(setups, folderId, (folder) => ({
665
+ ...folder,
666
+ features: folder.features.filter((feature) => tagKey(feature) !== tagKey(tag))
667
+ }));
668
+ var placedAsBefore = (list, before, tag) => {
669
+ const without = list.filter((each) => each !== tag);
670
+ const at = before?.indexOf(tag) ?? -1;
671
+ if (at >= 0) {
672
+ without.splice(Math.min(at, without.length), 0, tag);
673
+ }
674
+ return without;
675
+ };
676
+ var restoreFeature = (setups, saved, tag) => {
677
+ const was = new Map(placedFolders(saved).map(({ folder }) => [folder.id, folder]));
678
+ const visit = (folder) => ({
679
+ ...folder,
680
+ features: placedAsBefore(folder.features, was.get(folder.id)?.features, tag),
681
+ folders: folder.folders.map(visit)
682
+ });
683
+ return setups.map((setup) => visit(setup));
684
+ };
685
+ var currentFor = (list, stamp, now) => stamp && stamp !== now ? [] : [...list ?? []];
686
+ var ignoredAs = (done, kernel) => ({
687
+ done: [...done].sort((a, b) => a - b),
688
+ doneKernel: done.length > 0 ? kernel : null
689
+ });
690
+ var withIgnored = (plan, faces, kernel) => ignoredAs([.../* @__PURE__ */ new Set([...currentFor(plan.done, plan.doneKernel, kernel), ...faces])], kernel);
691
+ var withoutIgnored = (plan, faces, kernel) => {
692
+ const these = new Set(faces);
693
+ return ignoredAs(
694
+ currentFor(plan.done, plan.doneKernel, kernel).filter((r) => !these.has(r)),
695
+ kernel
696
+ );
697
+ };
698
+ var ignoredAsSaved = (plan, saved, faces, kernel) => {
699
+ const these = new Set(faces);
700
+ return withIgnored(
701
+ withoutIgnored(plan, faces, kernel),
702
+ currentFor(saved.done, saved.doneKernel, kernel).filter((r) => these.has(r)),
703
+ kernel
704
+ );
705
+ };
706
+ var pinsWhere = (setups, test) => placedFolders(setups).flatMap(
707
+ ({ folder }) => folder.features.filter(test).map((tag) => ({ setupId: folder.id, tag }))
708
+ );
709
+ var statementOf = (setups) => {
710
+ const frames = framesOf(setups);
711
+ if (frames.length === 0) {
712
+ return null;
713
+ }
714
+ return {
715
+ setups: frames.map((frame) => ({
716
+ orientations: [{ direction: frame.direction, features: frame.features }]
717
+ }))
718
+ };
719
+ };
720
+ var holdsFor = (setups, planned) => {
721
+ const frames = framesOf(setups);
722
+ const holds2 = /* @__PURE__ */ new Map();
723
+ for (const setup of planned) {
724
+ const direction = setup.direction;
725
+ const frame = direction ? frames.find((each) => sameDirection(each.direction, direction)) : void 0;
726
+ const hold = frame ? setups.find((each) => each.id === frame.holdId) : void 0;
727
+ const key = hold?.id ?? `setup:${setup.idx}`;
728
+ const found = holds2.get(key);
729
+ if (found) {
730
+ found.setupIdxs.push(setup.idx);
731
+ } else {
732
+ holds2.set(key, { key, name: hold?.name ?? `Setup ${setup.idx + 1}`, setupIdxs: [setup.idx] });
733
+ }
734
+ }
735
+ return [...holds2.values()];
736
+ };
737
+ var tiltsIn = (setups, setupId) => placedFolders(setups).filter((placed) => placed.setup.id === setupId && placed.toolOrientation).map((placed) => placed.direction);
738
+ var moveFolder = (setups, id, to, newId = `${id}:parent`) => {
739
+ const left = findPlaced(setups, id)?.parent?.id ?? null;
740
+ const moved = placeFolder(setups, id, to, newId);
741
+ return Array.isArray(moved) && left ? liftLoneFolder(moved, left) : moved;
742
+ };
743
+ var placeFolder = (setups, id, to, newId) => {
744
+ const moved = findPlaced(setups, id);
745
+ if (!moved) {
746
+ return { refused: GONE };
747
+ }
748
+ const move = { setups, moved, to, newId };
749
+ if (to.parentId === parentIdOf(moved)) {
750
+ return withLanded(move, moved.folder);
751
+ }
752
+ if (to.parentId === null) {
753
+ return placeAtTop(move);
754
+ }
755
+ const target = findPlaced(setups, to.parentId);
756
+ if (!target) {
757
+ return { refused: GONE };
758
+ }
759
+ return placeInside(move, target);
760
+ };
761
+ var parentIdOf = (placed) => placed.parent?.id ?? null;
762
+ var withLanded = ({ setups, moved, to }, folder) => putAt(takenOut(setups, moved), folder, to);
763
+ var takenOut = (setups, placed) => parentIdOf(placed) === null ? setups.filter((setup) => setup.id !== placed.folder.id) : pruneFolder(setups, placed.folder.id);
764
+ var putAt = (setups, folder, to) => {
765
+ if (to.parentId === null) {
766
+ const next = [...setups];
767
+ next.splice(to.index, 0, folder);
768
+ return next;
769
+ }
770
+ return updateFolder(setups, to.parentId, (parent) => {
771
+ const children = [...parent.folders];
772
+ children.splice(to.index, 0, folder);
773
+ return { ...parent, folders: children };
774
+ });
775
+ };
776
+ var placeAtTop = (move) => {
777
+ const { moved } = move;
778
+ if (!moved.toolOrientation) {
779
+ return { refused: `It is cut from ${moved.setup.name}\u2019s direction, a setup already.` };
780
+ }
781
+ const { direction: tilt, ...rest } = moved.folder;
782
+ const setup = { ...rest, direction: tilt };
783
+ return indexedForTiltsIn(withLanded(move, setup), setup.id);
784
+ };
785
+ var indexedForTiltsIn = (setups, setupId) => {
786
+ const tilts = tiltsIn(
787
+ setups.map((each) => each.id === setupId ? { ...each, process: "3+2" } : each),
788
+ setupId
789
+ );
790
+ if (tilts.length === 0) {
791
+ return setups;
792
+ }
793
+ return setups.map(
794
+ (each) => each.id === setupId ? { ...each, process: indexedFor(each.direction, tilts) } : each
795
+ );
796
+ };
797
+ var placeInside = (move, target) => {
798
+ const { moved } = move;
799
+ if (idsIn(moved.folder).includes(target.folder.id)) {
800
+ return { refused: "A folder can\u2019t go inside itself." };
801
+ }
802
+ if (target.depth + heightOf(moved.folder) > MAX_DEPTH) {
803
+ return { refused: TOO_DEEP };
804
+ }
805
+ if (target.folder.folders.length === 0) {
806
+ return wrapBoth(move, target);
807
+ }
808
+ if (sameDirection(moved.direction, target.direction)) {
809
+ return withLanded(move, following(moved.folder));
810
+ }
811
+ return placeAsTilt(move, target);
812
+ };
813
+ var placeAsTilt = (move, target) => {
814
+ const { setups, moved } = move;
815
+ const taken = frameFacing(takenOut(setups, moved), moved.direction);
816
+ if (taken) {
817
+ return alreadyCut(taken);
818
+ }
819
+ const tilt = { ...following(moved.folder), direction: moved.direction };
820
+ return indexedToReach(withLanded(move, tilt), target.setup.id);
821
+ };
822
+ var indexedToReach = (setups, setupId) => {
823
+ const indexed = setups.map(
824
+ (each) => each.id === setupId ? { ...each, process: each.process ?? "3+1" } : each
825
+ );
826
+ const tilts = tiltsIn(indexed, setupId);
827
+ return indexed.map(
828
+ (each) => each.id === setupId ? { ...each, process: each.process === "3+2" ? "3+2" : indexedFor(each.direction, tilts) } : each
829
+ );
830
+ };
831
+ var wrapBoth = (move, target) => {
832
+ const { setups, moved, newId } = move;
833
+ if (tooDeepWrapped(moved, target)) {
834
+ return { refused: TOO_DEEP };
835
+ }
836
+ const base = takenOut(setups, moved);
837
+ const sameWay = sameDirection(moved.direction, target.direction);
838
+ const taken = sameWay ? void 0 : frameFacing(base, moved.direction);
839
+ if (taken) {
840
+ return alreadyCut(taken);
841
+ }
842
+ const placed = inPlaceOf(base, target, parentOfBoth(move, target));
843
+ if (sameWay) {
844
+ return placed;
845
+ }
846
+ return indexedToReach(placed, target.depth === 0 ? newId : target.setup.id);
847
+ };
848
+ var tooDeepWrapped = (moved, target) => target.depth + 1 + heightOf(moved.folder) > MAX_DEPTH || target.depth + 1 + heightOf(target.folder) - 1 > MAX_DEPTH;
849
+ var parentOfBoth = ({ setups, moved, newId }, target) => ({
850
+ id: newId,
851
+ name: nextFolderName(namesIn(setups)),
852
+ features: [],
853
+ folders: [following(target.folder), besideTarget(moved, target)],
854
+ ...target.toolOrientation ? { direction: target.direction } : {}
855
+ });
856
+ var besideTarget = (moved, target) => sameDirection(moved.direction, target.direction) ? following(moved.folder) : { ...following(moved.folder), direction: moved.direction };
857
+ var inPlaceOf = (setups, target, parent) => {
858
+ if (target.depth !== 0) {
859
+ return replaceChild(setups, target.parent.id, target.folder.id, parent);
860
+ }
861
+ return setups.map(
862
+ (each) => each.id === target.folder.id ? {
863
+ ...parent,
864
+ direction: target.setup.direction,
865
+ ...target.setup.process ? { process: target.setup.process } : {}
866
+ } : each
867
+ );
868
+ };
869
+ var moveFeature = (setups, tag, featureDirection, folderId, newId) => {
870
+ const dropped = findPlaced(setups, folderId);
871
+ if (!dropped) {
872
+ return { refused: GONE };
873
+ }
874
+ const target = rowTarget(setups, dropped, featureDirection);
875
+ if (!target) {
876
+ return { refused: `No folder in ${dropped.folder.name} is cut from that direction.` };
877
+ }
878
+ if (!sameDirection(target.direction, featureDirection)) {
879
+ return { refused: "Rows stay in folders cut from their own direction." };
880
+ }
881
+ if (target.folder.features.includes(tag)) {
882
+ return [...setups];
883
+ }
884
+ const result = pinInto(setups, target.folder.id, tag, newId);
885
+ return "setups" in result ? result.setups : { refused: result.refused };
886
+ };
887
+ var rowTarget = (setups, dropped, featureDirection) => {
888
+ if (dropped.folder.folders.length === 0) {
889
+ return dropped;
890
+ }
891
+ const inside = new Set(idsIn(dropped.folder));
892
+ const leaf = placedFolders(setups).find(
893
+ (each) => inside.has(each.folder.id) && each.folder.folders.length === 0 && sameDirection(each.direction, featureDirection)
894
+ );
895
+ return leaf ?? (sameDirection(dropped.direction, featureDirection) ? dropped : void 0);
896
+ };
897
+
898
+ // src/direction-analysis.ts
899
+ var directionAnalysis = (direction, { analyzed, discovered, pending }) => (
900
+ // Nothing is known before the report is read: nothing to say about it either.
901
+ !analyzed || analyzed.some((each) => sameDirection(each, direction)) ? "analyzed" : !discovered?.some((each) => sameDirection(each, direction)) ? "undetected" : pending ? "analyzing" : "unanalyzed"
902
+ );
903
+
904
+ // src/readings.ts
905
+ var faceRegions = (regions, region) => {
906
+ const origin = regions.find((candidate) => candidate.idx === region)?.splitOrigin;
907
+ return origin === void 0 ? [region] : regions.filter((candidate) => candidate.splitOrigin === origin).map(({ idx }) => idx);
908
+ };
909
+ var regionsOf = (feature) => feature.regionIdxs ?? [];
910
+ var readingsFor = (at, scope, context) => {
911
+ const wanted = new Set(at.regions);
912
+ const owners = context.features.filter(
913
+ (feature) => regionsOf(feature).some((region) => wanted.has(region))
914
+ );
915
+ const read = scope.kind === "ignore" ? ignoreOrder(owners) : sizeOrder(owners, scope, at.ranked, context);
916
+ const tags = read.map((feature) => feature.tag);
917
+ return { tags, default: defaultOf(read, at.prefer ?? null, scope, context) };
918
+ };
919
+ var rankOf2 = (ranked, tag) => {
920
+ const at = ranked?.indexOf(tag) ?? -1;
921
+ return at < 0 ? ranked?.length ?? 0 : at;
922
+ };
923
+ var sizeOrder = (owners, scope, ranked, context) => {
924
+ const seen = /* @__PURE__ */ new Set();
925
+ return owners.filter((feature) => {
926
+ const key = tagKey(feature.tag);
927
+ if (seen.has(key)) {
928
+ return false;
929
+ }
930
+ seen.add(key);
931
+ if (scope.kind === "all") {
932
+ return true;
933
+ }
934
+ const direction = context.featureOf(feature.tag)?.direction;
935
+ return direction !== void 0 && sameDirection(direction, scope.direction);
936
+ }).sort(
937
+ (a, b) => regionsOf(b).length - regionsOf(a).length || rankOf2(ranked, a.tag) - rankOf2(ranked, b.tag)
938
+ );
939
+ };
940
+ var ignoreOrder = (owners) => {
941
+ const sorted = [...owners].sort(
942
+ (a, b) => Number(isFallback(a.featureType)) - Number(isFallback(b.featureType)) || regionsOf(a).length - regionsOf(b).length || a.tag.localeCompare(b.tag)
943
+ );
944
+ const seen = /* @__PURE__ */ new Set();
945
+ return sorted.filter((feature) => {
946
+ const key = [...new Set(regionsOf(feature))].sort((a, b) => a - b).join(",");
947
+ return seen.has(key) ? false : (seen.add(key), true);
948
+ });
949
+ };
950
+ var defaultOf = (read, prefer, scope, context) => {
951
+ const preferred = prefer ? read.find((feature) => tagKey(feature.tag) === tagKey(prefer)) : null;
952
+ if (preferred) {
953
+ return preferred.tag;
954
+ }
955
+ if (scope.kind === "ignore") {
956
+ return read.find((feature) => !isFallback(feature.featureType))?.tag ?? null;
957
+ }
958
+ const pinned2 = read.find((feature) => {
959
+ const home = context.pinnedIn?.(feature.tag) ?? null;
960
+ return scope.kind === "folder" ? home === scope.id : home !== null;
961
+ });
962
+ return pinned2?.tag ?? read[0]?.tag ?? null;
963
+ };
964
+
965
+ // src/ignore.ts
966
+ var ignoreEdit = (plan, regions, context) => {
967
+ const ignored = withIgnored(plan, regions, context.kernel);
968
+ const done = new Set(ignored.done);
969
+ const these = new Set(regions);
970
+ const unpinned = pinsWhere(plan.setups, (tag) => {
971
+ const faces = context.regionsOf(tag);
972
+ return faces.some((face) => these.has(face)) && faces.every((face) => done.has(face));
973
+ });
974
+ return {
975
+ intent: {
976
+ setups: unpinned.reduce(
977
+ (all, pin) => unpinFeature(all, pin.setupId, pin.tag),
978
+ [...plan.setups]
979
+ ),
980
+ ...ignored
981
+ },
982
+ unpinned
983
+ };
984
+ };
985
+ var unpinnedNote = (unpinned, setups) => {
986
+ const folders = [
987
+ ...new Set(unpinned.map((pin) => findPlaced(setups, pin.setupId)?.folder.name ?? "its folder"))
988
+ ];
989
+ return folders.length > 0 ? ` Unpinned from ${folders.join(", ")}.` : "";
990
+ };
991
+ var unignoredNote = (regions) => regions.length > 0 ? ` ${regionLabel(regions)} no longer ignored.` : "";
992
+ var unignoreEdit = (plan, regions, context) => ({ setups: [...plan.setups], ...withoutIgnored(plan, regions, context.kernel) });
993
+ var featureAt = (region, features) => featuresAt(region, features).find((feature) => !isFallback(feature.featureType)) ?? null;
994
+ var featuresAt = (region, features) => {
995
+ const owners = features.filter((feature) => (feature.regionIdxs ?? []).includes(region)).sort(
996
+ (a, b) => Number(isFallback(a.featureType)) - Number(isFallback(b.featureType)) || (a.regionIdxs?.length ?? 0) - (b.regionIdxs?.length ?? 0) || a.tag.localeCompare(b.tag)
997
+ );
998
+ const seen = /* @__PURE__ */ new Set();
999
+ return owners.filter((feature) => {
1000
+ const key = [...new Set(feature.regionIdxs ?? [])].sort((a, b) => a - b).join(",");
1001
+ return seen.has(key) ? false : (seen.add(key), true);
1002
+ });
1003
+ };
1004
+ var ignoreSelection = ({
1005
+ region,
1006
+ mode,
1007
+ features,
1008
+ regions
1009
+ }) => {
1010
+ if (mode === "face") {
1011
+ return { tag: null, regions: faceRegions(regions, region) };
1012
+ }
1013
+ if (mode === "region") {
1014
+ return { tag: null, regions: [region] };
1015
+ }
1016
+ const feature = featureAt(region, features);
1017
+ return {
1018
+ tag: feature?.tag ?? null,
1019
+ regions: feature ? [...new Set(feature.regionIdxs ?? [])] : [region]
1020
+ };
1021
+ };
1022
+ var regionLabel = (regions) => {
1023
+ const ids = [...regions].sort((a, b) => a - b);
1024
+ return `${ids.length === 1 ? "Region" : "Regions"} ${ids.join(", ")}`;
1025
+ };
1026
+ var takeBackIgnore = (plan, saved, regions, context) => ({
1027
+ setups: [...plan.setups],
1028
+ ...ignoredAsSaved(plan, saved, regions, context.kernel)
1029
+ });
1030
+ var ignoreClick = ({
1031
+ region,
1032
+ ignored,
1033
+ regions
1034
+ }) => {
1035
+ if (ignored.has(region)) {
1036
+ return { kind: "unignore", regions: regions.filter((each) => ignored.has(each)) };
1037
+ }
1038
+ return { kind: "ignore", regions: [...regions] };
1039
+ };
1040
+
1041
+ // src/engine-plan.ts
1042
+ var featureTypeLabel = (type) => {
1043
+ const words = type.replaceAll("_", " ");
1044
+ return words.charAt(0).toUpperCase() + words.slice(1);
1045
+ };
1046
+ var notMachinedOf = (setup) => setup.unmachined;
1047
+ var planRunOf = (plan) => plan.jobId ? `${plan.planId}/${plan.jobId}` : plan.planId;
1048
+
1049
+ // src/not-machined.ts
1050
+ var REASON_SHORT = {
1051
+ library: "no tool in your libraries",
1052
+ sharpCorner: "corner too sharp",
1053
+ planned: "planned, not cut",
1054
+ leftOut: "not planned",
1055
+ noFeature: "not a feature",
1056
+ notFinished: "not finished"
1057
+ };
1058
+ var reasonLine = (reason, detail) => `${REASON_SHORT[reason].charAt(0).toUpperCase()}${REASON_SHORT[reason].slice(1)}: ${detail}`;
1059
+ var indexOf = (setups, at) => setups[at].idx ?? at;
1060
+ var alike = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z > 0.9999;
1061
+ var isFromBelow = (direction) => (direction?.z ?? 0) < -0.5;
1062
+ var whyOf = (tag, setups, direction) => {
1063
+ if (tag === null) {
1064
+ return { kind: "noFeature" };
1065
+ }
1066
+ const key = tag.toLowerCase();
1067
+ const planned = setups.findIndex(
1068
+ (setup) => (setup.unmachined ?? []).some((each) => each.toLowerCase() === key)
1069
+ );
1070
+ if (planned >= 0) {
1071
+ return { kind: "planned", setup: indexOf(setups, planned) };
1072
+ }
1073
+ const facing = direction ? setups.findIndex((setup) => setup.direction && alike(setup.direction, direction)) : -1;
1074
+ return { kind: "leftOut", setup: facing >= 0 ? indexOf(setups, facing) : null };
1075
+ };
1076
+ var notFinishedIn = (setup) => {
1077
+ const cut = new Set(
1078
+ setup.actions.flatMap((action) => action.featureTags ?? []).map((tag) => tag.toLowerCase())
1079
+ );
1080
+ return [...new Set(setup.unmachined ?? [])].filter((tag) => cut.has(tag.toLowerCase()));
1081
+ };
1082
+ var notFinishedOf = (setups) => {
1083
+ const seen = /* @__PURE__ */ new Set();
1084
+ return setups.flatMap(
1085
+ (setup, at) => notFinishedIn(setup).flatMap((tag) => {
1086
+ if (seen.has(tag.toLowerCase())) {
1087
+ return [];
1088
+ }
1089
+ seen.add(tag.toLowerCase());
1090
+ return [{ tag, setup: indexOf(setups, at) }];
1091
+ })
1092
+ );
1093
+ };
1094
+ var whyText = (why, setupName) => {
1095
+ if (why.kind === "noFeature") {
1096
+ return reasonLine(why.kind, "nothing can be told to cut this face");
1097
+ }
1098
+ const name = why.setup === null ? null : setupName?.(why.setup) ?? null;
1099
+ if (why.kind === "notFinished") {
1100
+ return reasonLine(
1101
+ why.kind,
1102
+ name ? `roughed in ${name}, but no pass finishes it` : "roughed, but no pass finishes it"
1103
+ );
1104
+ }
1105
+ if (why.kind === "planned") {
1106
+ return reasonLine(why.kind, name ? `in ${name}, but no pass cuts it` : "no pass cuts it");
1107
+ }
1108
+ return reasonLine(why.kind, name ? `in no setup, though ${name} faces its way` : "in no setup");
1109
+ };
1110
+
1111
+ // src/plan-view.ts
1112
+ var PART_ROW = "part";
1113
+ var IGNORE = "ignore";
1114
+ var featureTypePlural = (type) => {
1115
+ if (!type) {
1116
+ return "Other";
1117
+ }
1118
+ const label = featureTypeLabel(type);
1119
+ return /(s|x|ch|sh)$/.test(label) ? `${label}es` : `${label}s`;
1120
+ };
1121
+ var isHoleType = (type) => type?.endsWith("_hole") ?? false;
1122
+ var boreKey = (bore) => bore.diameter.toFixed(4);
1123
+ var featureGroups = (rows) => [...rowsByType(rows)].map(([key, members]) => typeGroup(key, members));
1124
+ var rowsByType = (rows) => {
1125
+ const byType = /* @__PURE__ */ new Map();
1126
+ for (const row of rows) {
1127
+ const key = row.type ?? "";
1128
+ byType.set(key, [...byType.get(key) ?? [], row]);
1129
+ }
1130
+ return byType;
1131
+ };
1132
+ var tagsOf = (rows) => rows.map((r) => r.tag);
1133
+ var typeGroup = (key, members) => {
1134
+ const type = key || void 0;
1135
+ const label = featureTypePlural(type);
1136
+ if (!isHoleType(type)) {
1137
+ return {
1138
+ key: `t:${key}`,
1139
+ level: "type",
1140
+ type,
1141
+ label,
1142
+ rows: members,
1143
+ groups: [],
1144
+ tags: tagsOf(members)
1145
+ };
1146
+ }
1147
+ const unread = members.filter((row) => !hasBore(row));
1148
+ const groups = boreGroups(key, members);
1149
+ return {
1150
+ key: `t:${key}`,
1151
+ level: "type",
1152
+ type,
1153
+ label,
1154
+ rows: unread,
1155
+ groups,
1156
+ tags: [...groups.flatMap((group) => group.tags), ...tagsOf(unread)]
1157
+ };
1158
+ };
1159
+ var boreGroups = (type, members) => [...rowsByBore(members)].sort(([, a], [, b]) => a.bore.diameter - b.bore.diameter).map(
1160
+ ([bore, at]) => ({
1161
+ key: `t:${type}:${bore}`,
1162
+ level: "bore",
1163
+ type,
1164
+ label: at.bore.label,
1165
+ rows: at.rows,
1166
+ groups: [],
1167
+ tags: tagsOf(at.rows)
1168
+ })
1169
+ );
1170
+ var hasBore = (row) => Boolean(row.bore);
1171
+ var rowsByBore = (rows) => {
1172
+ const byBore = /* @__PURE__ */ new Map();
1173
+ for (const row of rows) {
1174
+ if (!hasBore(row)) {
1175
+ continue;
1176
+ }
1177
+ const at = byBore.get(boreKey(row.bore));
1178
+ if (at) {
1179
+ at.rows.push(row);
1180
+ } else {
1181
+ byBore.set(boreKey(row.bore), { bore: row.bore, rows: [row] });
1182
+ }
1183
+ }
1184
+ return byBore;
1185
+ };
1186
+ var allFolders = (views) => views.flatMap((view) => [view, ...allFolders(view.folders)]);
1187
+ var rowsOf = (folder) => folder.orientations.flatMap((orientation) => orientation.features);
1188
+ var listsTag = (folder, tag) => {
1189
+ const key = tagKey(tag);
1190
+ return rowsOf(folder).some((row) => tagKey(row.tag) === key);
1191
+ };
1192
+ var folderPath = (setups, folderId) => {
1193
+ const folders = allFolders(setups);
1194
+ const names = [];
1195
+ let at = folders.find((folder) => folder.id === folderId);
1196
+ while (at) {
1197
+ names.unshift(at.name);
1198
+ const parentId = at.parentId;
1199
+ at = parentId ? folders.find((folder) => folder.id === parentId) : void 0;
1200
+ }
1201
+ return names.join(" \u203A ");
1202
+ };
1203
+ var flatView = (view) => ({
1204
+ ...view,
1205
+ depth: 0,
1206
+ setupId: view.id,
1207
+ parentId: null,
1208
+ toolOrientation: false,
1209
+ isFrame: true,
1210
+ folders: []
1211
+ });
1212
+ var featureRow = (part, tag) => {
1213
+ const bore = part.featureBore?.(tag);
1214
+ return {
1215
+ tag,
1216
+ label: part.featureLabel(tag),
1217
+ type: part.featureType?.(tag),
1218
+ ...bore ? { bore } : {}
1219
+ };
1220
+ };
1221
+ var unique = (tags) => [...new Set(tags)];
1222
+ var planView = (plan, part) => plan.setups.map(
1223
+ (setup, index) => plannedSetupView(setup, index, plannedDirections(plan, setup, index), part)
1224
+ );
1225
+ var plannedDirections = (plan, setup, index) => {
1226
+ const stated = plan.setupPlan?.setups[index]?.orientations;
1227
+ if (stated) {
1228
+ return stated.map((orientation) => ({
1229
+ direction: orientation.direction,
1230
+ tags: unique(orientation.features)
1231
+ }));
1232
+ }
1233
+ if (!setup.direction) {
1234
+ return [];
1235
+ }
1236
+ return [
1237
+ {
1238
+ direction: setup.direction,
1239
+ tags: unique(setup.actions.flatMap((action) => action.featureTags))
1240
+ }
1241
+ ];
1242
+ };
1243
+ var plannedSetupView = (setup, index, directions, part) => {
1244
+ const row = (tag) => featureRow(part, tag);
1245
+ return flatView({
1246
+ id: `setup-${setup.idx}`,
1247
+ name: `Setup ${index + 1}`,
1248
+ process: directions.length > 1 ? "3+2" : "3axis",
1249
+ orientations: directions.map(({ direction, tags }, at) => ({
1250
+ id: `setup-${setup.idx}-${at}`,
1251
+ direction,
1252
+ directionIndex: part.directionIndex(direction),
1253
+ features: tags.map(row)
1254
+ })),
1255
+ notMachined: unique(notMachinedOf(setup)).map(row),
1256
+ notFinished: notFinishedIn(setup),
1257
+ operations: setup.actions.length
1258
+ });
1259
+ };
1260
+ var planCounts = (setups) => ({
1261
+ setups: setups.length,
1262
+ orientations: allFolders(setups).filter((view) => view.isFrame).reduce((sum, frame) => sum + frame.orientations.length, 0)
1263
+ });
1264
+ var DONE_COLOR = 6583435;
1265
+ var UNMACHINED_FACE_COLOR = 14427686;
1266
+ var IGNORED_WEIGHT = 0.6;
1267
+ var NOT_MACHINED_WEIGHT = 0.6;
1268
+ var PART_WEIGHT = 0.85;
1269
+ var OPEN_WEIGHT = 0.8;
1270
+ var OTHER_FOLDER_COLOR = 5395026;
1271
+ var OTHER_FOLDER_WEIGHT = 0.75;
1272
+ var folderRows = ({ setups, openId, colorOf }) => {
1273
+ const folders = allFolders(setups);
1274
+ const open = folders.find((folder) => folder.id === openId);
1275
+ const rows = ownerRowsByTag(folders, colorOf, selectedIn(open));
1276
+ return {
1277
+ open,
1278
+ rowsOf: (face) => face.owners.flatMap((tag) => rows.get(tagKey(tag)) ?? [])
1279
+ };
1280
+ };
1281
+ var selectedIn = (open) => {
1282
+ const inOpen = new Set(open ? allFolders([open]) : []);
1283
+ const way = open?.orientations[0]?.direction;
1284
+ return (folder, orientation) => inOpen.has(folder) && way !== void 0 && sameDirection(orientation.direction, way);
1285
+ };
1286
+ var ownerRowsByTag = (folders, colorOf, isSelected) => {
1287
+ const rows = /* @__PURE__ */ new Map();
1288
+ const orientations = folders.flatMap(
1289
+ (folder) => folder.orientations.map((orientation) => ({ folder, orientation }))
1290
+ );
1291
+ for (const { folder, orientation } of orientations) {
1292
+ const selected = isSelected(folder, orientation);
1293
+ for (const feature of orientation.features) {
1294
+ const key = tagKey(feature.tag);
1295
+ rows.set(key, [...rows.get(key) ?? [], { color: colorOf(orientation), selected }]);
1296
+ }
1297
+ }
1298
+ return rows;
1299
+ };
1300
+ var planPaint = ({
1301
+ uncovered = true,
1302
+ ...input
1303
+ }) => {
1304
+ const { faces, everyFolder = true } = input;
1305
+ const { open, rowsOf: rowsOf2 } = folderRows(input);
1306
+ const showing = { folderOpen: open !== void 0, everyFolder };
1307
+ const fromFolder = (face) => folderPaint(face, rowsOf2(face), showing);
1308
+ return [
1309
+ ...faces.pinned.flatMap(fromFolder),
1310
+ ...faces.machined.flatMap(fromFolder),
1311
+ ...faces.ignored.map((face) => facePaint(face, DONE_COLOR, IGNORED_WEIGHT)),
1312
+ ...uncovered ? faces.notMachined.map((face) => facePaint(face, UNMACHINED_FACE_COLOR, NOT_MACHINED_WEIGHT)) : []
1313
+ ];
1314
+ };
1315
+ var facePaint = (face, color, weight) => ({
1316
+ region: face.region,
1317
+ color,
1318
+ weight
1319
+ });
1320
+ var folderPaint = (face, own, { folderOpen, everyFolder }) => {
1321
+ const row = own.find((each) => each.selected) ?? own[0];
1322
+ if (!row) {
1323
+ return [];
1324
+ }
1325
+ if (!folderOpen) {
1326
+ return everyFolder ? [facePaint(face, row.color, PART_WEIGHT)] : [];
1327
+ }
1328
+ if (row.selected) {
1329
+ return [facePaint(face, row.color, OPEN_WEIGHT)];
1330
+ }
1331
+ return everyFolder ? [facePaint(face, OTHER_FOLDER_COLOR, OTHER_FOLDER_WEIGHT)] : [];
1332
+ };
1333
+ var planHatch = (input) => {
1334
+ const { faces, everyFolder = true } = input;
1335
+ const { open, rowsOf: rowsOf2 } = folderRows(input);
1336
+ if (!everyFolder) {
1337
+ return [];
1338
+ }
1339
+ const hatchOf = (face) => {
1340
+ const own = rowsOf2(face);
1341
+ const secondary = open ? own.filter((row) => !row.selected) : own.filter((row) => row.color !== own[0]?.color);
1342
+ const colors = new Set(secondary.map((row) => row.color));
1343
+ return [...colors].map((color) => ({ region: face.region, color }));
1344
+ };
1345
+ return [...faces.pinned.flatMap(hatchOf), ...faces.machined.flatMap(hatchOf)];
1346
+ };
1347
+ var draftView = (draft, part) => {
1348
+ const placed = placedFolders(draft.setups);
1349
+ const view = (here) => ({
1350
+ id: here.folder.id,
1351
+ name: here.folder.name,
1352
+ process: here.depth === 0 ? here.setup.process ?? "3axis" : "3axis",
1353
+ orientations: [
1354
+ {
1355
+ id: `${here.folder.id}-0`,
1356
+ direction: here.direction,
1357
+ directionIndex: part.directionIndex(here.direction),
1358
+ features: unique(here.folder.features).map((tag) => featureRow(part, tag))
1359
+ }
1360
+ ],
1361
+ depth: here.depth,
1362
+ setupId: here.setup.id,
1363
+ parentId: here.parent?.id ?? null,
1364
+ toolOrientation: here.toolOrientation,
1365
+ isFrame: here.frameId === here.folder.id,
1366
+ folders: placed.filter((child) => child.parent?.id === here.folder.id).map(view)
1367
+ });
1368
+ return placed.filter((each) => each.depth === 0).map(view);
1369
+ };
1370
+ var refusalClauses = (error) => {
1371
+ if (error?.startsWith("stale_regions:")) {
1372
+ return [
1373
+ "Ignored faces are from an older analysis of this part. Clear them, then ignore them again."
1374
+ ];
1375
+ }
1376
+ if (error?.startsWith("unknown_regions:")) {
1377
+ return [
1378
+ "Some ignored faces are not on this part\u2019s current analysis. Clear them, then ignore them again."
1379
+ ];
1380
+ }
1381
+ if (!error?.startsWith("refused_user_plan:")) {
1382
+ return null;
1383
+ }
1384
+ const listed = error.slice(error.indexOf("refused:") + "refused:".length);
1385
+ return listed.split(";").map((clause) => clause.trim()).filter(Boolean);
1386
+ };
1387
+
1388
+ // src/pin.ts
1389
+ var coversAll = (outer, inner) => {
1390
+ const has = new Set(outer);
1391
+ return inner.length > 0 && inner.every((region) => has.has(region));
1392
+ };
1393
+ var replacedBy = (setups, tag, regionsOf2) => {
1394
+ const regions = regionsOf2(tag);
1395
+ return pinsWhere(setups, (other) => other !== tag && coversAll(regions, regionsOf2(other)));
1396
+ };
1397
+ var unignorePinned = (plan, tags, context) => {
1398
+ const ignored = new Set(currentFor(plan.done, plan.doneKernel, context.kernel));
1399
+ const unignored = [...new Set(tags.flatMap((tag) => context.regionsOf(tag)))].filter((region) => ignored.has(region)).sort((a, b) => a - b);
1400
+ return {
1401
+ intent: unignored.length > 0 ? unignoreEdit(plan, unignored, context) : plan,
1402
+ unignored
1403
+ };
1404
+ };
1405
+ var pinEdit = (plan, tag, at, context) => {
1406
+ const result = "folderId" in at ? pinInto(plan.setups, at.folderId, tag, at.newId) : pinFrom(plan.setups, at.direction, tag, at.newId);
1407
+ if ("refused" in result) {
1408
+ return result;
1409
+ }
1410
+ const { setupId, added, unpinned } = result;
1411
+ if (unpinned) {
1412
+ return {
1413
+ intent: { ...plan, setups: result.setups },
1414
+ setupId,
1415
+ added,
1416
+ unpinned,
1417
+ replaced: [],
1418
+ unignored: []
1419
+ };
1420
+ }
1421
+ return { ...withPin(plan, tag, result.setups, context), setupId, added, unpinned };
1422
+ };
1423
+ var withPin = (plan, tag, pinned2, context) => {
1424
+ const replaced = replacedBy(plan.setups, tag, context.regionsOf);
1425
+ const setups = replaced.reduce(
1426
+ (all, each) => unpinFeature(all, each.setupId, each.tag),
1427
+ [...pinned2]
1428
+ );
1429
+ return {
1430
+ ...unignorePinned({ ...plan, setups }, [tag], context),
1431
+ replaced: replaced.map((each) => each.tag)
1432
+ };
1433
+ };
1434
+ var letGo = (plan, tag) => {
1435
+ const was = pinnedIn(plan.setups, tag);
1436
+ return was ? { ...plan, setups: unpinFeature(plan.setups, was, tag) } : plan;
1437
+ };
1438
+ var takeBack = (plan, saved, tag, context) => {
1439
+ const home = pinnedIn(plan.setups, tag);
1440
+ const pinnedSince = home !== null && home !== pinnedIn(saved.setups, tag);
1441
+ const replaced = pinnedSince ? replacedBy(saved.setups, tag, context.regionsOf) : [];
1442
+ const tags = [.../* @__PURE__ */ new Set([tag, ...replaced.map((each) => each.tag)])];
1443
+ return {
1444
+ ...plan,
1445
+ setups: tags.reduce((all, each) => restoreFeature(all, saved.setups, each), [...plan.setups]),
1446
+ ...pinnedSince ? ignoredAsSaved(plan, saved, context.regionsOf(tag), context.kernel) : {}
1447
+ };
1448
+ };
1449
+ var folderClick = (plan, feature, folder, context) => {
1450
+ if (feature.inFolder) {
1451
+ return {
1452
+ intent: { ...plan, setups: unpinFeature(plan.setups, folder.id, feature.tag) },
1453
+ note: `${feature.label} taken out of ${folder.name}.`
1454
+ };
1455
+ }
1456
+ const pinned2 = pinHere(plan, feature.tag, folder.id, context);
1457
+ return {
1458
+ intent: pinned2.intent,
1459
+ note: `${feature.label} pinned to ${folder.name}.${unignoredNote(pinned2.unignored)}`
1460
+ };
1461
+ };
1462
+ var pinHere = (plan, tag, folderId, context) => {
1463
+ if (pinnedIn(plan.setups, tag) === folderId) {
1464
+ return { intent: plan, unignored: [] };
1465
+ }
1466
+ const pinned2 = pinEdit(plan, tag, { folderId }, context);
1467
+ return "refused" in pinned2 ? { intent: plan, unignored: [] } : pinned2;
1468
+ };
1469
+ var chooseReading = (plan, saved, line, reading, read, folder, context) => {
1470
+ const choice = { plan, saved, line, reading, read, folder, context };
1471
+ return line.target === "pin" ? chosenForPin(choice) : chosenForTakeOut(choice);
1472
+ };
1473
+ var chosenForPin = ({
1474
+ plan,
1475
+ saved,
1476
+ line,
1477
+ reading,
1478
+ folder,
1479
+ context
1480
+ }) => {
1481
+ if (reading === line.tag) {
1482
+ return { intent: plan, chosen: [reading] };
1483
+ }
1484
+ const left = leaving(plan, saved, line.tag, context);
1485
+ return { intent: pinHere(left, reading, folder.id, context).intent, chosen: [reading] };
1486
+ };
1487
+ var chosenForTakeOut = (choice) => {
1488
+ const { plan, saved, line, reading, folder, context } = choice;
1489
+ const cleared = chosenBefore(choice).reduce((all, tag) => leaving(all, saved, tag, context), plan);
1490
+ const out = pinnedIn(cleared.setups, line.tag) === pinnedIn(plan.setups, line.tag) ? cleared : letGo(cleared, line.tag);
1491
+ return {
1492
+ intent: reading === line.tag ? out : pinHere(out, reading, folder.id, context).intent,
1493
+ chosen: reading === line.tag ? [line.tag] : [reading, line.tag]
1494
+ };
1495
+ };
1496
+ var leaving = (plan, saved, tag, context) => letGo(takeBack(plan, saved, tag, context), tag);
1497
+ var chosenBefore = ({
1498
+ plan,
1499
+ saved,
1500
+ line,
1501
+ reading,
1502
+ read,
1503
+ folder
1504
+ }) => read.filter(
1505
+ (each) => each !== line.tag && each !== reading && pinnedIn(plan.setups, each) === folder.id && pinnedIn(saved.setups, each) !== folder.id
1506
+ );
1507
+ var readingOf = (folders, coverage) => {
1508
+ const faces = facesByRegion(coverage);
1509
+ return { folders, kindOf: (region) => faces.get(region)?.kind ?? "open" };
1510
+ };
1511
+ var readingDetail = (faces, state, way) => [`${faces} face${faces === 1 ? "" : "s"}`, ...way ? [way] : [], state].join(" \xB7 ");
1512
+ var stateOf = (tag, plan, folderId, regionsOf2) => {
1513
+ const faces = regionsOf2(tag);
1514
+ const kind = kindAmong(faces.map(plan.kindOf));
1515
+ const said = (text, folders = []) => stateSaid(text, folders, folderId);
1516
+ if (kind === "ignored") {
1517
+ return said(COVERAGE_LABELS.ignored);
1518
+ }
1519
+ const own = plan.folders.filter((folder) => listsTag(folder, tag));
1520
+ if (own.length > 0) {
1521
+ return said(COVERAGE_LABELS.pinned, own);
1522
+ }
1523
+ const around = pinCovering(plan.folders, faces, regionsOf2);
1524
+ if (around) {
1525
+ return regionsOf2(around.row.tag).length === faces.length ? said(COVERAGE_LABELS.pinned, around.folders) : said(`Part of ${around.row.label}`, around.folders);
1526
+ }
1527
+ const shared = foldersSharing(plan.folders, faces, regionsOf2);
1528
+ if (shared.length > 0 && kind === "pinned") {
1529
+ return said(COVERAGE_LABELS.pinned, shared);
1530
+ }
1531
+ return said(COVERAGE_LABELS[kind]);
1532
+ };
1533
+ var kindAmong = (kinds) => COVERAGE_KINDS.find((each) => kinds.includes(each)) ?? "open";
1534
+ var stateSaid = (text, folders, folderId) => {
1535
+ const first = folders[0];
1536
+ const where = !first || first.id === folderId ? "" : ` in ${first.name}`;
1537
+ const more = folders.length > 1 ? ` +${folders.length - 1}` : "";
1538
+ return { text: `${text}${where}${more}`, folders: folders.map((folder) => folder.id) };
1539
+ };
1540
+ var pinCovering = (folders, faces, regionsOf2) => {
1541
+ const around = folders.flatMap(
1542
+ (folder) => rowsOf(folder).filter((row) => coversAll(regionsOf2(row.tag), faces)).map((row) => ({ folder, row }))
1543
+ );
1544
+ const first = around[0];
1545
+ if (!first) {
1546
+ return void 0;
1547
+ }
1548
+ return { row: first.row, folders: [...new Set(around.map((each) => each.folder))] };
1549
+ };
1550
+ var foldersSharing = (folders, faces, regionsOf2) => folders.filter(
1551
+ (folder) => rowsOf(folder).some((row) => regionsOf2(row.tag).some((region) => faces.includes(region)))
1552
+ );
1553
+
1554
+ // src/draft-action.ts
1555
+ var DRAFT_ACTION_KINDS = [
1556
+ "addFolder",
1557
+ "renameFolder",
1558
+ "remove",
1559
+ "moveFolder",
1560
+ "moveWithin",
1561
+ "addSetup",
1562
+ "setProcess",
1563
+ "setToolOrientation",
1564
+ "pinInto",
1565
+ "pinFrom",
1566
+ "moveFeature",
1567
+ "unpin",
1568
+ "ignore",
1569
+ "unignore",
1570
+ "choosePinReading",
1571
+ "chooseIgnoreReading",
1572
+ "fromPlan"
1573
+ ];
1574
+ var NO_FACTS = Object.freeze({
1575
+ replaced: Object.freeze([]),
1576
+ unignored: Object.freeze([]),
1577
+ unpinned: Object.freeze([]),
1578
+ added: false,
1579
+ letGo: false,
1580
+ home: null
1581
+ });
1582
+ var TOO_MANY = `A plan holds at most ${MAX_SETUPS} setups.`;
1583
+ var OTHER_WAY = "Rows stay in folders cut from their own direction.";
1584
+ var TAKEN = "That id is taken: each folder needs an id of its own.";
1585
+ var NOT_AN_ACTION = "That is not a plan action.";
1586
+ var NEEDS_SAVED = "This needs the plan as applied.";
1587
+ var NOT_EMPTY = "Start from Toolpath\u2019s plan while the plan holds no folders.";
1588
+ var applyDraftAction = (draft, action, rules, saved) => {
1589
+ if (typeof action !== "object" || action === null) {
1590
+ return { refused: NOT_AN_ACTION };
1591
+ }
1592
+ const result = carryOut({ draft, rules, saved }, action);
1593
+ if ("refused" in result) {
1594
+ return result;
1595
+ }
1596
+ const unignored = action.kind === "chooseIgnoreReading" ? [] : ignoredNoLonger(draft, result.draft, rules);
1597
+ return { ...result, facts: { ...NO_FACTS, ...result.facts, unignored } };
1598
+ };
1599
+ var ignoredNoLonger = (before, after, rules) => {
1600
+ const still = new Set(ignoredIn(after, rules));
1601
+ return ignoredIn(before, rules).filter((region) => !still.has(region)).sort((a, b) => a - b);
1602
+ };
1603
+ var ignoredIn = (plan, rules) => currentFor(plan.done, plan.doneKernel, rules.kernel);
1604
+ var withConstraints = (draft, intent, more = {}) => ({ ...more, draft: { ...draft, ...intent } });
1605
+ var withSetups = (draft, setups, folderId) => withConstraints(draft, { ...draft, setups }, folderId ? { folderId } : {});
1606
+ var withEdited = (draft, edited, folderId) => Array.isArray(edited) ? withSetups(draft, edited, folderId) : { refused: edited.refused };
1607
+ var holds = (draft, id) => findPlaced(draft.setups, id) !== void 0;
1608
+ var anyTaken = (draft, ...ids) => ids.some((id) => holds(draft, id)) || new Set(ids).size < ids.length;
1609
+ var pinned = ({ draft, rules }, at, tag) => {
1610
+ const pin = pinEdit(draft, tag, at, rules);
1611
+ if ("refused" in pin) {
1612
+ return pin;
1613
+ }
1614
+ return withConstraints(draft, pin.intent, {
1615
+ facts: { replaced: pin.replaced, added: pin.added, letGo: pin.unpinned, home: pin.setupId }
1616
+ });
1617
+ };
1618
+ var applyAddFolder = ({ draft }, action) => {
1619
+ const parent = findPlaced(draft.setups, action.parentId);
1620
+ if (!parent) {
1621
+ return { refused: GONE };
1622
+ }
1623
+ if (parent.depth >= MAX_DEPTH) {
1624
+ return { refused: TOO_DEEP };
1625
+ }
1626
+ if (anyTaken(draft, action.id, action.workId)) {
1627
+ return { refused: TAKEN };
1628
+ }
1629
+ const setups = addFolder(draft.setups, action.parentId, action.id, action.workId);
1630
+ return withSetups(draft, setups, action.id);
1631
+ };
1632
+ var applyRenameFolder = ({ draft }, action) => {
1633
+ if (!holds(draft, action.id)) {
1634
+ return { refused: GONE };
1635
+ }
1636
+ return withSetups(draft, renameFolder(draft.setups, action.id, action.name));
1637
+ };
1638
+ var applyRemove = ({ draft }, action) => {
1639
+ if (!Array.isArray(action.folders)) {
1640
+ return { refused: NOT_AN_ACTION };
1641
+ }
1642
+ const unpinned = (action.features ?? []).reduce(
1643
+ (setups, pin) => unpinFeature(setups, pin.setupId, pin.tag),
1644
+ [...draft.setups]
1645
+ );
1646
+ const folders = action.folders.filter((id) => holds(draft, id));
1647
+ return withSetups(draft, removeFolders(unpinned, folders));
1648
+ };
1649
+ var applyMoveFolder = ({ draft }, action) => {
1650
+ if (!holds(draft, action.id)) {
1651
+ return { refused: GONE };
1652
+ }
1653
+ if (anyTaken(draft, action.newId)) {
1654
+ return { refused: TAKEN };
1655
+ }
1656
+ const moved = moveFolder(draft.setups, action.id, action.to, action.newId);
1657
+ return withEdited(draft, moved, action.id);
1658
+ };
1659
+ var applyMoveWithin = ({ draft }, action) => {
1660
+ if (!holds(draft, action.id)) {
1661
+ return { refused: GONE };
1662
+ }
1663
+ if (!Number.isInteger(action.by)) {
1664
+ return { refused: "A folder moves a whole number of places." };
1665
+ }
1666
+ return withSetups(draft, moveWithin(draft.setups, action.id, action.by));
1667
+ };
1668
+ var applyAddSetup = ({ draft }, action) => {
1669
+ const facing = frameFacing(draft.setups, action.direction);
1670
+ if (facing) {
1671
+ return { refused: `${facing.name} is cut from that direction already.` };
1672
+ }
1673
+ if (draft.setups.length >= MAX_SETUPS) {
1674
+ return { refused: TOO_MANY };
1675
+ }
1676
+ if (anyTaken(draft, action.id)) {
1677
+ return { refused: TAKEN };
1678
+ }
1679
+ return withSetups(draft, addSetup(draft.setups, action.direction, action.id), action.id);
1680
+ };
1681
+ var applySetProcess = ({ draft }, action) => {
1682
+ if (!draft.setups.some((setup) => setup.id === action.setupId)) {
1683
+ return { refused: holds(draft, action.setupId) ? "Only a setup has a hold." : GONE };
1684
+ }
1685
+ const process = action.process ?? void 0;
1686
+ return withEdited(draft, setSetupProcess(draft.setups, action.setupId, process));
1687
+ };
1688
+ var applySetToolOrientation = ({ draft }, action) => {
1689
+ if (!holds(draft, action.folderId)) {
1690
+ return { refused: GONE };
1691
+ }
1692
+ return withEdited(draft, setToolOrientation(draft.setups, action.folderId, action.direction));
1693
+ };
1694
+ var applyPinInto = (editing, action) => {
1695
+ const { setups } = editing.draft;
1696
+ const target = findPlaced(setups, action.folderId);
1697
+ if (!target) {
1698
+ return { refused: GONE };
1699
+ }
1700
+ if (anyTaken(editing.draft, action.newId)) {
1701
+ return { refused: TAKEN };
1702
+ }
1703
+ if (pinnedIn(setups, action.tag) === action.folderId) {
1704
+ return pinned(editing, { folderId: action.folderId }, action.tag);
1705
+ }
1706
+ if (!sameDirection(target.direction, action.direction)) {
1707
+ return { refused: OTHER_WAY };
1708
+ }
1709
+ return pinned(editing, { folderId: action.folderId, newId: action.newId }, action.tag);
1710
+ };
1711
+ var applyPinFrom = (editing, action) => {
1712
+ if (anyTaken(editing.draft, action.newId)) {
1713
+ return { refused: TAKEN };
1714
+ }
1715
+ return pinned(editing, { direction: action.direction, newId: action.newId }, action.tag);
1716
+ };
1717
+ var applyMoveFeature = ({ draft, rules }, action) => {
1718
+ if (anyTaken(draft, action.newId)) {
1719
+ return { refused: TAKEN };
1720
+ }
1721
+ const { tag, direction, folderId, newId } = action;
1722
+ const moved = moveFeature(draft.setups, tag, direction, folderId, newId);
1723
+ if (!Array.isArray(moved)) {
1724
+ return { refused: moved.refused };
1725
+ }
1726
+ const pin = withPin(draft, tag, moved, rules);
1727
+ return withConstraints(draft, pin.intent, {
1728
+ folderId: pinnedIn(moved, tag) ?? folderId,
1729
+ facts: { replaced: pin.replaced }
1730
+ });
1731
+ };
1732
+ var applyUnpin = ({ draft }, action) => {
1733
+ const home = pinnedIn(draft.setups, action.tag);
1734
+ if (!home) {
1735
+ return { draft };
1736
+ }
1737
+ return withSetups(draft, unpinFeature(draft.setups, home, action.tag));
1738
+ };
1739
+ var applyIgnore = ({ draft, rules }, action) => {
1740
+ if (!Array.isArray(action.regions)) {
1741
+ return { refused: NOT_AN_ACTION };
1742
+ }
1743
+ const ignored = ignoreEdit(draft, action.regions, rules);
1744
+ return withConstraints(draft, ignored.intent, { facts: { unpinned: ignored.unpinned } });
1745
+ };
1746
+ var applyUnignore = ({ draft, rules }, action) => {
1747
+ if (!Array.isArray(action.regions)) {
1748
+ return { refused: NOT_AN_ACTION };
1749
+ }
1750
+ return withConstraints(draft, unignoreEdit(draft, action.regions, rules));
1751
+ };
1752
+ var applyChoosePinReading = ({ draft, rules, saved }, action) => {
1753
+ const placed = findPlaced(draft.setups, action.folderId);
1754
+ if (!placed) {
1755
+ return { draft };
1756
+ }
1757
+ if (!saved) {
1758
+ return { refused: NEEDS_SAVED };
1759
+ }
1760
+ const clicked = withClickStanding(draft, action, placed, rules);
1761
+ const chosen = chooseReading(
1762
+ clicked,
1763
+ saved,
1764
+ { tag: action.tag, target: action.target },
1765
+ action.reading,
1766
+ action.readings,
1767
+ { id: action.folderId, direction: placed.direction },
1768
+ rules
1769
+ );
1770
+ return withConstraints(draft, chosen.intent);
1771
+ };
1772
+ var withClickStanding = (draft, action, placed, rules) => {
1773
+ const inFolder = pinnedIn(draft.setups, action.tag) === action.folderId;
1774
+ const standing = inFolder !== (action.target === "takeOut");
1775
+ if (standing) {
1776
+ return draft;
1777
+ }
1778
+ const feature = { tag: action.tag, label: action.tag, direction: placed.direction, inFolder };
1779
+ const folder = { id: action.folderId, name: placed.folder.name };
1780
+ return folderClick(draft, feature, folder, rules).intent;
1781
+ };
1782
+ var applyChooseIgnoreReading = ({ draft, rules, saved }, action) => {
1783
+ if (!saved) {
1784
+ return { refused: NEEDS_SAVED };
1785
+ }
1786
+ const clicked = action.click.regions;
1787
+ const ignored = new Set(ignoredIn(draft, rules));
1788
+ const stands = clicked.every((region) => ignored.has(region));
1789
+ if (stands && action.tag === action.click.tag) {
1790
+ return { draft };
1791
+ }
1792
+ const back = stands ? takeBackIgnore(draft, saved, clicked, rules) : draft;
1793
+ const chosen = ignoreEdit(back, rules.regionsOf(action.tag), rules);
1794
+ return withConstraints(draft, chosen.intent, { facts: { unpinned: chosen.unpinned } });
1795
+ };
1796
+ var applyFromPlan = ({ draft, rules }, action) => {
1797
+ if (draft.setups.length > 0) {
1798
+ return { refused: NOT_EMPTY };
1799
+ }
1800
+ let count = 0;
1801
+ const newId = () => `${action.idSeed}-${count++}`;
1802
+ if (!action.pins) {
1803
+ return withSetups(draft, foldersFromPlan(action.planned, newId));
1804
+ }
1805
+ const stated = planFromResults(action.planned, newId);
1806
+ const tags = pinsWhere(stated, () => true).map((pin) => pin.tag);
1807
+ return withConstraints(draft, unignorePinned({ ...draft, setups: stated }, tags, rules).intent);
1808
+ };
1809
+ var CARRY_OUT = {
1810
+ addFolder: applyAddFolder,
1811
+ renameFolder: applyRenameFolder,
1812
+ remove: applyRemove,
1813
+ moveFolder: applyMoveFolder,
1814
+ moveWithin: applyMoveWithin,
1815
+ addSetup: applyAddSetup,
1816
+ setProcess: applySetProcess,
1817
+ setToolOrientation: applySetToolOrientation,
1818
+ pinInto: applyPinInto,
1819
+ pinFrom: applyPinFrom,
1820
+ moveFeature: applyMoveFeature,
1821
+ unpin: applyUnpin,
1822
+ ignore: applyIgnore,
1823
+ unignore: applyUnignore,
1824
+ choosePinReading: applyChoosePinReading,
1825
+ chooseIgnoreReading: applyChooseIgnoreReading,
1826
+ fromPlan: applyFromPlan
1827
+ };
1828
+ var carryOut = (editing, action) => {
1829
+ if (!Object.hasOwn(CARRY_OUT, action.kind)) {
1830
+ return { refused: NOT_AN_ACTION };
1831
+ }
1832
+ const carry = CARRY_OUT[action.kind];
1833
+ return carry(editing, action);
1834
+ };
1835
+ var actionsFor = (draft, target, rules) => {
1836
+ const allowed = new Set(actionsOn(draft, target, rules.kernel));
1837
+ return DRAFT_ACTION_KINDS.filter((kind) => allowed.has(kind));
1838
+ };
1839
+ var actionsOn = (draft, target, kernel) => {
1840
+ if ("folder" in target) {
1841
+ return folderActions(draft.setups, target.folder);
1842
+ }
1843
+ if ("feature" in target) {
1844
+ return featureActions(draft.setups, target.feature, target.direction);
1845
+ }
1846
+ if ("faces" in target) {
1847
+ return faceActions(draft, target.faces, kernel);
1848
+ }
1849
+ return partActions(draft.setups);
1850
+ };
1851
+ var when = (yes, ...kinds) => yes ? kinds : [];
1852
+ var partActions = (setups) => [
1853
+ ...when(setups.length < MAX_SETUPS, "addSetup"),
1854
+ ...when(setups.length > 0, "remove")
1855
+ ];
1856
+ var folderActions = (setups, id) => {
1857
+ const placed = findPlaced(setups, id);
1858
+ if (!placed) {
1859
+ return [];
1860
+ }
1861
+ const top = placed.depth === 0;
1862
+ const beside = top ? setups : placed.parent?.folders ?? [];
1863
+ const toolOriented = isIndexed(placed.setup) || placed.toolOrientation;
1864
+ return [
1865
+ "renameFolder",
1866
+ "remove",
1867
+ ...when(placed.depth < MAX_DEPTH, "addFolder"),
1868
+ ...when(beside.length > 1, "moveWithin", "moveFolder"),
1869
+ ...when(canMoveAway(setups, placed.folder.id, placed.parent?.id ?? null), "moveFolder"),
1870
+ ...when(top, "setProcess"),
1871
+ ...when(!top && toolOriented, "setToolOrientation")
1872
+ ];
1873
+ };
1874
+ var canMoveAway = (setups, id, parentId) => {
1875
+ const landings = [null, ...placedFolders(setups).map((placed) => placed.folder.id)].filter(
1876
+ (landing) => landing !== parentId && landing !== id
1877
+ );
1878
+ return landings.some(
1879
+ (landing) => Array.isArray(moveFolder(setups, id, { parentId: landing, index: 0 }))
1880
+ );
1881
+ };
1882
+ var PROBE_ID = "\0probe";
1883
+ var featureActions = (setups, tag, direction) => {
1884
+ const home = pinnedIn(setups, tag);
1885
+ const facingIds = placedFolders(setups).filter((each) => each.folder.id !== home && sameDirection(each.direction, direction)).map((each) => each.folder.id);
1886
+ return [
1887
+ ...when(facingIds.length > 0, "pinInto"),
1888
+ ...when(canPinFromItsWay(setups, home, direction), "pinFrom"),
1889
+ ...when(canMoveFeature(setups, tag, direction, facingIds), "moveFeature"),
1890
+ ...when(home !== null, "unpin")
1891
+ ];
1892
+ };
1893
+ var canPinFromItsWay = (setups, home, direction) => {
1894
+ const frame = frameFacing(setups, direction);
1895
+ if (!frame) {
1896
+ return setups.length < MAX_SETUPS;
1897
+ }
1898
+ const inFrame = home !== null && placedFolders(setups).some((each) => each.folder.id === home && each.frameId === frame.id);
1899
+ return !inFrame;
1900
+ };
1901
+ var canMoveFeature = (setups, tag, direction, facingIds) => {
1902
+ const home = pinnedIn(setups, tag);
1903
+ return facingIds.some((folderId) => {
1904
+ const moved = moveFeature(setups, tag, direction, folderId, PROBE_ID);
1905
+ return Array.isArray(moved) && pinnedIn(moved, tag) !== home;
1906
+ });
1907
+ };
1908
+ var faceActions = (draft, regions, kernel) => {
1909
+ const ignored = new Set(currentFor(draft.done, draft.doneKernel, kernel));
1910
+ const anyOpen = regions.some((region) => !ignored.has(region));
1911
+ const anyIgnored = regions.some((region) => ignored.has(region));
1912
+ return [...when(anyOpen, "ignore"), ...when(anyIgnored, "unignore")];
1913
+ };
1914
+
1915
+ // src/folder-sort.ts
1916
+ var heightOf2 = (folder) => folder.folders.length === 0 ? 0 : 1 + Math.max(...folder.folders.map(heightOf2));
1917
+ var flattenFolders = (setups, isOpen, carriedId = null) => {
1918
+ const walk = (folder) => [
1919
+ { id: folder.id, parentId: folder.parentId, depth: folder.depth, height: heightOf2(folder) },
1920
+ ...folder.id !== carriedId && isOpen(folder.id) ? folder.folders.flatMap(walk) : []
1921
+ ];
1922
+ return setups.flatMap(walk);
1923
+ };
1924
+ var projectFolder = (items, activeId, gap, offsetX, indent, maxDepth) => {
1925
+ const active = items.find((item) => item.id === activeId);
1926
+ if (!active) {
1927
+ return null;
1928
+ }
1929
+ const rest = items.filter((item) => item.id !== activeId);
1930
+ const index = Math.max(0, Math.min(gap, rest.length));
1931
+ const moved = [...rest.slice(0, index), active, ...rest.slice(index)];
1932
+ const wanted = active.depth + Math.round(offsetX / indent);
1933
+ const depth = depthAllowed(moved, index, wanted, maxDepth);
1934
+ const parentId = parentAt(moved, index, depth);
1935
+ const unmoved = moved.every((item, at) => item.id === items[at]?.id);
1936
+ if (unmoved && depth === active.depth) {
1937
+ return { depth, parentId, at: null };
1938
+ }
1939
+ return { depth, parentId, at: dropAtFor(moved, index, depth, parentId) };
1940
+ };
1941
+ var depthAllowed = (moved, index, wanted, maxDepth) => {
1942
+ const active = moved[index];
1943
+ const previous = moved[index - 1];
1944
+ const next = moved[index + 1];
1945
+ const deepest = Math.min(previous ? previous.depth + 1 : 0, maxDepth - active.height);
1946
+ const shallowest = Math.min(next ? next.depth : 0, deepest);
1947
+ return Math.max(shallowest, Math.min(wanted, deepest));
1948
+ };
1949
+ var parentAt = (moved, index, depth) => {
1950
+ const previous = moved[index - 1];
1951
+ if (depth === 0 || !previous) {
1952
+ return null;
1953
+ }
1954
+ if (depth > previous.depth) {
1955
+ return previous.id;
1956
+ }
1957
+ const above = moved.slice(0, index).reverse().find((item) => item.depth === depth);
1958
+ return above?.parentId ?? null;
1959
+ };
1960
+ var gapAt = (centres, activeId, y) => centres.filter((row) => row.id !== activeId && row.centreY < y).length;
1961
+ var dropAtFor = (moved, index, depth, parentId) => {
1962
+ for (const item of moved.slice(0, index).reverse()) {
1963
+ if (item.depth < depth) {
1964
+ break;
1965
+ }
1966
+ if (item.depth === depth) {
1967
+ return { id: item.id, zone: "after" };
1968
+ }
1969
+ }
1970
+ for (const item of moved.slice(index + 1)) {
1971
+ if (item.depth < depth) {
1972
+ break;
1973
+ }
1974
+ if (item.depth === depth) {
1975
+ return { id: item.id, zone: "before" };
1976
+ }
1977
+ }
1978
+ return parentId ? { id: parentId, zone: "inside" } : { id: PART_ROW, zone: "first" };
1979
+ };
1980
+
1981
+ // src/plan-checks.ts
1982
+ var planChecks = (draft, context) => {
1983
+ if (!statementOf(draft.setups)) {
1984
+ return [];
1985
+ }
1986
+ const stated = framesOf(draft.setups);
1987
+ return [
1988
+ ...stated.flatMap((frame) => frameChecks(frame, context)),
1989
+ ...otherWayChecks(draft.setups, context),
1990
+ ...kernelChecks(draft, stated, context)
1991
+ ];
1992
+ };
1993
+ var frameChecks = (frame, context) => [
1994
+ ...analysisChecks(frame.name, directionAnalysis(frame.direction, context.directions)),
1995
+ // One check for each pin of a feature the part does not have.
1996
+ ...Array.from(
1997
+ { length: frame.features.filter((tag) => isUnknownFeature(tag, context)).length },
1998
+ () => ({
1999
+ tone: "blocking",
2000
+ text: `A feature pinned in ${frame.name} is not one of this part\u2019s. Unpin it.`
2001
+ })
2002
+ )
2003
+ ];
2004
+ var NOT_ANALYZED = {
2005
+ undetected: (name) => `${name} faces a direction the part was not analyzed from. Delete it and add it again.`,
2006
+ analyzing: (name) => `Toolpath is analyzing ${name}\u2019s direction. Its features arrive when it is done.`,
2007
+ unanalyzed: (name) => `Toolpath has not analyzed ${name}\u2019s direction. Open ${name} and analyze it.`
2008
+ };
2009
+ var analysisChecks = (name, analysis) => {
2010
+ if (analysis === "analyzed") {
2011
+ return [];
2012
+ }
2013
+ return [
2014
+ {
2015
+ tone: analysis === "analyzing" ? "warning" : "blocking",
2016
+ text: NOT_ANALYZED[analysis](name)
2017
+ }
2018
+ ];
2019
+ };
2020
+ var isUnknownFeature = (tag, context) => {
2021
+ const feature = context.feature(tag);
2022
+ return Boolean(context.directions.analyzed) && !feature;
2023
+ };
2024
+ var otherWayChecks = (setups, context) => placedFolders(setups).flatMap(
2025
+ (placed) => placed.folder.features.flatMap((tag) => {
2026
+ const feature = context.feature(tag);
2027
+ if (!feature?.direction || sameDirection(feature.direction, placed.direction)) {
2028
+ return [];
2029
+ }
2030
+ return [
2031
+ {
2032
+ tone: "blocking",
2033
+ text: `${feature.label} is cut from another direction than ${placed.folder.name}. Move it to a folder facing its way, or unpin it.`
2034
+ }
2035
+ ];
2036
+ })
2037
+ );
2038
+ var kernelChecks = (draft, stated, context) => {
2039
+ const pinned2 = stated.some((frame) => frame.features.length > 0);
2040
+ const kernelMoved = draft.kernelVersion && context.kernelVersion && draft.kernelVersion !== context.kernelVersion;
2041
+ if (!pinned2 || !kernelMoved) {
2042
+ return [];
2043
+ }
2044
+ return [
2045
+ {
2046
+ tone: "warning",
2047
+ text: "Toolpath has been updated since these features were pinned. If they are refused, pin them again."
2048
+ }
2049
+ ];
2050
+ };
2051
+ var sameTag = (a, b) => a.replace(/^0+/, "").toLowerCase() === b.replace(/^0+/, "").toLowerCase();
2052
+ var describeClause = (clause, draft, label, tags) => clause.replace(/\bsetup (\d+)(, orientation \d+)?/, (whole, index) => {
2053
+ const setup = framesOf(draft.setups)[Number(index)];
2054
+ return setup ? setup.name : whole;
2055
+ }).replace(/FeatureTag\(([0-9a-fA-F]+)\)/g, (whole, hex) => {
2056
+ const tag = tags.find((candidate) => sameTag(candidate, hex));
2057
+ return (tag && label(tag)) ?? whole;
2058
+ }).replace(
2059
+ / is not among the features offered$/,
2060
+ " could not be measured, so Toolpath cannot plan it \u2014 unpin it, or pin its faces as another feature"
2061
+ );
2062
+ var changedSinceResults = (changed, setups) => changed && setups > 0;
2063
+ var confirmChecks = (own, context) => [
2064
+ ...own.filter((check) => check.tone !== "info"),
2065
+ ...changedChecks(context.changed),
2066
+ ...notMachinedChecks(context.notMachined),
2067
+ ...setupChecks(context.setups),
2068
+ ...emptyFolderChecks(context.folders)
2069
+ ];
2070
+ var featuresCounted = (count) => `${count} feature${count === 1 ? "" : "s"}`;
2071
+ var changedChecks = (changed) => changed ? [{ tone: "info", text: "Constraints changed since these results." }] : [];
2072
+ var notMachinedChecks = (notMachined) => {
2073
+ if (notMachined === null) {
2074
+ return [];
2075
+ }
2076
+ if (notMachined > 0) {
2077
+ return [
2078
+ {
2079
+ tone: "warning",
2080
+ text: `${featuresCounted(notMachined)} nothing machines.`,
2081
+ show: { kind: "notMachined" }
2082
+ }
2083
+ ];
2084
+ }
2085
+ return [{ tone: "ok", text: "Every face that needs machining is machined." }];
2086
+ };
2087
+ var setupChecks = (setups) => {
2088
+ const count = countOf(setups, cutByNoneOf);
2089
+ const roughCount = countOf(setups, notFinishedOf2);
2090
+ const checks = [];
2091
+ if (roughCount > 0) {
2092
+ checks.push(notFinishedCheck(roughCount, firstWith(setups, notFinishedOf2)));
2093
+ }
2094
+ if (count > 0) {
2095
+ checks.push({
2096
+ tone: "warning",
2097
+ text: `${featuresCounted(count)} can\u2019t be cut as planned.`,
2098
+ show: { kind: "setup", idx: firstWith(setups, cutByNoneOf).idx }
2099
+ });
2100
+ } else if (roughCount === 0 && setups.length > 0) {
2101
+ checks.push({ tone: "ok", text: "Every setup can cut what is in it." });
2102
+ }
2103
+ return checks;
2104
+ };
2105
+ var notFinishedOf2 = (setup) => setup.notMachined.filter((tag) => setup.notFinished.includes(tag));
2106
+ var cutByNoneOf = (setup) => setup.notMachined.filter((tag) => !setup.notFinished.includes(tag));
2107
+ var countOf = (setups, left) => setups.reduce((sum, setup) => sum + left(setup).length, 0);
2108
+ var firstWith = (setups, left) => setups.find((setup) => left(setup).length > 0);
2109
+ var notFinishedCheck = (count, first) => {
2110
+ const them = count === 1 ? "it" : "them";
2111
+ return {
2112
+ tone: "warning",
2113
+ text: `${featuresCounted(count)} not finished: Toolpath roughs ${them}, but no pass finishes ${them}.`,
2114
+ show: { kind: "setup", idx: first.idx }
2115
+ };
2116
+ };
2117
+ var emptyFolderChecks = (folders) => {
2118
+ const empty = folders.filter((folder) => !folder.machined);
2119
+ if (!empty[0]) {
2120
+ return [];
2121
+ }
2122
+ return [
2123
+ {
2124
+ tone: "warning",
2125
+ text: `${empty.length} folder${empty.length === 1 ? " has nothing in it" : "s have nothing in them"}.`,
2126
+ show: { kind: "folder", setupId: empty[0].id }
2127
+ }
2128
+ ];
2129
+ };
2130
+ var allClear = (checks) => checks.every((check) => check.tone === "ok");
2131
+
2132
+ // src/same-json.ts
2133
+ var sortedKeys = (value) => Array.isArray(value) ? value.map(sortedKeys) : typeof value === "object" && value !== null ? Object.fromEntries(
2134
+ Object.keys(value).sort().map((key) => [key, sortedKeys(value[key])])
2135
+ ) : value;
2136
+ var sameJson = (a, b) => JSON.stringify(sortedKeys(a)) === JSON.stringify(sortedKeys(b));
2137
+
2138
+ // src/plan-session.ts
2139
+ var HISTORY = 100;
2140
+ var CONSTRAINT_KEYS = {
2141
+ setups: true,
2142
+ done: true,
2143
+ doneKernel: true
2144
+ };
2145
+ var isConstraint = (key) => Object.hasOwn(CONSTRAINT_KEYS, key);
2146
+ var fieldsOnly = (change) => Object.fromEntries(Object.entries(change).filter(([key]) => !isConstraint(key)));
2147
+ var EDITS = ["act", "edit", "undo", "redo", "discard"];
2148
+ var sameOf = (rules) => rules.same ?? ((a, b) => a === b || sameJson(a, b));
2149
+ var followSaved = (held, was, now) => {
2150
+ const next = { ...held };
2151
+ for (const key of Object.keys(now)) {
2152
+ if (!isConstraint(key) && sameJson(held[key], was[key])) {
2153
+ next[key] = now[key];
2154
+ }
2155
+ }
2156
+ return next;
2157
+ };
2158
+ var startSession = (saved, { held, version } = {}) => ({
2159
+ saved,
2160
+ draft: held ?? saved,
2161
+ past: [],
2162
+ future: [],
2163
+ run: null,
2164
+ refused: null,
2165
+ folderId: null,
2166
+ facts: null,
2167
+ version: version ?? null
2168
+ });
2169
+ var isHeld = (session, rules = {}) => !sameOf(rules)(session.draft, session.saved);
2170
+ var canUndo = (session, rules = {}) => !rules.locked && session.past.length > 0;
2171
+ var canRedo = (session, rules = {}) => !rules.locked && session.future.length > 0;
2172
+ var planSession = (session, event, rules) => {
2173
+ const same = sameOf(rules);
2174
+ if (rules.locked && EDITS.includes(event.type)) {
2175
+ return session;
2176
+ }
2177
+ const settled = settle(session);
2178
+ if (!Object.hasOwn(TAKE, event.type)) {
2179
+ return settled;
2180
+ }
2181
+ const take = TAKE[event.type];
2182
+ return take({ session, settled, rules, same }, event);
2183
+ };
2184
+ var settle = (session) => session.refused === null && session.folderId === null && session.facts === null ? session : { ...session, refused: null, folderId: null, facts: null };
2185
+ var takeAct = ({ session, settled, rules, same }, event) => {
2186
+ const result = applyDraftAction(session.draft, event.action, rules, session.saved);
2187
+ if ("refused" in result) {
2188
+ return { ...settled, refused: result.refused };
2189
+ }
2190
+ const next = step(settled, result.draft, event.coalesce, same);
2191
+ return { ...next, folderId: result.folderId ?? null, facts: result.facts };
2192
+ };
2193
+ var takeEdit = ({ session, settled, same }, event) => {
2194
+ const fields = fieldsOnly(event.change(session.draft));
2195
+ return step(settled, { ...session.draft, ...fields }, event.coalesce, same);
2196
+ };
2197
+ var takeDiscard = ({
2198
+ session,
2199
+ settled,
2200
+ same
2201
+ }) => step(settled, session.saved, void 0, same);
2202
+ var takeUndo = ({ session, settled }) => {
2203
+ const back = session.past.at(-1);
2204
+ if (back === void 0) {
2205
+ return settled;
2206
+ }
2207
+ return {
2208
+ ...settled,
2209
+ draft: back,
2210
+ past: session.past.slice(0, -1),
2211
+ future: [session.draft, ...session.future],
2212
+ run: null
2213
+ };
2214
+ };
2215
+ var takeRedo = ({ session, settled }) => {
2216
+ const [forward, ...rest] = session.future;
2217
+ if (forward === void 0) {
2218
+ return settled;
2219
+ }
2220
+ return {
2221
+ ...settled,
2222
+ draft: forward,
2223
+ past: [...session.past, session.draft],
2224
+ future: rest,
2225
+ run: null
2226
+ };
2227
+ };
2228
+ var takeApplied = ({ session, settled }, event) => {
2229
+ if (older(session, event.version)) {
2230
+ return settled;
2231
+ }
2232
+ return { ...settled, saved: event.sent, run: null, version: event.version ?? session.version };
2233
+ };
2234
+ var takeRead = ({ session, settled, rules, same }, event) => {
2235
+ if (older(session, event.version)) {
2236
+ return settled;
2237
+ }
2238
+ const held = !same(session.draft, session.saved);
2239
+ const rebase = rules.rebase ?? followSaved;
2240
+ return {
2241
+ ...settled,
2242
+ saved: event.saved,
2243
+ draft: held ? rebase(session.draft, session.saved, event.saved) : event.saved,
2244
+ run: null,
2245
+ version: event.version ?? session.version
2246
+ };
2247
+ };
2248
+ var older = (session, version) => version !== void 0 && session.version !== null && version < session.version;
2249
+ var step = (session, next, coalesce, same) => {
2250
+ if (same(next, session.draft)) {
2251
+ return session;
2252
+ }
2253
+ const joined = coalesce !== void 0 && session.run?.key === coalesce ? session.run : null;
2254
+ const moved = { ...session, draft: next, future: [] };
2255
+ if (joined && same(next, joined.before)) {
2256
+ return { ...moved, past: session.past.slice(0, -1), run: null };
2257
+ }
2258
+ if (joined) {
2259
+ return moved;
2260
+ }
2261
+ return {
2262
+ ...moved,
2263
+ past: [...session.past.slice(-(HISTORY - 1)), session.draft],
2264
+ run: coalesce === void 0 ? null : { key: coalesce, before: session.draft }
2265
+ };
2266
+ };
2267
+ var TAKE = {
2268
+ act: takeAct,
2269
+ edit: takeEdit,
2270
+ undo: takeUndo,
2271
+ redo: takeRedo,
2272
+ discard: takeDiscard,
2273
+ applied: takeApplied,
2274
+ read: takeRead
2275
+ };
2276
+
2277
+ // src/results-view.ts
2278
+ var EMPTY = { setups: [], leftOut: [] };
2279
+ var resultsView = ({
2280
+ plan,
2281
+ constraints,
2282
+ part
2283
+ }) => {
2284
+ if (!plan) {
2285
+ return EMPTY;
2286
+ }
2287
+ const frames = framesOf(constraints?.setups ?? []);
2288
+ const planned = planView(plan, part).map((view, idx) => ({
2289
+ view,
2290
+ idx,
2291
+ facing: framesFacing(view, frames)
2292
+ }));
2293
+ const matched = new Set(planned.flatMap(({ facing }) => facing.map((frame) => frame.id)));
2294
+ return {
2295
+ setups: planned.map((each) => resultsSetup(each, plan.setups[each.idx], frames)),
2296
+ leftOut: frames.filter((frame) => !matched.has(frame.id)).map((frame) => ({ folderId: frame.id, name: frameName(frame, frames) }))
2297
+ };
2298
+ };
2299
+ var frameName = (frame, frames) => {
2300
+ const hold = frame.id === frame.holdId ? null : frames.find((each) => each.id === frame.holdId);
2301
+ return hold ? `${hold.name} \u203A ${frame.name}` : frame.name;
2302
+ };
2303
+ var framesFacing = (view, frames) => view.orientations.flatMap((orientation) => {
2304
+ const frame = frames.find((each) => sameDirection(each.direction, orientation.direction));
2305
+ return frame ? [frame] : [];
2306
+ });
2307
+ var resultsSetup = ({ view, idx, facing }, setup, frames) => {
2308
+ const frame = facing[0];
2309
+ return {
2310
+ ...view,
2311
+ idx,
2312
+ name: frame ? frameName(frame, frames) : view.name,
2313
+ rows: setupRows(view, setup),
2314
+ fromFolder: frame?.id ?? null
2315
+ };
2316
+ };
2317
+ var setupRows = (view, setup) => {
2318
+ const listed = listedOnce(view);
2319
+ const listedKeys = new Set(listed.map((feature) => feature.tag.toLowerCase()));
2320
+ const unlisted = view.notMachined.filter((each) => !listedKeys.has(each.tag.toLowerCase()));
2321
+ const notFinished = new Set(view.notFinished.map((tag) => tag.toLowerCase()));
2322
+ const notMachined = new Set(view.notMachined.map((row) => row.tag.toLowerCase()));
2323
+ return [...listed, ...unlisted].map((feature) => {
2324
+ const key = feature.tag.toLowerCase();
2325
+ return {
2326
+ ...feature,
2327
+ tool: toolsCutting(setup, key),
2328
+ mark: markOf(key, notFinished, notMachined)
2329
+ };
2330
+ });
2331
+ };
2332
+ var listedOnce = (view) => {
2333
+ const keys = /* @__PURE__ */ new Set();
2334
+ return view.orientations.flatMap((orientation) => orientation.features).filter((feature) => {
2335
+ const key = feature.tag.toLowerCase();
2336
+ if (keys.has(key)) {
2337
+ return false;
2338
+ }
2339
+ keys.add(key);
2340
+ return true;
2341
+ });
2342
+ };
2343
+ var toolsCutting = (setup, key) => {
2344
+ const cuts = (action) => (action.featureTags ?? []).some((tag) => tag.toLowerCase() === key);
2345
+ const tools = [...new Set(setup.actions.filter(cuts).map((action) => action.toolName))];
2346
+ return tools.length > 0 ? tools.join(", ") : null;
2347
+ };
2348
+ var markOf = (key, notFinished, notMachined) => {
2349
+ if (notFinished.has(key)) {
2350
+ return "notFinished";
2351
+ }
2352
+ return notMachined.has(key) ? "notMachined" : false;
2353
+ };
2354
+ var setupOf = (setups, tag) => {
2355
+ const key = tag.toLowerCase();
2356
+ return setups.find(
2357
+ (setup) => setup.rows.some((row) => row.tag.toLowerCase() === key && row.mark !== "notMachined")
2358
+ ) ?? null;
2359
+ };
2360
+
2361
+ // src/units.ts
2362
+ import { MM_PER_INCH } from "@toolpath/tool-support";
2363
+ var isUnits = (value) => value === "in" || value === "mm";
2364
+ var KG_M3_PER_LB_IN3 = 27679.9047;
2365
+ var LB_PER_KG = 2.2046226;
2366
+ var FT_PER_M = 3.2808399;
2367
+ var trim = (value, places) => String(Number(value.toFixed(places)));
2368
+ var formatLength = (mm2, units) => units === "mm" ? `${trim(mm2, 3)} mm` : `${trim(mm2 / MM_PER_INCH, 4)}"`;
2369
+ var formatDimensions = (sizeMm, units, shape = "box") => {
2370
+ const axes = shape === "cylinder" ? [sizeMm.length, sizeMm.height] : [sizeMm.length, sizeMm.width, sizeMm.height];
2371
+ const prefix = shape === "cylinder" ? "\xD8 " : "";
2372
+ return units === "mm" ? `${prefix}${axes.map((value) => trim(value, 2)).join(" x ")} mm` : `${prefix}${axes.map((value) => trim(value / MM_PER_INCH, 3)).join(" x ")} in`;
2373
+ };
2374
+ var formatArea = (mm2, units) => units === "mm" ? `${trim(mm2, 1)} mm\xB2` : `${trim(mm2 / MM_PER_INCH ** 2, 3)} in\xB2`;
2375
+ var formatFeed = (mmPerMin, units) => units === "mm" ? `${Math.round(mmPerMin)} mm/min` : `${trim(mmPerMin / MM_PER_INCH, 1)} in/min`;
2376
+ var formatSurfaceSpeed = (mPerMin, units) => units === "mm" ? `${trim(mPerMin, 1)} m/min` : `${Math.round(mPerMin * FT_PER_M)} SFM`;
2377
+ var formatWeight = (kg, units) => units === "mm" ? `${trim(kg, 3)} kg` : `${trim(kg * LB_PER_KG, 3)} lb`;
2378
+ var PRECISION = {
2379
+ // A tenth of a kg/m³ is under 0.000004 lb/in³, well inside the fourth place a datasheet prints.
2380
+ density: { mm: 1, in: 4 },
2381
+ // A thousandth of a millimetre is 0.00004", inside half a ten-thousandth of an inch.
2382
+ length: { mm: 3, in: 4 },
2383
+ // Unitless: the same in either system.
2384
+ ratio: { mm: 2, in: 2 },
2385
+ angle: { mm: 1, in: 1 },
2386
+ // Whole things — flutes — in either system.
2387
+ count: { mm: 0, in: 0 }
2388
+ };
2389
+ var IMPERIAL_PER_METRIC = {
2390
+ density: 1 / KG_M3_PER_LB_IN3,
2391
+ length: 1 / MM_PER_INCH,
2392
+ ratio: 1,
2393
+ angle: 1,
2394
+ count: 1
2395
+ };
2396
+ var roundTo = (value, places) => Number(value.toFixed(places));
2397
+ var toStored = (quantity, value) => roundTo(value, PRECISION[quantity].mm);
2398
+ var toInput = (quantity, canonical, units) => roundTo(
2399
+ units === "mm" ? canonical : canonical * IMPERIAL_PER_METRIC[quantity],
2400
+ PRECISION[quantity][units]
2401
+ );
2402
+ var fromInput = (quantity, shown, units) => toStored(quantity, units === "mm" ? shown : shown / IMPERIAL_PER_METRIC[quantity]);
2403
+ var stepFor = (quantity, units) => {
2404
+ const places = PRECISION[quantity][units];
2405
+ return places === 0 ? "1" : `0.${"0".repeat(places - 1)}1`;
2406
+ };
2407
+ var densityUnit = (units) => units === "in" ? "lb/in\xB3" : "kg/m\xB3";
2408
+ var parseToolLength = (text, units) => {
2409
+ const cleaned = text.trim().replace(/\s*("|in|mm)$/i, "").trim();
2410
+ const mixed = /^(?:(\d+)[\s-]+)?(\d+)\/(\d+)$/.exec(cleaned);
2411
+ const shown = mixed ? Number(mixed[1] ?? 0) + Number(mixed[2]) / Number(mixed[3]) : /^(\d+\.?\d*|\.\d+)$/.test(cleaned) ? Number(cleaned) : Number.NaN;
2412
+ return Number.isFinite(shown) && shown >= 0 ? fromInput("length", shown, units) : void 0;
2413
+ };
2414
+
2415
+ // src/stock.ts
2416
+ var STOCK_LABELS = {
2417
+ automaticFlatBar: "Flat Bar",
2418
+ automaticRoundStock: "Round Stock",
2419
+ fixedBox: "Fixed Box",
2420
+ fixedCylinder: "Fixed Cylinder"
2421
+ };
2422
+ var FLAT_BAR_FIELDS = [
2423
+ ["oversizeX", "Width (X)"],
2424
+ ["oversizeY", "Depth (Y)"],
2425
+ ["oversizeZTop", "Thickness (Z)"],
2426
+ ["snapX", "Width (X)"],
2427
+ ["snapY", "Depth (Y)"],
2428
+ ["snapZ", "Thickness (Z)"],
2429
+ ["oversizeZBottom", "Stock Grip Amt."]
2430
+ ];
2431
+ var ROUND_STOCK_FIELDS = [
2432
+ ["radialOversize", "Radial"],
2433
+ ["axialOversize", "Front"],
2434
+ ["diaRoundToNearest", "Diameter"],
2435
+ ["lengthRoundToNearest", "Length"],
2436
+ ["gripStock", "Stock Grip Amt."]
2437
+ ];
2438
+ var defaultStock = (mode = "automaticFlatBar", units = "in") => {
2439
+ const oversize = units === "in" ? 0.762 : 0.5;
2440
+ const increment = units === "in" ? 6.35 : 5;
2441
+ return mode === "automaticFlatBar" ? {
2442
+ mode,
2443
+ oversizeX: oversize,
2444
+ oversizeY: oversize,
2445
+ oversizeZTop: oversize,
2446
+ oversizeZBottom: increment,
2447
+ snapX: increment,
2448
+ snapY: increment,
2449
+ snapZ: increment
2450
+ } : {
2451
+ mode,
2452
+ radialOversize: oversize,
2453
+ axialOversize: oversize,
2454
+ gripStock: increment,
2455
+ diaRoundToNearest: increment,
2456
+ lengthRoundToNearest: increment,
2457
+ // Milling flow: stand the round along the first cutting direction, explicitly.
2458
+ verticalAxis: true
2459
+ };
2460
+ };
2461
+ var ENVELOPE_MM = [2032, 1270, 762];
2462
+ var CHOOSE_STOCK = "Choose automatic stock, a fixed box or a fixed cylinder.";
2463
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2464
+ var mm = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
2465
+ var parseStock = (value) => {
2466
+ if (!record(value)) {
2467
+ return CHOOSE_STOCK;
2468
+ }
2469
+ const { mode } = value;
2470
+ if (mode === "automaticFlatBar" || mode === "automaticRoundStock") {
2471
+ return parseAutomaticStock(value, mode);
2472
+ }
2473
+ if (mode !== "fixedBox" && mode !== "fixedCylinder") {
2474
+ return CHOOSE_STOCK;
2475
+ }
2476
+ return parseFixedStock(value, mode);
2477
+ };
2478
+ var hasUnknownKey = (value, allowed) => Object.keys(value).some((key) => !allowed.includes(key));
2479
+ var parseAutomaticStock = (value, mode) => {
2480
+ const fields = mode === "automaticFlatBar" ? FLAT_BAR_FIELDS : ROUND_STOCK_FIELDS;
2481
+ const keys = fields.map(([key]) => key);
2482
+ const allowed = ["mode", ...keys, ...mode === "automaticRoundStock" ? ["verticalAxis"] : []];
2483
+ if (hasUnknownKey(value, allowed)) {
2484
+ return "The automatic stock contains an unknown setting.";
2485
+ }
2486
+ if (keys.some((key) => value[key] !== void 0 && !mm(value[key], 0, 762))) {
2487
+ return "Stock oversize, grip and rounding must be between 0 and 30 in (762 mm).";
2488
+ }
2489
+ if (mode === "automaticRoundStock" && value.verticalAxis !== void 0 && typeof value.verticalAxis !== "boolean") {
2490
+ return "Choose whether Round Stock stands along the cutting direction.";
2491
+ }
2492
+ return value;
2493
+ };
2494
+ var parseFixedStock = (value, mode) => {
2495
+ const allowed = [
2496
+ "mode",
2497
+ "position",
2498
+ "positionOffset",
2499
+ ...mode === "fixedBox" ? ["dimensions"] : ["diameter", "length"]
2500
+ ];
2501
+ if (hasUnknownKey(value, allowed)) {
2502
+ return "The fixed stock contains an unknown setting.";
2503
+ }
2504
+ if (!["modelCentered", "offsetFromTop", "offsetFromBottom"].includes(String(value.position))) {
2505
+ return "Choose the stock position.";
2506
+ }
2507
+ if (value.positionOffset !== void 0 && !mm(value.positionOffset, -2032, 2032)) {
2508
+ return "The stock position offset must be between -80 and 80 in (-2032 and 2032 mm).";
2509
+ }
2510
+ const sizeProblem = mode === "fixedBox" ? fixedBoxSizeProblem(value.dimensions) : fixedCylinderSizeProblem(value);
2511
+ return sizeProblem ?? value;
2512
+ };
2513
+ var positiveSide = (n) => mm(n, 0, 2032) && n > 0;
2514
+ var outsideEnvelope = (sides) => [...sides].sort((a, b) => b - a).some((n, i) => n > ENVELOPE_MM[i]);
2515
+ var fixedBoxSizeProblem = (dims) => {
2516
+ if (!record(dims) || hasUnknownKey(dims, ["x", "y", "z"]) || ![dims.x, dims.y, dims.z].every(positiveSide)) {
2517
+ return "Give the fixed box positive X, Y and Z dimensions.";
2518
+ }
2519
+ if (outsideEnvelope([dims.x, dims.y, dims.z])) {
2520
+ return "The box must fit within 80 \xD7 50 \xD7 30 in in some order.";
2521
+ }
2522
+ return null;
2523
+ };
2524
+ var fixedCylinderSizeProblem = (value) => {
2525
+ if (![value.diameter, value.length].every(positiveSide)) {
2526
+ return "Give the fixed cylinder a positive diameter and length.";
2527
+ }
2528
+ const { diameter, length } = value;
2529
+ if (outsideEnvelope([diameter, diameter, length])) {
2530
+ return "The cylinder\u2019s diameter \xD7 diameter \xD7 length must fit within 80 \xD7 50 \xD7 30 in in some order.";
2531
+ }
2532
+ return null;
2533
+ };
2534
+ var sameStock = (a, b) => sameJson(a ?? null, b ?? null);
2535
+ var stockEdited = (policy, baseline) => policy ? !echoesStock(policy, baseline?.stock) : false;
2536
+ var AXES = ["x", "y", "z"];
2537
+ var AXIS_TOLERANCE = 1e-6;
2538
+ var FIT_TOLERANCE_MM = 1e-6;
2539
+ var NO_FIRST_FOLDER = "Add a folder first: a fixed box is measured from the first setup.";
2540
+ var TILTED_FIRST_FOLDER = "A fixed box needs a first folder facing along X, Y or Z.";
2541
+ var BOX_ENVELOPE = "The box must fit 80 \xD7 50 in across the first folder\u2019s direction and 30 in along it.";
2542
+ var FIX_STOCK_FIRST = "Fix the stock first";
2543
+ var axisAlong = (direction) => {
2544
+ const along = AXES.filter((axis2) => Math.abs(direction[axis2]) > AXIS_TOLERANCE);
2545
+ const axis = along.length === 1 ? along[0] : null;
2546
+ return axis ? { axis, sign: direction[axis] > 0 ? 1 : -1 } : null;
2547
+ };
2548
+ var boxFrameOf = (first) => first === null ? NO_FIRST_FOLDER : axisAlong(first) ?? TILTED_FIRST_FOLDER;
2549
+ var across = (axis) => AXES.filter((each) => each !== axis);
2550
+ var stockRequest = (stock, first) => {
2551
+ if (stock.mode !== "fixedBox") {
2552
+ return stock;
2553
+ }
2554
+ const frame = boxFrameOf(first);
2555
+ if (typeof frame === "string") {
2556
+ return frame;
2557
+ }
2558
+ const [a, b] = across(frame.axis);
2559
+ const { dimensions } = stock;
2560
+ const sent = { x: dimensions[a], y: dimensions[b], z: dimensions[frame.axis] };
2561
+ const [length, width, depth] = ENVELOPE_MM;
2562
+ return Math.max(sent.x, sent.y) > length + FIT_TOLERANCE_MM || Math.min(sent.x, sent.y) > width + FIT_TOLERANCE_MM || sent.z > depth + FIT_TOLERANCE_MM ? BOX_ENVELOPE : { ...stock, dimensions: sent };
2563
+ };
2564
+ var vectorOf = (value) => record(value) && [value.x, value.y, value.z].every((n) => typeof n === "number") ? { x: value.x, y: value.y, z: value.z } : null;
2565
+ var placedAlong = (envelope) => {
2566
+ const resolved = record(envelope) ? envelope.resolved : null;
2567
+ const input = record(envelope) ? envelope.input : null;
2568
+ if (record(resolved) && resolved.shape === "cylinder") {
2569
+ const stood = record(input) && (input.mode === "fixedCylinder" || input.mode === "automaticRoundStock" && input.verticalAxis === true);
2570
+ const along = stood ? vectorOf(resolved.axis) : null;
2571
+ const axis = along && axisAlong(along);
2572
+ return axis ? { x: 0, y: 0, z: 0, [axis.axis]: axis.sign } : null;
2573
+ }
2574
+ return vectorOf(record(resolved) && record(resolved.frame) ? resolved.frame.axis : null);
2575
+ };
2576
+ var echoesStock = (policy, envelope) => {
2577
+ const sent = parseStock(record(envelope) ? envelope.input : void 0);
2578
+ if (typeof sent === "string") {
2579
+ return false;
2580
+ }
2581
+ if (policy.mode !== "fixedBox") {
2582
+ return sameStock(policy, sent);
2583
+ }
2584
+ const placed = placedAlong(envelope);
2585
+ const firsts = placed ? [placed] : AXES.map((axis) => ({ x: 0, y: 0, z: 0, [axis]: 1 }));
2586
+ return firsts.some((first) => {
2587
+ const request = stockRequest(policy, first);
2588
+ return typeof request !== "string" && sameStock(request, sent);
2589
+ });
2590
+ };
2591
+ var cross = (a, b) => ({
2592
+ x: a.y * b.z - a.z * b.y,
2593
+ y: a.z * b.x - a.x * b.z,
2594
+ z: a.x * b.y - a.y * b.x
2595
+ });
2596
+ var unit = ({ axis, sign }) => ({
2597
+ x: 0,
2598
+ y: 0,
2599
+ z: 0,
2600
+ [axis]: sign
2601
+ });
2602
+ var stockFrame = (first, part) => {
2603
+ const z = first && axisAlong(first);
2604
+ if (!z) {
2605
+ return null;
2606
+ }
2607
+ const along = unit(z);
2608
+ const level = cross(along, { x: 0, y: 0, z: 1 });
2609
+ const flip = along.y < 0 ? -1 : 1;
2610
+ const toolX = axisAlong(level) ? { x: level.x * flip, y: level.y * flip, z: level.z * flip } : { x: along.z < 0 ? -1 : 1, y: 0, z: 0 };
2611
+ const toolY = cross(along, toolX);
2612
+ const extent = (v) => Math.abs(AXES.reduce((sum, k) => sum + v[k] * (part.max[k] - part.min[k]), 0));
2613
+ const x = extent(toolY) > extent(toolX) ? toolY : toolX;
2614
+ return { x: axisAlong(x), y: axisAlong(cross(along, x)), z };
2615
+ };
2616
+ var spanAlong = (sign, [low, high]) => sign > 0 ? [low, high] : [-high, -low];
2617
+ var partSpan = (part, { axis, sign }) => spanAlong(sign, [part.min[axis], part.max[axis]]);
2618
+ var extentAlong = (part, frameAxis) => {
2619
+ const [low, high] = partSpan(part, frameAxis);
2620
+ return high - low;
2621
+ };
2622
+ var stockStart = (position, offset, [low, high], length) => position === "offsetFromTop" ? high + offset - length : position === "offsetFromBottom" ? low - offset : (low + high - length) / 2;
2623
+ var snapUp = (size, increment) => increment === 0 ? size : Math.ceil(Math.round(size / increment * 1e6) / 1e6) * increment;
2624
+ var placeBlock = (frame, part, size, position, offset) => {
2625
+ const min = { ...part.min };
2626
+ const max = { ...part.max };
2627
+ for (const k of AXES) {
2628
+ const span = partSpan(part, frame[k]);
2629
+ const start = k === "z" ? stockStart(position, offset, span, size.z) : stockStart("modelCentered", 0, span, size[k]);
2630
+ const { axis, sign } = frame[k];
2631
+ [min[axis], max[axis]] = spanAlong(sign, [start, start + size[k]]);
2632
+ }
2633
+ return { min, max };
2634
+ };
2635
+ var placeCylinder = (frame, part, along, diameter, length, start) => {
2636
+ const { axis, sign } = frame[along];
2637
+ const middle = (k) => (part.min[k] + part.max[k]) / 2;
2638
+ return {
2639
+ origin: {
2640
+ x: middle("x"),
2641
+ y: middle("y"),
2642
+ z: middle("z"),
2643
+ [axis]: sign * start(partSpan(part, frame[along]))
2644
+ },
2645
+ axis: unit(frame[along]),
2646
+ diameter,
2647
+ length
2648
+ };
2649
+ };
2650
+ var fixedBoxPlacement = (stock, first, part) => {
2651
+ const frame = stockFrame(first, part);
2652
+ if (!frame) {
2653
+ return null;
2654
+ }
2655
+ const { dimensions } = stock;
2656
+ const [x, y] = [dimensions[frame.x.axis], dimensions[frame.y.axis]];
2657
+ const size = { x: Math.max(x, y), y: Math.min(x, y), z: dimensions[frame.z.axis] };
2658
+ return {
2659
+ ...placeBlock(frame, part, size, stock.position, stock.positionOffset ?? 0),
2660
+ turned: x < y
2661
+ };
2662
+ };
2663
+ var stockPlacement = (stock, first, part) => {
2664
+ if (stock.mode === "fixedBox") {
2665
+ const box = fixedBoxPlacement(stock, first, part);
2666
+ return box && { box };
2667
+ }
2668
+ const frame = stockFrame(first, part);
2669
+ if (!frame) {
2670
+ return null;
2671
+ }
2672
+ if (stock.mode === "fixedCylinder") {
2673
+ return { cylinder: placeFixedCylinder(stock, frame, part) };
2674
+ }
2675
+ if (stock.mode === "automaticFlatBar") {
2676
+ return { box: placeFlatBar(stock, frame, part) };
2677
+ }
2678
+ return { cylinder: placeRoundStock(stock, frame, part) };
2679
+ };
2680
+ var placeFixedCylinder = (stock, frame, part) => {
2681
+ const { diameter, length, position, positionOffset = 0 } = stock;
2682
+ return placeCylinder(
2683
+ frame,
2684
+ part,
2685
+ "z",
2686
+ diameter,
2687
+ length,
2688
+ (span) => stockStart(position, positionOffset, span, length)
2689
+ );
2690
+ };
2691
+ var placeFlatBar = (stock, frame, part) => {
2692
+ const extent = (k) => extentAlong(part, frame[k]);
2693
+ const { oversizeX, oversizeY, oversizeZTop, oversizeZBottom, snapX, snapY, snapZ } = {
2694
+ ...defaultStock(stock.mode),
2695
+ ...stock
2696
+ };
2697
+ const size = {
2698
+ x: snapUp(extent("x") + 2 * oversizeX, snapX),
2699
+ y: snapUp(extent("y") + 2 * oversizeY, snapY),
2700
+ z: snapUp(extent("z") + oversizeZTop + oversizeZBottom, snapZ)
2701
+ };
2702
+ return placeBlock(frame, part, size, "offsetFromBottom", oversizeZBottom);
2703
+ };
2704
+ var placeRoundStock = (stock, frame, part) => {
2705
+ const extent = (k) => extentAlong(part, frame[k]);
2706
+ const { radialOversize, axialOversize, gripStock, diaRoundToNearest, lengthRoundToNearest } = {
2707
+ ...defaultStock(stock.mode),
2708
+ ...stock
2709
+ };
2710
+ const along = stock.verticalAxis ? "z" : "x";
2711
+ const [a, b] = across(along);
2712
+ const diameter = snapUp(Math.hypot(extent(a), extent(b)) + 2 * radialOversize, diaRoundToNearest);
2713
+ const length = snapUp(gripStock + extent(along) + axialOversize, lengthRoundToNearest);
2714
+ return placeCylinder(frame, part, along, diameter, length, ([low]) => low - gripStock);
2715
+ };
2716
+ var fixedBoxCheck = (stock, first, size, units) => {
2717
+ if (stock?.mode !== "fixedBox") {
2718
+ return null;
2719
+ }
2720
+ const request = stockRequest(stock, first);
2721
+ if (typeof request === "string") {
2722
+ return unsendableBox(request);
2723
+ }
2724
+ if (!size) {
2725
+ return null;
2726
+ }
2727
+ const part = { min: { x: 0, y: 0, z: 0 }, max: { x: size.length, y: size.width, z: size.height } };
2728
+ const box = fixedBoxPlacement(stock, first, part);
2729
+ if (!box) {
2730
+ return null;
2731
+ }
2732
+ return partOutsideBox(box, part, units) ?? turnedBox(box);
2733
+ };
2734
+ var unsendableBox = (request) => ({
2735
+ problem: request,
2736
+ applyReason: request === BOX_ENVELOPE ? FIX_STOCK_FIRST : request
2737
+ });
2738
+ var partOutsideBox = (box, part, units) => {
2739
+ const worst = furthestOutside(box, part);
2740
+ if (worst && worst.by > FIT_TOLERANCE_MM) {
2741
+ return {
2742
+ problem: `The part stands ${formatLength(worst.by, units)} outside the box along ${worst.axis.toUpperCase()}.`,
2743
+ applyReason: FIX_STOCK_FIRST
2744
+ };
2745
+ }
2746
+ return null;
2747
+ };
2748
+ var furthestOutside = (box, part) => {
2749
+ const [worst] = AXES.map((axis) => ({
2750
+ axis,
2751
+ by: Math.max(box.min[axis] - part.min[axis], part.max[axis] - box.max[axis])
2752
+ })).sort((p, q) => q.by - p.by);
2753
+ return worst;
2754
+ };
2755
+ var turnedBox = (box) => box.turned ? {
2756
+ note: "Toolpath turns the box a quarter turn about the first folder\u2019s direction, to lay its longer side along the part\u2019s."
2757
+ } : null;
2758
+
2759
+ // src/tool-crib.ts
2760
+ var implicitLibraryIds = (crib) => crib.filter((entry) => entry.kind === "implicit").map((entry) => entry.id);
2761
+ var suitsMaterial = (materialFamilies, family) => family === null || materialFamilies.length === 0 || materialFamilies.includes(family);
2762
+ var startingLibrary = (libraries, family) => {
2763
+ const suiting = libraries.filter((library) => suitsMaterial(library.materialFamilies, family));
2764
+ return (family === null ? void 0 : suiting.find((library) => library.materialFamilies.includes(family))) ?? suiting.find((library) => library.isDefault) ?? suiting[0] ?? libraries.find((library) => library.isDefault) ?? libraries[0];
2765
+ };
2766
+ var offeredLibraries = (libraries, inCrib, family) => {
2767
+ const left = libraries.filter((library) => !inCrib.includes(library.id));
2768
+ const suits = (library) => suitsMaterial(library.materialFamilies, family);
2769
+ return [...left.filter(suits), ...left.filter((library) => !suits(library))];
2770
+ };
2771
+ var cribOf = (library) => [{ kind: "implicit", id: library.id }];
2772
+ var cribUntouched = (crib, libraries, family) => {
2773
+ const start = startingLibrary(libraries, family);
2774
+ return crib.length === 0 || start !== void 0 && crib.length === 1 && crib[0]?.id === start.id;
2775
+ };
2776
+ var addLibrary = (crib, id) => crib.some((entry) => entry.id === id) ? crib : [...crib, { kind: "implicit", id }];
2777
+ var removeLibrary = (crib, id) => crib.filter((entry) => entry.id !== id);
2778
+ var swapLibraries = (crib, a, b) => crib.map(
2779
+ (entry) => entry.id === a ? { ...entry, id: b } : entry.id === b ? { ...entry, id: a } : entry
2780
+ );
2781
+ export {
2782
+ BOX_ENVELOPE,
2783
+ COVERAGE_KINDS,
2784
+ COVERAGE_LABELS,
2785
+ DONE_COLOR,
2786
+ DRAFT_ACTION_KINDS,
2787
+ FLAT_BAR_FIELDS,
2788
+ HISTORY,
2789
+ IGNORE,
2790
+ MAX_DEPTH,
2791
+ MAX_NAME,
2792
+ MAX_SETUPS,
2793
+ MM_PER_INCH,
2794
+ NO_FIRST_FOLDER,
2795
+ OTHER_FOLDER_COLOR,
2796
+ PART_ROW,
2797
+ PRECISION,
2798
+ REASON_SHORT,
2799
+ ROUND_STOCK_FIELDS,
2800
+ STOCK_LABELS,
2801
+ TILTED_FIRST_FOLDER,
2802
+ UNMACHINED_FACE_COLOR,
2803
+ actionsFor,
2804
+ addFolder,
2805
+ addLibrary,
2806
+ addSetup,
2807
+ allClear,
2808
+ allFolders,
2809
+ applyDraftAction,
2810
+ areaOf,
2811
+ axisAlong,
2812
+ canRedo,
2813
+ canUndo,
2814
+ changedSinceResults,
2815
+ chooseReading,
2816
+ classifyFaces,
2817
+ confirmChecks,
2818
+ copyDirection,
2819
+ cribOf,
2820
+ cribUntouched,
2821
+ currentFor,
2822
+ defaultStock,
2823
+ densityUnit,
2824
+ describeClause,
2825
+ directionAnalysis,
2826
+ draftView,
2827
+ echoesStock,
2828
+ faceRegions,
2829
+ facesByRegion,
2830
+ featureGroups,
2831
+ featureTypeLabel,
2832
+ featureTypePlural,
2833
+ featuresAt,
2834
+ findPlaced,
2835
+ fixedBoxCheck,
2836
+ fixedBoxPlacement,
2837
+ flattenFolders,
2838
+ folderClick,
2839
+ folderPath,
2840
+ foldersFromPlan,
2841
+ formatArea,
2842
+ formatDimensions,
2843
+ formatFeed,
2844
+ formatLength,
2845
+ formatSurfaceSpeed,
2846
+ formatWeight,
2847
+ frameFacing,
2848
+ framesOf,
2849
+ fromInput,
2850
+ gapAt,
2851
+ groupFaces,
2852
+ heightOf,
2853
+ holdsFor,
2854
+ ignoreClick,
2855
+ ignoreEdit,
2856
+ ignoreSelection,
2857
+ ignoredAs,
2858
+ ignoredAsSaved,
2859
+ implicitLibraryIds,
2860
+ indexedFor,
2861
+ isDirection,
2862
+ isFallback,
2863
+ isFromBelow,
2864
+ isHeld,
2865
+ isHoleType,
2866
+ isIndexed,
2867
+ isUnits,
2868
+ liftLoneFolder,
2869
+ listsTag,
2870
+ moveFeature,
2871
+ moveFolder,
2872
+ moveSetup,
2873
+ moveWithin,
2874
+ nextFolderName,
2875
+ notFinishedIn,
2876
+ notFinishedOf,
2877
+ notMachinedOf,
2878
+ offeredLibraries,
2879
+ parseDraftSetups,
2880
+ parseStock,
2881
+ parseToolLength,
2882
+ pinEdit,
2883
+ pinFrom,
2884
+ pinHomes,
2885
+ pinInto,
2886
+ pinOf,
2887
+ pinTargetOf,
2888
+ pinnedIn,
2889
+ pinsWhere,
2890
+ placedAlong,
2891
+ placedFolders,
2892
+ planChecks,
2893
+ planCounts,
2894
+ planFromResults,
2895
+ planHatch,
2896
+ planPaint,
2897
+ planRunOf,
2898
+ planSession,
2899
+ planTags,
2900
+ planView,
2901
+ projectFolder,
2902
+ readingDetail,
2903
+ readingOf,
2904
+ readingsFor,
2905
+ reasonLine,
2906
+ refusalClauses,
2907
+ regionLabel,
2908
+ removeFolder,
2909
+ removeFolders,
2910
+ removeLibrary,
2911
+ renameFolder,
2912
+ replacedBy,
2913
+ restoreFeature,
2914
+ resultsView,
2915
+ roundTo,
2916
+ rowsOf,
2917
+ sameDirection,
2918
+ sameJson,
2919
+ sameStock,
2920
+ setSetupProcess,
2921
+ setToolOrientation,
2922
+ setupOf,
2923
+ sortedKeys,
2924
+ startSession,
2925
+ startingLibrary,
2926
+ stateOf,
2927
+ statementOf,
2928
+ stepFor,
2929
+ stockEdited,
2930
+ stockPlacement,
2931
+ stockRequest,
2932
+ suitsMaterial,
2933
+ swapLibraries,
2934
+ tagKey,
2935
+ tagSet,
2936
+ takeBack,
2937
+ takeBackIgnore,
2938
+ toInput,
2939
+ toStored,
2940
+ unignoreEdit,
2941
+ unignorePinned,
2942
+ unignoredNote,
2943
+ unpinFeature,
2944
+ unpinnedNote,
2945
+ whyOf,
2946
+ whyText,
2947
+ withFolders,
2948
+ withIgnored,
2949
+ withPin,
2950
+ withoutIgnored
2951
+ };