@toolpath/dfm 0.3.1 → 0.5.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 CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
- FEATURE_SUBJECTS,
2
+ MILLING_RULES,
3
3
  OPS,
4
4
  PRESENCE_METRIC,
5
- QUICK_METRICS,
6
- QUICK_SUBJECTS,
7
5
  RANGE_HIGHS,
8
6
  RANGE_LOWS,
9
7
  RULE_COLORS,
8
+ TURNING_RULES,
9
+ directionLabel,
10
10
  featureRows,
11
11
  featureTypeLabel,
12
12
  formatMeasured,
@@ -34,7 +34,7 @@ import {
34
34
  withMetric,
35
35
  withOp,
36
36
  withSubject
37
- } from "./chunk-5MPHT7BC.js";
37
+ } from "./chunk-4IRNLDE6.js";
38
38
 
39
39
  // src/rule-list/rule-list.tsx
40
40
  import { useMemo as useMemo2, useState as useState8 } from "react";
@@ -73,17 +73,21 @@ var isField = (target) => target instanceof HTMLSelectElement || target instance
73
73
  import { useState as useState2 } from "react";
74
74
 
75
75
  // src/rule-list/options.ts
76
- var subjectChoices = (current, all) => {
77
- const quick = [...QUICK_SUBJECTS, ...QUICK_SUBJECTS.includes(current) ? [] : [current]];
78
- const first = { choices: quick.map((value) => ({ value, label: subjectLabel(value) })) };
76
+ var subjectChoices = (current, all, vocabulary = MILLING_RULES) => {
77
+ const { quickSubjects } = vocabulary;
78
+ const quick = [...quickSubjects, ...quickSubjects.includes(current) ? [] : [current]];
79
+ const first = {
80
+ choices: quick.map((value) => ({ value, label: subjectLabel(value, vocabulary) }))
81
+ };
79
82
  if (!all) return { groups: [first], more: "More feature types\u2026" };
80
- const rest = FEATURE_SUBJECTS.filter((each) => !quick.includes(each.value));
83
+ const rest = vocabulary.subjects.filter((each) => !quick.includes(each.value));
81
84
  return { groups: [first, { heading: "More feature types", choices: rest }] };
82
85
  };
83
- var metricChoices = (subject, current, all) => {
84
- const offered = metricsForSubject(subject);
85
- const quick = offered.filter((each) => QUICK_METRICS.includes(each.key) || each.key === current);
86
- quick.sort((a, b) => rank(a.key) - rank(b.key));
86
+ var metricChoices = (subject, current, all, vocabulary = MILLING_RULES) => {
87
+ const { quickMetrics } = vocabulary;
88
+ const offered = metricsForSubject(subject, vocabulary);
89
+ const quick = offered.filter((each) => quickMetrics.includes(each.key) || each.key === current);
90
+ quick.sort((a, b) => rank(quickMetrics, a.key) - rank(quickMetrics, b.key));
87
91
  const rest = offered.filter((each) => !quick.includes(each));
88
92
  const first = { choices: quick.map(choice) };
89
93
  if (!all) return { groups: [first], more: rest.length > 0 ? "More measures\u2026" : void 0 };
@@ -95,9 +99,9 @@ var metricChoices = (subject, current, all) => {
95
99
  };
96
100
  };
97
101
  var choice = ({ key, label }) => ({ value: key, label });
98
- var rank = (key) => {
99
- const at = QUICK_METRICS.indexOf(key);
100
- return at === -1 ? QUICK_METRICS.length : at;
102
+ var rank = (quickMetrics, key) => {
103
+ const at = quickMetrics.indexOf(key);
104
+ return at === -1 ? quickMetrics.length : at;
101
105
  };
102
106
  var grouped = (items) => {
103
107
  const groups = /* @__PURE__ */ new Map();
@@ -275,7 +279,10 @@ var keepKeys = (event) => {
275
279
 
276
280
  // src/rule-list/new-rule.tsx
277
281
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
278
- var NewRule = ({ onAdd }) => {
282
+ var NewRule = ({
283
+ onAdd,
284
+ vocabulary
285
+ }) => {
279
286
  const [all, setAll] = useState2(false);
280
287
  return /* @__PURE__ */ jsxs2("li", { className: "flex items-start gap-1.5 py-1 pr-1.5", children: [
281
288
  /* @__PURE__ */ jsx2("span", { "aria-hidden": true, className: "size-7 shrink-0" }),
@@ -292,7 +299,7 @@ var NewRule = ({ onAdd }) => {
292
299
  label: "New rule",
293
300
  placeholder: "+ New Rule",
294
301
  value: "",
295
- choices: subjectChoices("any", all),
302
+ choices: subjectChoices("any", all, vocabulary),
296
303
  onChange: onAdd,
297
304
  onMore: () => setAll(true),
298
305
  className: "font-normal text-gray-400 dark:text-zinc-500"
@@ -309,23 +316,6 @@ import { IconButton, cn as cn7 } from "@toolpath/ui";
309
316
  // src/rule-list/feature-rows.tsx
310
317
  import { useMemo } from "react";
311
318
 
312
- // src/rule-list/direction-label.ts
313
- var EPSILON = 1e-6;
314
- var trim = (value) => Number.parseFloat(value.toFixed(3)).toString();
315
- var directionLabel = (direction) => {
316
- const axes = [
317
- ["X", direction.x],
318
- ["Y", direction.y],
319
- ["Z", direction.z]
320
- ];
321
- const nonZero = axes.filter(([, value]) => Math.abs(value) > EPSILON);
322
- const [axis] = nonZero;
323
- if (nonZero.length === 1 && axis && Math.abs(Math.abs(axis[1]) - 1) < EPSILON) {
324
- return `${axis[1] > 0 ? "+" : "\u2212"}${axis[0]}`;
325
- }
326
- return axes.map(([, value]) => trim(value)).join(", ");
327
- };
328
-
329
319
  // src/rule-list/feature-button.tsx
330
320
  import { cn as cn3 } from "@toolpath/ui";
331
321
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -347,6 +337,7 @@ var FeatureButton = ({
347
337
  rule,
348
338
  check,
349
339
  units,
340
+ vocabulary,
350
341
  selected,
351
342
  selectedGroup,
352
343
  onInspect,
@@ -354,6 +345,7 @@ var FeatureButton = ({
354
345
  onBack
355
346
  }) => {
356
347
  const feature = featureOf.get(tag);
348
+ const { name, detail } = feature ? vocabulary.featureLabel(feature) : { name: "Feature", detail: "" };
357
349
  return /* @__PURE__ */ jsxs3(
358
350
  "button",
359
351
  {
@@ -373,9 +365,9 @@ var FeatureButton = ({
373
365
  className: FEATURE_ROW,
374
366
  children: [
375
367
  /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1 truncate", children: [
376
- featureTypeLabel(feature?.featureType ?? "feature"),
368
+ name,
377
369
  " ",
378
- /* @__PURE__ */ jsx3("span", { className: "font-normal text-gray-300 dark:text-zinc-500", children: feature ? `from ${directionLabel(feature.machiningDirection)}` : "" })
370
+ /* @__PURE__ */ jsx3("span", { className: "font-normal text-gray-300 dark:text-zinc-500", children: detail })
379
371
  ] }),
380
372
  /* @__PURE__ */ jsx3(HitFigure, { hit: ruleHit(check, rule, tag), units })
381
373
  ]
@@ -620,10 +612,11 @@ var RANGE_LOW = {
620
612
  var RuleSentence = ({
621
613
  rule,
622
614
  units,
615
+ vocabulary,
623
616
  onChange,
624
617
  autoFocus
625
618
  }) => {
626
- const info = metricInfo(rule.metric);
619
+ const info = metricInfo(rule.metric, vocabulary);
627
620
  const flag = info?.quantity === "flag" || info?.quantity === "presence";
628
621
  const presence = info?.quantity === "presence";
629
622
  const ranged = info !== void 0 && !flag && rule.op === "between";
@@ -637,8 +630,8 @@ var RuleSentence = ({
637
630
  {
638
631
  label: "Features",
639
632
  value: rule.subject,
640
- choices: subjectChoices(rule.subject, allSubjects),
641
- onChange: (value) => onChange(withSubject(rule, value, units)),
633
+ choices: subjectChoices(rule.subject, allSubjects, vocabulary),
634
+ onChange: (value) => onChange(withSubject(rule, value, units, vocabulary)),
642
635
  onMore: () => setAllSubjects(true)
643
636
  }
644
637
  ),
@@ -677,8 +670,8 @@ var RuleSentence = ({
677
670
  label: "Measure",
678
671
  title: info?.hint,
679
672
  value: rule.metric,
680
- choices: metricChoices(rule.subject, rule.metric, allMetrics),
681
- onChange: (value) => onChange(withMetric(rule, value, units)),
673
+ choices: metricChoices(rule.subject, rule.metric, allMetrics, vocabulary),
674
+ onChange: (value) => onChange(withMetric(rule, value, units, vocabulary)),
682
675
  onMore: () => setAllMetrics(true),
683
676
  autoFocus
684
677
  }
@@ -775,6 +768,7 @@ var RuleRow = ({
775
768
  features,
776
769
  sheets,
777
770
  units,
771
+ vocabulary,
778
772
  ...list
779
773
  }) => {
780
774
  const [picking, setPicking] = useState6(false);
@@ -846,7 +840,16 @@ var RuleRow = ({
846
840
  className: "mt-1.5"
847
841
  }
848
842
  ),
849
- /* @__PURE__ */ jsx10(RuleSentence, { rule, units, onChange, autoFocus: focusOnMount }),
843
+ /* @__PURE__ */ jsx10(
844
+ RuleSentence,
845
+ {
846
+ rule,
847
+ units,
848
+ vocabulary,
849
+ onChange,
850
+ autoFocus: focusOnMount
851
+ }
852
+ ),
850
853
  /* @__PURE__ */ jsxs7("div", { className: "relative mt-1 shrink-0", children: [
851
854
  /* @__PURE__ */ jsx10(
852
855
  IconButton,
@@ -900,6 +903,7 @@ var RuleRow = ({
900
903
  features,
901
904
  sheets,
902
905
  units,
906
+ vocabulary,
903
907
  onBack: () => swatchRef.current?.focus(),
904
908
  ...list
905
909
  }
@@ -960,11 +964,7 @@ var useTimedValue = (ms) => {
960
964
  import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
961
965
  var NOTE_MS = 4e3;
962
966
  var COPIED_MS = 2e3;
963
- var EXPORT_FILE_NAME = "dfm-rules.json";
964
- var RuleSetActions = ({
965
- rules,
966
- actions
967
- }) => {
967
+ var RuleSetActions = ({ rules, actions, vocabulary }) => {
968
968
  const [note, setNote] = useTimedValue(NOTE_MS);
969
969
  const [copied, setCopied] = useTimedValue(COPIED_MS);
970
970
  const fileInput = useRef4(null);
@@ -977,7 +977,7 @@ var RuleSetActions = ({
977
977
  setNote("Could not import: the file could not be read.");
978
978
  return;
979
979
  }
980
- const read = parseRuleSetFile(text);
980
+ const read = parseRuleSetFile(text, vocabulary);
981
981
  if ("error" in read) {
982
982
  setNote(`Could not import: ${read.error}`);
983
983
  return;
@@ -987,7 +987,7 @@ var RuleSetActions = ({
987
987
  };
988
988
  const copyPrompt = async () => {
989
989
  try {
990
- await navigator.clipboard.writeText(llmRulePrompt(rules.rules));
990
+ await navigator.clipboard.writeText(llmRulePrompt(rules.rules, vocabulary));
991
991
  } catch {
992
992
  setNote("Could not copy the prompt. Allow clipboard access and try again.");
993
993
  return;
@@ -998,7 +998,7 @@ var RuleSetActions = ({
998
998
  return /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2 px-4 pt-2 pb-3", children: [
999
999
  /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-center gap-2", children: [
1000
1000
  /* @__PURE__ */ jsx11(Button, { variant: "secondary", onClick: () => fileInput.current?.click(), children: /* @__PURE__ */ jsx11(ActionLabel, { icon: /* @__PURE__ */ jsx11(UploadSimpleIcon, { weight: "bold" }), children: "Import\u2026" }) }),
1001
- /* @__PURE__ */ jsx11(Button, { variant: "secondary", onClick: () => downloadRuleSet(rules), children: /* @__PURE__ */ jsx11(ActionLabel, { icon: /* @__PURE__ */ jsx11(DownloadSimpleIcon, { weight: "bold" }), children: "Export" }) }),
1001
+ /* @__PURE__ */ jsx11(Button, { variant: "secondary", onClick: () => downloadRuleSet(rules, vocabulary), children: /* @__PURE__ */ jsx11(ActionLabel, { icon: /* @__PURE__ */ jsx11(DownloadSimpleIcon, { weight: "bold" }), children: "Export" }) }),
1002
1002
  /* @__PURE__ */ jsx11(
1003
1003
  Button,
1004
1004
  {
@@ -1041,13 +1041,13 @@ var ActionLabel = ({ icon, children, hidden = false }) => /* @__PURE__ */ jsxs8(
1041
1041
  ]
1042
1042
  }
1043
1043
  );
1044
- var downloadRuleSet = (rules) => {
1044
+ var downloadRuleSet = (rules, vocabulary) => {
1045
1045
  const url = URL.createObjectURL(
1046
- new Blob([serializeRuleSet(rules.rules)], { type: "application/json" })
1046
+ new Blob([serializeRuleSet(rules.rules, {}, vocabulary)], { type: "application/json" })
1047
1047
  );
1048
1048
  const link = document.createElement("a");
1049
1049
  link.href = url;
1050
- link.download = EXPORT_FILE_NAME;
1050
+ link.download = vocabulary.fileName;
1051
1051
  link.click();
1052
1052
  URL.revokeObjectURL(url);
1053
1053
  };
@@ -1068,6 +1068,7 @@ var RuleList = ({
1068
1068
  sheets,
1069
1069
  units,
1070
1070
  actions,
1071
+ vocabulary = MILLING_RULES,
1071
1072
  ...list
1072
1073
  }) => {
1073
1074
  const [openIds, setOpenIds] = useState8(/* @__PURE__ */ new Set());
@@ -1102,6 +1103,7 @@ var RuleList = ({
1102
1103
  features,
1103
1104
  sheets,
1104
1105
  units,
1106
+ vocabulary,
1105
1107
  ...list
1106
1108
  },
1107
1109
  rule.id
@@ -1115,15 +1117,16 @@ var RuleList = ({
1115
1117
  /* @__PURE__ */ jsx12(
1116
1118
  NewRule,
1117
1119
  {
1120
+ vocabulary,
1118
1121
  onAdd: (subject) => {
1119
- const rule = makeRule(subject, PRESENCE_METRIC.key, units);
1122
+ const rule = makeRule(subject, PRESENCE_METRIC.key, units, void 0, vocabulary);
1120
1123
  rules.add(rule);
1121
1124
  setAdded(rule.id);
1122
1125
  }
1123
1126
  }
1124
1127
  )
1125
1128
  ] }),
1126
- /* @__PURE__ */ jsx12(RuleSetActions, { rules, actions })
1129
+ /* @__PURE__ */ jsx12(RuleSetActions, { rules, actions, vocabulary })
1127
1130
  ] })
1128
1131
  );
1129
1132
  };
@@ -1799,14 +1802,16 @@ var faintText = (look) => look.muted === "strong" ? "text-gray dark:text-zinc-30
1799
1802
 
1800
1803
  // src/feature-panel/feature-name.tsx
1801
1804
  import { Fragment as Fragment7, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
1802
- var FeatureName = ({ feature, label, look }) => /* @__PURE__ */ jsxs14(Fragment7, { children: [
1803
- label ?? featureTypeLabel(feature.featureType),
1804
- " ",
1805
- /* @__PURE__ */ jsxs14("span", { className: cn9("font-normal", faintText(look)), children: [
1806
- "from ",
1807
- directionLabel(feature.machiningDirection)
1808
- ] })
1809
- ] });
1805
+ var FeatureName = ({ feature, label, look, vocabulary = MILLING_RULES }) => {
1806
+ const { name, detail } = vocabulary.featureLabel(feature);
1807
+ return /* @__PURE__ */ jsxs14(Fragment7, { children: [
1808
+ label ?? name,
1809
+ detail ? /* @__PURE__ */ jsxs14(Fragment7, { children: [
1810
+ " ",
1811
+ /* @__PURE__ */ jsx17("span", { className: cn9("font-normal", faintText(look)), children: detail })
1812
+ ] }) : null
1813
+ ] });
1814
+ };
1810
1815
  var RequiredBadge = () => /* @__PURE__ */ jsx17("span", { className: "rounded-full bg-gray-100 px-1.5 text-2xs font-semibold text-gray-500 uppercase dark:bg-zinc-800 dark:text-zinc-300", children: "Required" });
1811
1816
 
1812
1817
  // src/feature-panel/fold-section.tsx
@@ -2120,10 +2125,12 @@ var FeatureDetails = ({
2120
2125
  record,
2121
2126
  PopOut,
2122
2127
  look: lookOption,
2123
- folds
2128
+ folds,
2129
+ vocabulary = MILLING_RULES
2124
2130
  }) => {
2125
2131
  const look = resolveLook(lookOption);
2126
- const name = /* @__PURE__ */ jsx22(FeatureName, { feature, label, look });
2132
+ const name = /* @__PURE__ */ jsx22(FeatureName, { feature, label, look, vocabulary });
2133
+ const { detail: cutFrom } = vocabulary.featureLabel(feature);
2127
2134
  const [out, setOut] = useState13(/* @__PURE__ */ new Set());
2128
2135
  const openOut = useCallback2((id) => setOut((was) => new Set(was).add(id)), []);
2129
2136
  const closeOut = useCallback2(
@@ -2142,8 +2149,7 @@ var FeatureDetails = ({
2142
2149
  /* @__PURE__ */ jsx22("h3", { className: "truncate text-sm font-semibold text-gray-600 dark:text-zinc-100", children: name }),
2143
2150
  /* @__PURE__ */ jsxs19("p", { className: cn12("flex flex-wrap items-center gap-1.5 text-xs", secondaryText(look)), children: [
2144
2151
  /* @__PURE__ */ jsx22(Swatch, { color: directionColor, className: "size-2.5" }),
2145
- "Machined from ",
2146
- directionLabel(feature.machiningDirection),
2152
+ cutFrom ? `Machined ${cutFrom}` : "Machined",
2147
2153
  required ? /* @__PURE__ */ jsx22(RequiredBadge, {}) : null,
2148
2154
  status
2149
2155
  ] })
@@ -2157,7 +2163,15 @@ var FeatureDetails = ({
2157
2163
  children
2158
2164
  ] }),
2159
2165
  /* @__PURE__ */ jsx22(FoldSection, { id: "rules", title: "Rules broken", folds, children: /* @__PURE__ */ jsx22(BrokenRuleList, { rules, look }) }),
2160
- /* @__PURE__ */ jsx22(MeasurementSections, { measurements, look, folds }),
2166
+ /* @__PURE__ */ jsx22(
2167
+ MeasurementSections,
2168
+ {
2169
+ measurements,
2170
+ considerationsTitle: vocabulary.considerationsTitle,
2171
+ look,
2172
+ folds
2173
+ }
2174
+ ),
2161
2175
  /* @__PURE__ */ jsx22(
2162
2176
  ReachSection2,
2163
2177
  {
@@ -2221,7 +2235,7 @@ var Explained = ({ note, children }) => note ? /* @__PURE__ */ jsx22(Tooltip, {
2221
2235
  }
2222
2236
  ) }) : children;
2223
2237
  var None = ({ look }) => /* @__PURE__ */ jsx22("p", { className: cn12("text-sm", secondaryText(look)), children: "None" });
2224
- var MeasurementSections = ({ measurements, look, folds }) => {
2238
+ var MeasurementSections = ({ measurements, considerationsTitle, look, folds }) => {
2225
2239
  if (measurements.status !== "ready") {
2226
2240
  return /* @__PURE__ */ jsx22(Fragment10, { children: /* @__PURE__ */ jsx22(FoldSection, { id: "measurements", title: "Measurements", folds, children: measurements.status === "loading" ? /* @__PURE__ */ jsx22(Reading, { children: "Reading the feature\u2019s datasheet\u2026" }) : /* @__PURE__ */ jsx22(ReadFailure, { failure: measurements }) }) });
2227
2241
  }
@@ -2229,7 +2243,7 @@ var MeasurementSections = ({ measurements, look, folds }) => {
2229
2243
  const milling = measurements.value.filter((row) => row.milling);
2230
2244
  return /* @__PURE__ */ jsxs19(Fragment10, { children: [
2231
2245
  /* @__PURE__ */ jsx22(FoldSection, { id: "measurements", title: "Measurements", folds, children: /* @__PURE__ */ jsx22(MeasurementList, { rows: measured, look }) }),
2232
- milling.length > 0 ? /* @__PURE__ */ jsx22(FoldSection, { id: "milling", title: "Milling considerations", folds, children: /* @__PURE__ */ jsx22(MeasurementList, { rows: milling, look }) }) : null
2246
+ milling.length > 0 ? /* @__PURE__ */ jsx22(FoldSection, { id: "milling", title: considerationsTitle, folds, children: /* @__PURE__ */ jsx22(MeasurementList, { rows: milling, look }) }) : null
2233
2247
  ] });
2234
2248
  };
2235
2249
  var MeasurementList = ({ rows, look }) => rows.length > 0 ? /* @__PURE__ */ jsx22("dl", { className: "grid grid-cols-[1fr_auto] items-center gap-x-3 gap-y-1 text-sm", children: rows.map((row) => /* @__PURE__ */ jsxs19("div", { className: "contents", children: [
@@ -2297,7 +2311,8 @@ var CandidateList = ({
2297
2311
  onHover,
2298
2312
  face,
2299
2313
  look: lookOption,
2300
- folds
2314
+ folds,
2315
+ vocabulary
2301
2316
  }) => {
2302
2317
  const look = resolveLook(lookOption);
2303
2318
  const count = `Candidate features (${rows.length})`;
@@ -2328,14 +2343,15 @@ var CandidateList = ({
2328
2343
  chosen: row.feature.tag === selected,
2329
2344
  onSelect,
2330
2345
  onHover,
2331
- look
2346
+ look,
2347
+ vocabulary
2332
2348
  },
2333
2349
  row.feature.tag
2334
2350
  )) })
2335
2351
  }
2336
2352
  ) });
2337
2353
  };
2338
- var Row = ({ row, chosen, onSelect, onHover, look }) => {
2354
+ var Row = ({ row, chosen, onSelect, onHover, look, vocabulary }) => {
2339
2355
  const { feature } = row;
2340
2356
  const info = look.selected === "info";
2341
2357
  return /* @__PURE__ */ jsxs20(
@@ -2367,7 +2383,7 @@ var Row = ({ row, chosen, onSelect, onHover, look }) => {
2367
2383
  children: [
2368
2384
  /* @__PURE__ */ jsxs20("span", { className: "flex w-full items-center gap-2", children: [
2369
2385
  row.color ? /* @__PURE__ */ jsx23(Swatch, { color: row.color }) : /* @__PURE__ */ jsx23("span", { "aria-hidden": true, className: "size-4 shrink-0" }),
2370
- /* @__PURE__ */ jsx23("span", { className: "min-w-0 flex-1 truncate", children: /* @__PURE__ */ jsx23(FeatureName, { feature, label: row.label, look }) }),
2386
+ /* @__PURE__ */ jsx23("span", { className: "min-w-0 flex-1 truncate", children: /* @__PURE__ */ jsx23(FeatureName, { feature, label: row.label, look, vocabulary }) }),
2371
2387
  row.ruleCount ? /* @__PURE__ */ jsxs20("span", { className: cn13("text-2xs tabular-nums", secondaryText(look)), children: [
2372
2388
  row.ruleCount,
2373
2389
  " ",
@@ -2527,6 +2543,7 @@ var FeaturePanel = ({
2527
2543
  className,
2528
2544
  look: lookOption,
2529
2545
  folds,
2546
+ vocabulary,
2530
2547
  children
2531
2548
  }) => {
2532
2549
  const look = resolveLook(lookOption);
@@ -2579,7 +2596,8 @@ var FeaturePanel = ({
2579
2596
  ...onDropFace ? { onStop: () => onDropFace(index) } : {}
2580
2597
  },
2581
2598
  look: lookOption,
2582
- folds
2599
+ folds,
2600
+ vocabulary
2583
2601
  },
2584
2602
  `${each.region ?? "list"}-${index}`
2585
2603
  )),
@@ -2602,7 +2620,8 @@ var FeaturePanel = ({
2602
2620
  onSelect: (tag) => onSelect(tag, active),
2603
2621
  onHover,
2604
2622
  look: lookOption,
2605
- folds
2623
+ folds,
2624
+ vocabulary
2606
2625
  }
2607
2626
  ) : null,
2608
2627
  face?.selected ? children : /* @__PURE__ */ jsx25("p", { className: cn15("px-4 py-3 text-sm", secondaryText(look)), children: face && face.rows.length > 0 ? "Choose a feature to read its details." : "No feature owns this face." })
@@ -2616,10 +2635,12 @@ export {
2616
2635
  ComparisonTable,
2617
2636
  FeatureDetails,
2618
2637
  FeaturePanel,
2638
+ MILLING_RULES,
2619
2639
  ReachChart,
2620
2640
  ReachDrawing,
2621
2641
  RuleList,
2622
2642
  Swatch,
2643
+ TURNING_RULES,
2623
2644
  candidateListKeys,
2624
2645
  isField,
2625
2646
  listKeys,
@@ -1,5 +1,5 @@
1
- import { e as DfmHit, U as Units, f as FeaturePinch, g as FeatureSheet, D as DfmRule } from '../feature-record-BUoFFjN6.js';
2
- export { B as BrokenRule, C as CRITICAL_COLOR, h as ClearanceFit, a as DfmCheck, b as DfmFeature, i as DfmMetric, j as DfmOp, k as DfmQuantity, l as DfmSources, m as FactsKind, n as FeatureAcross, c as FeatureProfile, d as FeatureRecord, F as FeatureSheets, o as FeatureThreading, M as Measurement, P as PINCH_TOOL_MM, p as PinchDisc, q as RULE_COLORS, r as RangeHigh, s as RangeLow, R as ReachCurve, t as ReportFeature, u as RuleResult, S as SurfaceToolKind, W as WARNING_COLOR, v as brokenRules, w as checkPart, x as compareColors, y as describeRule, z as dfmFeatures, A as featureColor, E as featureMeasurements, G as featureProfile, H as featureSheet, I as featureTypeLabel, J as isNoReading, K as isSurfaceType, L as makeRule, N as maxBallDiameter, O as parseRules, Q as peakHeight, T as readPinch, V as readReachCurve, X as runStart, Y as trimmedCurve, Z as withSurroundedFaces } from '../feature-record-BUoFFjN6.js';
1
+ import { h as DfmHit, U as Units, i as FeaturePinch, j as FeatureSheet, g as RuleProcess, D as DfmRule, R as RuleVocabulary, b as DfmFeature, a as DfmCheck } from '../feature-record-CfjgDCJU.js';
2
+ export { B as BrokenRule, C as CRITICAL_COLOR, k as ClearanceFit, l as DfmMetric, m as DfmOp, n as DfmQuantity, o as DfmSources, p as DfmSubject, q as FactsKind, r as FeatureAcross, d as FeatureProfile, e as FeatureRecord, F as FeatureSheets, s as FeatureThreading, t as FeatureUndercut, f as MILLING_RULES, M as Measurement, P as PINCH_TOOL_MM, u as PinchDisc, v as RULE_COLORS, w as RangeHigh, x as RangeLow, c as ReachCurve, y as ReportFeature, z as RuleResult, S as SurfaceToolKind, T as TURNING_RULES, A as TurnedFactsKind, W as WARNING_COLOR, E as brokenRules, G as checkPart, H as compareColors, I as describeRule, J as dfmFeatures, K as featureColor, L as featureMeasurements, N as featureProfile, O as featureSheet, Q as featureTypeLabel, V as isNoReading, X as isSurfaceType, Y as makeRule, Z as maxBallDiameter, _ as parseRules, $ as peakHeight, a0 as readPinch, a1 as readReachCurve, a2 as runStart, a3 as trimmedCurve, a4 as turnedTypeLabel, a5 as withSurroundedFaces } from '../feature-record-CfjgDCJU.js';
3
3
 
4
4
  /** The figure that broke a rule, bare: `7.5`, `0.05"`. Empty for a yes/no or "any size". */
5
5
  declare const hitFigure: (hit: DfmHit, units: Units) => string;
@@ -87,6 +87,11 @@ interface RuleSetFile {
87
87
  kind: typeof FILE_KIND;
88
88
  /** The file format's version, for a later reader. */
89
89
  version: 1;
90
+ /**
91
+ * Which rules these are: milled features' or turned ones'. Always written;
92
+ * a file without it was written before turning rules, so it is milling.
93
+ */
94
+ process?: RuleProcess;
90
95
  /** What the set is for: "Aluminium prototypes", "Acme Corp sheet work". */
91
96
  name?: string;
92
97
  description?: string;
@@ -95,21 +100,23 @@ interface RuleSetFile {
95
100
  rules: DfmRule[];
96
101
  }
97
102
  declare const FILE_KIND = "toolpath-dfm-rules";
98
- /** The rules as a file, pretty-printed. */
103
+ /** The rules as a file, pretty-printed, saying which process they are for. */
99
104
  declare const serializeRuleSet: (rules: readonly DfmRule[], meta?: {
100
105
  name?: string;
101
106
  description?: string;
102
- }) => string;
107
+ }, vocabulary?: RuleVocabulary) => string;
103
108
  /**
104
109
  * The rules in a file, or the reason it cannot be read. Lenient about the
105
110
  * wrapper — a bare array of rules is accepted, and so is an object with a
106
111
  * `rules` list and nothing else, since an LLM may well hand back either —
107
- * strict about every rule, which `parseRules` checks field by field. Rules
108
- * without an id, or whose ids collide, are given fresh ones. A file that says
109
- * its lengths are in anything but millimetres is refused rather than read as
110
- * millimetres; one that does not say is read as millimetres.
112
+ * strict about every rule, which `parseRules` checks field by field in the
113
+ * vocabulary given. Rules without an id, or whose ids collide, are given
114
+ * fresh ones. A file that says its lengths are in anything but millimetres is
115
+ * refused rather than read as millimetres; one that does not say is read as
116
+ * millimetres. A file of the other process's rules is refused, saying where it
117
+ * belongs, rather than read as rules it is not.
111
118
  */
112
- declare const parseRuleSetFile: (text: string) => {
119
+ declare const parseRuleSetFile: (text: string, vocabulary?: RuleVocabulary) => {
113
120
  rules: DfmRule[];
114
121
  skipped: number;
115
122
  name?: string;
@@ -118,11 +125,50 @@ declare const parseRuleSetFile: (text: string) => {
118
125
  };
119
126
  /**
120
127
  * A prompt that lets any LLM interview a user and write a rule set this
121
- * viewer reads. Built from the metric and subject tables themselves, so it
122
- * never drifts from what the parser accepts: every subject value, every
123
- * metric key with its unit and meaning and the kinds of feature that report
124
- * it, every comparison, and the palette.
128
+ * viewer reads, for one process. Built from the vocabulary's metric and
129
+ * subject tables themselves, so it never drifts from what the parser accepts:
130
+ * every subject value, every metric key with its unit and meaning and the
131
+ * kinds of feature that report it, every comparison, and the palette.
132
+ */
133
+ declare const llmRulePrompt: (current?: readonly DfmRule[], vocabulary?: RuleVocabulary) => string;
134
+
135
+ /** The end of the part a turned feature is cut from: the end the axis points toward, or the other. */
136
+ type TurnedApproach = 'PlusZ' | 'MinusZ';
137
+ /**
138
+ * A turned feature as the Engine's report gives it, cut down to what the
139
+ * rules read. `@toolpath/api`'s `TurnedFeature` is one, so the package takes
140
+ * no dependency on the SDK for a type.
141
+ */
142
+ interface TurnedReportFeature {
143
+ readonly featureTag: string;
144
+ /** Snake case, open-ended: `od_groove`. */
145
+ readonly featureType: string;
146
+ /** The end it is cut from; null or absent on older reports, and for an `unknown` feature. */
147
+ readonly approach?: TurnedApproach | null;
148
+ readonly regionIdxs: readonly number[];
149
+ /** Its turning datasheet as the API sent it, or null where it was not measured. */
150
+ readonly datasheet: unknown;
151
+ }
152
+ /**
153
+ * One stretch of a turned part: its per-end copies as one feature, named by
154
+ * the first copy's tag. Its machining direction is along the axis toward the
155
+ * end it is cut from — `+Z` or `−Z` — and zero where it is cut from both or
156
+ * neither; it is there to make a stretch a `DfmFeature`, not to be read.
157
+ */
158
+ interface TurnedStretch extends DfmFeature {
159
+ /** The report's features it was merged from, in the report's order. */
160
+ readonly copies: readonly TurnedReportFeature[];
161
+ /** The ends it is cut from, in the order its copies give them. */
162
+ readonly approaches: readonly TurnedApproach[];
163
+ }
164
+ /** The report's turned features with each stretch's per-end copies merged, in the order each first comes. */
165
+ declare const turnedStretches: (features: readonly TurnedReportFeature[]) => TurnedStretch[];
166
+ /**
167
+ * Checks every stretch of a turned part against turning rules. Every stretch
168
+ * is required — a turned part has one reading of each face — so `otherTags`
169
+ * is always empty and nothing is painted whole; each broken stretch's faces
170
+ * are painted its worst rule's colour.
125
171
  */
126
- declare const llmRulePrompt: (current?: readonly DfmRule[]) => string;
172
+ declare const checkTurnedPart: (stretches: readonly TurnedStretch[], rules: readonly DfmRule[]) => DfmCheck;
127
173
 
128
- export { DfmHit, DfmRule, FILE_KIND, type FaceTriangles, FeaturePinch, FeatureSheet, type PinchMark, type PlacedTool, type RuleSetFile, Units, type Vec3, hitFigure, llmRulePrompt, parseRuleSetFile, pinchLabel, pinchMark, placePinchTool, sameDepth, serializeRuleSet };
174
+ export { DfmCheck, DfmFeature, DfmHit, DfmRule, FILE_KIND, type FaceTriangles, FeaturePinch, FeatureSheet, type PinchMark, type PlacedTool, RuleProcess, type RuleSetFile, RuleVocabulary, type TurnedReportFeature, type TurnedStretch, Units, type Vec3, checkTurnedPart, hitFigure, llmRulePrompt, parseRuleSetFile, pinchLabel, pinchMark, placePinchTool, sameDepth, serializeRuleSet, turnedStretches };