@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,8 +1,11 @@
1
1
  import {
2
2
  CRITICAL_COLOR,
3
3
  FILE_KIND,
4
+ ID_KINDS,
4
5
  LENGTH_PLACES,
6
+ MILLING_RULES,
5
7
  RULE_COLORS,
8
+ TURNING_RULES,
6
9
  TYPE_SUBJECT,
7
10
  WARNING_COLOR,
8
11
  breaks,
@@ -26,10 +29,12 @@ import {
26
29
  runStart,
27
30
  sameDepth,
28
31
  serializeRuleSet,
32
+ subjectInfo,
29
33
  trimmedCurve,
34
+ turnedTypeLabel,
30
35
  unitLabel,
31
36
  worseUpward
32
- } from "../chunk-5MPHT7BC.js";
37
+ } from "../chunk-4IRNLDE6.js";
33
38
 
34
39
  // src/model/broken-rules.ts
35
40
  var byColor = (hits) => [...hits].sort((a, b) => compareColors(a.rule.color, b.rule.color));
@@ -41,12 +46,12 @@ var worstHits = (hits) => {
41
46
  return true;
42
47
  });
43
48
  };
44
- var brokenRules = (check, tag, units) => worstHits(check.hitsByTag.get(tag.toLowerCase()) ?? []).map((hit) => {
49
+ var brokenRules = (check, tag, units, vocabulary = MILLING_RULES) => worstHits(check.hitsByTag.get(tag.toLowerCase()) ?? []).map((hit) => {
45
50
  const figure = hitFigure(hit, units);
46
51
  return {
47
52
  key: hit.rule.id,
48
53
  color: hit.rule.color,
49
- text: describeRule(hit.rule, units),
54
+ text: describeRule(hit.rule, units, vocabulary),
50
55
  ...figure ? { figure } : {}
51
56
  };
52
57
  });
@@ -106,9 +111,9 @@ var featureIs = (subject, featureType, sheet) => {
106
111
  return false;
107
112
  }
108
113
  };
109
- var at = (record, path) => path.split(".").reduce(
114
+ var at = (record2, path) => path.split(".").reduce(
110
115
  (value, key) => typeof value === "object" && value !== null ? value[key] : void 0,
111
- record
116
+ record2
112
117
  );
113
118
  var readPath = (datasheet, metric) => {
114
119
  for (const path of metric.path ?? []) {
@@ -217,6 +222,22 @@ var requiredTags = (features) => {
217
222
  features.filter((feature) => feature.regionIdxs.some((region) => owners.get(region) === 1)).map((feature) => feature.tag.toLowerCase())
218
223
  );
219
224
  };
225
+ var worstColor = (hits) => hits?.map((hit) => hit.rule.color).sort(compareColors)[0];
226
+ var ruleResults = (rules, features, hitsByTag, required) => rules.map((rule) => {
227
+ const hits = features.flatMap((feature) => {
228
+ const tag = feature.tag.toLowerCase();
229
+ const hit = hitsByTag.get(tag)?.find((each) => each.rule.id === rule.id);
230
+ return hit ? [{ tag: feature.tag, value: hit.value, metric: hit.metric, required: required.has(tag) }] : [];
231
+ });
232
+ const [first] = hits;
233
+ const up = first ? worseUpward(rule, first.metric) : true;
234
+ hits.sort((a, b) => up ? b.value - a.value : a.value - b.value);
235
+ return {
236
+ rule,
237
+ tags: hits.filter((hit) => hit.required).map((hit) => hit.tag),
238
+ otherTags: hits.filter((hit) => !hit.required).map((hit) => hit.tag)
239
+ };
240
+ });
220
241
  var QUALIFIERS = ["undercut", "filleted", "tslot", "dovetail"];
221
242
  var EDGE_TYPES = ["fillet", "chamfer", "sink"];
222
243
  var featureComplexity = (featureType) => {
@@ -226,7 +247,6 @@ var featureComplexity = (featureType) => {
226
247
  };
227
248
  var CONTOUR = "contour_surface";
228
249
  var unavoidableFaces = (features, hitsByTag, regionAreas) => {
229
- const worstColor = (feature) => hitsByTag.get(feature.tag.toLowerCase())?.map((hit) => hit.rule.color).sort(compareColors)[0];
230
250
  const areaOf = (feature) => [...new Set(feature.regionIdxs)].reduce(
231
251
  (sum, region) => sum + (regionAreas.get(region) ?? 0),
232
252
  0
@@ -242,7 +262,7 @@ var unavoidableFaces = (features, hitsByTag, regionAreas) => {
242
262
  for (const [region, candidates] of owners) {
243
263
  const colored = candidates.map((feature) => ({
244
264
  feature,
245
- color: worstColor(feature),
265
+ color: worstColor(hitsByTag.get(feature.tag.toLowerCase())),
246
266
  area: areaOf(feature)
247
267
  }));
248
268
  const isContour = ({ feature }) => feature.featureType === CONTOUR;
@@ -269,10 +289,10 @@ var unavoidableFaces = (features, hitsByTag, regionAreas) => {
269
289
  };
270
290
  var withSurroundedFaces = (check, features, adjacency) => {
271
291
  const paintedFor = new Map(check.paintedFaces);
272
- const regionsOf = /* @__PURE__ */ new Map();
292
+ const regionsOf2 = /* @__PURE__ */ new Map();
273
293
  for (const feature of features) {
274
294
  const key = feature.tag.toLowerCase();
275
- regionsOf.set(key, new Set(feature.regionIdxs));
295
+ regionsOf2.set(key, new Set(feature.regionIdxs));
276
296
  const whole = check.paintedWhole.get(key);
277
297
  if (whole) {
278
298
  for (const region of feature.regionIdxs)
@@ -290,7 +310,7 @@ var withSurroundedFaces = (check, features, adjacency) => {
290
310
  counts.set(key, { paint: paint2, count: (counts.get(key)?.count ?? 0) + 1 });
291
311
  }
292
312
  const most = Math.max(0, ...[...counts.values()].map(({ count }) => count));
293
- const [paint] = [...counts].filter(([key, { count }]) => count === most && regionsOf.get(key)?.has(region)).map(([, { paint: paint2 }]) => paint2).sort((a, b) => compareColors(a.color, b.color));
313
+ const [paint] = [...counts].filter(([key, { count }]) => count === most && regionsOf2.get(key)?.has(region)).map(([, { paint: paint2 }]) => paint2).sort((a, b) => compareColors(a.color, b.color));
294
314
  if (paint) surrounded.set(region, paint);
295
315
  }
296
316
  if (surrounded.size === 0) return check;
@@ -304,25 +324,10 @@ var checkPart = (features, sources, rules) => {
304
324
  featureHits(features, feature, sources, rules)
305
325
  ])
306
326
  );
307
- const byRule = rules.map((rule) => {
308
- const hits = features.flatMap((feature) => {
309
- const tag = feature.tag.toLowerCase();
310
- const hit = hitsByTag.get(tag)?.find((each) => each.rule.id === rule.id);
311
- return hit ? [{ tag: feature.tag, value: hit.value, metric: hit.metric, required: required.has(tag) }] : [];
312
- });
313
- const [first] = hits;
314
- const up = first ? worseUpward(rule, first.metric) : true;
315
- hits.sort((a, b) => up ? b.value - a.value : a.value - b.value);
316
- return {
317
- rule,
318
- tags: hits.filter((hit) => hit.required).map((hit) => hit.tag),
319
- otherTags: hits.filter((hit) => !hit.required).map((hit) => hit.tag)
320
- };
321
- });
322
327
  return {
323
328
  hitsByTag,
324
329
  required,
325
- byRule,
330
+ byRule: ruleResults(rules, features, hitsByTag, required),
326
331
  ...unavoidableFaces(features, hitsByTag, sources.regionAreas)
327
332
  };
328
333
  };
@@ -715,8 +720,8 @@ var featureMeasurements = ({
715
720
  if (ld && top !== null && bottom !== void 0) {
716
721
  const across = ld.basis === "bore" ? `D = facts.diameter = ${mm(ld.across)}` : ld.basis === "pinch" ? `D = widest tool (the tightest pinch disc, tolerance taken out) = ${mm(ld.across)}${ld.atLeast ? " (at most, so L/D is at least)" : ""}` : `D = widest tool = 2 \xD7 inside corner radius = ${mm(ld.across)}${ld.atLeast ? " (at most, so L/D is at least)" : ""} (no pinch points reported)`;
717
722
  const ratioOf = (depth) => {
718
- const ratio = (depth / ld.across).toFixed(3);
719
- return ld.atLeast ? `\u2265 ${ratio}` : ratio;
723
+ const ratio2 = (depth / ld.across).toFixed(3);
724
+ return ld.atLeast ? `\u2265 ${ratio2}` : ratio2;
720
725
  };
721
726
  if (ld.drilling) {
722
727
  rows.push({
@@ -948,16 +953,16 @@ var undercutRows = (sheet, undercut, top, length) => {
948
953
  );
949
954
  if (top !== null && zMin !== void 0) {
950
955
  const reach = top - zMin;
951
- const ratio = (reach / head).toFixed(3);
956
+ const ratio2 = (reach / head).toFixed(3);
952
957
  rows.push({
953
958
  key: "topLd",
954
959
  label: "L/D to top of part",
955
960
  milling: true,
956
- value: ratio,
961
+ value: ratio2,
957
962
  derivation: [
958
963
  `L = depth below top of part = ${mm(reach)}`,
959
964
  `D = max cutting diameter = ${mm(head)}`,
960
- `L / D = ${mm(reach)} / ${mm(head)} = ${ratio}`
965
+ `L / D = ${mm(reach)} / ${mm(head)} = ${ratio2}`
961
966
  ]
962
967
  });
963
968
  }
@@ -1208,14 +1213,217 @@ var placePinchTool = (mark, triangles, direction, origin = { x: 0, y: 0, z: 0 })
1208
1213
  across: asVec(across)
1209
1214
  };
1210
1215
  };
1216
+
1217
+ // src/model/turned-check.ts
1218
+ var regionsOf = (feature) => Array.isArray(feature.regionIdxs) ? feature.regionIdxs : [];
1219
+ var stretchKey = (feature) => {
1220
+ const regions = regionsOf(feature);
1221
+ return regions.length === 0 ? `tag:${feature.featureTag}` : `${feature.featureType}:${[...regions].sort((a, b) => a - b).join(",")}`;
1222
+ };
1223
+ var isApproach = (value) => value === "PlusZ" || value === "MinusZ";
1224
+ var turnedStretches = (features) => {
1225
+ const groups = /* @__PURE__ */ new Map();
1226
+ for (const feature of features) {
1227
+ const key = stretchKey(feature);
1228
+ groups.set(key, [...groups.get(key) ?? [], feature]);
1229
+ }
1230
+ const merged = [...groups.values()].flatMap((copies) => {
1231
+ const [first] = copies;
1232
+ if (!first) return [];
1233
+ const approaches = [...new Set(copies.map((each) => each.approach).filter(isApproach))];
1234
+ const only = approaches.length === 1 ? approaches[0] : void 0;
1235
+ const z = only === "PlusZ" ? 1 : only === "MinusZ" ? -1 : 0;
1236
+ return [{ first, copies, approaches, machiningDirection: { x: 0, y: 0, z } }];
1237
+ });
1238
+ const numbered = dfmFeatures(
1239
+ merged.map(({ first, machiningDirection }) => ({
1240
+ featureTag: first.featureTag,
1241
+ featureType: first.featureType,
1242
+ machiningDirection,
1243
+ regionIdxs: regionsOf(first)
1244
+ }))
1245
+ );
1246
+ return merged.flatMap(({ copies, approaches }, at2) => {
1247
+ const feature = numbered[at2];
1248
+ return feature ? [{ ...feature, copies, approaches }] : [];
1249
+ });
1250
+ };
1251
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1252
+ var finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1253
+ var ratio = (top, bottom) => top !== null && bottom !== null && bottom > 0 ? top / bottom : null;
1254
+ var narrowestStep = (facts) => {
1255
+ const steps = Array.isArray(facts.steps) ? facts.steps : [];
1256
+ const diameters = steps.map((step) => finite(record(step)?.diameter)).filter((each) => each !== null && each > 0);
1257
+ return diameters.length > 0 ? Math.min(...diameters) : null;
1258
+ };
1259
+ var threadSpec = (facts, field) => finite(record(facts.threadSpec)?.[field]) ?? finite(record(record(facts.thread)?.threadSpec)?.[field]);
1260
+ var partEnds = (stretches) => {
1261
+ const zs = stretches.flatMap(
1262
+ (stretch) => stretch.copies.flatMap((copy) => {
1263
+ const sheet = record(copy.datasheet);
1264
+ return [finite(sheet?.zMin), finite(sheet?.zMax)].filter(
1265
+ (each) => each !== null
1266
+ );
1267
+ })
1268
+ );
1269
+ return zs.length > 0 ? { low: Math.min(...zs), high: Math.max(...zs) } : null;
1270
+ };
1271
+ var turnedLd = (sheet, facts, kind, approach, ends) => {
1272
+ if (kind === "AxialHole") return ratio(finite(facts.depth), narrowestStep(facts));
1273
+ const inside = ID_KINDS;
1274
+ if (!kind || !inside.includes(kind) || !ends || !approach) return null;
1275
+ const zMin = finite(sheet.zMin);
1276
+ const zMax = finite(sheet.zMax);
1277
+ const reach = approach === "PlusZ" ? zMin === null ? null : ends.high - zMin : zMax === null ? null : zMax - ends.low;
1278
+ const rMin = finite(sheet.rMin);
1279
+ const bore = finite(sheet.entryDiameter) ?? (rMin === null ? null : 2 * rMin);
1280
+ return ratio(reach, bore);
1281
+ };
1282
+ var HAIR = 1e-3;
1283
+ var FLAT_CONE = 179.5;
1284
+ var copyMeasure = (metric, copy, ends) => {
1285
+ const sheet = record(copy.datasheet);
1286
+ if (!sheet) return null;
1287
+ const facts = record(sheet.facts) ?? {};
1288
+ const kind = typeof facts.kind === "string" ? facts.kind : kindOfType(copy.featureType);
1289
+ const kinds = metric.kinds;
1290
+ if (kinds && !(kind && kinds.includes(kind))) return null;
1291
+ const doubled = (value) => {
1292
+ const radius = finite(value);
1293
+ return radius === null ? null : 2 * radius;
1294
+ };
1295
+ switch (metric.key) {
1296
+ case "maxDiameter":
1297
+ return doubled(sheet.rMax);
1298
+ case "minDiameter":
1299
+ return doubled(sheet.rMin);
1300
+ case "length": {
1301
+ const zMin = finite(sheet.zMin);
1302
+ const zMax = finite(sheet.zMax);
1303
+ return zMin === null || zMax === null || zMax - zMin < HAIR ? null : zMax - zMin;
1304
+ }
1305
+ case "ld":
1306
+ return turnedLd(sheet, facts, kind, copy.approach ?? null, ends);
1307
+ case "grooveWidth":
1308
+ return finite(facts.width);
1309
+ case "grooveDepth":
1310
+ case "threadDepth":
1311
+ case "holeDepth":
1312
+ return finite(facts.depth);
1313
+ case "grooveAspect":
1314
+ return ratio(finite(facts.depth), finite(facts.width));
1315
+ case "threadPitch":
1316
+ return finite(facts.pitch) ?? threadSpec(facts, "threadPitch");
1317
+ case "threadDiameter":
1318
+ return threadSpec(facts, "basicDiameter");
1319
+ case "holeDiameter":
1320
+ return narrowestStep(facts);
1321
+ case "holeSteps":
1322
+ return Array.isArray(facts.steps) ? facts.steps.length : null;
1323
+ case "flatBottom": {
1324
+ if (facts.bottom === void 0) return 0;
1325
+ const cone = finite(record(facts.bottom)?.coneDeg);
1326
+ return cone === null ? null : cone >= FLAT_CONE ? 1 : 0;
1327
+ }
1328
+ // Zero is a figure here: a sharp corner, which a "radius ≤" rule is most of all about.
1329
+ case "cornerRadius":
1330
+ return finite(sheet.minConcaveRadius);
1331
+ case "sharpCorner": {
1332
+ const radius = finite(sheet.minConcaveRadius);
1333
+ return radius === null ? 0 : radius <= HAIR ? 1 : 0;
1334
+ }
1335
+ // Absent where nothing stands between the feature and its end: nothing to pass, so clean.
1336
+ case "entryDiameter":
1337
+ return finite(sheet.entryDiameter);
1338
+ case "axialTool":
1339
+ return typeof sheet.mounting === "string" ? sheet.mounting === "Axial" ? 1 : 0 : null;
1340
+ case "tolerance":
1341
+ return finite(record(sheet.toleranceBand)?.atolMax);
1342
+ default:
1343
+ return null;
1344
+ }
1345
+ };
1346
+ var kindOfType = (featureType) => featureType.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
1347
+ var stretchFacts = (stretch) => stretch.copies.map((copy) => record(record(copy.datasheet)?.facts)).find((each) => each !== void 0);
1348
+ var stretchIs = (subject, stretch) => {
1349
+ const type = stretch.featureType.toLowerCase();
1350
+ if (subject.startsWith(TYPE_SUBJECT)) return type === subject.slice(TYPE_SUBJECT.length);
1351
+ const facts = stretchFacts(stretch);
1352
+ const kind = typeof facts?.kind === "string" ? facts.kind : kindOfType(type);
1353
+ const hole = kind === "AxialHole";
1354
+ switch (subject) {
1355
+ case "any":
1356
+ return true;
1357
+ case "thread":
1358
+ return kind === "OdThread" || kind === "IdThread" || hole && record(facts?.thread) !== void 0;
1359
+ case "throughHole":
1360
+ return hole && facts?.through === true;
1361
+ case "blindHole":
1362
+ return hole && facts?.through === false;
1363
+ case "threadedHole":
1364
+ return hole && record(facts?.thread) !== void 0;
1365
+ default: {
1366
+ const kinds = subjectInfo(subject, TURNING_RULES)?.kinds;
1367
+ return kinds !== void 0 && kinds.includes(kind);
1368
+ }
1369
+ }
1370
+ };
1371
+ var stretchHit = (stretch, rule, metric, ends) => {
1372
+ if (metric.quantity === "presence") return { rule, metric, value: 1 };
1373
+ const measured = stretch.copies.filter((copy) => record(copy.datasheet) !== void 0);
1374
+ if (measured.length === 0) return null;
1375
+ const values = [];
1376
+ for (const copy of measured) {
1377
+ const value2 = copyMeasure(metric, copy, ends);
1378
+ if (value2 === null || !breaks(rule, metric.quantity, value2)) return null;
1379
+ values.push(value2);
1380
+ }
1381
+ const value = worseUpward(rule, metric) ? Math.min(...values) : Math.max(...values);
1382
+ return { rule, metric, value };
1383
+ };
1384
+ var checkTurnedPart = (stretches, rules) => {
1385
+ const ends = partEnds(stretches);
1386
+ const hitsByTag = new Map(
1387
+ stretches.map((stretch) => [
1388
+ stretch.tag.toLowerCase(),
1389
+ rules.flatMap((rule) => {
1390
+ const metric = metricInfo(rule.metric, TURNING_RULES);
1391
+ if (!metric || !stretchIs(rule.subject, stretch)) return [];
1392
+ const hit = stretchHit(stretch, rule, metric, ends);
1393
+ return hit ? [hit] : [];
1394
+ })
1395
+ ])
1396
+ );
1397
+ const required = new Set(stretches.map((stretch) => stretch.tag.toLowerCase()));
1398
+ const paintedFaces = /* @__PURE__ */ new Map();
1399
+ for (const stretch of stretches) {
1400
+ const color = worstColor(hitsByTag.get(stretch.tag.toLowerCase()));
1401
+ if (!color) continue;
1402
+ for (const region of stretch.regionIdxs) {
1403
+ const painted = paintedFaces.get(region);
1404
+ if (!painted || compareColors(color, painted.color) < 0)
1405
+ paintedFaces.set(region, { tag: stretch.tag, color });
1406
+ }
1407
+ }
1408
+ return {
1409
+ hitsByTag,
1410
+ required,
1411
+ byRule: ruleResults(rules, stretches, hitsByTag, required),
1412
+ paintedFaces,
1413
+ paintedWhole: /* @__PURE__ */ new Map()
1414
+ };
1415
+ };
1211
1416
  export {
1212
1417
  CRITICAL_COLOR,
1213
1418
  FILE_KIND,
1419
+ MILLING_RULES,
1214
1420
  PINCH_TOOL_MM,
1215
1421
  RULE_COLORS,
1422
+ TURNING_RULES,
1216
1423
  WARNING_COLOR,
1217
1424
  brokenRules,
1218
1425
  checkPart,
1426
+ checkTurnedPart,
1219
1427
  compareColors,
1220
1428
  describeRule,
1221
1429
  dfmFeatures,
@@ -1242,5 +1450,7 @@ export {
1242
1450
  sameDepth,
1243
1451
  serializeRuleSet,
1244
1452
  trimmedCurve,
1453
+ turnedStretches,
1454
+ turnedTypeLabel,
1245
1455
  withSurroundedFaces
1246
1456
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolpath/dfm",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Toolpath DFM rules: the rule list, the rules and the part checker, and the feature panel",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -64,8 +64,8 @@
64
64
  "tsup": "8.5.1",
65
65
  "typescript": "5.9.3",
66
66
  "vitest": "4.1.10",
67
- "@toolpath/ui": "^1.0.0",
68
- "@toolpath/api": "^0.7.0"
67
+ "@toolpath/api": "^0.8.1",
68
+ "@toolpath/ui": "^1.0.0"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsup src/index.ts src/model/index.ts --format esm --dts --clean --external react --external react-dom --external @toolpath/ui",