@toolpath/dfm 0.5.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -186
- package/dist/{chunk-4IRNLDE6.js → chunk-IB42AM6A.js} +1517 -96
- package/dist/index.d.ts +1 -389
- package/dist/index.js +93 -2610
- package/dist/model/index.d.ts +690 -3
- package/dist/model/index.js +64 -1388
- package/package.json +5 -23
- package/dfm.css +0 -10
- package/dist/feature-record-CfjgDCJU.d.ts +0 -599
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
// src/model/dfm-metrics.ts
|
|
2
|
+
import { MM_PER_INCH } from "@toolpath/tool-support";
|
|
3
|
+
|
|
4
|
+
// src/model/direction-label.ts
|
|
5
|
+
var EPSILON = 1e-6;
|
|
6
|
+
var trim = (value) => Number.parseFloat(value.toFixed(3)).toString();
|
|
7
|
+
var directionLabel = (direction) => {
|
|
8
|
+
const axes = [
|
|
9
|
+
["X", direction.x],
|
|
10
|
+
["Y", direction.y],
|
|
11
|
+
["Z", direction.z]
|
|
12
|
+
];
|
|
13
|
+
const nonZero = axes.filter(([, value]) => Math.abs(value) > EPSILON);
|
|
14
|
+
const [axis] = nonZero;
|
|
15
|
+
if (nonZero.length === 1 && axis && Math.abs(Math.abs(axis[1]) - 1) < EPSILON) {
|
|
16
|
+
return `${axis[1] > 0 ? "+" : "\u2212"}${axis[0]}`;
|
|
17
|
+
}
|
|
18
|
+
return axes.map(([, value]) => trim(value)).join(", ");
|
|
19
|
+
};
|
|
20
|
+
|
|
1
21
|
// src/model/geometry.ts
|
|
2
22
|
var dfmFeatures = (features) => {
|
|
3
23
|
const read = features.map(
|
|
@@ -59,26 +79,6 @@ var featureLd = (features, feature, sheets) => {
|
|
|
59
79
|
};
|
|
60
80
|
};
|
|
61
81
|
|
|
62
|
-
// src/model/dfm-metrics.ts
|
|
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
82
|
// src/model/dfm-metrics.ts
|
|
83
83
|
var define = ({ start, path, ...spec }) => ({
|
|
84
84
|
...spec,
|
|
@@ -488,7 +488,7 @@ var FEATURE_METRICS = [
|
|
|
488
488
|
op: "gte",
|
|
489
489
|
start: { mm: 500, inch: 508 },
|
|
490
490
|
path: ["facts.wallLength.length", "facts.length"],
|
|
491
|
-
kinds: ["Pocket", "Boss", "Wall", "Profile"],
|
|
491
|
+
kinds: ["Pocket", "Boss", "Wall", "CircularWall", "Profile"],
|
|
492
492
|
hint: "How far a pass around its outline travels, with a tool of no width"
|
|
493
493
|
}),
|
|
494
494
|
define({
|
|
@@ -980,11 +980,11 @@ var GROUP_SUBJECTS = [
|
|
|
980
980
|
{ value: "pocket", label: "Pockets", group: "Groups", kinds: POCKET_KINDS },
|
|
981
981
|
{ value: "slot", label: "Slots", group: "Groups", kinds: UNDERCUT_KINDS },
|
|
982
982
|
{ value: "undercut", label: "Undercuts", group: "Groups", kinds: UNDERCUT_KINDS },
|
|
983
|
-
{ value: "boss", label: "Bosses", group: "Groups", kinds: ["Boss"] },
|
|
983
|
+
{ value: "boss", label: "Bosses", group: "Groups", kinds: ["Boss", "CircularWall"] },
|
|
984
984
|
{ value: "countersink", label: "Countersinks", group: "Groups", kinds: ["Hole", "Chamfer"] },
|
|
985
985
|
{ value: "profile", label: "Profiles", group: "Groups", kinds: ["Profile"] },
|
|
986
986
|
{ value: "contour", label: "3D surfaces", group: "Groups", kinds: SURFACE_KINDS },
|
|
987
|
-
{ value: "wall", label: "Walls", group: "Groups", kinds: ["Wall"] },
|
|
987
|
+
{ value: "wall", label: "Walls", group: "Groups", kinds: ["Wall", "CircularWall"] },
|
|
988
988
|
{ value: "face", label: "Faces", group: "Groups", kinds: ["Face", "Three"] },
|
|
989
989
|
{ value: "fillet", label: "Fillets", group: "Groups", kinds: SURFACE_KINDS },
|
|
990
990
|
{ value: "chamfer", label: "Chamfers", group: "Groups", kinds: ["Chamfer"] }
|
|
@@ -1007,6 +1007,7 @@ var TYPES = [
|
|
|
1007
1007
|
["filleted_open_pocket", "Filleted open pockets", POCKET_KINDS],
|
|
1008
1008
|
["boss", "Bosses", ["Boss"]],
|
|
1009
1009
|
["filleted_boss", "Filleted bosses", ["Boss"]],
|
|
1010
|
+
["circular_wall", "Circular walls", ["CircularWall"]],
|
|
1010
1011
|
["wall", "Wall features", ["Wall"]],
|
|
1011
1012
|
["face", "Face features", ["Face"]],
|
|
1012
1013
|
["slanted_face", "Slanted faces", ["Face", "Three"]],
|
|
@@ -1100,8 +1101,8 @@ import { MM_PER_INCH as MM_PER_INCH2 } from "@toolpath/tool-support";
|
|
|
1100
1101
|
var LENGTH_PLACES = { mm: 3, inch: 3 };
|
|
1101
1102
|
var FIELD_LENGTH_PLACES = { mm: 3, inch: 4 };
|
|
1102
1103
|
var UNIT_LABELS = { mm: "mm", inch: "in" };
|
|
1103
|
-
var inUnits = (
|
|
1104
|
-
var lengthValue = (
|
|
1104
|
+
var inUnits = (mm2, units) => units === "inch" ? mm2 / MM_PER_INCH2 : mm2;
|
|
1105
|
+
var lengthValue = (mm2, units) => inUnits(mm2, units).toFixed(LENGTH_PLACES[units]);
|
|
1105
1106
|
var unitLabel = (units) => UNIT_LABELS[units];
|
|
1106
1107
|
|
|
1107
1108
|
// src/model/dfm-rules.ts
|
|
@@ -1129,8 +1130,8 @@ var RULE_COLORS = [
|
|
|
1129
1130
|
];
|
|
1130
1131
|
var compareColors = (a, b) => {
|
|
1131
1132
|
const rank = (color) => {
|
|
1132
|
-
const
|
|
1133
|
-
return
|
|
1133
|
+
const at2 = RULE_COLORS.findIndex((each) => each.value === color);
|
|
1134
|
+
return at2 === -1 ? RULE_COLORS.length : at2;
|
|
1134
1135
|
};
|
|
1135
1136
|
return rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0);
|
|
1136
1137
|
};
|
|
@@ -1176,7 +1177,7 @@ var quantityUnit = (quantity, units) => {
|
|
|
1176
1177
|
return "";
|
|
1177
1178
|
}
|
|
1178
1179
|
};
|
|
1179
|
-
var withUnit = (value,
|
|
1180
|
+
var withUnit = (value, unit2) => unit2 === "" ? value : unit2 === "\xB0" || unit2 === "%" ? `${value}${unit2}` : `${value} ${unit2}`;
|
|
1180
1181
|
var toUnits = (quantity, value, units) => units !== "inch" ? value : quantity === "length" ? value / MM_PER_INCH3 : quantity === "area" ? value / SQUARE_MM_PER_SQUARE_INCH : value;
|
|
1181
1182
|
var toField = (quantity, value, units) => {
|
|
1182
1183
|
const shown = toUnits(quantity, value, units);
|
|
@@ -1215,8 +1216,8 @@ var describeRule = (rule, units, vocabulary = MILLING_RULES) => {
|
|
|
1215
1216
|
if (!info) return subject;
|
|
1216
1217
|
if (info.quantity === "presence") return `${subject}, ${info.label}`;
|
|
1217
1218
|
if (info.quantity === "flag") return `${subject} with ${info.label}`;
|
|
1218
|
-
const
|
|
1219
|
-
const shown = (value) => withUnit(toField(info.quantity, value, units),
|
|
1219
|
+
const unit2 = quantityUnit(info.quantity, units);
|
|
1220
|
+
const shown = (value) => withUnit(toField(info.quantity, value, units), unit2);
|
|
1220
1221
|
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)}`;
|
|
1221
1222
|
};
|
|
1222
1223
|
var newRuleId = () => Math.random().toString(36).slice(2, 10);
|
|
@@ -1298,6 +1299,988 @@ var parseRule = (raw, vocabulary) => {
|
|
|
1298
1299
|
};
|
|
1299
1300
|
var parseRules = (raw, vocabulary = MILLING_RULES) => Array.isArray(raw) ? raw.flatMap((each) => parseRule(each, vocabulary) ?? []) : null;
|
|
1300
1301
|
|
|
1302
|
+
// src/model/dfm-explain.ts
|
|
1303
|
+
var figureless = (hit) => hit.metric.quantity === "flag" || hit.metric.quantity === "presence";
|
|
1304
|
+
var hitFigure = (hit, units) => figureless(hit) ? "" : formatMeasured(hit.metric.quantity, hit.value, units);
|
|
1305
|
+
|
|
1306
|
+
// src/model/broken-rules.ts
|
|
1307
|
+
var byColor = (hits) => [...hits].sort((a, b) => compareColors(a.rule.color, b.rule.color));
|
|
1308
|
+
var worstHits = (hits) => {
|
|
1309
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1310
|
+
return byColor(hits).filter((hit) => {
|
|
1311
|
+
if (seen.has(hit.metric.key)) return false;
|
|
1312
|
+
seen.add(hit.metric.key);
|
|
1313
|
+
return true;
|
|
1314
|
+
});
|
|
1315
|
+
};
|
|
1316
|
+
var brokenRules = (check, tag, units, vocabulary = MILLING_RULES) => worstHits(check.hitsByTag.get(tag.toLowerCase()) ?? []).map((hit) => {
|
|
1317
|
+
const figure = hitFigure(hit, units);
|
|
1318
|
+
return {
|
|
1319
|
+
key: hit.rule.id,
|
|
1320
|
+
color: hit.rule.color,
|
|
1321
|
+
text: describeRule(hit.rule, units, vocabulary),
|
|
1322
|
+
...figure ? { figure } : {}
|
|
1323
|
+
};
|
|
1324
|
+
});
|
|
1325
|
+
var featureColor = (check, tag, region = null) => {
|
|
1326
|
+
const key = tag.toLowerCase();
|
|
1327
|
+
const face = region === null ? void 0 : check.paintedFaces.get(region);
|
|
1328
|
+
if (face && face.tag.toLowerCase() === key) return face.color;
|
|
1329
|
+
const whole = check.paintedWhole.get(key);
|
|
1330
|
+
if (whole) return whole.color;
|
|
1331
|
+
if (!check.required.has(key)) return void 0;
|
|
1332
|
+
return byColor(check.hitsByTag.get(key) ?? [])[0]?.rule.color;
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
// src/model/dfm-flags.ts
|
|
1336
|
+
var howCut = (sheet) => {
|
|
1337
|
+
const drilled = isDrilled(sheet);
|
|
1338
|
+
return { drilled, milled: !drilled };
|
|
1339
|
+
};
|
|
1340
|
+
var featureIs = (subject, featureType, sheet) => {
|
|
1341
|
+
const type = featureType.toLowerCase();
|
|
1342
|
+
const kind = sheet?.kind?.toLowerCase() ?? "";
|
|
1343
|
+
const boss = sheet?.standsOnAFloor === true;
|
|
1344
|
+
if (subject.startsWith(TYPE_SUBJECT)) return type === subject.slice(TYPE_SUBJECT.length);
|
|
1345
|
+
switch (subject) {
|
|
1346
|
+
case "any":
|
|
1347
|
+
return true;
|
|
1348
|
+
case "milled":
|
|
1349
|
+
return howCut(sheet).milled;
|
|
1350
|
+
case "drilled":
|
|
1351
|
+
return howCut(sheet).drilled;
|
|
1352
|
+
case "hole":
|
|
1353
|
+
return type.includes("hole") || kind === "hole";
|
|
1354
|
+
case "threadedHole":
|
|
1355
|
+
return sheet?.threading !== void 0;
|
|
1356
|
+
case "pocket":
|
|
1357
|
+
return type.includes("pocket") || kind === "pocket";
|
|
1358
|
+
case "slot":
|
|
1359
|
+
return type.includes("slot") || kind === "tslot";
|
|
1360
|
+
case "undercut":
|
|
1361
|
+
return kind === "tslot" || type.includes("undercut") || type.includes("tslot");
|
|
1362
|
+
case "contour":
|
|
1363
|
+
return type.includes("contour") || kind.includes("contour");
|
|
1364
|
+
case "wall":
|
|
1365
|
+
return !boss && (type.includes("wall") || kind === "wall");
|
|
1366
|
+
case "face":
|
|
1367
|
+
return type === "face" || kind === "face";
|
|
1368
|
+
case "fillet":
|
|
1369
|
+
return type.split("_").includes("fillet") || kind.includes("fillet");
|
|
1370
|
+
case "chamfer":
|
|
1371
|
+
return type.includes("chamfer") || kind === "chamfer";
|
|
1372
|
+
case "boss":
|
|
1373
|
+
return boss || type.includes("boss") || kind === "boss";
|
|
1374
|
+
case "countersink":
|
|
1375
|
+
return type === "sink";
|
|
1376
|
+
case "profile":
|
|
1377
|
+
return type === "profile" || kind === "profile";
|
|
1378
|
+
default:
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
var at = (record2, path) => path.split(".").reduce(
|
|
1383
|
+
(value, key) => typeof value === "object" && value !== null ? value[key] : void 0,
|
|
1384
|
+
record2
|
|
1385
|
+
);
|
|
1386
|
+
var readPath = (datasheet, metric) => {
|
|
1387
|
+
for (const path of metric.path ?? []) {
|
|
1388
|
+
const value = at(datasheet, path);
|
|
1389
|
+
if (value === void 0 || value === null) continue;
|
|
1390
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
1391
|
+
if (metric.quantity === "flag") return 1;
|
|
1392
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
return metric.quantity === "flag" && metric.path ? 0 : null;
|
|
1396
|
+
};
|
|
1397
|
+
var TILT = 1e-6;
|
|
1398
|
+
var isTilted = ({ x, y, z }) => [x, y, z].filter((each) => Math.abs(each) > TILT).length > 1;
|
|
1399
|
+
var featureMeasure = (metric, features, feature, { sheets, datasheets, regionAreas }) => {
|
|
1400
|
+
const tag = feature.tag.toLowerCase();
|
|
1401
|
+
const sheet = sheets[tag];
|
|
1402
|
+
const datasheet = datasheets[tag];
|
|
1403
|
+
const positive = (value) => value !== void 0 && value > 0 ? { value } : null;
|
|
1404
|
+
switch (metric.key) {
|
|
1405
|
+
case "present":
|
|
1406
|
+
return { value: 1 };
|
|
1407
|
+
case "tilted":
|
|
1408
|
+
return { value: isTilted(feature.machiningDirection) ? 1 : 0 };
|
|
1409
|
+
case "faceCount":
|
|
1410
|
+
return { value: new Set(feature.regionIdxs).size };
|
|
1411
|
+
case "ld": {
|
|
1412
|
+
const ld = featureLd(features, feature, sheets);
|
|
1413
|
+
return ld ? { value: ld.ratio, drilling: ld.drilling } : null;
|
|
1414
|
+
}
|
|
1415
|
+
case "featureLd": {
|
|
1416
|
+
const ld = featureLd(features, feature, sheets);
|
|
1417
|
+
return ld?.featureRatio !== void 0 ? { value: ld.featureRatio, drilling: ld.drilling } : null;
|
|
1418
|
+
}
|
|
1419
|
+
case "depthBelowTop": {
|
|
1420
|
+
const top = partTop(features, sheets, feature.machiningDirection);
|
|
1421
|
+
return top !== null && sheet?.zMin !== void 0 ? { value: top - sheet.zMin } : null;
|
|
1422
|
+
}
|
|
1423
|
+
case "featureDepth":
|
|
1424
|
+
return sheet?.zMax !== void 0 && sheet.zMin !== void 0 ? { value: sheet.zMax - sheet.zMin } : null;
|
|
1425
|
+
// Zero is no measure at all for these: no bore, no tool, a sharp floor rather than a tiny fillet.
|
|
1426
|
+
case "diameter":
|
|
1427
|
+
return positive(sheet?.diameter);
|
|
1428
|
+
// Zero is a measure here: a sharp corner, which a "radius ≤" rule is most of all about.
|
|
1429
|
+
case "cornerRadius":
|
|
1430
|
+
return sheet?.cornerRadius === void 0 ? null : { value: sheet.cornerRadius };
|
|
1431
|
+
case "maxTool":
|
|
1432
|
+
return positive(sheet?.maxTool);
|
|
1433
|
+
case "floorFillet":
|
|
1434
|
+
return positive(sheet?.filletRadius);
|
|
1435
|
+
case "sharpCorner":
|
|
1436
|
+
return sheet ? { value: sheet.sharpCorners ? 1 : 0 } : null;
|
|
1437
|
+
case "noToolFits":
|
|
1438
|
+
return sheet ? { value: sheet.noToolFits ? 1 : 0 } : null;
|
|
1439
|
+
case "pinchPoints": {
|
|
1440
|
+
const points = at(datasheet, "pinchPoints");
|
|
1441
|
+
return Array.isArray(points) ? { value: points.length } : null;
|
|
1442
|
+
}
|
|
1443
|
+
// A disc under a hundredth is a sharp corner, which `sharpCorner` is for: see `pinchDiameter`.
|
|
1444
|
+
case "pinchDiameter":
|
|
1445
|
+
return positive(sheet?.pinchDiameter);
|
|
1446
|
+
case "reachHeight": {
|
|
1447
|
+
const heights = at(datasheet, "reachCurve.verticalOffset");
|
|
1448
|
+
const numbers2 = Array.isArray(heights) ? heights.filter(
|
|
1449
|
+
(each) => typeof each === "number" && Number.isFinite(each)
|
|
1450
|
+
) : [];
|
|
1451
|
+
return numbers2.length > 0 ? { value: Math.max(...numbers2) } : null;
|
|
1452
|
+
}
|
|
1453
|
+
case "surfaceArea": {
|
|
1454
|
+
const areas = at(datasheet, "areas");
|
|
1455
|
+
if (typeof areas === "object" && areas !== null) {
|
|
1456
|
+
const parts = Object.values(areas).filter(
|
|
1457
|
+
(each) => typeof each === "number" && Number.isFinite(each)
|
|
1458
|
+
);
|
|
1459
|
+
if (parts.length > 0) return { value: parts.reduce((sum, each) => sum + each, 0) };
|
|
1460
|
+
}
|
|
1461
|
+
const faces = [...new Set(feature.regionIdxs)].flatMap(
|
|
1462
|
+
(region) => regionAreas.get(region) ?? []
|
|
1463
|
+
);
|
|
1464
|
+
return faces.length > 0 ? { value: faces.reduce((sum, each) => sum + each, 0) } : null;
|
|
1465
|
+
}
|
|
1466
|
+
default: {
|
|
1467
|
+
if (!datasheet) return null;
|
|
1468
|
+
const value = readPath(datasheet, metric);
|
|
1469
|
+
return value === null ? null : { value };
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
var featureHits = (features, feature, sources, rules) => {
|
|
1474
|
+
const sheet = sources.sheets[feature.tag.toLowerCase()];
|
|
1475
|
+
return rules.flatMap((rule) => {
|
|
1476
|
+
const info = metricInfo(rule.metric);
|
|
1477
|
+
if (!info || !featureIs(rule.subject, feature.featureType, sheet)) return [];
|
|
1478
|
+
const measured = featureMeasure(info, features, feature, sources);
|
|
1479
|
+
if (!measured || !breaks(rule, info.quantity, measured.value)) return [];
|
|
1480
|
+
return [{ rule, metric: info, ...measured }];
|
|
1481
|
+
});
|
|
1482
|
+
};
|
|
1483
|
+
var requiredTags = (features) => {
|
|
1484
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1485
|
+
for (const feature of features) {
|
|
1486
|
+
for (const region of new Set(feature.regionIdxs))
|
|
1487
|
+
owners.set(region, (owners.get(region) ?? 0) + 1);
|
|
1488
|
+
}
|
|
1489
|
+
return new Set(
|
|
1490
|
+
features.filter((feature) => feature.regionIdxs.some((region) => owners.get(region) === 1)).map((feature) => feature.tag.toLowerCase())
|
|
1491
|
+
);
|
|
1492
|
+
};
|
|
1493
|
+
var worstColor = (hits) => hits?.map((hit) => hit.rule.color).sort(compareColors)[0];
|
|
1494
|
+
var ruleResults = (rules, features, hitsByTag, required) => rules.map((rule) => {
|
|
1495
|
+
const hits = features.flatMap((feature) => {
|
|
1496
|
+
const tag = feature.tag.toLowerCase();
|
|
1497
|
+
const hit = hitsByTag.get(tag)?.find((each) => each.rule.id === rule.id);
|
|
1498
|
+
return hit ? [{ tag: feature.tag, value: hit.value, metric: hit.metric, required: required.has(tag) }] : [];
|
|
1499
|
+
});
|
|
1500
|
+
const [first] = hits;
|
|
1501
|
+
const up = first ? worseUpward(rule, first.metric) : true;
|
|
1502
|
+
hits.sort((a, b) => up ? b.value - a.value : a.value - b.value);
|
|
1503
|
+
return {
|
|
1504
|
+
rule,
|
|
1505
|
+
tags: hits.filter((hit) => hit.required).map((hit) => hit.tag),
|
|
1506
|
+
otherTags: hits.filter((hit) => !hit.required).map((hit) => hit.tag)
|
|
1507
|
+
};
|
|
1508
|
+
});
|
|
1509
|
+
var QUALIFIERS = ["undercut", "filleted", "tslot", "dovetail"];
|
|
1510
|
+
var EDGE_TYPES = ["fillet", "chamfer", "sink"];
|
|
1511
|
+
var featureComplexity = (featureType) => {
|
|
1512
|
+
const words = featureType.toLowerCase().split("_");
|
|
1513
|
+
const edge = EDGE_TYPES.some((type) => words.includes(type)) ? 1 : 0;
|
|
1514
|
+
return edge + QUALIFIERS.filter((word) => words.includes(word)).length;
|
|
1515
|
+
};
|
|
1516
|
+
var CONTOUR = "contour_surface";
|
|
1517
|
+
var unavoidableFaces = (features, hitsByTag, regionAreas) => {
|
|
1518
|
+
const areaOf = (feature) => [...new Set(feature.regionIdxs)].reduce(
|
|
1519
|
+
(sum, region) => sum + (regionAreas.get(region) ?? 0),
|
|
1520
|
+
0
|
|
1521
|
+
);
|
|
1522
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1523
|
+
for (const feature of features) {
|
|
1524
|
+
for (const region of new Set(feature.regionIdxs)) {
|
|
1525
|
+
owners.set(region, [...owners.get(region) ?? [], feature]);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
const faces = /* @__PURE__ */ new Map();
|
|
1529
|
+
const whole = /* @__PURE__ */ new Map();
|
|
1530
|
+
for (const [region, candidates] of owners) {
|
|
1531
|
+
const colored = candidates.map((feature) => ({
|
|
1532
|
+
feature,
|
|
1533
|
+
color: worstColor(hitsByTag.get(feature.tag.toLowerCase())),
|
|
1534
|
+
area: areaOf(feature)
|
|
1535
|
+
}));
|
|
1536
|
+
const isContour = ({ feature }) => feature.featureType === CONTOUR;
|
|
1537
|
+
const machined = colored.filter((each) => !isContour(each));
|
|
1538
|
+
const pool = machined.length > 0 ? machined : colored;
|
|
1539
|
+
if (pool.some(({ color }) => color === void 0)) continue;
|
|
1540
|
+
const contour = colored.filter(isContour).reduce((kept, each) => kept === void 0 || each.area > kept.area ? each : kept, void 0);
|
|
1541
|
+
if (contour?.color && colored.every(({ color }) => color !== void 0)) {
|
|
1542
|
+
whole.set(contour.feature.tag.toLowerCase(), {
|
|
1543
|
+
tag: contour.feature.tag,
|
|
1544
|
+
color: contour.color
|
|
1545
|
+
});
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
const best = pool.reduce((kept, each) => {
|
|
1549
|
+
const plainer = featureComplexity(kept.feature.featureType) - featureComplexity(each.feature.featureType);
|
|
1550
|
+
if (plainer !== 0) return plainer > 0 ? each : kept;
|
|
1551
|
+
const milder = compareColors(each.color ?? "", kept.color ?? "");
|
|
1552
|
+
return milder > 0 || milder === 0 && each.area > kept.area ? each : kept;
|
|
1553
|
+
});
|
|
1554
|
+
if (best.color) faces.set(region, { tag: best.feature.tag, color: best.color });
|
|
1555
|
+
}
|
|
1556
|
+
return { paintedFaces: faces, paintedWhole: whole };
|
|
1557
|
+
};
|
|
1558
|
+
var withSurroundedFaces = (check, features, adjacency) => {
|
|
1559
|
+
const paintedFor = new Map(check.paintedFaces);
|
|
1560
|
+
const regionsOf2 = /* @__PURE__ */ new Map();
|
|
1561
|
+
for (const feature of features) {
|
|
1562
|
+
const key = feature.tag.toLowerCase();
|
|
1563
|
+
regionsOf2.set(key, new Set(feature.regionIdxs));
|
|
1564
|
+
const whole = check.paintedWhole.get(key);
|
|
1565
|
+
if (whole) {
|
|
1566
|
+
for (const region of feature.regionIdxs)
|
|
1567
|
+
if (!paintedFor.has(region)) paintedFor.set(region, whole);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const surrounded = /* @__PURE__ */ new Map();
|
|
1571
|
+
for (const [region, neighbours] of adjacency) {
|
|
1572
|
+
if (paintedFor.has(region)) continue;
|
|
1573
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1574
|
+
for (const neighbour of neighbours) {
|
|
1575
|
+
const paint2 = neighbour === region ? void 0 : paintedFor.get(neighbour);
|
|
1576
|
+
if (!paint2) continue;
|
|
1577
|
+
const key = paint2.tag.toLowerCase();
|
|
1578
|
+
counts.set(key, { paint: paint2, count: (counts.get(key)?.count ?? 0) + 1 });
|
|
1579
|
+
}
|
|
1580
|
+
const most = Math.max(0, ...[...counts.values()].map(({ count }) => count));
|
|
1581
|
+
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));
|
|
1582
|
+
if (paint) surrounded.set(region, paint);
|
|
1583
|
+
}
|
|
1584
|
+
if (surrounded.size === 0) return check;
|
|
1585
|
+
return { ...check, paintedFaces: new Map([...check.paintedFaces, ...surrounded]) };
|
|
1586
|
+
};
|
|
1587
|
+
var checkPart = (features, sources, rules) => {
|
|
1588
|
+
const required = requiredTags(features);
|
|
1589
|
+
const hitsByTag = new Map(
|
|
1590
|
+
features.map((feature) => [
|
|
1591
|
+
feature.tag.toLowerCase(),
|
|
1592
|
+
featureHits(features, feature, sources, rules)
|
|
1593
|
+
])
|
|
1594
|
+
);
|
|
1595
|
+
return {
|
|
1596
|
+
hitsByTag,
|
|
1597
|
+
required,
|
|
1598
|
+
byRule: ruleResults(rules, features, hitsByTag, required),
|
|
1599
|
+
...unavoidableFaces(features, hitsByTag, sources.regionAreas)
|
|
1600
|
+
};
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
// src/model/pinch.ts
|
|
1604
|
+
var asNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1605
|
+
var recordOf = (value) => typeof value === "object" && value !== null ? value : {};
|
|
1606
|
+
var clearanceFit = (datasheet) => {
|
|
1607
|
+
const sheet = recordOf(datasheet);
|
|
1608
|
+
const facts = recordOf(sheet.facts);
|
|
1609
|
+
const chamfer = facts.kind === "Chamfer";
|
|
1610
|
+
const band = recordOf(chamfer ? recordOf(facts.three).cd : facts.cd);
|
|
1611
|
+
const tolerance = recordOf(sheet.toleranceBand);
|
|
1612
|
+
const strict = asNumber(recordOf(band.ignore).min);
|
|
1613
|
+
if (strict === void 0) return void 0;
|
|
1614
|
+
const bandPath = chamfer ? "facts.three.cd" : "facts.cd";
|
|
1615
|
+
const loose = asNumber(recordOf(band.deviate).min);
|
|
1616
|
+
const atStrict = asNumber(tolerance.atolIgnore);
|
|
1617
|
+
const atLoose = asNumber(tolerance.atolDeviate);
|
|
1618
|
+
const source = { bandPath, strict, loose, atStrict, atLoose };
|
|
1619
|
+
if (loose === void 0 || atStrict === void 0 || atLoose === void 0 || atLoose <= atStrict)
|
|
1620
|
+
return { ...source, slack: 0, resolved: false };
|
|
1621
|
+
const growth = (loose - strict) / (atLoose - atStrict);
|
|
1622
|
+
const slack = growth > 0 ? Math.min(growth * atStrict, strict) : 0;
|
|
1623
|
+
return { ...source, slack, resolved: true };
|
|
1624
|
+
};
|
|
1625
|
+
var readPinch = (datasheet) => {
|
|
1626
|
+
const sheet = recordOf(datasheet);
|
|
1627
|
+
const zMin = asNumber(sheet.zMin);
|
|
1628
|
+
const zMax = asNumber(sheet.zMax);
|
|
1629
|
+
if (zMin === void 0 || zMax === void 0 || !Array.isArray(sheet.pinchPoints)) return null;
|
|
1630
|
+
const fit = clearanceFit(datasheet);
|
|
1631
|
+
const slack = fit?.slack ?? 0;
|
|
1632
|
+
const discs = sheet.pinchPoints.flatMap((point) => {
|
|
1633
|
+
const centre = recordOf(recordOf(point).center);
|
|
1634
|
+
const x = asNumber(centre.x);
|
|
1635
|
+
const y = asNumber(centre.y);
|
|
1636
|
+
const diameter = asNumber(recordOf(point).diameter);
|
|
1637
|
+
return x === void 0 || y === void 0 || diameter === void 0 || diameter < 0 ? [] : [{ x, y, diameter: Math.max(diameter - slack, 0) }];
|
|
1638
|
+
});
|
|
1639
|
+
if (discs.length === 0) return null;
|
|
1640
|
+
return { discs, zMin, zMax, ...fit && !fit.resolved ? { unresolved: true } : {} };
|
|
1641
|
+
};
|
|
1642
|
+
|
|
1643
|
+
// src/model/feature-sheet.ts
|
|
1644
|
+
var SHARP_MM = 0.1;
|
|
1645
|
+
var SHARP_RADIUS_MM = 0.01;
|
|
1646
|
+
var PINCH_TOOL_MM = 0.01;
|
|
1647
|
+
var asNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1648
|
+
var recordOf2 = (value) => typeof value === "object" && value !== null ? value : {};
|
|
1649
|
+
var definedOnly = (fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== void 0));
|
|
1650
|
+
var undercutOf = (facts) => {
|
|
1651
|
+
const unmeasured = facts.isInvalidGeometry === true || recordOf2(facts.cd).measurementFailed === true ? true : void 0;
|
|
1652
|
+
if (facts.kind === "Tslot")
|
|
1653
|
+
return definedOnly({
|
|
1654
|
+
undercutDepth: asNumber2(facts.undercutDepth),
|
|
1655
|
+
maxEntry: asNumber2(facts.maxEntryCd),
|
|
1656
|
+
isClosed: facts.isClosed === true ? true : void 0,
|
|
1657
|
+
unmeasured
|
|
1658
|
+
});
|
|
1659
|
+
if (facts.kind === "Dovetail")
|
|
1660
|
+
return definedOnly({
|
|
1661
|
+
taperDeg: asNumber2(facts.taperDeg),
|
|
1662
|
+
floorWidth: asNumber2(facts.floorWidth),
|
|
1663
|
+
topOpeningWidth: asNumber2(facts.topOpeningWidth),
|
|
1664
|
+
isExternal: facts.isExternal === true ? true : void 0,
|
|
1665
|
+
unmeasured
|
|
1666
|
+
});
|
|
1667
|
+
return void 0;
|
|
1668
|
+
};
|
|
1669
|
+
var threadingOf = (facts) => {
|
|
1670
|
+
const spec = recordOf2(recordOf2(facts.threading).spec);
|
|
1671
|
+
const basicDiameter = asNumber2(spec.basicDiameter);
|
|
1672
|
+
const threadPitch = asNumber2(spec.threadPitch);
|
|
1673
|
+
if (basicDiameter === void 0 || threadPitch === void 0 || threadPitch <= 0) return void 0;
|
|
1674
|
+
return { basicDiameter, threadPitch };
|
|
1675
|
+
};
|
|
1676
|
+
var noToolFits = (band) => {
|
|
1677
|
+
const { max } = band;
|
|
1678
|
+
if (typeof max === "string") return /^-inf/i.test(max.trim());
|
|
1679
|
+
return typeof max === "number" && (max === Number.NEGATIVE_INFINITY || Number.isFinite(max) && max < SHARP_MM);
|
|
1680
|
+
};
|
|
1681
|
+
var insideCornerRadius = (fit) => Math.max((fit.strict - fit.slack) / 2, 0);
|
|
1682
|
+
var pinchCentres = (points) => Array.isArray(points) ? points.flatMap((point) => {
|
|
1683
|
+
const centre = recordOf2(recordOf2(point).center);
|
|
1684
|
+
const x = asNumber2(centre.x);
|
|
1685
|
+
const y = asNumber2(centre.y);
|
|
1686
|
+
return x === void 0 || y === void 0 ? [] : [{ x, y }];
|
|
1687
|
+
}) : [];
|
|
1688
|
+
var ZERO_MM = 5e-5;
|
|
1689
|
+
var isCutterlessTslot = (datasheet) => {
|
|
1690
|
+
const facts = recordOf2(recordOf2(datasheet).facts);
|
|
1691
|
+
if (facts.kind !== "Tslot") return false;
|
|
1692
|
+
const leaves = (value) => typeof value === "object" && value !== null ? Object.values(value).flatMap(leaves) : [value];
|
|
1693
|
+
const band = leaves(facts.cd);
|
|
1694
|
+
return band.length > 0 && band.every((each) => {
|
|
1695
|
+
const value = typeof each === "string" && each.trim() !== "" ? Number(each) : each;
|
|
1696
|
+
return typeof value === "number" && Math.abs(value) < ZERO_MM;
|
|
1697
|
+
});
|
|
1698
|
+
};
|
|
1699
|
+
var isSharpCorneredFillet = (featureType, datasheet) => {
|
|
1700
|
+
if (typeof featureType !== "string" || !featureType.toLowerCase().split("_").includes("fillet"))
|
|
1701
|
+
return false;
|
|
1702
|
+
const facts = recordOf2(recordOf2(datasheet).facts);
|
|
1703
|
+
return facts.hasSharpCorner === true || recordOf2(facts.three).hasSharpCorner === true;
|
|
1704
|
+
};
|
|
1705
|
+
var isNoReading = (featureType, datasheet) => isCutterlessTslot(datasheet) || isSharpCorneredFillet(featureType, datasheet);
|
|
1706
|
+
var featureSheet = (datasheet) => {
|
|
1707
|
+
const sheet = recordOf2(datasheet);
|
|
1708
|
+
const facts = recordOf2(sheet.facts);
|
|
1709
|
+
const kind = typeof facts.kind === "string" ? facts.kind : void 0;
|
|
1710
|
+
const band = recordOf2(kind === "Chamfer" ? recordOf2(facts.three).cd : facts.cd);
|
|
1711
|
+
const ignoreBand = recordOf2(band.ignore);
|
|
1712
|
+
const fit = clearanceFit(datasheet);
|
|
1713
|
+
const fields = {
|
|
1714
|
+
kind,
|
|
1715
|
+
zMin: asNumber2(sheet.zMin),
|
|
1716
|
+
zMax: asNumber2(sheet.zMax),
|
|
1717
|
+
extendedZMax: asNumber2(sheet.extendedZMax),
|
|
1718
|
+
maxTool: asNumber2(ignoreBand.min),
|
|
1719
|
+
diameter: kind === "Hole" ? asNumber2(facts.diameter) : void 0,
|
|
1720
|
+
tipAngle: kind === "Hole" ? asNumber2(facts.fullConeDeg) : void 0,
|
|
1721
|
+
filletRadius: asNumber2(facts.filletRadius),
|
|
1722
|
+
// A bevel's angle wherever the facts carry one: a chamfer's, and any other kind that bevels.
|
|
1723
|
+
chamferAngle: asNumber2(recordOf2(facts.bevel).angleDeg)
|
|
1724
|
+
};
|
|
1725
|
+
const toolFitPath = facts.toolFit !== void 0 ? "facts.toolFit" : "facts.three.toolFit";
|
|
1726
|
+
const toolFit = recordOf2(facts.toolFit ?? recordOf2(facts.three).toolFit);
|
|
1727
|
+
fields.fitToolDiameter = asNumber2(toolFit.toolDiameter);
|
|
1728
|
+
fields.fitCornerRadius = asNumber2(toolFit.cornerRadius);
|
|
1729
|
+
const ballOnly = (facts.useOnlyBallToolsForFinish ?? recordOf2(facts.three).useOnlyBallToolsForFinish) === true;
|
|
1730
|
+
const standsOnAFloor = kind === "CircularWall" && facts.standsOnAFloor === true;
|
|
1731
|
+
const measured = Object.fromEntries(
|
|
1732
|
+
Object.entries(fields).filter(([, value]) => value !== void 0)
|
|
1733
|
+
);
|
|
1734
|
+
const cramped = Object.keys(ignoreBand).length > 0 && noToolFits(ignoreBand);
|
|
1735
|
+
const walled = kind !== "Hole" && sheet.hasWall === true;
|
|
1736
|
+
const clearance = walled ? fit : void 0;
|
|
1737
|
+
const cornerRadius = clearance ? insideCornerRadius(clearance) : void 0;
|
|
1738
|
+
const sharp = cornerRadius !== void 0 && cornerRadius < SHARP_RADIUS_MM && !cramped;
|
|
1739
|
+
const threading = kind === "Hole" ? threadingOf(facts) : void 0;
|
|
1740
|
+
const undercut = undercutOf(facts);
|
|
1741
|
+
const pinched = (readPinch(datasheet)?.discs ?? []).filter(
|
|
1742
|
+
(disc) => disc.diameter >= PINCH_TOOL_MM
|
|
1743
|
+
);
|
|
1744
|
+
return {
|
|
1745
|
+
...measured,
|
|
1746
|
+
...pinched.length > 0 ? {
|
|
1747
|
+
pinchDiameter: Math.min(...pinched.map((disc) => disc.diameter)),
|
|
1748
|
+
pinchPlaces: pinched.length
|
|
1749
|
+
} : {},
|
|
1750
|
+
...cornerRadius !== void 0 ? { cornerRadius } : {},
|
|
1751
|
+
...clearance && !clearance.resolved ? { cornerRadiusUnresolved: true } : {},
|
|
1752
|
+
...clearance ? { clearance } : {},
|
|
1753
|
+
...Object.keys(toolFit).length > 0 ? { toolFitPath } : {},
|
|
1754
|
+
...sharp ? { sharpCorners: pinchCentres(sheet.pinchPoints) } : {},
|
|
1755
|
+
...cramped ? { noToolFits: true } : {},
|
|
1756
|
+
...ballOnly ? { ballOnly: true } : {},
|
|
1757
|
+
...standsOnAFloor ? { standsOnAFloor: true } : {},
|
|
1758
|
+
...threading ? { threading } : {},
|
|
1759
|
+
...undercut ? { undercut } : {}
|
|
1760
|
+
};
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
// src/model/feature-details.ts
|
|
1764
|
+
import { MM_PER_INCH as MM_PER_INCH4 } from "@toolpath/tool-support";
|
|
1765
|
+
var mm = (value) => `${trim3(value, 4)} mm`;
|
|
1766
|
+
var tol = (value) => `${trim3(value, 6)} mm`;
|
|
1767
|
+
var clearanceDerivation = (fit, clearance) => {
|
|
1768
|
+
const { bandPath, strict, loose, atStrict, atLoose } = fit;
|
|
1769
|
+
const lines = [`\u2300\u2081 = ${bandPath}.ignore.min = ${mm(strict)}`];
|
|
1770
|
+
if (loose === void 0 || atStrict === void 0 || atLoose === void 0 || atLoose <= atStrict) {
|
|
1771
|
+
lines.push(
|
|
1772
|
+
`${bandPath}.deviate.min or a tolerance is missing, so the tolerance cannot be taken out:`,
|
|
1773
|
+
`clearance \u2264 \u2300\u2081 = ${mm(clearance)}`
|
|
1774
|
+
);
|
|
1775
|
+
return lines;
|
|
1776
|
+
}
|
|
1777
|
+
const growth = (loose - strict) / (atLoose - atStrict);
|
|
1778
|
+
lines.push(
|
|
1779
|
+
` at t\u2081 = toleranceBand.atolIgnore = ${tol(atStrict)}`,
|
|
1780
|
+
`\u2300\u2082 = ${bandPath}.deviate.min = ${mm(loose)}`,
|
|
1781
|
+
` at t\u2082 = toleranceBand.atolDeviate = ${tol(atLoose)}`
|
|
1782
|
+
);
|
|
1783
|
+
if (growth <= 0) {
|
|
1784
|
+
lines.push("The tool does not grow with tolerance (a gap, not a corner): clearance = \u2300\u2081");
|
|
1785
|
+
} else {
|
|
1786
|
+
lines.push(
|
|
1787
|
+
`k = (\u2300\u2082 \u2212 \u2300\u2081) / (t\u2082 \u2212 t\u2081) = ${trim3(growth, 3)}`,
|
|
1788
|
+
`slack = k \xD7 t\u2081 = ${mm(growth * atStrict)}, what the tolerance adds to every tool`,
|
|
1789
|
+
`clearance = \u2300\u2081 \u2212 slack`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
lines.push(`clearance = ${mm(clearance)}`);
|
|
1793
|
+
return lines;
|
|
1794
|
+
};
|
|
1795
|
+
var trim3 = (value, places) => String(Number(value.toFixed(places)));
|
|
1796
|
+
var formatLength = (mm2, units, inchMark = false) => units === "inch" ? `${trim3(mm2 / MM_PER_INCH4, LENGTH_PLACES.inch)}${inchMark ? '"' : " in"}` : `${trim3(mm2, LENGTH_PLACES.mm)} mm`;
|
|
1797
|
+
var SURFACE_TYPES = /* @__PURE__ */ new Set([
|
|
1798
|
+
"contour_surface",
|
|
1799
|
+
"inner_fillet",
|
|
1800
|
+
"outer_fillet",
|
|
1801
|
+
"fillet"
|
|
1802
|
+
]);
|
|
1803
|
+
var isSurfaceType = (featureType) => SURFACE_TYPES.has(featureType);
|
|
1804
|
+
var maxBallDiameter = (sheet) => {
|
|
1805
|
+
const { fitToolDiameter: diameter, fitCornerRadius: corner } = sheet;
|
|
1806
|
+
if (corner === void 0 || corner <= 0) return void 0;
|
|
1807
|
+
return diameter !== void 0 && diameter > 0 ? Math.min(diameter, 2 * corner) : 2 * corner;
|
|
1808
|
+
};
|
|
1809
|
+
var ROW_ORDER = [
|
|
1810
|
+
"featureDepth",
|
|
1811
|
+
"undercutWidth",
|
|
1812
|
+
"floorWidth",
|
|
1813
|
+
"openingWidth",
|
|
1814
|
+
"undercutDepth",
|
|
1815
|
+
"taper",
|
|
1816
|
+
"diameter",
|
|
1817
|
+
"cornerRadius",
|
|
1818
|
+
"filletRadius",
|
|
1819
|
+
"floorFillet",
|
|
1820
|
+
"tipAngle",
|
|
1821
|
+
"thread",
|
|
1822
|
+
"bevelAngle",
|
|
1823
|
+
"finishTool",
|
|
1824
|
+
"undercutCutter",
|
|
1825
|
+
"maxShaft",
|
|
1826
|
+
"pinch",
|
|
1827
|
+
"fitCornerRadius",
|
|
1828
|
+
"maxBull",
|
|
1829
|
+
"maxBall",
|
|
1830
|
+
"depthBelowTop",
|
|
1831
|
+
"ld",
|
|
1832
|
+
"featureLd",
|
|
1833
|
+
"topLd"
|
|
1834
|
+
];
|
|
1835
|
+
var featureMeasurements = ({
|
|
1836
|
+
features,
|
|
1837
|
+
feature,
|
|
1838
|
+
sheets,
|
|
1839
|
+
units,
|
|
1840
|
+
inchMark = false
|
|
1841
|
+
}) => {
|
|
1842
|
+
const other = units === "inch" ? "mm" : "inch";
|
|
1843
|
+
const length = (mm2) => ({
|
|
1844
|
+
value: formatLength(mm2, units, inchMark),
|
|
1845
|
+
alt: formatLength(mm2, other, inchMark)
|
|
1846
|
+
});
|
|
1847
|
+
const sheet = sheets[feature.tag.toLowerCase()];
|
|
1848
|
+
const rows = [];
|
|
1849
|
+
const undercut = sheet?.undercut;
|
|
1850
|
+
const top = sheet ? partTop(features, sheets, feature.machiningDirection) : null;
|
|
1851
|
+
const bottom = sheet?.zMin;
|
|
1852
|
+
const topFrom = top === null ? void 0 : features.find((each) => sheets[each.tag.toLowerCase()]?.extendedZMax === top);
|
|
1853
|
+
const topLine = top === null ? "" : `Top of part = the highest extendedZMax of the features machined from this direction = ${mm(top)}${topFrom ? ` (${featureTypeLabel(topFrom.featureType)} ${topFrom.tag})` : ""}`;
|
|
1854
|
+
if (top !== null && bottom !== void 0) {
|
|
1855
|
+
rows.push({
|
|
1856
|
+
key: "depthBelowTop",
|
|
1857
|
+
label: "Depth below top of part",
|
|
1858
|
+
milling: true,
|
|
1859
|
+
...length(top - bottom),
|
|
1860
|
+
derivation: [topLine, `Top \u2212 zMin = ${mm(top)} \u2212 ${mm(bottom)} = ${mm(top - bottom)}`]
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
if (sheet?.zMax !== void 0 && bottom !== void 0 && !(undercut && sheet?.kind === "Tslot")) {
|
|
1864
|
+
rows.push({
|
|
1865
|
+
key: "featureDepth",
|
|
1866
|
+
label: "Feature depth",
|
|
1867
|
+
...length(sheet.zMax - bottom),
|
|
1868
|
+
derivation: [`zMax \u2212 zMin = ${mm(sheet.zMax)} \u2212 ${mm(bottom)} = ${mm(sheet.zMax - bottom)}`]
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
if (sheet?.diameter !== void 0) {
|
|
1872
|
+
rows.push({
|
|
1873
|
+
key: "diameter",
|
|
1874
|
+
label: "Diameter",
|
|
1875
|
+
...length(sheet.diameter),
|
|
1876
|
+
derivation: [`facts.diameter = ${mm(sheet.diameter)}`]
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
if (sheet?.cornerRadius !== void 0 && !undercut) {
|
|
1880
|
+
const radius = length(sheet.cornerRadius);
|
|
1881
|
+
rows.push({
|
|
1882
|
+
key: "cornerRadius",
|
|
1883
|
+
label: sheet.sharpCorners ? "Minimum radius (sharp)" : "Minimum radius",
|
|
1884
|
+
// Measured at one tolerance only, it is the most the modelled radius can be, not the radius itself.
|
|
1885
|
+
...sheet.cornerRadiusUnresolved ? { value: `\u2264 ${radius.value}`, alt: `\u2264 ${radius.alt}` } : radius,
|
|
1886
|
+
derivation: [
|
|
1887
|
+
"The tightest corner between walls: half the minimum clearance, the widest tool that reaches every point with the tolerance it was measured to taken out.",
|
|
1888
|
+
...sheet.clearance ? clearanceDerivation(sheet.clearance, 2 * sheet.cornerRadius) : [],
|
|
1889
|
+
`R = clearance / 2 = ${mm(sheet.cornerRadius)}${sheet.sharpCorners ? " (under 0.01 mm: sharp)" : ""}`
|
|
1890
|
+
]
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
if (!sheet?.sharpCorners && !undercut) {
|
|
1894
|
+
const clearance = sheet?.pinchDiameter ?? (sheet?.cornerRadius !== void 0 ? 2 * sheet.cornerRadius : void 0);
|
|
1895
|
+
if (clearance !== void 0) {
|
|
1896
|
+
const width = length(clearance);
|
|
1897
|
+
const places = sheet?.pinchPlaces;
|
|
1898
|
+
const floor = !SURFACE_TYPES.has(feature.featureType) && sheet?.filletRadius !== void 0 && sheet.filletRadius > 0 ? length(sheet.filletRadius) : void 0;
|
|
1899
|
+
const atMost = sheet?.cornerRadiusUnresolved ? "\u2264 " : "";
|
|
1900
|
+
rows.push({
|
|
1901
|
+
key: "pinch",
|
|
1902
|
+
label: "Max tool diameter",
|
|
1903
|
+
milling: true,
|
|
1904
|
+
// Measured at one tolerance only, it is the most the clearance can be, not the clearance itself.
|
|
1905
|
+
value: `${atMost}\u2300 ${width.value}${floor ? ` \xB7 R ${floor.value}` : ""}`,
|
|
1906
|
+
alt: `${atMost}\u2300 ${width.alt}${floor ? ` \xB7 R ${floor.alt}` : ""}`,
|
|
1907
|
+
derivation: [
|
|
1908
|
+
"The widest circle that fits where the feature is tightest: twice an inside corner\u2019s radius, or the width of a gap between walls.",
|
|
1909
|
+
...places !== void 0 ? [
|
|
1910
|
+
`pinchPoints: where the feature narrows to its tightest clearance, a disc each; ${places} ${places === 1 ? "place" : "places"} that tight, the tightest drawn.`,
|
|
1911
|
+
`min(pinchPoints[].diameter) is the cutter band\u2019s min, measured to a tolerance (t) the widest tool grows with: run back to none from two tolerances.`
|
|
1912
|
+
] : [
|
|
1913
|
+
"No pinch points reported: the cutter band\u2019s min, measured to a tolerance (t) the widest tool grows with, run back to none from two tolerances."
|
|
1914
|
+
],
|
|
1915
|
+
...sheet?.clearance ? clearanceDerivation(sheet.clearance, clearance) : [],
|
|
1916
|
+
...floor && sheet?.filletRadius !== void 0 ? [
|
|
1917
|
+
`A bull nose, its corner radius the floor fillet\u2019s: facts.filletRadius = ${mm(sheet.filletRadius)}`
|
|
1918
|
+
] : []
|
|
1919
|
+
]
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
if (sheet && undercut) rows.push(...undercutRows(sheet, undercut, top, length));
|
|
1924
|
+
const surface = SURFACE_TYPES.has(feature.featureType);
|
|
1925
|
+
if (surface && sheet) {
|
|
1926
|
+
const diameter = sheet.fitToolDiameter;
|
|
1927
|
+
const corner = sheet.fitCornerRadius;
|
|
1928
|
+
const fit = sheet.toolFitPath ?? "facts.toolFit";
|
|
1929
|
+
const ball = maxBallDiameter(sheet);
|
|
1930
|
+
rows.push({
|
|
1931
|
+
key: "finishTool",
|
|
1932
|
+
label: "Finishing tool",
|
|
1933
|
+
milling: true,
|
|
1934
|
+
value: sheet.ballOnly ? "Ball end mill" : "Ball or bull nose",
|
|
1935
|
+
derivation: [
|
|
1936
|
+
`facts.useOnlyBallToolsForFinish (or facts.three.\u2026) is ${sheet.ballOnly ? "true" : "not true"}`
|
|
1937
|
+
]
|
|
1938
|
+
});
|
|
1939
|
+
if (corner !== void 0 && corner > 0) {
|
|
1940
|
+
rows.push({
|
|
1941
|
+
key: "fitCornerRadius",
|
|
1942
|
+
label: "Max corner radius",
|
|
1943
|
+
milling: true,
|
|
1944
|
+
...length(corner),
|
|
1945
|
+
derivation: [
|
|
1946
|
+
"The tightest the surface turns inward: a tool with a larger corner radius cannot follow it.",
|
|
1947
|
+
`${fit}.cornerRadius = ${mm(corner)}`
|
|
1948
|
+
]
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
const sized = diameter !== void 0 && diameter > 0;
|
|
1952
|
+
if (!sheet.ballOnly && corner !== void 0 && corner > 0 && (!sized || ball === void 0 || ball < diameter)) {
|
|
1953
|
+
const size = sized ? length(diameter) : void 0;
|
|
1954
|
+
const radius = length(corner);
|
|
1955
|
+
rows.push({
|
|
1956
|
+
key: "maxBull",
|
|
1957
|
+
label: "Max bull nose",
|
|
1958
|
+
milling: true,
|
|
1959
|
+
// Drawable only at a diameter the fit gives: "any" is no size to draw.
|
|
1960
|
+
...sized ? { tool: "bull" } : {},
|
|
1961
|
+
value: `${size ? `\u2300 ${size.value}` : "any \u2300"} \xB7 R ${radius.value}`,
|
|
1962
|
+
alt: `${size ? `\u2300 ${size.alt}` : "any \u2300"} \xB7 R ${radius.alt}`,
|
|
1963
|
+
derivation: [
|
|
1964
|
+
sized ? "The largest full-diameter tool the surface\u2019s shape admits, with the largest corner radius it admits." : "The surface limits only the tool\u2019s corner radius, not its diameter: a bull nose of any size with a corner no larger than this.",
|
|
1965
|
+
...sized ? [`\u2300 = ${fit}.toolDiameter = ${mm(diameter)}`] : [`${fit}.toolDiameter is not reported`],
|
|
1966
|
+
`R = ${fit}.cornerRadius = ${mm(corner)}`
|
|
1967
|
+
]
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
if (ball !== void 0 && corner !== void 0) {
|
|
1971
|
+
const size = length(ball);
|
|
1972
|
+
rows.push({
|
|
1973
|
+
key: "maxBall",
|
|
1974
|
+
label: "Max ball nose",
|
|
1975
|
+
milling: true,
|
|
1976
|
+
tool: "ball",
|
|
1977
|
+
value: `\u2300 ${size.value}`,
|
|
1978
|
+
alt: `\u2300 ${size.alt}`,
|
|
1979
|
+
derivation: [
|
|
1980
|
+
"A ball\u2019s corner radius is half its diameter, so it must fit under the corner limit, and the diameter limit where there is one:",
|
|
1981
|
+
...diameter !== void 0 && diameter > 0 ? [
|
|
1982
|
+
`min(${fit}.toolDiameter, 2 \xD7 ${fit}.cornerRadius)`,
|
|
1983
|
+
`= min(${mm(diameter)}, ${mm(2 * corner)}) = ${mm(ball)}`
|
|
1984
|
+
] : [`2 \xD7 ${fit}.cornerRadius = ${mm(ball)} (${fit}.toolDiameter is not reported)`]
|
|
1985
|
+
]
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
const ld = undercut ? null : featureLd(features, feature, sheets);
|
|
1990
|
+
if (ld && top !== null && bottom !== void 0) {
|
|
1991
|
+
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)`;
|
|
1992
|
+
const ratioOf = (depth) => {
|
|
1993
|
+
const ratio2 = (depth / ld.across).toFixed(3);
|
|
1994
|
+
return ld.atLeast ? `\u2265 ${ratio2}` : ratio2;
|
|
1995
|
+
};
|
|
1996
|
+
if (ld.drilling) {
|
|
1997
|
+
rows.push({
|
|
1998
|
+
key: "ld",
|
|
1999
|
+
label: "Drilling L/D",
|
|
2000
|
+
milling: true,
|
|
2001
|
+
value: ratioOf(top - bottom),
|
|
2002
|
+
derivation: [
|
|
2003
|
+
`L = depth below top of part = ${mm(top - bottom)}`,
|
|
2004
|
+
across,
|
|
2005
|
+
`L / D = ${mm(top - bottom)} / ${mm(ld.across)} = ${ld.ratio.toFixed(3)}`
|
|
2006
|
+
]
|
|
2007
|
+
});
|
|
2008
|
+
} else {
|
|
2009
|
+
if (sheet?.zMax !== void 0) {
|
|
2010
|
+
rows.push({
|
|
2011
|
+
key: "featureLd",
|
|
2012
|
+
label: "Feature L/D",
|
|
2013
|
+
milling: true,
|
|
2014
|
+
value: ratioOf(sheet.zMax - bottom),
|
|
2015
|
+
derivation: [
|
|
2016
|
+
`L = feature depth = zMax \u2212 zMin = ${mm(sheet.zMax - bottom)}`,
|
|
2017
|
+
across,
|
|
2018
|
+
`L / D = ${mm(sheet.zMax - bottom)} / ${mm(ld.across)} = ${((sheet.zMax - bottom) / ld.across).toFixed(3)}`
|
|
2019
|
+
]
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
rows.push({
|
|
2023
|
+
key: "topLd",
|
|
2024
|
+
label: "L/D to top of part",
|
|
2025
|
+
milling: true,
|
|
2026
|
+
value: ratioOf(top - bottom),
|
|
2027
|
+
derivation: [
|
|
2028
|
+
`L = depth below top of part = ${mm(top - bottom)}`,
|
|
2029
|
+
across,
|
|
2030
|
+
`L / D = ${mm(top - bottom)} / ${mm(ld.across)} = ${ld.ratio.toFixed(3)}`
|
|
2031
|
+
]
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
if (sheet?.tipAngle !== void 0) {
|
|
2036
|
+
const flat = Math.abs(sheet.tipAngle - 180) < 0.05;
|
|
2037
|
+
rows.push({
|
|
2038
|
+
key: "tipAngle",
|
|
2039
|
+
label: "Tip angle",
|
|
2040
|
+
value: `${trim3(sheet.tipAngle, 3)}\xB0${flat ? " (flat)" : ""}`,
|
|
2041
|
+
derivation: [
|
|
2042
|
+
`facts.fullConeDeg = ${trim3(sheet.tipAngle, 3)}\xB0${flat ? " (180\xB0: flat bottom)" : ""}`
|
|
2043
|
+
]
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
if (sheet?.threading) {
|
|
2047
|
+
rows.push({
|
|
2048
|
+
key: "thread",
|
|
2049
|
+
label: "Thread",
|
|
2050
|
+
value: `${formatLength(sheet.threading.basicDiameter, units, inchMark)} \xD7 ${formatLength(sheet.threading.threadPitch, units, inchMark)} pitch`,
|
|
2051
|
+
derivation: [
|
|
2052
|
+
`facts.threading.spec.basicDiameter = ${mm(sheet.threading.basicDiameter)}`,
|
|
2053
|
+
`facts.threading.spec.threadPitch = ${mm(sheet.threading.threadPitch)}`
|
|
2054
|
+
]
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
if (surface) {
|
|
2058
|
+
if (sheet?.filletRadius !== void 0 && sheet.filletRadius !== 0) {
|
|
2059
|
+
rows.push({
|
|
2060
|
+
key: "filletRadius",
|
|
2061
|
+
label: "Fillet radius",
|
|
2062
|
+
...length(Math.abs(sheet.filletRadius)),
|
|
2063
|
+
derivation: [
|
|
2064
|
+
`|facts.filletRadius| = |${mm(sheet.filletRadius)}| (signed: + over an edge, \u2212 into a corner)`
|
|
2065
|
+
]
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
} else if (sheet?.filletRadius !== void 0 && sheet.filletRadius > 0) {
|
|
2069
|
+
rows.push({
|
|
2070
|
+
key: "floorFillet",
|
|
2071
|
+
label: "Floor fillet radius",
|
|
2072
|
+
...length(sheet.filletRadius),
|
|
2073
|
+
derivation: [`facts.filletRadius = ${mm(sheet.filletRadius)}`]
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
if (sheet?.chamferAngle !== void 0) {
|
|
2077
|
+
rows.push({
|
|
2078
|
+
key: "bevelAngle",
|
|
2079
|
+
label: "Chamfer angle",
|
|
2080
|
+
value: `${sheet.chamferAngle.toFixed(3)}\xB0`,
|
|
2081
|
+
derivation: [`facts.bevel.angleDeg = ${trim3(sheet.chamferAngle, 3)}\xB0`]
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
return rows.sort((a, b) => ROW_ORDER.indexOf(a.key) - ROW_ORDER.indexOf(b.key));
|
|
2085
|
+
};
|
|
2086
|
+
var undercutRows = (sheet, undercut, top, length) => {
|
|
2087
|
+
const rows = [];
|
|
2088
|
+
const tslot = sheet.kind === "Tslot";
|
|
2089
|
+
const { zMin, zMax } = sheet;
|
|
2090
|
+
const { floorWidth: floor, topOpeningWidth: opening } = undercut;
|
|
2091
|
+
const angle = undercut.taperDeg !== void 0 ? `${trim3(undercut.taperDeg, 3)}\xB0` : void 0;
|
|
2092
|
+
if (tslot && zMin !== void 0 && zMax !== void 0) {
|
|
2093
|
+
rows.push({
|
|
2094
|
+
key: "undercutWidth",
|
|
2095
|
+
label: "Undercut width",
|
|
2096
|
+
...length(zMax - zMin),
|
|
2097
|
+
derivation: [
|
|
2098
|
+
"The groove\u2019s height along the tool: the widest cutter it takes in one pass.",
|
|
2099
|
+
`zMax \u2212 zMin = ${mm(zMax)} \u2212 ${mm(zMin)} = ${mm(zMax - zMin)}`
|
|
2100
|
+
]
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
if (floor !== void 0) {
|
|
2104
|
+
rows.push({
|
|
2105
|
+
key: "floorWidth",
|
|
2106
|
+
label: "Floor width",
|
|
2107
|
+
...length(floor),
|
|
2108
|
+
derivation: ["The widest clearance over the floor", `facts.floorWidth = ${mm(floor)}`]
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
if (opening !== void 0) {
|
|
2112
|
+
rows.push({
|
|
2113
|
+
key: "openingWidth",
|
|
2114
|
+
label: "Opening width",
|
|
2115
|
+
...length(opening),
|
|
2116
|
+
derivation: [
|
|
2117
|
+
"The clearance through the opening at the top of the groove",
|
|
2118
|
+
`facts.topOpeningWidth = ${mm(opening)}`
|
|
2119
|
+
]
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
let depth = undercut.undercutDepth;
|
|
2123
|
+
let depthLine = depth !== void 0 ? `facts.undercutDepth = ${mm(depth)}` : "";
|
|
2124
|
+
if (depth === void 0 && floor !== void 0 && opening !== void 0) {
|
|
2125
|
+
if (floor > opening && !undercut.isExternal) {
|
|
2126
|
+
depth = (floor - opening) / 2;
|
|
2127
|
+
depthLine = `(facts.floorWidth \u2212 facts.topOpeningWidth) / 2 = (${mm(floor)} \u2212 ${mm(opening)}) / 2 = ${mm(depth)}`;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
if (depth !== void 0) {
|
|
2131
|
+
rows.push({
|
|
2132
|
+
key: "undercutDepth",
|
|
2133
|
+
label: "Undercut depth",
|
|
2134
|
+
...length(depth),
|
|
2135
|
+
derivation: [
|
|
2136
|
+
"How far the groove runs back from its opening: what the cutter\u2019s head reaches past its shaft.",
|
|
2137
|
+
depthLine
|
|
2138
|
+
]
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
if (angle) {
|
|
2142
|
+
rows.push({
|
|
2143
|
+
key: "taper",
|
|
2144
|
+
label: "Dovetail angle",
|
|
2145
|
+
value: `${angle} from the tool axis`,
|
|
2146
|
+
derivation: [`facts.taperDeg = ${angle}, between the overhanging wall and the tool axis`]
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
const cutter = (value, derivation, alt) => ({
|
|
2150
|
+
key: "undercutCutter",
|
|
2151
|
+
label: "Max cutting diameter",
|
|
2152
|
+
milling: true,
|
|
2153
|
+
value,
|
|
2154
|
+
...alt !== void 0 ? { alt } : {},
|
|
2155
|
+
derivation
|
|
2156
|
+
});
|
|
2157
|
+
if (undercut.unmeasured) {
|
|
2158
|
+
return [
|
|
2159
|
+
...rows,
|
|
2160
|
+
cutter("Not measured", [
|
|
2161
|
+
"The Engine could not measure the groove (facts.isInvalidGeometry or facts.cd.measurementFailed), so its figures offer no cutter."
|
|
2162
|
+
])
|
|
2163
|
+
];
|
|
2164
|
+
}
|
|
2165
|
+
if (sheet.noToolFits) {
|
|
2166
|
+
return [
|
|
2167
|
+
...rows,
|
|
2168
|
+
cutter("None fits", [
|
|
2169
|
+
"facts.cd.ignore.max is next to nothing: no cutter fits anywhere in the groove."
|
|
2170
|
+
])
|
|
2171
|
+
];
|
|
2172
|
+
}
|
|
2173
|
+
const band = sheet.maxTool !== void 0 && sheet.maxTool > 0 ? sheet.maxTool : void 0;
|
|
2174
|
+
const dropIn = undercut.isClosed ? undercut.maxEntry : void 0;
|
|
2175
|
+
const head = band === void 0 ? void 0 : Math.min(band, dropIn ?? Infinity);
|
|
2176
|
+
const opensTo = undercut.maxEntry ?? opening;
|
|
2177
|
+
const behind = head !== void 0 && depth !== void 0 ? head - 2 * depth : void 0;
|
|
2178
|
+
const limits = [opensTo, behind].filter((each) => each !== void 0);
|
|
2179
|
+
const shaft = limits.length > 0 ? Math.max(Math.min(...limits), 0) : void 0;
|
|
2180
|
+
const shaftLines = [
|
|
2181
|
+
"The most the cutter\u2019s shaft can be: what comes down through the opening above the groove, and leaves the head reaching the back of it.",
|
|
2182
|
+
...opensTo !== void 0 ? [`Opening: ${tslot ? "facts.maxEntryCd" : "facts.topOpeningWidth"} = ${mm(opensTo)}`] : [],
|
|
2183
|
+
...behind !== void 0 && head !== void 0 && depth !== void 0 ? [
|
|
2184
|
+
`Reach: max cutting diameter \u2212 2 \xD7 undercut depth = ${mm(head)} \u2212 2 \xD7 ${mm(depth)} = ${mm(behind)}`
|
|
2185
|
+
] : [],
|
|
2186
|
+
...shaft !== void 0 ? [`Max shaft = ${mm(shaft)}`] : []
|
|
2187
|
+
];
|
|
2188
|
+
if (head !== void 0 && band !== void 0) {
|
|
2189
|
+
const headLines = [
|
|
2190
|
+
"The biggest head that reaches every point of the groove.",
|
|
2191
|
+
`facts.cd.ignore.min = ${mm(band)}`,
|
|
2192
|
+
...dropIn !== void 0 ? [
|
|
2193
|
+
`A closed slot: the head comes down through the opening above it, facts.maxEntryCd = ${mm(dropIn)}`
|
|
2194
|
+
] : [],
|
|
2195
|
+
`\u2300 = ${mm(head)}`
|
|
2196
|
+
];
|
|
2197
|
+
if (head < SHARP_MM || shaft !== void 0 && shaft < SHARP_MM) {
|
|
2198
|
+
return [
|
|
2199
|
+
...rows,
|
|
2200
|
+
cutter("None fits", [
|
|
2201
|
+
...headLines,
|
|
2202
|
+
...shaft !== void 0 ? shaftLines : [],
|
|
2203
|
+
"No head that fits the groove leaves room for a shaft to carry it."
|
|
2204
|
+
])
|
|
2205
|
+
];
|
|
2206
|
+
}
|
|
2207
|
+
const size = length(head);
|
|
2208
|
+
const filletRadius = sheet.filletRadius !== void 0 && sheet.filletRadius > 0 ? sheet.filletRadius : void 0;
|
|
2209
|
+
const fillet = filletRadius !== void 0 ? length(filletRadius) : void 0;
|
|
2210
|
+
const describe = (unit2) => [`\u2300 ${size[unit2]}`, ...angle ? [angle] : [], ...fillet ? [`R ${fillet[unit2]}`] : []].join(
|
|
2211
|
+
" \xB7 "
|
|
2212
|
+
);
|
|
2213
|
+
rows.push(
|
|
2214
|
+
cutter(
|
|
2215
|
+
describe("value"),
|
|
2216
|
+
[
|
|
2217
|
+
...headLines,
|
|
2218
|
+
...angle ? [`Angle = facts.taperDeg = ${angle}, from the tool axis`] : [],
|
|
2219
|
+
...filletRadius !== void 0 ? [`R = facts.filletRadius = ${mm(filletRadius)}, the blend the head\u2019s corners cut`] : []
|
|
2220
|
+
],
|
|
2221
|
+
describe("alt")
|
|
2222
|
+
)
|
|
2223
|
+
);
|
|
2224
|
+
if (top !== null && zMin !== void 0) {
|
|
2225
|
+
const reach = top - zMin;
|
|
2226
|
+
const ratio2 = (reach / head).toFixed(3);
|
|
2227
|
+
rows.push({
|
|
2228
|
+
key: "topLd",
|
|
2229
|
+
label: "L/D to top of part",
|
|
2230
|
+
milling: true,
|
|
2231
|
+
value: ratio2,
|
|
2232
|
+
derivation: [
|
|
2233
|
+
`L = depth below top of part = ${mm(reach)}`,
|
|
2234
|
+
`D = max cutting diameter = ${mm(head)}`,
|
|
2235
|
+
`L / D = ${mm(reach)} / ${mm(head)} = ${ratio2}`
|
|
2236
|
+
]
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
if (shaft !== void 0 || tslot) {
|
|
2241
|
+
const size = shaft !== void 0 ? length(shaft) : void 0;
|
|
2242
|
+
rows.push({
|
|
2243
|
+
key: "maxShaft",
|
|
2244
|
+
label: "Max shaft diameter",
|
|
2245
|
+
milling: true,
|
|
2246
|
+
...size ? { value: `\u2300 ${size.value}`, alt: `\u2300 ${size.alt}` } : { value: "No limit" },
|
|
2247
|
+
derivation: size ? shaftLines : [
|
|
2248
|
+
"facts.maxEntryCd is not reported and the head or undercut depth is not known: nothing limits the shaft."
|
|
2249
|
+
]
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
return rows;
|
|
2253
|
+
};
|
|
2254
|
+
|
|
2255
|
+
// src/model/feature-record.ts
|
|
2256
|
+
var MAX_DATASHEET_FIELDS = 500;
|
|
2257
|
+
var formatLeaf = (value) => typeof value === "number" ? String(Number(value.toFixed(4))) : typeof value === "string" ? value : value === null ? "null" : String(value);
|
|
2258
|
+
var datasheetFields = (datasheet) => {
|
|
2259
|
+
const fields = [];
|
|
2260
|
+
let more = false;
|
|
2261
|
+
const walk = (value, path) => {
|
|
2262
|
+
if (fields.length >= MAX_DATASHEET_FIELDS) {
|
|
2263
|
+
more = true;
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
if (Array.isArray(value)) {
|
|
2267
|
+
if (value.every((each) => typeof each !== "object" || each === null)) {
|
|
2268
|
+
fields.push({ path, value: value.length === 0 ? "[]" : value.map(formatLeaf).join(", ") });
|
|
2269
|
+
} else {
|
|
2270
|
+
value.forEach((each, index) => walk(each, `${path}[${index}]`));
|
|
2271
|
+
}
|
|
2272
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2273
|
+
const entries = Object.entries(value);
|
|
2274
|
+
if (entries.length === 0) fields.push({ path, value: "{}" });
|
|
2275
|
+
for (const [key, each] of entries) walk(each, path ? `${path}.${key}` : key);
|
|
2276
|
+
} else {
|
|
2277
|
+
fields.push({ path, value: formatLeaf(value) });
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
if (datasheet !== null && datasheet !== void 0) walk(datasheet, "");
|
|
2281
|
+
return { fields, more };
|
|
2282
|
+
};
|
|
2283
|
+
|
|
1301
2284
|
// src/model/hole-groups.ts
|
|
1302
2285
|
var holeSize = (sheets, tag) => {
|
|
1303
2286
|
const sheet = sheets[tag.toLowerCase()];
|
|
@@ -1313,11 +2296,11 @@ var sameDepth = (a, b) => Math.abs(a - b) <= Math.max(DEPTH_FLOOR, Math.max(Math
|
|
|
1313
2296
|
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;
|
|
1314
2297
|
var sameHoleSets = (features, sheets) => {
|
|
1315
2298
|
const sizes = features.map((feature) => holeSize(sheets, feature.tag));
|
|
1316
|
-
const root = features.map((_,
|
|
1317
|
-
const find = (
|
|
1318
|
-
let top =
|
|
2299
|
+
const root = features.map((_, at2) => at2);
|
|
2300
|
+
const find = (at2) => {
|
|
2301
|
+
let top = at2;
|
|
1319
2302
|
while (root[top] !== top) top = root[top] ?? top;
|
|
1320
|
-
root[
|
|
2303
|
+
root[at2] = top;
|
|
1321
2304
|
return top;
|
|
1322
2305
|
};
|
|
1323
2306
|
for (let a = 0; a < features.length; a += 1) {
|
|
@@ -1331,8 +2314,8 @@ var sameHoleSets = (features, sheets) => {
|
|
|
1331
2314
|
}
|
|
1332
2315
|
}
|
|
1333
2316
|
const sets = /* @__PURE__ */ new Map();
|
|
1334
|
-
features.forEach((feature,
|
|
1335
|
-
const top = find(
|
|
2317
|
+
features.forEach((feature, at2) => {
|
|
2318
|
+
const top = find(at2);
|
|
1336
2319
|
sets.set(top, [...sets.get(top) ?? [], feature]);
|
|
1337
2320
|
});
|
|
1338
2321
|
return [...sets.values()];
|
|
@@ -1363,9 +2346,286 @@ var featureRows = (tags, features, sheets) => {
|
|
|
1363
2346
|
});
|
|
1364
2347
|
};
|
|
1365
2348
|
|
|
1366
|
-
// src/model/
|
|
1367
|
-
|
|
1368
|
-
var
|
|
2349
|
+
// src/model/pinch-mark.ts
|
|
2350
|
+
import { MM_PER_INCH as MM_PER_INCH5 } from "@toolpath/tool-support";
|
|
2351
|
+
var MAX_DRAWN_TOOL_MM = 3 * MM_PER_INCH5;
|
|
2352
|
+
var pinchMark = (datasheet, sheet, featureType) => {
|
|
2353
|
+
const pinch = readPinch(datasheet);
|
|
2354
|
+
const discs = pinch?.discs.filter(
|
|
2355
|
+
(disc) => disc.diameter >= PINCH_TOOL_MM && disc.diameter <= MAX_DRAWN_TOOL_MM
|
|
2356
|
+
) ?? [];
|
|
2357
|
+
if (!pinch || discs.length === 0) return null;
|
|
2358
|
+
const fillet = sheet?.filletRadius;
|
|
2359
|
+
const corner = !isSurfaceType(featureType) && fillet !== void 0 && fillet > 0 ? fillet : void 0;
|
|
2360
|
+
return { ...pinch, discs, ...corner !== void 0 ? { corner } : {} };
|
|
2361
|
+
};
|
|
2362
|
+
var pinchLabel = (mark, units) => {
|
|
2363
|
+
const length = (mm2) => `${lengthValue(mm2, units)} ${unitLabel(units)}`;
|
|
2364
|
+
const tightest = Math.min(...mark.discs.map((disc) => disc.diameter));
|
|
2365
|
+
const tool = mark.corner !== void 0 ? `bull nose R ${length(mark.corner)}` : "widest tool";
|
|
2366
|
+
const places = mark.discs.length;
|
|
2367
|
+
return {
|
|
2368
|
+
label: `${mark.unresolved ? "\u2264 " : ""}\u2300 ${length(tightest)}`,
|
|
2369
|
+
note: places > 1 ? `${tool} \xB7 ${places} places` : tool
|
|
2370
|
+
};
|
|
2371
|
+
};
|
|
2372
|
+
|
|
2373
|
+
// src/model/tool-frame.ts
|
|
2374
|
+
var extent = (values) => {
|
|
2375
|
+
let low = Infinity;
|
|
2376
|
+
let high = -Infinity;
|
|
2377
|
+
for (const value of values) {
|
|
2378
|
+
if (value < low) low = value;
|
|
2379
|
+
if (value > high) high = value;
|
|
2380
|
+
}
|
|
2381
|
+
return { low, high };
|
|
2382
|
+
};
|
|
2383
|
+
var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
2384
|
+
var cross = (a, b) => [
|
|
2385
|
+
a[1] * b[2] - a[2] * b[1],
|
|
2386
|
+
a[2] * b[0] - a[0] * b[2],
|
|
2387
|
+
a[0] * b[1] - a[1] * b[0]
|
|
2388
|
+
];
|
|
2389
|
+
var scale = (a, by) => [a[0] * by, a[1] * by, a[2] * by];
|
|
2390
|
+
var add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
2391
|
+
var unit = (a) => scale(a, 1 / Math.hypot(a[0], a[1], a[2]));
|
|
2392
|
+
var ON_DEPTH_MM = 0.05;
|
|
2393
|
+
var ON_CORNER_MM = 0.02;
|
|
2394
|
+
var POINT_DISC_MM = 0.01;
|
|
2395
|
+
var ON_WALL_MM = 0.1;
|
|
2396
|
+
var ON_WALL_SHARE = 0.02;
|
|
2397
|
+
var WALL_NORMAL_ALONG = 0.7;
|
|
2398
|
+
var SAME_PLACE_MM = 0.05;
|
|
2399
|
+
var MIN_HEIGHT_MM = 0.05;
|
|
2400
|
+
var readTriangles = ({ positions, normals }, origin) => {
|
|
2401
|
+
const triangles = [];
|
|
2402
|
+
for (let at2 = 0; at2 + 8 < positions.length; at2 += 9) {
|
|
2403
|
+
const corner = (k) => [
|
|
2404
|
+
(positions[at2 + k] ?? 0) - origin[0],
|
|
2405
|
+
(positions[at2 + k + 1] ?? 0) - origin[1],
|
|
2406
|
+
(positions[at2 + k + 2] ?? 0) - origin[2]
|
|
2407
|
+
];
|
|
2408
|
+
const n = at2 / 9 * 3;
|
|
2409
|
+
triangles.push({
|
|
2410
|
+
a: corner(0),
|
|
2411
|
+
b: corner(3),
|
|
2412
|
+
c: corner(6),
|
|
2413
|
+
normal: [normals[n] ?? 0, normals[n + 1] ?? 0, normals[n + 2] ?? 0]
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
return triangles;
|
|
2417
|
+
};
|
|
2418
|
+
var pointsOf = (triangles) => triangles.flatMap(({ a, b, c }) => [a, b, c]);
|
|
2419
|
+
var flatten = (point, { u, v }) => ({ x: dot(point, u), y: dot(point, v) });
|
|
2420
|
+
var wallSegments = (triangles, frame) => triangles.filter(({ normal }) => Math.abs(dot(normal, frame.w)) < WALL_NORMAL_ALONG).flatMap(({ a, b, c, normal }) => {
|
|
2421
|
+
const [fa, fb, fc] = [flatten(a, frame), flatten(b, frame), flatten(c, frame)];
|
|
2422
|
+
const air = flatten(normal, frame);
|
|
2423
|
+
return [
|
|
2424
|
+
{ p: fa, q: fb, air },
|
|
2425
|
+
{ p: fb, q: fc, air },
|
|
2426
|
+
{ p: fc, q: fa, air }
|
|
2427
|
+
];
|
|
2428
|
+
});
|
|
2429
|
+
var nearestOn = ({ p, q }, at2) => {
|
|
2430
|
+
const dx = q.x - p.x;
|
|
2431
|
+
const dy = q.y - p.y;
|
|
2432
|
+
const length = dx * dx + dy * dy;
|
|
2433
|
+
const t = length === 0 ? 0 : Math.min(1, Math.max(0, ((at2.x - p.x) * dx + (at2.y - p.y) * dy) / length));
|
|
2434
|
+
return { x: p.x + t * dx, y: p.y + t * dy };
|
|
2435
|
+
};
|
|
2436
|
+
var nearestWall = (segments, at2) => segments.reduce((best, segment) => {
|
|
2437
|
+
const point = nearestOn(segment, at2);
|
|
2438
|
+
const distance2 = Math.hypot(point.x - at2.x, point.y - at2.y);
|
|
2439
|
+
if (best && distance2 >= best.distance) return best;
|
|
2440
|
+
const open = (at2.x - point.x) * segment.air.x + (at2.y - point.y) * segment.air.y >= 0;
|
|
2441
|
+
return { point, distance: distance2, open };
|
|
2442
|
+
}, null);
|
|
2443
|
+
var fitsDiscs = (triangles, frame, discs) => {
|
|
2444
|
+
const points = discs.filter((disc) => disc.diameter < POINT_DISC_MM);
|
|
2445
|
+
const wide = discs.filter((disc) => disc.diameter >= POINT_DISC_MM);
|
|
2446
|
+
if (points.length > 0) {
|
|
2447
|
+
const flat = pointsOf(triangles).map((point) => flatten(point, frame));
|
|
2448
|
+
const onCorners = points.every(
|
|
2449
|
+
(disc) => flat.some((point) => Math.hypot(point.x - disc.x, point.y - disc.y) <= ON_CORNER_MM)
|
|
2450
|
+
);
|
|
2451
|
+
if (!onCorners) return false;
|
|
2452
|
+
}
|
|
2453
|
+
if (wide.length === 0) return points.length > 0;
|
|
2454
|
+
const segments = wallSegments(triangles, frame);
|
|
2455
|
+
if (segments.length === 0) return false;
|
|
2456
|
+
let touches = false;
|
|
2457
|
+
for (const disc of wide) {
|
|
2458
|
+
const nearest = nearestWall(segments, disc);
|
|
2459
|
+
if (!nearest || !nearest.open) return false;
|
|
2460
|
+
const slack = ON_WALL_MM + ON_WALL_SHARE * disc.diameter;
|
|
2461
|
+
const radius = disc.diameter / 2;
|
|
2462
|
+
if (nearest.distance < radius - slack) return false;
|
|
2463
|
+
if (nearest.distance <= radius + slack) touches = true;
|
|
2464
|
+
}
|
|
2465
|
+
return touches;
|
|
2466
|
+
};
|
|
2467
|
+
var placeInFrame = ({ u, v, w }, x, y, z) => add(add(scale(u, x), scale(v, y)), scale(w, z));
|
|
2468
|
+
var AXES = [
|
|
2469
|
+
[1, 0, 0],
|
|
2470
|
+
[0, 1, 0],
|
|
2471
|
+
[0, 0, 1]
|
|
2472
|
+
];
|
|
2473
|
+
var candidateFrames = (points, zMin, zMax, direction, strict = true) => {
|
|
2474
|
+
const along = unit(direction);
|
|
2475
|
+
const frames = [];
|
|
2476
|
+
for (const sign of [1, -1]) {
|
|
2477
|
+
const w = scale(along, sign);
|
|
2478
|
+
const { low, high } = extent(points.map((point) => dot(point, w)));
|
|
2479
|
+
if (strict && (Math.abs(low - zMin) > ON_DEPTH_MM || Math.abs(high - zMax) > ON_DEPTH_MM)) {
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
const across = AXES.reduce(
|
|
2483
|
+
(best, axis) => Math.abs(dot(axis, w)) < Math.abs(dot(best, w)) ? axis : best
|
|
2484
|
+
);
|
|
2485
|
+
const u0 = unit(add(across, scale(w, -dot(across, w))));
|
|
2486
|
+
const v0 = cross(w, u0);
|
|
2487
|
+
for (let quarter = 0; quarter < 4; quarter++) {
|
|
2488
|
+
const angle = quarter * Math.PI / 2;
|
|
2489
|
+
const u = add(scale(u0, Math.cos(angle)), scale(v0, Math.sin(angle)));
|
|
2490
|
+
for (const hand of [1, -1]) frames.push({ u, v: scale(cross(w, u), hand), w });
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
return frames;
|
|
2494
|
+
};
|
|
2495
|
+
var distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
2496
|
+
var exactFrame = (triangles, discs, zMin, zMax, direction) => {
|
|
2497
|
+
const fits = candidateFrames(pointsOf(triangles), zMin, zMax, direction).filter(
|
|
2498
|
+
(frame) => fitsDiscs(triangles, frame, discs)
|
|
2499
|
+
);
|
|
2500
|
+
const [first] = fits;
|
|
2501
|
+
if (!first) return null;
|
|
2502
|
+
const placed = (frame) => discs.map((disc) => placeInFrame(frame, disc.x, disc.y, zMin));
|
|
2503
|
+
const firstPlaces = placed(first);
|
|
2504
|
+
const agree = fits.every(
|
|
2505
|
+
(frame) => placed(frame).every((at2, index) => distance(at2, firstPlaces[index] ?? at2) <= SAME_PLACE_MM)
|
|
2506
|
+
);
|
|
2507
|
+
return agree ? first : null;
|
|
2508
|
+
};
|
|
2509
|
+
var discMiss = (triangles, frame, discs) => {
|
|
2510
|
+
let miss = 0;
|
|
2511
|
+
const points = discs.filter((disc) => disc.diameter < POINT_DISC_MM);
|
|
2512
|
+
if (points.length > 0) {
|
|
2513
|
+
const flat = pointsOf(triangles).map((point) => flatten(point, frame));
|
|
2514
|
+
for (const disc of points) {
|
|
2515
|
+
miss += extent(flat.map((point) => Math.hypot(point.x - disc.x, point.y - disc.y))).low;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
const wide = discs.filter((disc) => disc.diameter >= POINT_DISC_MM);
|
|
2519
|
+
if (wide.length === 0) return miss;
|
|
2520
|
+
const segments = wallSegments(triangles, frame);
|
|
2521
|
+
let touch = Infinity;
|
|
2522
|
+
for (const disc of wide) {
|
|
2523
|
+
const radius = disc.diameter / 2;
|
|
2524
|
+
const nearest = nearestWall(segments, disc);
|
|
2525
|
+
if (!nearest) return Infinity;
|
|
2526
|
+
if (!nearest.open) miss += disc.diameter * 4;
|
|
2527
|
+
miss += Math.max(0, radius - nearest.distance);
|
|
2528
|
+
touch = Math.min(touch, Math.abs(nearest.distance - radius));
|
|
2529
|
+
}
|
|
2530
|
+
return miss + touch;
|
|
2531
|
+
};
|
|
2532
|
+
var bestFrame = (triangles, discs, zMin, zMax, direction) => {
|
|
2533
|
+
if (triangles.length === 0 || discs.length === 0) return null;
|
|
2534
|
+
const exact = exactFrame(triangles, discs, zMin, zMax, direction);
|
|
2535
|
+
if (exact) return { frame: exact, lift: 0 };
|
|
2536
|
+
const points = pointsOf(triangles);
|
|
2537
|
+
const fitting = candidateFrames(points, zMin, zMax, direction);
|
|
2538
|
+
const frames = fitting.length > 0 ? fitting : candidateFrames(points, zMin, zMax, direction, false);
|
|
2539
|
+
const best = frames.reduce((was, frame) => {
|
|
2540
|
+
const miss = discMiss(triangles, frame, discs);
|
|
2541
|
+
return !was || miss < was.miss ? { frame, miss } : was;
|
|
2542
|
+
}, null);
|
|
2543
|
+
if (!best) return null;
|
|
2544
|
+
const { low } = extent(points.map((point) => dot(point, best.frame.w)));
|
|
2545
|
+
return { frame: best.frame, lift: fitting.length > 0 ? 0 : low - zMin };
|
|
2546
|
+
};
|
|
2547
|
+
var asVec = ([x, y, z]) => ({ x, y, z });
|
|
2548
|
+
var placePinchTool = (mark, triangles, direction, origin = { x: 0, y: 0, z: 0 }) => {
|
|
2549
|
+
const shift = [origin.x, origin.y, origin.z];
|
|
2550
|
+
const faces = readTriangles(triangles, shift);
|
|
2551
|
+
const found = bestFrame(faces, mark.discs, mark.zMin, mark.zMax, [
|
|
2552
|
+
direction.x,
|
|
2553
|
+
direction.y,
|
|
2554
|
+
direction.z
|
|
2555
|
+
]);
|
|
2556
|
+
const disc = mark.discs.reduce(
|
|
2557
|
+
(tightest, each) => !tightest || each.diameter < tightest.diameter ? each : tightest,
|
|
2558
|
+
null
|
|
2559
|
+
);
|
|
2560
|
+
if (!found || !disc) return null;
|
|
2561
|
+
const { frame, lift } = found;
|
|
2562
|
+
const radius = disc.diameter / 2;
|
|
2563
|
+
const base = add(placeInFrame(frame, disc.x, disc.y, mark.zMin + lift), shift);
|
|
2564
|
+
const contact = nearestWall(wallSegments(faces, frame), disc)?.point;
|
|
2565
|
+
const toward = contact ? { x: contact.x - disc.x, y: contact.y - disc.y } : { x: 1, y: 0 };
|
|
2566
|
+
const length = Math.hypot(toward.x, toward.y);
|
|
2567
|
+
const across = add(
|
|
2568
|
+
scale(frame.u, length > 1e-9 ? toward.x / length : 1),
|
|
2569
|
+
scale(frame.v, length > 1e-9 ? toward.y / length : 0)
|
|
2570
|
+
);
|
|
2571
|
+
return {
|
|
2572
|
+
base: asVec(base),
|
|
2573
|
+
axis: asVec(frame.w),
|
|
2574
|
+
diameter: disc.diameter,
|
|
2575
|
+
height: Math.max(mark.zMax - mark.zMin, MIN_HEIGHT_MM),
|
|
2576
|
+
cornerRadius: Math.min(Math.max(mark.corner ?? 0, 0), radius),
|
|
2577
|
+
across: asVec(across)
|
|
2578
|
+
};
|
|
2579
|
+
};
|
|
2580
|
+
|
|
2581
|
+
// src/model/reach.ts
|
|
2582
|
+
var numbers = (value) => Array.isArray(value) && value.every((each) => typeof each === "number" && Number.isFinite(each)) ? value : null;
|
|
2583
|
+
var readReachCurve = (datasheet) => {
|
|
2584
|
+
const curve = datasheet?.reachCurve;
|
|
2585
|
+
const offsets = numbers(curve?.horizontalOffset);
|
|
2586
|
+
const heights = numbers(curve?.verticalOffset);
|
|
2587
|
+
if (!offsets || !heights || offsets.length === 0 || offsets.length !== heights.length) return null;
|
|
2588
|
+
return { offsets, heights };
|
|
2589
|
+
};
|
|
2590
|
+
var runStart = ({ offsets }, index) => index === 0 ? 0 : offsets[index - 1] ?? 0;
|
|
2591
|
+
var peakHeight = ({ heights }) => Math.max(0, ...heights);
|
|
2592
|
+
var TAIL_MM = 12.7;
|
|
2593
|
+
var peakSlack = (peak) => Math.max(0.05, peak * 0.01);
|
|
2594
|
+
var peakStart = (curve) => {
|
|
2595
|
+
const peak = peakHeight(curve);
|
|
2596
|
+
const slack = peakSlack(peak);
|
|
2597
|
+
const index = curve.heights.findIndex((height) => height >= peak - slack);
|
|
2598
|
+
return runStart(curve, index === -1 ? 0 : index);
|
|
2599
|
+
};
|
|
2600
|
+
var trimmedCurve = (curve) => {
|
|
2601
|
+
const last = curve.offsets[curve.offsets.length - 1] ?? 0;
|
|
2602
|
+
const extent2 = peakStart(curve) + TAIL_MM;
|
|
2603
|
+
if (extent2 >= last) return { curve, cut: false };
|
|
2604
|
+
const keep = curve.offsets.findIndex((offset) => offset >= extent2);
|
|
2605
|
+
return {
|
|
2606
|
+
curve: {
|
|
2607
|
+
offsets: [...curve.offsets.slice(0, keep), extent2],
|
|
2608
|
+
heights: curve.heights.slice(0, keep + 1)
|
|
2609
|
+
},
|
|
2610
|
+
cut: true
|
|
2611
|
+
};
|
|
2612
|
+
};
|
|
2613
|
+
var featureAcross = (sheet) => {
|
|
2614
|
+
if (sheet?.diameter !== void 0 && sheet.diameter > 0)
|
|
2615
|
+
return { width: sheet.diameter, kind: "diameter" };
|
|
2616
|
+
if (sheet?.pinchDiameter !== void 0 && sheet.pinchDiameter > 0)
|
|
2617
|
+
return { width: sheet.pinchDiameter, kind: "clearance" };
|
|
2618
|
+
return null;
|
|
2619
|
+
};
|
|
2620
|
+
var isThroughType = (featureType) => /(^|_)through(_|$)/.test(featureType.toLowerCase());
|
|
2621
|
+
var featureProfile = (sheet, featureType) => {
|
|
2622
|
+
const depth = sheet?.zMax !== void 0 && sheet.zMin !== void 0 ? sheet.zMax - sheet.zMin : null;
|
|
2623
|
+
return {
|
|
2624
|
+
across: featureAcross(sheet),
|
|
2625
|
+
depth: depth !== null && depth > 0 ? depth : null,
|
|
2626
|
+
through: isThroughType(featureType)
|
|
2627
|
+
};
|
|
2628
|
+
};
|
|
1369
2629
|
|
|
1370
2630
|
// src/model/rule-set-file.ts
|
|
1371
2631
|
var FILE_KIND = "toolpath-dfm-rules";
|
|
@@ -1528,8 +2788,8 @@ var llmRulePrompt = (current = [], vocabulary = MILLING_RULES) => {
|
|
|
1528
2788
|
const groups = vocabulary.subjects.filter((each) => !each.value.startsWith(TYPE_SUBJECT));
|
|
1529
2789
|
const types = vocabulary.subjects.filter((each) => each.value.startsWith(TYPE_SUBJECT));
|
|
1530
2790
|
const metricLines = vocabulary.metrics.map((metric) => {
|
|
1531
|
-
const
|
|
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}${
|
|
2791
|
+
const unit2 = quantityUnit(metric.quantity, "mm");
|
|
2792
|
+
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}${unit2 ? `, in ${unit2}` : ""}; a new rule usually starts around ${metric.start.mm}`;
|
|
1533
2793
|
const kinds = metric.kinds ? ` Only features of kind ${metric.kinds.join("/")} report it.` : "";
|
|
1534
2794
|
return `- \`${metric.key}\` \u2014 "${metric.label}" (${kind}). ${metric.hint}.${kinds}`;
|
|
1535
2795
|
});
|
|
@@ -1898,52 +3158,203 @@ var TURNING_RULES = {
|
|
|
1898
3158
|
fileName: "dfm-turning-rules.json"
|
|
1899
3159
|
};
|
|
1900
3160
|
|
|
1901
|
-
// src/model/
|
|
1902
|
-
var
|
|
1903
|
-
var
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
const heights = numbers(curve?.verticalOffset);
|
|
1907
|
-
if (!offsets || !heights || offsets.length === 0 || offsets.length !== heights.length) return null;
|
|
1908
|
-
return { offsets, heights };
|
|
3161
|
+
// src/model/turned-check.ts
|
|
3162
|
+
var regionsOf = (feature) => Array.isArray(feature.regionIdxs) ? feature.regionIdxs : [];
|
|
3163
|
+
var stretchKey = (feature) => {
|
|
3164
|
+
const regions = regionsOf(feature);
|
|
3165
|
+
return regions.length === 0 ? `tag:${feature.featureTag}` : `${feature.featureType}:${[...regions].sort((a, b) => a - b).join(",")}`;
|
|
1909
3166
|
};
|
|
1910
|
-
var
|
|
1911
|
-
var
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
const
|
|
1918
|
-
|
|
3167
|
+
var isApproach = (value) => value === "PlusZ" || value === "MinusZ";
|
|
3168
|
+
var turnedStretches = (features) => {
|
|
3169
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3170
|
+
for (const feature of features) {
|
|
3171
|
+
const key = stretchKey(feature);
|
|
3172
|
+
groups.set(key, [...groups.get(key) ?? [], feature]);
|
|
3173
|
+
}
|
|
3174
|
+
const merged = [...groups.values()].flatMap((copies) => {
|
|
3175
|
+
const [first] = copies;
|
|
3176
|
+
if (!first) return [];
|
|
3177
|
+
const approaches = [...new Set(copies.map((each) => each.approach).filter(isApproach))];
|
|
3178
|
+
const only = approaches.length === 1 ? approaches[0] : void 0;
|
|
3179
|
+
const z = only === "PlusZ" ? 1 : only === "MinusZ" ? -1 : 0;
|
|
3180
|
+
return [{ first, copies, approaches, machiningDirection: { x: 0, y: 0, z } }];
|
|
3181
|
+
});
|
|
3182
|
+
const numbered = dfmFeatures(
|
|
3183
|
+
merged.map(({ first, machiningDirection }) => ({
|
|
3184
|
+
featureTag: first.featureTag,
|
|
3185
|
+
featureType: first.featureType,
|
|
3186
|
+
machiningDirection,
|
|
3187
|
+
regionIdxs: regionsOf(first)
|
|
3188
|
+
}))
|
|
3189
|
+
);
|
|
3190
|
+
return merged.flatMap(({ copies, approaches }, at2) => {
|
|
3191
|
+
const feature = numbered[at2];
|
|
3192
|
+
return feature ? [{ ...feature, copies, approaches }] : [];
|
|
3193
|
+
});
|
|
1919
3194
|
};
|
|
1920
|
-
var
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
3195
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
3196
|
+
var finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3197
|
+
var ratio = (top, bottom) => top !== null && bottom !== null && bottom > 0 ? top / bottom : null;
|
|
3198
|
+
var narrowestStep = (facts) => {
|
|
3199
|
+
const steps = Array.isArray(facts.steps) ? facts.steps : [];
|
|
3200
|
+
const diameters = steps.map((step) => finite(record(step)?.diameter)).filter((each) => each !== null && each > 0);
|
|
3201
|
+
return diameters.length > 0 ? Math.min(...diameters) : null;
|
|
3202
|
+
};
|
|
3203
|
+
var threadSpec = (facts, field) => finite(record(facts.threadSpec)?.[field]) ?? finite(record(record(facts.thread)?.threadSpec)?.[field]);
|
|
3204
|
+
var partEnds = (stretches) => {
|
|
3205
|
+
const zs = stretches.flatMap(
|
|
3206
|
+
(stretch) => stretch.copies.flatMap((copy) => {
|
|
3207
|
+
const sheet = record(copy.datasheet);
|
|
3208
|
+
return [finite(sheet?.zMin), finite(sheet?.zMax)].filter(
|
|
3209
|
+
(each) => each !== null
|
|
3210
|
+
);
|
|
3211
|
+
})
|
|
3212
|
+
);
|
|
3213
|
+
return zs.length > 0 ? { low: Math.min(...zs), high: Math.max(...zs) } : null;
|
|
3214
|
+
};
|
|
3215
|
+
var turnedLd = (sheet, facts, kind, approach, ends) => {
|
|
3216
|
+
if (kind === "AxialHole") return ratio(finite(facts.depth), narrowestStep(facts));
|
|
3217
|
+
const inside = ID_KINDS;
|
|
3218
|
+
if (!kind || !inside.includes(kind) || !ends || !approach) return null;
|
|
3219
|
+
const zMin = finite(sheet.zMin);
|
|
3220
|
+
const zMax = finite(sheet.zMax);
|
|
3221
|
+
const reach = approach === "PlusZ" ? zMin === null ? null : ends.high - zMin : zMax === null ? null : zMax - ends.low;
|
|
3222
|
+
const rMin = finite(sheet.rMin);
|
|
3223
|
+
const bore = finite(sheet.entryDiameter) ?? (rMin === null ? null : 2 * rMin);
|
|
3224
|
+
return ratio(reach, bore);
|
|
3225
|
+
};
|
|
3226
|
+
var HAIR = 1e-3;
|
|
3227
|
+
var FLAT_CONE = 179.5;
|
|
3228
|
+
var copyMeasure = (metric, copy, ends) => {
|
|
3229
|
+
const sheet = record(copy.datasheet);
|
|
3230
|
+
if (!sheet) return null;
|
|
3231
|
+
const facts = record(sheet.facts) ?? {};
|
|
3232
|
+
const kind = typeof facts.kind === "string" ? facts.kind : kindOfType(copy.featureType);
|
|
3233
|
+
const kinds = metric.kinds;
|
|
3234
|
+
if (kinds && !(kind && kinds.includes(kind))) return null;
|
|
3235
|
+
const doubled = (value) => {
|
|
3236
|
+
const radius = finite(value);
|
|
3237
|
+
return radius === null ? null : 2 * radius;
|
|
1931
3238
|
};
|
|
3239
|
+
switch (metric.key) {
|
|
3240
|
+
case "maxDiameter":
|
|
3241
|
+
return doubled(sheet.rMax);
|
|
3242
|
+
case "minDiameter":
|
|
3243
|
+
return doubled(sheet.rMin);
|
|
3244
|
+
case "length": {
|
|
3245
|
+
const zMin = finite(sheet.zMin);
|
|
3246
|
+
const zMax = finite(sheet.zMax);
|
|
3247
|
+
return zMin === null || zMax === null || zMax - zMin < HAIR ? null : zMax - zMin;
|
|
3248
|
+
}
|
|
3249
|
+
case "ld":
|
|
3250
|
+
return turnedLd(sheet, facts, kind, copy.approach ?? null, ends);
|
|
3251
|
+
case "grooveWidth":
|
|
3252
|
+
return finite(facts.width);
|
|
3253
|
+
case "grooveDepth":
|
|
3254
|
+
case "threadDepth":
|
|
3255
|
+
case "holeDepth":
|
|
3256
|
+
return finite(facts.depth);
|
|
3257
|
+
case "grooveAspect":
|
|
3258
|
+
return ratio(finite(facts.depth), finite(facts.width));
|
|
3259
|
+
case "threadPitch":
|
|
3260
|
+
return finite(facts.pitch) ?? threadSpec(facts, "threadPitch");
|
|
3261
|
+
case "threadDiameter":
|
|
3262
|
+
return threadSpec(facts, "basicDiameter");
|
|
3263
|
+
case "holeDiameter":
|
|
3264
|
+
return narrowestStep(facts);
|
|
3265
|
+
case "holeSteps":
|
|
3266
|
+
return Array.isArray(facts.steps) ? facts.steps.length : null;
|
|
3267
|
+
case "flatBottom": {
|
|
3268
|
+
if (facts.bottom === void 0) return 0;
|
|
3269
|
+
const cone = finite(record(facts.bottom)?.coneDeg);
|
|
3270
|
+
return cone === null ? null : cone >= FLAT_CONE ? 1 : 0;
|
|
3271
|
+
}
|
|
3272
|
+
// Zero is a figure here: a sharp corner, which a "radius ≤" rule is most of all about.
|
|
3273
|
+
case "cornerRadius":
|
|
3274
|
+
return finite(sheet.minConcaveRadius);
|
|
3275
|
+
case "sharpCorner": {
|
|
3276
|
+
const radius = finite(sheet.minConcaveRadius);
|
|
3277
|
+
return radius === null ? 0 : radius <= HAIR ? 1 : 0;
|
|
3278
|
+
}
|
|
3279
|
+
// Absent where nothing stands between the feature and its end: nothing to pass, so clean.
|
|
3280
|
+
case "entryDiameter":
|
|
3281
|
+
return finite(sheet.entryDiameter);
|
|
3282
|
+
case "axialTool":
|
|
3283
|
+
return typeof sheet.mounting === "string" ? sheet.mounting === "Axial" ? 1 : 0 : null;
|
|
3284
|
+
case "tolerance":
|
|
3285
|
+
return finite(record(sheet.toleranceBand)?.atolMax);
|
|
3286
|
+
default:
|
|
3287
|
+
return null;
|
|
3288
|
+
}
|
|
1932
3289
|
};
|
|
1933
|
-
var
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
3290
|
+
var kindOfType = (featureType) => featureType.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
3291
|
+
var stretchFacts = (stretch) => stretch.copies.map((copy) => record(record(copy.datasheet)?.facts)).find((each) => each !== void 0);
|
|
3292
|
+
var stretchIs = (subject, stretch) => {
|
|
3293
|
+
const type = stretch.featureType.toLowerCase();
|
|
3294
|
+
if (subject.startsWith(TYPE_SUBJECT)) return type === subject.slice(TYPE_SUBJECT.length);
|
|
3295
|
+
const facts = stretchFacts(stretch);
|
|
3296
|
+
const kind = typeof facts?.kind === "string" ? facts.kind : kindOfType(type);
|
|
3297
|
+
const hole = kind === "AxialHole";
|
|
3298
|
+
switch (subject) {
|
|
3299
|
+
case "any":
|
|
3300
|
+
return true;
|
|
3301
|
+
case "thread":
|
|
3302
|
+
return kind === "OdThread" || kind === "IdThread" || hole && record(facts?.thread) !== void 0;
|
|
3303
|
+
case "throughHole":
|
|
3304
|
+
return hole && facts?.through === true;
|
|
3305
|
+
case "blindHole":
|
|
3306
|
+
return hole && facts?.through === false;
|
|
3307
|
+
case "threadedHole":
|
|
3308
|
+
return hole && record(facts?.thread) !== void 0;
|
|
3309
|
+
default: {
|
|
3310
|
+
const kinds = subjectInfo(subject, TURNING_RULES)?.kinds;
|
|
3311
|
+
return kinds !== void 0 && kinds.includes(kind);
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
1939
3314
|
};
|
|
1940
|
-
var
|
|
1941
|
-
|
|
1942
|
-
const
|
|
3315
|
+
var stretchHit = (stretch, rule, metric, ends) => {
|
|
3316
|
+
if (metric.quantity === "presence") return { rule, metric, value: 1 };
|
|
3317
|
+
const measured = stretch.copies.filter((copy) => record(copy.datasheet) !== void 0);
|
|
3318
|
+
if (measured.length === 0) return null;
|
|
3319
|
+
const values = [];
|
|
3320
|
+
for (const copy of measured) {
|
|
3321
|
+
const value2 = copyMeasure(metric, copy, ends);
|
|
3322
|
+
if (value2 === null || !breaks(rule, metric.quantity, value2)) return null;
|
|
3323
|
+
values.push(value2);
|
|
3324
|
+
}
|
|
3325
|
+
const value = worseUpward(rule, metric) ? Math.min(...values) : Math.max(...values);
|
|
3326
|
+
return { rule, metric, value };
|
|
3327
|
+
};
|
|
3328
|
+
var checkTurnedPart = (stretches, rules) => {
|
|
3329
|
+
const ends = partEnds(stretches);
|
|
3330
|
+
const hitsByTag = new Map(
|
|
3331
|
+
stretches.map((stretch) => [
|
|
3332
|
+
stretch.tag.toLowerCase(),
|
|
3333
|
+
rules.flatMap((rule) => {
|
|
3334
|
+
const metric = metricInfo(rule.metric, TURNING_RULES);
|
|
3335
|
+
if (!metric || !stretchIs(rule.subject, stretch)) return [];
|
|
3336
|
+
const hit = stretchHit(stretch, rule, metric, ends);
|
|
3337
|
+
return hit ? [hit] : [];
|
|
3338
|
+
})
|
|
3339
|
+
])
|
|
3340
|
+
);
|
|
3341
|
+
const required = new Set(stretches.map((stretch) => stretch.tag.toLowerCase()));
|
|
3342
|
+
const paintedFaces = /* @__PURE__ */ new Map();
|
|
3343
|
+
for (const stretch of stretches) {
|
|
3344
|
+
const color = worstColor(hitsByTag.get(stretch.tag.toLowerCase()));
|
|
3345
|
+
if (!color) continue;
|
|
3346
|
+
for (const region of stretch.regionIdxs) {
|
|
3347
|
+
const painted = paintedFaces.get(region);
|
|
3348
|
+
if (!painted || compareColors(color, painted.color) < 0)
|
|
3349
|
+
paintedFaces.set(region, { tag: stretch.tag, color });
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
1943
3352
|
return {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
3353
|
+
hitsByTag,
|
|
3354
|
+
required,
|
|
3355
|
+
byRule: ruleResults(rules, stretches, hitsByTag, required),
|
|
3356
|
+
paintedFaces,
|
|
3357
|
+
paintedWhole: /* @__PURE__ */ new Map()
|
|
1947
3358
|
};
|
|
1948
3359
|
};
|
|
1949
3360
|
|
|
@@ -1951,16 +3362,11 @@ export {
|
|
|
1951
3362
|
directionLabel,
|
|
1952
3363
|
dfmFeatures,
|
|
1953
3364
|
featureTypeLabel,
|
|
1954
|
-
partTop,
|
|
1955
|
-
isDrilled,
|
|
1956
|
-
featureLd,
|
|
1957
3365
|
PRESENCE_METRIC,
|
|
1958
|
-
|
|
3366
|
+
QUICK_SUBJECTS,
|
|
1959
3367
|
MILLING_RULES,
|
|
1960
|
-
subjectInfo,
|
|
1961
3368
|
metricInfo,
|
|
1962
3369
|
metricsForSubject,
|
|
1963
|
-
LENGTH_PLACES,
|
|
1964
3370
|
lengthValue,
|
|
1965
3371
|
unitLabel,
|
|
1966
3372
|
OPS,
|
|
@@ -1984,22 +3390,37 @@ export {
|
|
|
1984
3390
|
withMetric,
|
|
1985
3391
|
withOp,
|
|
1986
3392
|
withSubject,
|
|
1987
|
-
breaks,
|
|
1988
|
-
worseUpward,
|
|
1989
3393
|
parseRules,
|
|
3394
|
+
hitFigure,
|
|
3395
|
+
brokenRules,
|
|
3396
|
+
featureColor,
|
|
3397
|
+
withSurroundedFaces,
|
|
3398
|
+
checkPart,
|
|
3399
|
+
readPinch,
|
|
3400
|
+
PINCH_TOOL_MM,
|
|
3401
|
+
isNoReading,
|
|
3402
|
+
featureSheet,
|
|
3403
|
+
isSurfaceType,
|
|
3404
|
+
maxBallDiameter,
|
|
3405
|
+
featureMeasurements,
|
|
3406
|
+
MAX_DATASHEET_FIELDS,
|
|
3407
|
+
datasheetFields,
|
|
1990
3408
|
sameDepth,
|
|
1991
3409
|
featureRows,
|
|
1992
|
-
|
|
3410
|
+
pinchMark,
|
|
3411
|
+
pinchLabel,
|
|
3412
|
+
placePinchTool,
|
|
3413
|
+
readReachCurve,
|
|
3414
|
+
runStart,
|
|
3415
|
+
peakHeight,
|
|
3416
|
+
trimmedCurve,
|
|
3417
|
+
featureProfile,
|
|
1993
3418
|
FILE_KIND,
|
|
1994
3419
|
serializeRuleSet,
|
|
1995
3420
|
parseRuleSetFile,
|
|
1996
3421
|
llmRulePrompt,
|
|
1997
|
-
ID_KINDS,
|
|
1998
3422
|
turnedTypeLabel,
|
|
1999
3423
|
TURNING_RULES,
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
peakHeight,
|
|
2003
|
-
trimmedCurve,
|
|
2004
|
-
featureProfile
|
|
3424
|
+
turnedStretches,
|
|
3425
|
+
checkTurnedPart
|
|
2005
3426
|
};
|