@toolpath/dfm 0.4.0 → 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.
@@ -1,5 +1,85 @@
1
+ // src/model/geometry.ts
2
+ var dfmFeatures = (features) => {
3
+ const read = features.map(
4
+ ({ featureTag, featureType, machiningDirection: { x, y, z }, regionIdxs }) => ({
5
+ tag: featureTag,
6
+ featureType,
7
+ machiningDirection: { x, y, z },
8
+ regionIdxs
9
+ })
10
+ );
11
+ const directions = [];
12
+ read.forEach(({ machiningDirection }) => {
13
+ if (!directions.some((each) => sameDirection(each, machiningDirection)))
14
+ directions.push(machiningDirection);
15
+ });
16
+ const numbered = directions.flatMap(
17
+ (direction) => read.filter((feature) => sameDirection(feature.machiningDirection, direction))
18
+ );
19
+ const numberOf = new Map(numbered.map((feature, index) => [feature, index + 1]));
20
+ return read.map((feature) => ({ ...feature, number: numberOf.get(feature) ?? 0 }));
21
+ };
22
+ var TOLERANCE = 1e-6;
23
+ var sameDirection = (a, b) => Math.abs(a.x - b.x) < TOLERANCE && Math.abs(a.y - b.y) < TOLERANCE && Math.abs(a.z - b.z) < TOLERANCE;
24
+ var featureTypeLabel = (featureType) => {
25
+ const words = featureType.replace(/_/g, " ").trim();
26
+ return words.charAt(0).toUpperCase() + words.slice(1);
27
+ };
28
+ var partTop = (features, sheets, direction) => {
29
+ let top = null;
30
+ for (const other of features) {
31
+ const zMax = sheets[other.tag.toLowerCase()]?.extendedZMax;
32
+ if (zMax !== void 0 && sameDirection(other.machiningDirection, direction)) {
33
+ top = top === null ? zMax : Math.max(top, zMax);
34
+ }
35
+ }
36
+ return top;
37
+ };
38
+ var FLAT_TIP_DEG = 0.5;
39
+ var isDrilled = (sheet) => sheet?.diameter !== void 0 && sheet.diameter > 0 && !(sheet.tipAngle !== void 0 && sheet.tipAngle >= 180 - FLAT_TIP_DEG);
40
+ var featureLd = (features, feature, sheets) => {
41
+ const sheet = sheets[feature.tag.toLowerCase()];
42
+ const top = sheet ? partTop(features, sheets, feature.machiningDirection) : null;
43
+ const bottom = sheet?.zMin;
44
+ if (!sheet || top === null || bottom === void 0) return null;
45
+ const drilling = isDrilled(sheet);
46
+ const [basis, across] = sheet.diameter !== void 0 ? ["bore", sheet.diameter] : sheet.pinchDiameter !== void 0 ? ["pinch", sheet.pinchDiameter] : [
47
+ "radius",
48
+ sheet.sharpCorners || sheet.cornerRadius === void 0 ? void 0 : 2 * sheet.cornerRadius
49
+ ];
50
+ if (across === void 0 || across <= 0) return null;
51
+ const atLeast = basis !== "bore" && sheet.cornerRadiusUnresolved === true;
52
+ return {
53
+ ratio: (top - bottom) / across,
54
+ ...sheet.zMax !== void 0 ? { featureRatio: (sheet.zMax - bottom) / across } : {},
55
+ drilling,
56
+ atLeast,
57
+ basis,
58
+ across
59
+ };
60
+ };
61
+
1
62
  // src/model/dfm-metrics.ts
2
63
  import { MM_PER_INCH } from "@toolpath/tool-support";
64
+
65
+ // src/model/direction-label.ts
66
+ var EPSILON = 1e-6;
67
+ var trim = (value) => Number.parseFloat(value.toFixed(3)).toString();
68
+ var directionLabel = (direction) => {
69
+ const axes = [
70
+ ["X", direction.x],
71
+ ["Y", direction.y],
72
+ ["Z", direction.z]
73
+ ];
74
+ const nonZero = axes.filter(([, value]) => Math.abs(value) > EPSILON);
75
+ const [axis] = nonZero;
76
+ if (nonZero.length === 1 && axis && Math.abs(Math.abs(axis[1]) - 1) < EPSILON) {
77
+ return `${axis[1] > 0 ? "+" : "\u2212"}${axis[0]}`;
78
+ }
79
+ return axes.map(([, value]) => trim(value)).join(", ");
80
+ };
81
+
82
+ // src/model/dfm-metrics.ts
3
83
  var define = ({ start, path, ...spec }) => ({
4
84
  ...spec,
5
85
  start: typeof start === "number" ? { mm: start, inch: start } : start,
@@ -988,12 +1068,26 @@ var QUICK_METRICS = [
988
1068
  "undercutDepth",
989
1069
  "tilted"
990
1070
  ];
991
- var subjectInfo = (value) => FEATURE_SUBJECTS.find((each) => each.value === value);
992
- var metricInfo = (key) => FEATURE_METRICS.find((each) => each.key === key);
993
- var metricsForSubject = (subject) => {
994
- const kinds = subjectInfo(subject)?.kinds;
995
- if (!kinds) return FEATURE_METRICS;
996
- return FEATURE_METRICS.filter(
1071
+ var MILLING_RULES = {
1072
+ process: "milling",
1073
+ metrics: FEATURE_METRICS,
1074
+ subjects: FEATURE_SUBJECTS,
1075
+ quickSubjects: QUICK_SUBJECTS,
1076
+ quickMetrics: QUICK_METRICS,
1077
+ fallbackMetric: LD_METRIC,
1078
+ featureLabel: (feature) => ({
1079
+ name: featureTypeLabel(feature.featureType),
1080
+ detail: `from ${directionLabel(feature.machiningDirection)}`
1081
+ }),
1082
+ considerationsTitle: "Milling considerations",
1083
+ fileName: "dfm-rules.json"
1084
+ };
1085
+ var subjectInfo = (value, vocabulary = MILLING_RULES) => vocabulary.subjects.find((each) => each.value === value);
1086
+ var metricInfo = (key, vocabulary = MILLING_RULES) => vocabulary.metrics.find((each) => each.key === key);
1087
+ var metricsForSubject = (subject, vocabulary = MILLING_RULES) => {
1088
+ const kinds = subjectInfo(subject, vocabulary)?.kinds;
1089
+ if (!kinds) return vocabulary.metrics;
1090
+ return vocabulary.metrics.filter(
997
1091
  (each) => !each.kinds || each.kinds.some((kind) => kinds.includes(kind))
998
1092
  );
999
1093
  };
@@ -1065,8 +1159,8 @@ var RETIRED_COLORS = {
1065
1159
  "#68b688": "#30a46c",
1066
1160
  "#6bb0b3": "#00a2c7"
1067
1161
  };
1068
- var subjectLabel = (value) => subjectInfo(value)?.label ?? value;
1069
- var trim = (value, places) => String(Number(value.toFixed(places)));
1162
+ var subjectLabel = (value, vocabulary = MILLING_RULES) => subjectInfo(value, vocabulary)?.label ?? value;
1163
+ var trim2 = (value, places) => String(Number(value.toFixed(places)));
1070
1164
  var SQUARE_MM_PER_SQUARE_INCH = MM_PER_INCH3 ** 2;
1071
1165
  var quantityUnit = (quantity, units) => {
1072
1166
  switch (quantity) {
@@ -1086,7 +1180,7 @@ var withUnit = (value, unit) => unit === "" ? value : unit === "\xB0" || unit ==
1086
1180
  var toUnits = (quantity, value, units) => units !== "inch" ? value : quantity === "length" ? value / MM_PER_INCH3 : quantity === "area" ? value / SQUARE_MM_PER_SQUARE_INCH : value;
1087
1181
  var toField = (quantity, value, units) => {
1088
1182
  const shown = toUnits(quantity, value, units);
1089
- return quantity === "length" ? trim(shown, FIELD_LENGTH_PLACES[units]) : quantity === "count" ? trim(shown, 0) : trim(shown, 3);
1183
+ return quantity === "length" ? trim2(shown, FIELD_LENGTH_PLACES[units]) : quantity === "count" ? trim2(shown, 0) : trim2(shown, 3);
1090
1184
  };
1091
1185
  var fromField = (metric, text, units) => {
1092
1186
  const parsed = Number(text.trim());
@@ -1099,25 +1193,25 @@ var formatMeasured = (quantity, value, units) => {
1099
1193
  const shown = toUnits(quantity, value, units);
1100
1194
  switch (quantity) {
1101
1195
  case "length":
1102
- return units === "inch" ? `${trim(shown, LENGTH_PLACES.inch)}"` : `${trim(shown, LENGTH_PLACES.mm)} mm`;
1196
+ return units === "inch" ? `${trim2(shown, LENGTH_PLACES.inch)}"` : `${trim2(shown, LENGTH_PLACES.mm)} mm`;
1103
1197
  case "area":
1104
1198
  return `${Number(shown.toFixed(3)).toLocaleString(void 0, { maximumFractionDigits: 3 })} ${quantityUnit("area", units)}`;
1105
1199
  case "angle":
1106
- return `${trim(shown, 3)}\xB0`;
1200
+ return `${trim2(shown, 3)}\xB0`;
1107
1201
  case "percent":
1108
- return `${trim(shown, 3)}%`;
1202
+ return `${trim2(shown, 3)}%`;
1109
1203
  case "count":
1110
- return trim(shown, 0);
1204
+ return trim2(shown, 0);
1111
1205
  case "flag":
1112
1206
  case "presence":
1113
1207
  return "";
1114
1208
  default:
1115
- return trim(shown, 3);
1209
+ return trim2(shown, 3);
1116
1210
  }
1117
1211
  };
1118
- var describeRule = (rule, units) => {
1119
- const info = metricInfo(rule.metric);
1120
- const subject = subjectLabel(rule.subject);
1212
+ var describeRule = (rule, units, vocabulary = MILLING_RULES) => {
1213
+ const info = metricInfo(rule.metric, vocabulary);
1214
+ const subject = subjectLabel(rule.subject, vocabulary);
1121
1215
  if (!info) return subject;
1122
1216
  if (info.quantity === "presence") return `${subject}, ${info.label}`;
1123
1217
  if (info.quantity === "flag") return `${subject} with ${info.label}`;
@@ -1126,8 +1220,8 @@ var describeRule = (rule, units) => {
1126
1220
  return rule.op === "between" ? `${subject} with ${shown(rule.value)} ${lowOf(rule) === "gt" ? "<" : "\u2264"} ${info.label} ${opSymbol(highOf(rule))} ${shown(upperOf(rule))}` : `${subject} with ${info.label} ${opSymbol(rule.op)} ${shown(rule.value)}`;
1127
1221
  };
1128
1222
  var newRuleId = () => Math.random().toString(36).slice(2, 10);
1129
- var makeRule = (subject, metricKey, units, color = WARNING_COLOR) => {
1130
- const info = metricInfo(metricKey) ?? LD_METRIC;
1223
+ var makeRule = (subject, metricKey, units, color = WARNING_COLOR, vocabulary = MILLING_RULES) => {
1224
+ const info = metricInfo(metricKey, vocabulary) ?? vocabulary.fallbackMetric;
1131
1225
  return {
1132
1226
  id: newRuleId(),
1133
1227
  subject,
@@ -1141,16 +1235,16 @@ var upperOf = (rule) => rule.max ?? rule.value + Math.max(Math.abs(rule.value),
1141
1235
  var lowOf = (rule) => rule.lowOp ?? "gte";
1142
1236
  var highOf = (rule) => rule.highOp ?? "lt";
1143
1237
  var withoutRange = ({ max: _max, lowOp: _low, highOp: _high, ...rest }) => rest;
1144
- var withMetric = (rule, metricKey, units) => {
1145
- const info = metricInfo(metricKey);
1238
+ var withMetric = (rule, metricKey, units, vocabulary = MILLING_RULES) => {
1239
+ const info = metricInfo(metricKey, vocabulary);
1146
1240
  if (!info) return rule;
1147
1241
  return { ...withoutRange(rule), metric: info.key, op: info.op, value: info.start[units] };
1148
1242
  };
1149
1243
  var withOp = (rule, op) => op === "between" ? { ...rule, op, max: upperOf(rule), lowOp: lowOf(rule), highOp: highOf(rule) } : { ...withoutRange(rule), op };
1150
- var withSubject = (rule, subject, units) => {
1151
- const metrics = metricsForSubject(subject);
1244
+ var withSubject = (rule, subject, units, vocabulary = MILLING_RULES) => {
1245
+ const metrics = metricsForSubject(subject, vocabulary);
1152
1246
  const moved = { ...rule, subject };
1153
- return metrics.some((each) => each.key === rule.metric) ? moved : withMetric(moved, (metrics[0] ?? LD_METRIC).key, units);
1247
+ return metrics.some((each) => each.key === rule.metric) ? moved : withMetric(moved, (metrics[0] ?? vocabulary.fallbackMetric).key, units, vocabulary);
1154
1248
  };
1155
1249
  var breaks = (rule, quantity, value) => {
1156
1250
  if (quantity === "presence") return true;
@@ -1171,7 +1265,7 @@ var breaks = (rule, quantity, value) => {
1171
1265
  var worseUpward = (rule, metric) => rule.op === "gte" || rule.op === "gt" ? true : rule.op === "lte" || rule.op === "lt" ? false : metric.op !== "lte" && metric.op !== "lt";
1172
1266
  var COLOR = /^#[0-9a-f]{6}$/i;
1173
1267
  var isValue = (value, metric) => typeof value === "number" && Number.isFinite(value) && (value >= 0 || metric?.signed === true) && (metric?.quantity !== "count" || Number.isInteger(value));
1174
- var parseRule = (raw) => {
1268
+ var parseRule = (raw, vocabulary) => {
1175
1269
  if (!raw || typeof raw !== "object") return null;
1176
1270
  const {
1177
1271
  id,
@@ -1184,8 +1278,8 @@ var parseRule = (raw) => {
1184
1278
  highOp,
1185
1279
  color
1186
1280
  } = raw;
1187
- const info = typeof key === "string" ? metricInfo(key) : void 0;
1188
- const valid = typeof id === "string" && typeof subject === "string" && subjectInfo(subject) !== void 0 && typeof key === "string" && info !== void 0 && OPS.includes(op) && isValue(value, info) && (op !== "between" || isValue(max, info)) && typeof color === "string" && COLOR.test(color);
1281
+ const info = typeof key === "string" ? metricInfo(key, vocabulary) : void 0;
1282
+ const valid = typeof id === "string" && typeof subject === "string" && subjectInfo(subject, vocabulary) !== void 0 && typeof key === "string" && info !== void 0 && OPS.includes(op) && isValue(value, info) && (op !== "between" || isValue(max, info)) && typeof color === "string" && COLOR.test(color);
1189
1283
  if (!valid) return null;
1190
1284
  const hex = color.toLowerCase();
1191
1285
  return {
@@ -1202,7 +1296,7 @@ var parseRule = (raw) => {
1202
1296
  color: RETIRED_COLORS[hex] ?? hex
1203
1297
  };
1204
1298
  };
1205
- var parseRules = (raw) => Array.isArray(raw) ? raw.flatMap((each) => parseRule(each) ?? []) : null;
1299
+ var parseRules = (raw, vocabulary = MILLING_RULES) => Array.isArray(raw) ? raw.flatMap((each) => parseRule(each, vocabulary) ?? []) : null;
1206
1300
 
1207
1301
  // src/model/hole-groups.ts
1208
1302
  var holeSize = (sheets, tag) => {
@@ -1214,9 +1308,9 @@ var holeSize = (sheets, tag) => {
1214
1308
  };
1215
1309
  var SAME_DEPTH = 0.05;
1216
1310
  var DEPTH_FLOOR = 0.01;
1217
- var TOLERANCE = 1e-6;
1311
+ var TOLERANCE2 = 1e-6;
1218
1312
  var sameDepth = (a, b) => Math.abs(a - b) <= Math.max(DEPTH_FLOOR, Math.max(Math.abs(a), Math.abs(b)) * SAME_DEPTH);
1219
- var sameDirection = (a, b) => Math.abs(a.x - b.x) < TOLERANCE && Math.abs(a.y - b.y) < TOLERANCE && Math.abs(a.z - b.z) < TOLERANCE;
1313
+ var sameDirection2 = (a, b) => Math.abs(a.x - b.x) < TOLERANCE2 && Math.abs(a.y - b.y) < TOLERANCE2 && Math.abs(a.z - b.z) < TOLERANCE2;
1220
1314
  var sameHoleSets = (features, sheets) => {
1221
1315
  const sizes = features.map((feature) => holeSize(sheets, feature.tag));
1222
1316
  const root = features.map((_, at) => at);
@@ -1230,7 +1324,7 @@ var sameHoleSets = (features, sheets) => {
1230
1324
  for (let b = a + 1; b < features.length; b += 1) {
1231
1325
  const [first, second] = [sizes[a], sizes[b]];
1232
1326
  const [one, other] = [features[a], features[b]];
1233
- if (first && second && one && other && sameDirection(one.machiningDirection, other.machiningDirection) && first.diameter.toFixed(3) === second.diameter.toFixed(3) && sameDepth(first.depth, second.depth)) {
1327
+ if (first && second && one && other && sameDirection2(one.machiningDirection, other.machiningDirection) && first.diameter.toFixed(3) === second.diameter.toFixed(3) && sameDepth(first.depth, second.depth)) {
1234
1328
  const [low, high] = [find(a), find(b)].sort((x, y) => x - y);
1235
1329
  if (high !== void 0 && low !== void 0) root[high] = low;
1236
1330
  }
@@ -1269,77 +1363,17 @@ var featureRows = (tags, features, sheets) => {
1269
1363
  });
1270
1364
  };
1271
1365
 
1272
- // src/model/geometry.ts
1273
- var dfmFeatures = (features) => {
1274
- const read = features.map(
1275
- ({ featureTag, featureType, machiningDirection: { x, y, z }, regionIdxs }) => ({
1276
- tag: featureTag,
1277
- featureType,
1278
- machiningDirection: { x, y, z },
1279
- regionIdxs
1280
- })
1281
- );
1282
- const directions = [];
1283
- read.forEach(({ machiningDirection }) => {
1284
- if (!directions.some((each) => sameDirection2(each, machiningDirection)))
1285
- directions.push(machiningDirection);
1286
- });
1287
- const numbered = directions.flatMap(
1288
- (direction) => read.filter((feature) => sameDirection2(feature.machiningDirection, direction))
1289
- );
1290
- const numberOf = new Map(numbered.map((feature, index) => [feature, index + 1]));
1291
- return read.map((feature) => ({ ...feature, number: numberOf.get(feature) ?? 0 }));
1292
- };
1293
- var TOLERANCE2 = 1e-6;
1294
- var sameDirection2 = (a, b) => Math.abs(a.x - b.x) < TOLERANCE2 && Math.abs(a.y - b.y) < TOLERANCE2 && Math.abs(a.z - b.z) < TOLERANCE2;
1295
- var featureTypeLabel = (featureType) => {
1296
- const words = featureType.replace(/_/g, " ").trim();
1297
- return words.charAt(0).toUpperCase() + words.slice(1);
1298
- };
1299
- var partTop = (features, sheets, direction) => {
1300
- let top = null;
1301
- for (const other of features) {
1302
- const zMax = sheets[other.tag.toLowerCase()]?.extendedZMax;
1303
- if (zMax !== void 0 && sameDirection2(other.machiningDirection, direction)) {
1304
- top = top === null ? zMax : Math.max(top, zMax);
1305
- }
1306
- }
1307
- return top;
1308
- };
1309
- var FLAT_TIP_DEG = 0.5;
1310
- var isDrilled = (sheet) => sheet?.diameter !== void 0 && sheet.diameter > 0 && !(sheet.tipAngle !== void 0 && sheet.tipAngle >= 180 - FLAT_TIP_DEG);
1311
- var featureLd = (features, feature, sheets) => {
1312
- const sheet = sheets[feature.tag.toLowerCase()];
1313
- const top = sheet ? partTop(features, sheets, feature.machiningDirection) : null;
1314
- const bottom = sheet?.zMin;
1315
- if (!sheet || top === null || bottom === void 0) return null;
1316
- const drilling = isDrilled(sheet);
1317
- const [basis, across] = sheet.diameter !== void 0 ? ["bore", sheet.diameter] : sheet.pinchDiameter !== void 0 ? ["pinch", sheet.pinchDiameter] : [
1318
- "radius",
1319
- sheet.sharpCorners || sheet.cornerRadius === void 0 ? void 0 : 2 * sheet.cornerRadius
1320
- ];
1321
- if (across === void 0 || across <= 0) return null;
1322
- const atLeast = basis !== "bore" && sheet.cornerRadiusUnresolved === true;
1323
- return {
1324
- ratio: (top - bottom) / across,
1325
- ...sheet.zMax !== void 0 ? { featureRatio: (sheet.zMax - bottom) / across } : {},
1326
- drilling,
1327
- atLeast,
1328
- basis,
1329
- across
1330
- };
1331
- };
1332
-
1333
1366
  // src/model/dfm-explain.ts
1334
1367
  var figureless = (hit) => hit.metric.quantity === "flag" || hit.metric.quantity === "presence";
1335
1368
  var hitFigure = (hit, units) => figureless(hit) ? "" : formatMeasured(hit.metric.quantity, hit.value, units);
1336
1369
 
1337
1370
  // src/model/rule-set-file.ts
1338
1371
  var FILE_KIND = "toolpath-dfm-rules";
1339
- var serializeRuleSet = (rules, meta = {}) => JSON.stringify(
1372
+ var serializeRuleSet = (rules, meta = {}, vocabulary = MILLING_RULES) => JSON.stringify(
1340
1373
  {
1341
1374
  kind: FILE_KIND,
1342
1375
  version: 1,
1376
+ process: vocabulary.process,
1343
1377
  ...meta.name ? { name: meta.name } : {},
1344
1378
  ...meta.description ? { description: meta.description } : {},
1345
1379
  units: "mm",
@@ -1348,7 +1382,13 @@ var serializeRuleSet = (rules, meta = {}) => JSON.stringify(
1348
1382
  null,
1349
1383
  2
1350
1384
  );
1351
- var parseRuleSetFile = (text) => {
1385
+ var PROCESS_NAMES = { milling: "Milling", turning: "Turning" };
1386
+ var fileProcess = (raw) => {
1387
+ if (Array.isArray(raw) || typeof raw !== "object" || raw === null) return "milling";
1388
+ const { process } = raw;
1389
+ return process === void 0 || process === null ? "milling" : process;
1390
+ };
1391
+ var parseRuleSetFile = (text, vocabulary = MILLING_RULES) => {
1352
1392
  let raw;
1353
1393
  try {
1354
1394
  raw = JSON.parse(text);
@@ -1360,6 +1400,14 @@ var parseRuleSetFile = (text) => {
1360
1400
  const units = Array.isArray(raw) ? void 0 : raw.units;
1361
1401
  if (units !== void 0 && units !== "mm")
1362
1402
  return { error: `Its lengths are in ${JSON.stringify(units)}; only "mm" can be read.` };
1403
+ const process = fileProcess(raw);
1404
+ if (process !== vocabulary.process) {
1405
+ return process === "milling" || process === "turning" ? {
1406
+ error: `This is a ${process} rule set \u2014 import it in ${PROCESS_NAMES[process]} mode.`
1407
+ } : {
1408
+ error: `Its process is ${JSON.stringify(process)}; only "milling" or "turning" can be read.`
1409
+ };
1410
+ }
1363
1411
  const seen = /* @__PURE__ */ new Set();
1364
1412
  const withIds = list.map((each) => {
1365
1413
  if (typeof each !== "object" || each === null) return each;
@@ -1368,7 +1416,7 @@ var parseRuleSetFile = (text) => {
1368
1416
  seen.add(id);
1369
1417
  return { ...rule, id };
1370
1418
  });
1371
- const rules = parseRules(withIds) ?? [];
1419
+ const rules = parseRules(withIds, vocabulary) ?? [];
1372
1420
  if (rules.length === 0) return { error: "None of its rules could be read." };
1373
1421
  const name = typeof raw === "object" && raw !== null && typeof raw.name === "string" ? raw.name : void 0;
1374
1422
  return { rules, skipped: list.length - rules.length, ...name ? { name } : {} };
@@ -1382,17 +1430,36 @@ var OP_WORDS = {
1382
1430
  ne: "not equal (\u2260)",
1383
1431
  between: "in a range: `value` \u2264 x < `max` by default; `lowOp` (gte|gt) and `highOp` (lt|lte) change the ends"
1384
1432
  };
1385
- var llmRulePrompt = (current = []) => {
1386
- const groups = FEATURE_SUBJECTS.filter((each) => !each.value.startsWith(TYPE_SUBJECT));
1387
- const types = FEATURE_SUBJECTS.filter((each) => each.value.startsWith(TYPE_SUBJECT));
1388
- const metricLines = FEATURE_METRICS.map((metric) => {
1389
- const unit = quantityUnit(metric.quantity, "mm");
1390
- const kind = metric.quantity === "flag" ? 'yes/no \u2014 write `op: "gte"` and `value: 0`; the rule fires whenever it is so' : metric.quantity === "presence" ? 'no measurement \u2014 write `op: "gte"` and `value: 0`; flags every feature of the subject' : `${metric.quantity}${unit ? `, in ${unit}` : ""}; a new rule usually starts around ${metric.start.mm}`;
1391
- const kinds = metric.kinds ? ` Only features of kind ${metric.kinds.join("/")} report it.` : "";
1392
- return `- \`${metric.key}\` \u2014 "${metric.label}" (${kind}). ${metric.hint}.${kinds}`;
1393
- });
1394
- const example = serializeRuleSet(
1395
- [
1433
+ var SCRIPTS = {
1434
+ milling: {
1435
+ intro: "You are helping a machinist or designer set up design-for-manufacturing (DFM) checks for CNC-milled parts in the Toolpath CAD viewer. Interview them, then produce ONE JSON document in the format below.",
1436
+ opening: "Begin the interview in your very first reply \u2014 do not summarise this prompt, ask whether to start, or explain the format. Run it in four rounds, in a conversational tone, following up where an answer needs it: an opening round about the shop, then a milling round, then a drilling round, then a closing round. Ask only about things the metrics below can measure from the model geometry: depths, diameters, corner radii, tool sizes, areas, feature kinds. Do not ask about surface finish, tolerances, material hardness, fixturing, quantities or lead time \u2014 nothing here measures them.",
1437
+ rounds: [
1438
+ "### Round 1 \u2014 the shop",
1439
+ "1. What they make and in what materials. This sets the defaults, not a rule.",
1440
+ "2. Examples of features that have challenged them before: a deep pocket that snapped a tool, a hole that needed a special drill, a corner radius that forced a re-quote, an undercut nobody saw until CAM. Ask for the feature and its rough size, then turn each into one rule: a deep pocket is `pocket` with `featureLd` or `depthBelowTop`; a small hole is `hole` with `diameter`; a deep hole is `hole` with `ld`; a tight corner is `milled` with `cornerRadius` or `maxTool`. If no metric below measures the example, say so and move on; never invent a metric.",
1441
+ "",
1442
+ "### Round 2 \u2014 milling (pockets, slots, walls, bosses, profiles, 3D surfaces)",
1443
+ "3. The smallest end mill they are happy to run: `milled` with `maxTool` below it, and `milled` with `cornerRadius` below half of it.",
1444
+ "4. The depth-to-diameter ratio where milled work starts to cost them, and where it becomes a problem: two or three bands of `milled` with `ld` (warning, caution, critical).",
1445
+ "5. The deepest their end mills reach below the top of the part: `milled` with `depthBelowTop`. Whether they also want a feature's own depth capped: `milled` with `featureDepth`.",
1446
+ "6. Sharp inside corners: whether every one should be flagged (`milled` with `sharpCorner`), or only on certain feature kinds.",
1447
+ "7. Floor fillets: whether a floor fillet larger than their ball or bull-nose tooling should be flagged (`pocket` with `floorFillet` above a radius).",
1448
+ "8. Milled features to call out on sight regardless of size: `undercut` with `present`; `contour` with `present` for 3D surfaces; `any` with `tilted` for 5-axis work; `boss` with `present` for thin standing material.",
1449
+ "9. Large features that drive cycle time, if they quote: `pocket` with `projectedFloorArea` or `milled` with `surfaceArea` above a size.",
1450
+ "",
1451
+ "### Round 3 \u2014 drilling (holes, threads, countersinks)",
1452
+ "10. The smallest drill they stock: `hole` with `diameter` below it. Whether they want holes that have no drill fit, so must be milled, called out: `hole` with `maxDrill` at 0.",
1453
+ "11. The depth-to-diameter ratio where holes get hard, and where they need gun drilling or a second setup: two bands of `hole` with `ld`, usually looser than milled work.",
1454
+ "12. Threads: the smallest thread they tap (`hole` with `threadDiameter` below it), and whether every threaded hole should be marked (`threadedHole` with `present`).",
1455
+ "13. Countersinks and counterbores: whether to call out countersinks (`chamfer` with `countersink`) or counterbored holes (`hole` with `counterbore`).",
1456
+ "",
1457
+ "### Round 4 \u2014 closing",
1458
+ "14. Anything they want flagged across every feature, milled or drilled \u2014 for example one overall depth below the top of the part their longest tooling cannot reach: `any` with `depthBelowTop`.",
1459
+ "15. Which of the rules so far are critical (red), which are cautions (orange) and which are just worth a look (yellow)."
1460
+ ],
1461
+ counting: "A rule only fires on features that have the measurement; a feature that lacks it is not flagged. Only *required* features count \u2014 ones that own a face no other feature could machine.",
1462
+ example: [
1396
1463
  { id: "r1", subject: "hole", metric: "ld", op: "gte", value: 8, color: "#ffd60a" },
1397
1464
  {
1398
1465
  id: "r2",
@@ -1407,42 +1474,82 @@ var llmRulePrompt = (current = []) => {
1407
1474
  },
1408
1475
  { id: "r3", subject: "milled", metric: "sharpCorner", op: "gte", value: 0, color: "#e5484d" },
1409
1476
  { id: "r4", subject: "undercut", metric: "present", op: "gte", value: 0, color: "#8e4ec6" }
1477
+ ]
1478
+ },
1479
+ turning: {
1480
+ intro: "You are helping a machinist or designer set up design-for-manufacturing (DFM) checks for parts turned on a lathe in the Toolpath CAD viewer. Interview them, then produce ONE JSON document in the format below.",
1481
+ opening: "Begin the interview in your very first reply \u2014 do not summarise this prompt, ask whether to start, or explain the format. Run it in three rounds, in a conversational tone, following up where an answer needs it: an opening round about the shop, then a turning round, then a closing round. Ask only about things the metrics below can measure from the model geometry: diameters, lengths, grooves, threads, axial holes, inside corner radii, tolerance, feature kinds. Do not ask about surface finish, material hardness, workholding, quantities or lead time \u2014 nothing here measures them.",
1482
+ rounds: [
1483
+ "### Round 1 \u2014 the shop",
1484
+ "1. What they make, in what materials, and on what lathes: the largest bar or chuck, and whether they have live tooling. This sets the defaults, not a rule.",
1485
+ "2. Examples of turned features that have challenged them before: a narrow groove that broke an insert, a long small bore that chattered, a fine thread, a sharp inside corner no insert could make. Ask for the feature and its rough size, then turn each into one rule: a narrow groove is `groove` with `grooveWidth`; a deep bore is `id` with `ld`; a fine thread is `thread` with `threadPitch`; a small drilled hole is `hole` with `holeDiameter`. If no metric below measures the example, say so and move on; never invent a metric.",
1486
+ "",
1487
+ "### Round 2 \u2014 turning (outside, inside, faces, grooves, threads, axial holes)",
1488
+ "3. The largest diameter their lathes take: `any` with `maxDiameter` above it. The longest stretch they turn without support: `od` with `length`.",
1489
+ "4. Grooves: the narrowest grooving insert they stock (`groove` with `grooveWidth` below it), and how far they can plunge (`groove` with `grooveDepth`, or `grooveAspect` for depth over width).",
1490
+ "5. Boring: the depth-to-diameter ratio where a boring bar starts to chatter, and where it becomes a problem: two bands of `id` with `ld`. The narrowest bore their smallest bar passes: `id` with `entryDiameter` below it.",
1491
+ "6. Threads: the finest pitch they cut (`thread` with `threadPitch` below it), and the smallest thread (`thread` with `threadDiameter` below it).",
1492
+ "7. Axial holes: the smallest drill they stock (`hole` with `holeDiameter` below it), the deepest they drill (`hole` with `holeDepth`, or `hole` with `ld`), and whether to call out stepped holes (`hole` with `holeSteps`), flat bottoms (`blindHole` with `flatBottom`) or tapped holes (`threadedHole` with `present`).",
1493
+ "8. Inside corners: the smallest nose radius they run (`any` with `cornerRadius` below it), and whether every sharp inside corner should be flagged (`any` with `sharpCorner`).",
1494
+ "9. Turned features to call out on sight regardless of size: `face` with `axialTool` for work only an axially mounted tool reaches; `type:unknown` with `present` for a stretch the Engine could not name.",
1495
+ "",
1496
+ "### Round 3 \u2014 closing",
1497
+ "10. The tightest tolerance they hold without special care: `any` with `tolerance` below it.",
1498
+ "11. Which of the rules so far are critical (red), which are cautions (orange) and which are just worth a look (yellow)."
1410
1499
  ],
1411
- { name: "Example set", description: "Shows one rule of each shape" }
1500
+ counting: "A rule only fires on features that have the measurement; a feature that lacks it is not flagged. A stretch both ends of the part reach is one feature, and breaks a rule only if it breaks it from both ends.",
1501
+ example: [
1502
+ {
1503
+ id: "r1",
1504
+ subject: "groove",
1505
+ metric: "grooveWidth",
1506
+ op: "lte",
1507
+ value: 1.5,
1508
+ color: "#ffd60a"
1509
+ },
1510
+ {
1511
+ id: "r2",
1512
+ subject: "id",
1513
+ metric: "ld",
1514
+ op: "between",
1515
+ value: 4,
1516
+ max: 8,
1517
+ lowOp: "gte",
1518
+ highOp: "lt",
1519
+ color: "#f76b15"
1520
+ },
1521
+ { id: "r3", subject: "any", metric: "sharpCorner", op: "gte", value: 0, color: "#e5484d" },
1522
+ { id: "r4", subject: "thread", metric: "present", op: "gte", value: 0, color: "#8e4ec6" }
1523
+ ]
1524
+ }
1525
+ };
1526
+ var llmRulePrompt = (current = [], vocabulary = MILLING_RULES) => {
1527
+ const script = SCRIPTS[vocabulary.process];
1528
+ const groups = vocabulary.subjects.filter((each) => !each.value.startsWith(TYPE_SUBJECT));
1529
+ const types = vocabulary.subjects.filter((each) => each.value.startsWith(TYPE_SUBJECT));
1530
+ const metricLines = vocabulary.metrics.map((metric) => {
1531
+ const unit = quantityUnit(metric.quantity, "mm");
1532
+ const kind = metric.quantity === "flag" ? 'yes/no \u2014 write `op: "gte"` and `value: 0`; the rule fires whenever it is so' : metric.quantity === "presence" ? 'no measurement \u2014 write `op: "gte"` and `value: 0`; flags every feature of the subject' : `${metric.quantity}${unit ? `, in ${unit}` : ""}; a new rule usually starts around ${metric.start.mm}`;
1533
+ const kinds = metric.kinds ? ` Only features of kind ${metric.kinds.join("/")} report it.` : "";
1534
+ return `- \`${metric.key}\` \u2014 "${metric.label}" (${kind}). ${metric.hint}.${kinds}`;
1535
+ });
1536
+ const example = serializeRuleSet(
1537
+ script.example,
1538
+ { name: "Example set", description: "Shows one rule of each shape" },
1539
+ vocabulary
1412
1540
  );
1413
1541
  return [
1414
1542
  "# Write a Toolpath DFM rule set",
1415
1543
  "",
1416
- "You are helping a machinist or designer set up design-for-manufacturing (DFM) checks for CNC-milled parts in the Toolpath CAD viewer. Interview them, then produce ONE JSON document in the format below.",
1544
+ script.intro,
1417
1545
  "",
1418
- "Begin the interview in your very first reply \u2014 do not summarise this prompt, ask whether to start, or explain the format. Run it in four rounds, in a conversational tone, following up where an answer needs it: an opening round about the shop, then a milling round, then a drilling round, then a closing round. Ask only about things the metrics below can measure from the model geometry: depths, diameters, corner radii, tool sizes, areas, feature kinds. Do not ask about surface finish, tolerances, material hardness, fixturing, quantities or lead time \u2014 nothing here measures them.",
1546
+ script.opening,
1419
1547
  "",
1420
1548
  'Where the user has current rules (listed at the end), do not ask whether to keep them. Weave each one into the relevant question as a suggested starting point \u2014 "your set currently warns at 4\xD7D; where does deep work start to cost you?" \u2014 so the user accepts, adjusts or drops it in their own words. The finished set should feel like theirs, not a copy of the old one.',
1421
1549
  "",
1422
1550
  "Each question is tied to the subject and metric it becomes. Skip one when an earlier answer has already settled it, and offer sensible defaults from their answers rather than asking about every metric. Every rule needs an `op` and a `value`, even a yes/no one.",
1423
1551
  "",
1424
- "### Round 1 \u2014 the shop",
1425
- "1. What they make and in what materials. This sets the defaults, not a rule.",
1426
- "2. Examples of features that have challenged them before: a deep pocket that snapped a tool, a hole that needed a special drill, a corner radius that forced a re-quote, an undercut nobody saw until CAM. Ask for the feature and its rough size, then turn each into one rule: a deep pocket is `pocket` with `featureLd` or `depthBelowTop`; a small hole is `hole` with `diameter`; a deep hole is `hole` with `ld`; a tight corner is `milled` with `cornerRadius` or `maxTool`. If no metric below measures the example, say so and move on; never invent a metric.",
1427
- "",
1428
- "### Round 2 \u2014 milling (pockets, slots, walls, bosses, profiles, 3D surfaces)",
1429
- "3. The smallest end mill they are happy to run: `milled` with `maxTool` below it, and `milled` with `cornerRadius` below half of it.",
1430
- "4. The depth-to-diameter ratio where milled work starts to cost them, and where it becomes a problem: two or three bands of `milled` with `ld` (warning, caution, critical).",
1431
- "5. The deepest their end mills reach below the top of the part: `milled` with `depthBelowTop`. Whether they also want a feature's own depth capped: `milled` with `featureDepth`.",
1432
- "6. Sharp inside corners: whether every one should be flagged (`milled` with `sharpCorner`), or only on certain feature kinds.",
1433
- "7. Floor fillets: whether a floor fillet larger than their ball or bull-nose tooling should be flagged (`pocket` with `floorFillet` above a radius).",
1434
- "8. Milled features to call out on sight regardless of size: `undercut` with `present`; `contour` with `present` for 3D surfaces; `any` with `tilted` for 5-axis work; `boss` with `present` for thin standing material.",
1435
- "9. Large features that drive cycle time, if they quote: `pocket` with `projectedFloorArea` or `milled` with `surfaceArea` above a size.",
1436
- "",
1437
- "### Round 3 \u2014 drilling (holes, threads, countersinks)",
1438
- "10. The smallest drill they stock: `hole` with `diameter` below it. Whether they want holes that have no drill fit, so must be milled, called out: `hole` with `maxDrill` at 0.",
1439
- "11. The depth-to-diameter ratio where holes get hard, and where they need gun drilling or a second setup: two bands of `hole` with `ld`, usually looser than milled work.",
1440
- "12. Threads: the smallest thread they tap (`hole` with `threadDiameter` below it), and whether every threaded hole should be marked (`threadedHole` with `present`).",
1441
- "13. Countersinks and counterbores: whether to call out countersinks (`chamfer` with `countersink`) or counterbored holes (`hole` with `counterbore`).",
1442
- "",
1443
- "### Round 4 \u2014 closing",
1444
- "14. Anything they want flagged across every feature, milled or drilled \u2014 for example one overall depth below the top of the part their longest tooling cannot reach: `any` with `depthBelowTop`.",
1445
- "15. Which of the rules so far are critical (red), which are cautions (orange) and which are just worth a look (yellow).",
1552
+ ...script.rounds,
1446
1553
  "",
1447
1554
  `The user may stop at any point \u2014 "that's enough", "just give me the rules", "generate it" \u2014 and you then finish from what you have: rules for every answer so far, their current rules carried over for anything not yet discussed, and sensible defaults from the shop description for the rest. Never insist on completing a round first, and never leave a placeholder for an unanswered question.`,
1448
1555
  "",
@@ -1450,7 +1557,7 @@ var llmRulePrompt = (current = []) => {
1450
1557
  "",
1451
1558
  "## Format",
1452
1559
  "",
1453
- 'Top level: `{"kind":"toolpath-dfm-rules","version":1,"name":"\u2026","description":"\u2026","units":"mm","rules":[\u2026]}`.',
1560
+ `Top level: \`{"kind":"toolpath-dfm-rules","version":1,"process":"${vocabulary.process}","name":"\u2026","description":"\u2026","units":"mm","rules":[\u2026]}\`.`,
1454
1561
  "Every length is in millimetres, areas in mm\xB2, angles in degrees, ratios plain. Convert from inches yourself (1 in = 25.4 mm).",
1455
1562
  "",
1456
1563
  "Each rule is one sentence \u2014 *{subject} with {metric} {op} {value}* \u2014 with the fields:",
@@ -1462,7 +1569,7 @@ var llmRulePrompt = (current = []) => {
1462
1569
  "- `value`: the threshold (canonical units). For `between`, the low end; add `max` for the high end.",
1463
1570
  "- `color`: `#rrggbb`, from the palette: " + RULE_COLORS.map((each) => `${each.name} \`${each.value}\``).join(", ") + ". Red for critical, orange for caution, yellow for warning; purple, green and cyan for anything else. Where a feature breaks several rules the part shows the earliest palette colour.",
1464
1571
  "",
1465
- "A rule only fires on features that have the measurement; a feature that lacks it is not flagged. Only *required* features count \u2014 ones that own a face no other feature could machine.",
1572
+ script.counting,
1466
1573
  "",
1467
1574
  "## Subjects (`subject`)",
1468
1575
  "",
@@ -1488,13 +1595,309 @@ var llmRulePrompt = (current = []) => {
1488
1595
  "Use these as the suggested starting points in your questions, as described above. Do not ask whether to keep them; do not copy them unchanged unless the user confirms each threshold:",
1489
1596
  "",
1490
1597
  "```json",
1491
- serializeRuleSet(current),
1598
+ serializeRuleSet(current, {}, vocabulary),
1492
1599
  "```"
1493
1600
  ] : [],
1494
1601
  ""
1495
1602
  ].join("\n");
1496
1603
  };
1497
1604
 
1605
+ // src/model/turning-metrics.ts
1606
+ var OD_KINDS = ["OdProfile", "OdGroove", "OdThread"];
1607
+ var ID_KINDS = ["IdProfile", "IdGroove", "IdThread"];
1608
+ var FACE_KINDS = ["Face", "FaceProfile", "FaceGroove"];
1609
+ var GROOVE_KINDS = ["OdGroove", "IdGroove", "FaceGroove"];
1610
+ var THREAD_KINDS = ["OdThread", "IdThread", "AxialHole"];
1611
+ var PROFILE_KINDS = ["OdProfile", "IdProfile", "FaceProfile"];
1612
+ var HOLE_KINDS2 = ["AxialHole"];
1613
+ var SIZE2 = "Size";
1614
+ var GROOVES = "Grooves";
1615
+ var THREADS = "Threads";
1616
+ var HOLES = "Holes";
1617
+ var TOOLS2 = "Tool access";
1618
+ var TOLERANCE3 = "Tolerance";
1619
+ var TURNING_METRICS = [
1620
+ PRESENCE_METRIC,
1621
+ define({
1622
+ key: "maxDiameter",
1623
+ label: "diameter",
1624
+ group: SIZE2,
1625
+ quantity: "length",
1626
+ op: "gte",
1627
+ start: { mm: 100, inch: 101.6 },
1628
+ hint: "Its largest diameter: what the lathe must swing, or the bar it is cut from"
1629
+ }),
1630
+ define({
1631
+ key: "minDiameter",
1632
+ label: "smallest diameter",
1633
+ group: SIZE2,
1634
+ quantity: "length",
1635
+ op: "lte",
1636
+ start: { mm: 6, inch: 6.35 },
1637
+ kinds: ["OdProfile", "IdProfile", "OdGroove", "IdGroove", "OdThread", "IdThread", "AxialHole"],
1638
+ hint: "Its smallest diameter: how thin the outside gets, or how narrow a bore. Not for faces"
1639
+ }),
1640
+ define({
1641
+ key: "length",
1642
+ label: "length along the axis",
1643
+ group: SIZE2,
1644
+ quantity: "length",
1645
+ op: "gte",
1646
+ start: { mm: 50, inch: 50.8 },
1647
+ kinds: [
1648
+ "AxialHole",
1649
+ "FaceGroove",
1650
+ "FaceProfile",
1651
+ "IdGroove",
1652
+ "IdProfile",
1653
+ "IdThread",
1654
+ "OdGroove",
1655
+ "OdProfile",
1656
+ "OdThread",
1657
+ "Unknown"
1658
+ ],
1659
+ hint: "How far it runs along the spindle axis. None for a flat face"
1660
+ }),
1661
+ define({
1662
+ key: "ld",
1663
+ label: "L/D from the end",
1664
+ group: SIZE2,
1665
+ quantity: "ratio",
1666
+ op: "gte",
1667
+ start: 4,
1668
+ kinds: ["AxialHole", "IdProfile", "IdGroove", "IdThread"],
1669
+ hint: "How far in a boring bar or drill reaches over the bore it goes through: a hole\u2019s depth over its narrowest step; inside work, from the end it is cut from to its far end, over its entry diameter. Cut from both ends, the easier end"
1670
+ }),
1671
+ define({
1672
+ key: "grooveWidth",
1673
+ label: "groove width",
1674
+ group: GROOVES,
1675
+ quantity: "length",
1676
+ op: "lte",
1677
+ start: { mm: 1.5, inch: 1.5875 },
1678
+ kinds: GROOVE_KINDS,
1679
+ hint: "How wide the groove is: the narrowest insert that cuts it in one plunge"
1680
+ }),
1681
+ define({
1682
+ key: "grooveDepth",
1683
+ label: "groove depth",
1684
+ group: GROOVES,
1685
+ quantity: "length",
1686
+ op: "gte",
1687
+ start: { mm: 5, inch: 5.08 },
1688
+ kinds: GROOVE_KINDS,
1689
+ hint: "How far the groove goes in: radially on the outside or in a bore, along the axis in a face"
1690
+ }),
1691
+ define({
1692
+ key: "grooveAspect",
1693
+ label: "groove depth/width",
1694
+ group: GROOVES,
1695
+ quantity: "ratio",
1696
+ op: "gte",
1697
+ start: 3,
1698
+ kinds: GROOVE_KINDS,
1699
+ hint: "Its depth over its width: how far a grooving insert reaches past its own width"
1700
+ }),
1701
+ define({
1702
+ key: "threadPitch",
1703
+ label: "thread pitch",
1704
+ group: THREADS,
1705
+ quantity: "length",
1706
+ op: "lte",
1707
+ start: { mm: 0.5, inch: 0.4233 },
1708
+ kinds: THREAD_KINDS,
1709
+ hint: "The distance between thread crests (0.423 mm is 60 TPI)"
1710
+ }),
1711
+ define({
1712
+ key: "threadDiameter",
1713
+ label: "thread diameter",
1714
+ group: THREADS,
1715
+ quantity: "length",
1716
+ op: "lte",
1717
+ start: { mm: 6, inch: 6.35 },
1718
+ kinds: THREAD_KINDS,
1719
+ hint: "The catalog thread\u2019s nominal major diameter. None where the catalog lists no thread for it"
1720
+ }),
1721
+ define({
1722
+ key: "threadDepth",
1723
+ label: "thread depth",
1724
+ group: THREADS,
1725
+ quantity: "length",
1726
+ op: "gte",
1727
+ start: { mm: 1, inch: 1.27 },
1728
+ kinds: ["OdThread", "IdThread"],
1729
+ hint: "How deep the thread form is cut"
1730
+ }),
1731
+ define({
1732
+ key: "holeDiameter",
1733
+ label: "hole diameter",
1734
+ group: HOLES,
1735
+ quantity: "length",
1736
+ op: "lte",
1737
+ start: { mm: 3, inch: 3.175 },
1738
+ kinds: HOLE_KINDS2,
1739
+ hint: "Its narrowest step: the drill it is drilled with"
1740
+ }),
1741
+ define({
1742
+ key: "holeDepth",
1743
+ label: "hole depth",
1744
+ group: HOLES,
1745
+ quantity: "length",
1746
+ op: "gte",
1747
+ start: { mm: 50, inch: 50.8 },
1748
+ kinds: HOLE_KINDS2,
1749
+ hint: "How far it runs along the axis"
1750
+ }),
1751
+ define({
1752
+ key: "holeSteps",
1753
+ label: "number of steps",
1754
+ group: HOLES,
1755
+ quantity: "count",
1756
+ op: "gte",
1757
+ start: 2,
1758
+ kinds: HOLE_KINDS2,
1759
+ hint: "How many diameters it steps through, from the mouth inward"
1760
+ }),
1761
+ define({
1762
+ key: "flatBottom",
1763
+ label: "a flat bottom",
1764
+ group: HOLES,
1765
+ quantity: "flag",
1766
+ op: "gte",
1767
+ start: 0,
1768
+ kinds: HOLE_KINDS2,
1769
+ hint: "A blind hole that ends flat rather than in a drill point"
1770
+ }),
1771
+ define({
1772
+ key: "cornerRadius",
1773
+ label: "inside corner radius",
1774
+ group: TOOLS2,
1775
+ quantity: "length",
1776
+ op: "lte",
1777
+ start: { mm: 0.4, inch: 0.3969 },
1778
+ hint: "The radius of its tightest concave corner, which an insert\u2019s nose radius must fit \u2014 zero when sharp. None where it has no inside corner"
1779
+ }),
1780
+ define({
1781
+ key: "sharpCorner",
1782
+ label: "a sharp inside corner",
1783
+ group: TOOLS2,
1784
+ quantity: "flag",
1785
+ op: "gte",
1786
+ start: 0,
1787
+ hint: "A concave corner with no radius, which no insert makes"
1788
+ }),
1789
+ define({
1790
+ key: "entryDiameter",
1791
+ label: "entry diameter",
1792
+ group: TOOLS2,
1793
+ quantity: "length",
1794
+ op: "lte",
1795
+ start: { mm: 10, inch: 9.525 },
1796
+ hint: "For a feature in a hole, the narrowest diameter a bar passes on its way in from the end it is cut from. None where nothing stands between"
1797
+ }),
1798
+ define({
1799
+ key: "axialTool",
1800
+ label: "an axially mounted tool",
1801
+ group: TOOLS2,
1802
+ quantity: "flag",
1803
+ op: "gte",
1804
+ start: 0,
1805
+ hint: "Only a tool mounted along the spindle axis reaches it: a drill, or a face-grooving tool"
1806
+ }),
1807
+ define({
1808
+ key: "tolerance",
1809
+ label: "tolerance",
1810
+ group: TOLERANCE3,
1811
+ quantity: "length",
1812
+ op: "lte",
1813
+ start: { mm: 0.01, inch: 0.0127 },
1814
+ hint: "The most it may deviate from the model"
1815
+ })
1816
+ ];
1817
+ var GROUP_SUBJECTS2 = [
1818
+ { value: "any", label: "Any turned feature", group: "Groups" },
1819
+ { value: "od", label: "Outside (OD) features", group: "Groups", kinds: OD_KINDS },
1820
+ { value: "id", label: "Inside (ID) features", group: "Groups", kinds: ID_KINDS },
1821
+ { value: "face", label: "End-face features", group: "Groups", kinds: FACE_KINDS },
1822
+ { value: "groove", label: "Grooves", group: "Groups", kinds: GROOVE_KINDS },
1823
+ { value: "thread", label: "Threads", group: "Groups", kinds: THREAD_KINDS },
1824
+ { value: "hole", label: "Axial holes", group: "Groups", kinds: HOLE_KINDS2 },
1825
+ { value: "throughHole", label: "Through holes", group: "Groups", kinds: HOLE_KINDS2 },
1826
+ { value: "blindHole", label: "Blind holes", group: "Groups", kinds: HOLE_KINDS2 },
1827
+ { value: "threadedHole", label: "Threaded holes", group: "Groups", kinds: HOLE_KINDS2 },
1828
+ { value: "profile", label: "Profiles", group: "Groups", kinds: PROFILE_KINDS }
1829
+ ];
1830
+ var TYPES2 = [
1831
+ ["od_profile", "OD profiles", "OdProfile"],
1832
+ ["od_groove", "OD grooves", "OdGroove"],
1833
+ ["od_thread", "OD threads", "OdThread"],
1834
+ ["face", "Faces", "Face"],
1835
+ ["face_profile", "Face profiles", "FaceProfile"],
1836
+ ["face_groove", "Face grooves", "FaceGroove"],
1837
+ ["axial_hole", "Axial hole features", "AxialHole"],
1838
+ ["id_profile", "ID profiles", "IdProfile"],
1839
+ ["id_groove", "ID grooves", "IdGroove"],
1840
+ ["id_thread", "ID threads", "IdThread"],
1841
+ ["unknown", "Unknown features", "Unknown"]
1842
+ ];
1843
+ var TURNING_SUBJECTS = [
1844
+ ...GROUP_SUBJECTS2,
1845
+ ...TYPES2.map(([type, label, kind]) => ({
1846
+ value: `${TYPE_SUBJECT}${type}`,
1847
+ label,
1848
+ group: "Feature types",
1849
+ kinds: [kind]
1850
+ }))
1851
+ ];
1852
+ var QUICK_TURNING_SUBJECTS = [
1853
+ "any",
1854
+ "od",
1855
+ "id",
1856
+ "face",
1857
+ "groove",
1858
+ "thread",
1859
+ "hole"
1860
+ ];
1861
+ var QUICK_TURNING_METRICS = [
1862
+ "present",
1863
+ "maxDiameter",
1864
+ "length",
1865
+ "ld",
1866
+ "grooveWidth",
1867
+ "grooveDepth",
1868
+ "threadPitch",
1869
+ "holeDiameter",
1870
+ "holeDepth",
1871
+ "cornerRadius",
1872
+ "sharpCorner",
1873
+ "entryDiameter"
1874
+ ];
1875
+ var CAPITALS = { od: "OD", id: "ID" };
1876
+ var turnedTypeLabel = (featureType) => {
1877
+ const words = featureType.split("_").filter((word) => word !== "").map((word) => CAPITALS[word] ?? word).join(" ");
1878
+ return words.charAt(0).toUpperCase() + words.slice(1);
1879
+ };
1880
+ var endLabel = (feature) => {
1881
+ const approaches = "approaches" in feature && Array.isArray(feature.approaches) ? feature.approaches : feature.machiningDirection.z > 0 ? ["PlusZ"] : feature.machiningDirection.z < 0 ? ["MinusZ"] : [];
1882
+ const plus = approaches.includes("PlusZ");
1883
+ const minus = approaches.includes("MinusZ");
1884
+ return plus && minus ? "from both ends" : plus ? "from the +Z end" : minus ? "from the \u2212Z end" : "";
1885
+ };
1886
+ var TURNING_RULES = {
1887
+ process: "turning",
1888
+ metrics: TURNING_METRICS,
1889
+ subjects: TURNING_SUBJECTS,
1890
+ quickSubjects: QUICK_TURNING_SUBJECTS,
1891
+ quickMetrics: QUICK_TURNING_METRICS,
1892
+ fallbackMetric: PRESENCE_METRIC,
1893
+ featureLabel: (feature) => ({
1894
+ name: turnedTypeLabel(feature.featureType),
1895
+ detail: endLabel(feature)
1896
+ }),
1897
+ considerationsTitle: "Turning considerations",
1898
+ fileName: "dfm-turning-rules.json"
1899
+ };
1900
+
1498
1901
  // src/model/reach.ts
1499
1902
  var numbers = (value) => Array.isArray(value) && value.every((each) => typeof each === "number" && Number.isFinite(each)) ? value : null;
1500
1903
  var readReachCurve = (datasheet) => {
@@ -1545,11 +1948,16 @@ var featureProfile = (sheet, featureType) => {
1545
1948
  };
1546
1949
 
1547
1950
  export {
1951
+ directionLabel,
1952
+ dfmFeatures,
1953
+ featureTypeLabel,
1954
+ partTop,
1955
+ isDrilled,
1956
+ featureLd,
1548
1957
  PRESENCE_METRIC,
1549
1958
  TYPE_SUBJECT,
1550
- FEATURE_SUBJECTS,
1551
- QUICK_SUBJECTS,
1552
- QUICK_METRICS,
1959
+ MILLING_RULES,
1960
+ subjectInfo,
1553
1961
  metricInfo,
1554
1962
  metricsForSubject,
1555
1963
  LENGTH_PLACES,
@@ -1581,16 +1989,14 @@ export {
1581
1989
  parseRules,
1582
1990
  sameDepth,
1583
1991
  featureRows,
1584
- dfmFeatures,
1585
- featureTypeLabel,
1586
- partTop,
1587
- isDrilled,
1588
- featureLd,
1589
1992
  hitFigure,
1590
1993
  FILE_KIND,
1591
1994
  serializeRuleSet,
1592
1995
  parseRuleSetFile,
1593
1996
  llmRulePrompt,
1997
+ ID_KINDS,
1998
+ turnedTypeLabel,
1999
+ TURNING_RULES,
1594
2000
  readReachCurve,
1595
2001
  runStart,
1596
2002
  peakHeight,