@toolpath/dfm 0.3.1 → 0.4.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.
@@ -232,6 +232,38 @@ interface FeatureSheet {
232
232
  noToolFits?: true;
233
233
  /** For a hole the model threads: its thread. */
234
234
  threading?: FeatureThreading;
235
+ /** For an undercut — a T-slot or a dovetail: what its cutter is held to. */
236
+ undercut?: FeatureUndercut;
237
+ }
238
+ /**
239
+ * An undercut's own figures, from its `facts`: a T-slot's or a dovetail's.
240
+ * Millimetres and degrees. Each is left out where the Engine did not measure
241
+ * it, or where nothing limits it.
242
+ */
243
+ interface FeatureUndercut {
244
+ /** T-slot: how far the groove runs back from its opening, radially (`facts.undercutDepth`). */
245
+ undercutDepth?: number;
246
+ /**
247
+ * T-slot: the widest tool that comes down through the opening above it
248
+ * (`facts.maxEntryCd`), which holds the cutter's shaft. Left out where
249
+ * nothing above limits it; zero where nothing fits.
250
+ */
251
+ maxEntry?: number;
252
+ /** Dovetail: between the overhanging wall and the tool axis, degrees (`facts.taperDeg`). */
253
+ taperDeg?: number;
254
+ /** Dovetail: the widest clearance over the floor, the width the groove cuts at. */
255
+ floorWidth?: number;
256
+ /** Dovetail: the clearance through the opening at the top of the groove. */
257
+ topOpeningWidth?: number;
258
+ /** Dovetail: the groove's widths vary from place to place — it runs out somewhere. */
259
+ isExternal?: true;
260
+ /** T-slot: its walls close on themselves in plan view, so a cutter's head comes down through its opening. */
261
+ isClosed?: true;
262
+ /**
263
+ * The Engine could not measure it (`facts.isInvalidGeometry` or
264
+ * `facts.cd.measurementFailed`), so its figures offer no cutter.
265
+ */
266
+ unmeasured?: true;
235
267
  }
236
268
  /** A modelled thread, from a hole's `facts.threading`. Millimetres. */
237
269
  interface FeatureThreading {
@@ -500,4 +532,4 @@ interface FeatureRecord {
500
532
  readonly datasheet: unknown;
501
533
  }
502
534
 
503
- export { featureColor as A, type BrokenRule as B, CRITICAL_COLOR as C, type DfmRule as D, featureMeasurements as E, type FeatureSheets as F, featureProfile as G, featureSheet as H, featureTypeLabel as I, isNoReading as J, isSurfaceType as K, makeRule as L, type Measurement as M, maxBallDiameter as N, parseRules as O, PINCH_TOOL_MM as P, peakHeight as Q, type ReachCurve as R, type SurfaceToolKind as S, readPinch as T, type Units as U, readReachCurve as V, WARNING_COLOR as W, runStart as X, trimmedCurve as Y, withSurroundedFaces as Z, type DfmCheck as a, type DfmFeature as b, type FeatureProfile as c, type FeatureRecord as d, type DfmHit as e, type FeaturePinch as f, type FeatureSheet as g, type ClearanceFit as h, type DfmMetric as i, type DfmOp as j, type DfmQuantity as k, type DfmSources as l, type FactsKind as m, type FeatureAcross as n, type FeatureThreading as o, type PinchDisc as p, RULE_COLORS as q, type RangeHigh as r, type RangeLow as s, type ReportFeature as t, type RuleResult as u, brokenRules as v, checkPart as w, compareColors as x, describeRule as y, dfmFeatures as z };
535
+ export { dfmFeatures as A, type BrokenRule as B, CRITICAL_COLOR as C, type DfmRule as D, featureColor as E, type FeatureSheets as F, featureMeasurements as G, featureProfile as H, featureSheet as I, featureTypeLabel as J, isNoReading as K, isSurfaceType as L, type Measurement as M, makeRule as N, maxBallDiameter as O, PINCH_TOOL_MM as P, parseRules as Q, type ReachCurve as R, type SurfaceToolKind as S, peakHeight as T, type Units as U, readPinch as V, WARNING_COLOR as W, readReachCurve as X, runStart as Y, trimmedCurve as Z, withSurroundedFaces as _, type DfmCheck as a, type DfmFeature as b, type FeatureProfile as c, type FeatureRecord as d, type DfmHit as e, type FeaturePinch as f, type FeatureSheet as g, type ClearanceFit as h, type DfmMetric as i, type DfmOp as j, type DfmQuantity as k, type DfmSources as l, type FactsKind as m, type FeatureAcross as n, type FeatureThreading as o, type FeatureUndercut as p, type PinchDisc as q, RULE_COLORS as r, type RangeHigh as s, type RangeLow as t, type ReportFeature as u, type RuleResult as v, brokenRules as w, checkPart as x, compareColors as y, describeRule as z };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, FC, KeyboardEvent, Ref, ComponentType, ReactElement, HTMLAttributes } from 'react';
3
- import { D as DfmRule, a as DfmCheck, b as DfmFeature, F as FeatureSheets, U as Units, R as ReachCurve, c as FeatureProfile, B as BrokenRule, M as Measurement, d as FeatureRecord } from './feature-record-BUoFFjN6.js';
3
+ import { D as DfmRule, a as DfmCheck, b as DfmFeature, F as FeatureSheets, U as Units, R as ReachCurve, c as FeatureProfile, B as BrokenRule, M as Measurement, d as FeatureRecord } from './feature-record-DHbkVpx0.js';
4
4
 
5
5
  /**
6
6
  * The rules the list shows, and how to change them. The app owns them —
@@ -1,5 +1,5 @@
1
- import { e as DfmHit, U as Units, f as FeaturePinch, g as FeatureSheet, D as DfmRule } from '../feature-record-BUoFFjN6.js';
2
- export { B as BrokenRule, C as CRITICAL_COLOR, h as ClearanceFit, a as DfmCheck, b as DfmFeature, i as DfmMetric, j as DfmOp, k as DfmQuantity, l as DfmSources, m as FactsKind, n as FeatureAcross, c as FeatureProfile, d as FeatureRecord, F as FeatureSheets, o as FeatureThreading, M as Measurement, P as PINCH_TOOL_MM, p as PinchDisc, q as RULE_COLORS, r as RangeHigh, s as RangeLow, R as ReachCurve, t as ReportFeature, u as RuleResult, S as SurfaceToolKind, W as WARNING_COLOR, v as brokenRules, w as checkPart, x as compareColors, y as describeRule, z as dfmFeatures, A as featureColor, E as featureMeasurements, G as featureProfile, H as featureSheet, I as featureTypeLabel, J as isNoReading, K as isSurfaceType, L as makeRule, N as maxBallDiameter, O as parseRules, Q as peakHeight, T as readPinch, V as readReachCurve, X as runStart, Y as trimmedCurve, Z as withSurroundedFaces } from '../feature-record-BUoFFjN6.js';
1
+ import { e as DfmHit, U as Units, f as FeaturePinch, g as FeatureSheet, D as DfmRule } from '../feature-record-DHbkVpx0.js';
2
+ export { B as BrokenRule, C as CRITICAL_COLOR, h as ClearanceFit, a as DfmCheck, b as DfmFeature, i as DfmMetric, j as DfmOp, k as DfmQuantity, l as DfmSources, m as FactsKind, n as FeatureAcross, c as FeatureProfile, d as FeatureRecord, F as FeatureSheets, o as FeatureThreading, p as FeatureUndercut, M as Measurement, P as PINCH_TOOL_MM, q as PinchDisc, r as RULE_COLORS, s as RangeHigh, t as RangeLow, R as ReachCurve, u as ReportFeature, v as RuleResult, S as SurfaceToolKind, W as WARNING_COLOR, w as brokenRules, x as checkPart, y as compareColors, z as describeRule, A as dfmFeatures, E as featureColor, G as featureMeasurements, H as featureProfile, I as featureSheet, J as featureTypeLabel, K as isNoReading, L as isSurfaceType, N as makeRule, O as maxBallDiameter, Q as parseRules, T as peakHeight, V as readPinch, X as readReachCurve, Y as runStart, Z as trimmedCurve, _ as withSurroundedFaces } from '../feature-record-DHbkVpx0.js';
3
3
 
4
4
  /** The figure that broke a rule, bare: `7.5`, `0.05"`. Empty for a yes/no or "any size". */
5
5
  declare const hitFigure: (hit: DfmHit, units: Units) => string;
@@ -327,6 +327,164 @@ var checkPart = (features, sources, rules) => {
327
327
  };
328
328
  };
329
329
 
330
+ // src/model/pinch.ts
331
+ var asNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
332
+ var recordOf = (value) => typeof value === "object" && value !== null ? value : {};
333
+ var clearanceFit = (datasheet) => {
334
+ const sheet = recordOf(datasheet);
335
+ const facts = recordOf(sheet.facts);
336
+ const chamfer = facts.kind === "Chamfer";
337
+ const band = recordOf(chamfer ? recordOf(facts.three).cd : facts.cd);
338
+ const tolerance = recordOf(sheet.toleranceBand);
339
+ const strict = asNumber(recordOf(band.ignore).min);
340
+ if (strict === void 0) return void 0;
341
+ const bandPath = chamfer ? "facts.three.cd" : "facts.cd";
342
+ const loose = asNumber(recordOf(band.deviate).min);
343
+ const atStrict = asNumber(tolerance.atolIgnore);
344
+ const atLoose = asNumber(tolerance.atolDeviate);
345
+ const source = { bandPath, strict, loose, atStrict, atLoose };
346
+ if (loose === void 0 || atStrict === void 0 || atLoose === void 0 || atLoose <= atStrict)
347
+ return { ...source, slack: 0, resolved: false };
348
+ const growth = (loose - strict) / (atLoose - atStrict);
349
+ const slack = growth > 0 ? Math.min(growth * atStrict, strict) : 0;
350
+ return { ...source, slack, resolved: true };
351
+ };
352
+ var readPinch = (datasheet) => {
353
+ const sheet = recordOf(datasheet);
354
+ const zMin = asNumber(sheet.zMin);
355
+ const zMax = asNumber(sheet.zMax);
356
+ if (zMin === void 0 || zMax === void 0 || !Array.isArray(sheet.pinchPoints)) return null;
357
+ const fit = clearanceFit(datasheet);
358
+ const slack = fit?.slack ?? 0;
359
+ const discs = sheet.pinchPoints.flatMap((point) => {
360
+ const centre = recordOf(recordOf(point).center);
361
+ const x = asNumber(centre.x);
362
+ const y = asNumber(centre.y);
363
+ const diameter = asNumber(recordOf(point).diameter);
364
+ return x === void 0 || y === void 0 || diameter === void 0 || diameter < 0 ? [] : [{ x, y, diameter: Math.max(diameter - slack, 0) }];
365
+ });
366
+ if (discs.length === 0) return null;
367
+ return { discs, zMin, zMax, ...fit && !fit.resolved ? { unresolved: true } : {} };
368
+ };
369
+
370
+ // src/model/feature-sheet.ts
371
+ var SHARP_MM = 0.1;
372
+ var SHARP_RADIUS_MM = 0.01;
373
+ var PINCH_TOOL_MM = 0.01;
374
+ var asNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
375
+ var recordOf2 = (value) => typeof value === "object" && value !== null ? value : {};
376
+ var definedOnly = (fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== void 0));
377
+ var undercutOf = (facts) => {
378
+ const unmeasured = facts.isInvalidGeometry === true || recordOf2(facts.cd).measurementFailed === true ? true : void 0;
379
+ if (facts.kind === "Tslot")
380
+ return definedOnly({
381
+ undercutDepth: asNumber2(facts.undercutDepth),
382
+ maxEntry: asNumber2(facts.maxEntryCd),
383
+ isClosed: facts.isClosed === true ? true : void 0,
384
+ unmeasured
385
+ });
386
+ if (facts.kind === "Dovetail")
387
+ return definedOnly({
388
+ taperDeg: asNumber2(facts.taperDeg),
389
+ floorWidth: asNumber2(facts.floorWidth),
390
+ topOpeningWidth: asNumber2(facts.topOpeningWidth),
391
+ isExternal: facts.isExternal === true ? true : void 0,
392
+ unmeasured
393
+ });
394
+ return void 0;
395
+ };
396
+ var threadingOf = (facts) => {
397
+ const spec = recordOf2(recordOf2(facts.threading).spec);
398
+ const basicDiameter = asNumber2(spec.basicDiameter);
399
+ const threadPitch = asNumber2(spec.threadPitch);
400
+ if (basicDiameter === void 0 || threadPitch === void 0 || threadPitch <= 0) return void 0;
401
+ return { basicDiameter, threadPitch };
402
+ };
403
+ var noToolFits = (band) => {
404
+ const { max } = band;
405
+ if (typeof max === "string") return /^-inf/i.test(max.trim());
406
+ return typeof max === "number" && (max === Number.NEGATIVE_INFINITY || Number.isFinite(max) && max < SHARP_MM);
407
+ };
408
+ var insideCornerRadius = (fit) => Math.max((fit.strict - fit.slack) / 2, 0);
409
+ var pinchCentres = (points) => Array.isArray(points) ? points.flatMap((point) => {
410
+ const centre = recordOf2(recordOf2(point).center);
411
+ const x = asNumber2(centre.x);
412
+ const y = asNumber2(centre.y);
413
+ return x === void 0 || y === void 0 ? [] : [{ x, y }];
414
+ }) : [];
415
+ var ZERO_MM = 5e-5;
416
+ var isCutterlessTslot = (datasheet) => {
417
+ const facts = recordOf2(recordOf2(datasheet).facts);
418
+ if (facts.kind !== "Tslot") return false;
419
+ const leaves = (value) => typeof value === "object" && value !== null ? Object.values(value).flatMap(leaves) : [value];
420
+ const band = leaves(facts.cd);
421
+ return band.length > 0 && band.every((each) => {
422
+ const value = typeof each === "string" && each.trim() !== "" ? Number(each) : each;
423
+ return typeof value === "number" && Math.abs(value) < ZERO_MM;
424
+ });
425
+ };
426
+ var isSharpCorneredFillet = (featureType, datasheet) => {
427
+ if (typeof featureType !== "string" || !featureType.toLowerCase().split("_").includes("fillet"))
428
+ return false;
429
+ const facts = recordOf2(recordOf2(datasheet).facts);
430
+ return facts.hasSharpCorner === true || recordOf2(facts.three).hasSharpCorner === true;
431
+ };
432
+ var isNoReading = (featureType, datasheet) => isCutterlessTslot(datasheet) || isSharpCorneredFillet(featureType, datasheet);
433
+ var featureSheet = (datasheet) => {
434
+ const sheet = recordOf2(datasheet);
435
+ const facts = recordOf2(sheet.facts);
436
+ const kind = typeof facts.kind === "string" ? facts.kind : void 0;
437
+ const band = recordOf2(kind === "Chamfer" ? recordOf2(facts.three).cd : facts.cd);
438
+ const ignoreBand = recordOf2(band.ignore);
439
+ const fit = clearanceFit(datasheet);
440
+ const fields = {
441
+ kind,
442
+ zMin: asNumber2(sheet.zMin),
443
+ zMax: asNumber2(sheet.zMax),
444
+ extendedZMax: asNumber2(sheet.extendedZMax),
445
+ maxTool: asNumber2(ignoreBand.min),
446
+ diameter: kind === "Hole" ? asNumber2(facts.diameter) : void 0,
447
+ tipAngle: kind === "Hole" ? asNumber2(facts.fullConeDeg) : void 0,
448
+ filletRadius: asNumber2(facts.filletRadius),
449
+ // A bevel's angle wherever the facts carry one: a chamfer's, and any other kind that bevels.
450
+ chamferAngle: asNumber2(recordOf2(facts.bevel).angleDeg)
451
+ };
452
+ const toolFitPath = facts.toolFit !== void 0 ? "facts.toolFit" : "facts.three.toolFit";
453
+ const toolFit = recordOf2(facts.toolFit ?? recordOf2(facts.three).toolFit);
454
+ fields.fitToolDiameter = asNumber2(toolFit.toolDiameter);
455
+ fields.fitCornerRadius = asNumber2(toolFit.cornerRadius);
456
+ const ballOnly = (facts.useOnlyBallToolsForFinish ?? recordOf2(facts.three).useOnlyBallToolsForFinish) === true;
457
+ const measured = Object.fromEntries(
458
+ Object.entries(fields).filter(([, value]) => value !== void 0)
459
+ );
460
+ const cramped = Object.keys(ignoreBand).length > 0 && noToolFits(ignoreBand);
461
+ const walled = kind !== "Hole" && sheet.hasWall === true;
462
+ const clearance = walled ? fit : void 0;
463
+ const cornerRadius = clearance ? insideCornerRadius(clearance) : void 0;
464
+ const sharp = cornerRadius !== void 0 && cornerRadius < SHARP_RADIUS_MM && !cramped;
465
+ const threading = kind === "Hole" ? threadingOf(facts) : void 0;
466
+ const undercut = undercutOf(facts);
467
+ const pinched = (readPinch(datasheet)?.discs ?? []).filter(
468
+ (disc) => disc.diameter >= PINCH_TOOL_MM
469
+ );
470
+ return {
471
+ ...measured,
472
+ ...pinched.length > 0 ? {
473
+ pinchDiameter: Math.min(...pinched.map((disc) => disc.diameter)),
474
+ pinchPlaces: pinched.length
475
+ } : {},
476
+ ...cornerRadius !== void 0 ? { cornerRadius } : {},
477
+ ...clearance && !clearance.resolved ? { cornerRadiusUnresolved: true } : {},
478
+ ...clearance ? { clearance } : {},
479
+ ...Object.keys(toolFit).length > 0 ? { toolFitPath } : {},
480
+ ...sharp ? { sharpCorners: pinchCentres(sheet.pinchPoints) } : {},
481
+ ...cramped ? { noToolFits: true } : {},
482
+ ...ballOnly ? { ballOnly: true } : {},
483
+ ...threading ? { threading } : {},
484
+ ...undercut ? { undercut } : {}
485
+ };
486
+ };
487
+
330
488
  // src/model/feature-details.ts
331
489
  import { MM_PER_INCH } from "@toolpath/tool-support";
332
490
  var mm = (value) => `${trim(value, 4)} mm`;
@@ -375,6 +533,11 @@ var maxBallDiameter = (sheet) => {
375
533
  };
376
534
  var ROW_ORDER = [
377
535
  "featureDepth",
536
+ "undercutWidth",
537
+ "floorWidth",
538
+ "openingWidth",
539
+ "undercutDepth",
540
+ "taper",
378
541
  "diameter",
379
542
  "cornerRadius",
380
543
  "filletRadius",
@@ -383,6 +546,8 @@ var ROW_ORDER = [
383
546
  "thread",
384
547
  "bevelAngle",
385
548
  "finishTool",
549
+ "undercutCutter",
550
+ "maxShaft",
386
551
  "pinch",
387
552
  "fitCornerRadius",
388
553
  "maxBull",
@@ -406,6 +571,7 @@ var featureMeasurements = ({
406
571
  });
407
572
  const sheet = sheets[feature.tag.toLowerCase()];
408
573
  const rows = [];
574
+ const undercut = sheet?.undercut;
409
575
  const top = sheet ? partTop(features, sheets, feature.machiningDirection) : null;
410
576
  const bottom = sheet?.zMin;
411
577
  const topFrom = top === null ? void 0 : features.find((each) => sheets[each.tag.toLowerCase()]?.extendedZMax === top);
@@ -419,7 +585,7 @@ var featureMeasurements = ({
419
585
  derivation: [topLine, `Top \u2212 zMin = ${mm(top)} \u2212 ${mm(bottom)} = ${mm(top - bottom)}`]
420
586
  });
421
587
  }
422
- if (sheet?.zMax !== void 0 && bottom !== void 0) {
588
+ if (sheet?.zMax !== void 0 && bottom !== void 0 && !(undercut && sheet?.kind === "Tslot")) {
423
589
  rows.push({
424
590
  key: "featureDepth",
425
591
  label: "Feature depth",
@@ -435,7 +601,7 @@ var featureMeasurements = ({
435
601
  derivation: [`facts.diameter = ${mm(sheet.diameter)}`]
436
602
  });
437
603
  }
438
- if (sheet?.cornerRadius !== void 0) {
604
+ if (sheet?.cornerRadius !== void 0 && !undercut) {
439
605
  const radius = length(sheet.cornerRadius);
440
606
  rows.push({
441
607
  key: "cornerRadius",
@@ -449,7 +615,7 @@ var featureMeasurements = ({
449
615
  ]
450
616
  });
451
617
  }
452
- if (!sheet?.sharpCorners) {
618
+ if (!sheet?.sharpCorners && !undercut) {
453
619
  const clearance = sheet?.pinchDiameter ?? (sheet?.cornerRadius !== void 0 ? 2 * sheet.cornerRadius : void 0);
454
620
  if (clearance !== void 0) {
455
621
  const width = length(clearance);
@@ -479,6 +645,7 @@ var featureMeasurements = ({
479
645
  });
480
646
  }
481
647
  }
648
+ if (sheet && undercut) rows.push(...undercutRows(sheet, undercut, top, length));
482
649
  const surface = SURFACE_TYPES.has(feature.featureType);
483
650
  if (surface && sheet) {
484
651
  const diameter = sheet.fitToolDiameter;
@@ -544,7 +711,7 @@ var featureMeasurements = ({
544
711
  });
545
712
  }
546
713
  }
547
- const ld = featureLd(features, feature, sheets);
714
+ const ld = undercut ? null : featureLd(features, feature, sheets);
548
715
  if (ld && top !== null && bottom !== void 0) {
549
716
  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)`;
550
717
  const ratioOf = (depth) => {
@@ -641,141 +808,173 @@ var featureMeasurements = ({
641
808
  }
642
809
  return rows.sort((a, b) => ROW_ORDER.indexOf(a.key) - ROW_ORDER.indexOf(b.key));
643
810
  };
644
-
645
- // src/model/pinch.ts
646
- var asNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
647
- var recordOf = (value) => typeof value === "object" && value !== null ? value : {};
648
- var clearanceFit = (datasheet) => {
649
- const sheet = recordOf(datasheet);
650
- const facts = recordOf(sheet.facts);
651
- const chamfer = facts.kind === "Chamfer";
652
- const band = recordOf(chamfer ? recordOf(facts.three).cd : facts.cd);
653
- const tolerance = recordOf(sheet.toleranceBand);
654
- const strict = asNumber(recordOf(band.ignore).min);
655
- if (strict === void 0) return void 0;
656
- const bandPath = chamfer ? "facts.three.cd" : "facts.cd";
657
- const loose = asNumber(recordOf(band.deviate).min);
658
- const atStrict = asNumber(tolerance.atolIgnore);
659
- const atLoose = asNumber(tolerance.atolDeviate);
660
- const source = { bandPath, strict, loose, atStrict, atLoose };
661
- if (loose === void 0 || atStrict === void 0 || atLoose === void 0 || atLoose <= atStrict)
662
- return { ...source, slack: 0, resolved: false };
663
- const growth = (loose - strict) / (atLoose - atStrict);
664
- const slack = growth > 0 ? Math.min(growth * atStrict, strict) : 0;
665
- return { ...source, slack, resolved: true };
666
- };
667
- var readPinch = (datasheet) => {
668
- const sheet = recordOf(datasheet);
669
- const zMin = asNumber(sheet.zMin);
670
- const zMax = asNumber(sheet.zMax);
671
- if (zMin === void 0 || zMax === void 0 || !Array.isArray(sheet.pinchPoints)) return null;
672
- const fit = clearanceFit(datasheet);
673
- const slack = fit?.slack ?? 0;
674
- const discs = sheet.pinchPoints.flatMap((point) => {
675
- const centre = recordOf(recordOf(point).center);
676
- const x = asNumber(centre.x);
677
- const y = asNumber(centre.y);
678
- const diameter = asNumber(recordOf(point).diameter);
679
- return x === void 0 || y === void 0 || diameter === void 0 || diameter < 0 ? [] : [{ x, y, diameter: Math.max(diameter - slack, 0) }];
680
- });
681
- if (discs.length === 0) return null;
682
- return { discs, zMin, zMax, ...fit && !fit.resolved ? { unresolved: true } : {} };
683
- };
684
-
685
- // src/model/feature-sheet.ts
686
- var SHARP_MM = 0.1;
687
- var SHARP_RADIUS_MM = 0.01;
688
- var PINCH_TOOL_MM = 0.01;
689
- var asNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
690
- var recordOf2 = (value) => typeof value === "object" && value !== null ? value : {};
691
- var threadingOf = (facts) => {
692
- const spec = recordOf2(recordOf2(facts.threading).spec);
693
- const basicDiameter = asNumber2(spec.basicDiameter);
694
- const threadPitch = asNumber2(spec.threadPitch);
695
- if (basicDiameter === void 0 || threadPitch === void 0 || threadPitch <= 0) return void 0;
696
- return { basicDiameter, threadPitch };
697
- };
698
- var noToolFits = (band) => {
699
- const { max } = band;
700
- if (typeof max === "string") return /^-inf/i.test(max.trim());
701
- return typeof max === "number" && (max === Number.NEGATIVE_INFINITY || Number.isFinite(max) && max < SHARP_MM);
702
- };
703
- var insideCornerRadius = (fit) => Math.max((fit.strict - fit.slack) / 2, 0);
704
- var pinchCentres = (points) => Array.isArray(points) ? points.flatMap((point) => {
705
- const centre = recordOf2(recordOf2(point).center);
706
- const x = asNumber2(centre.x);
707
- const y = asNumber2(centre.y);
708
- return x === void 0 || y === void 0 ? [] : [{ x, y }];
709
- }) : [];
710
- var ZERO_MM = 5e-5;
711
- var isCutterlessTslot = (datasheet) => {
712
- const facts = recordOf2(recordOf2(datasheet).facts);
713
- if (facts.kind !== "Tslot") return false;
714
- const leaves = (value) => typeof value === "object" && value !== null ? Object.values(value).flatMap(leaves) : [value];
715
- const band = leaves(facts.cd);
716
- return band.length > 0 && band.every((each) => {
717
- const value = typeof each === "string" && each.trim() !== "" ? Number(each) : each;
718
- return typeof value === "number" && Math.abs(value) < ZERO_MM;
811
+ var undercutRows = (sheet, undercut, top, length) => {
812
+ const rows = [];
813
+ const tslot = sheet.kind === "Tslot";
814
+ const { zMin, zMax } = sheet;
815
+ const { floorWidth: floor, topOpeningWidth: opening } = undercut;
816
+ const angle = undercut.taperDeg !== void 0 ? `${trim(undercut.taperDeg, 3)}\xB0` : void 0;
817
+ if (tslot && zMin !== void 0 && zMax !== void 0) {
818
+ rows.push({
819
+ key: "undercutWidth",
820
+ label: "Undercut width",
821
+ ...length(zMax - zMin),
822
+ derivation: [
823
+ "The groove\u2019s height along the tool: the widest cutter it takes in one pass.",
824
+ `zMax \u2212 zMin = ${mm(zMax)} \u2212 ${mm(zMin)} = ${mm(zMax - zMin)}`
825
+ ]
826
+ });
827
+ }
828
+ if (floor !== void 0) {
829
+ rows.push({
830
+ key: "floorWidth",
831
+ label: "Floor width",
832
+ ...length(floor),
833
+ derivation: ["The widest clearance over the floor", `facts.floorWidth = ${mm(floor)}`]
834
+ });
835
+ }
836
+ if (opening !== void 0) {
837
+ rows.push({
838
+ key: "openingWidth",
839
+ label: "Opening width",
840
+ ...length(opening),
841
+ derivation: [
842
+ "The clearance through the opening at the top of the groove",
843
+ `facts.topOpeningWidth = ${mm(opening)}`
844
+ ]
845
+ });
846
+ }
847
+ let depth = undercut.undercutDepth;
848
+ let depthLine = depth !== void 0 ? `facts.undercutDepth = ${mm(depth)}` : "";
849
+ if (depth === void 0 && floor !== void 0 && opening !== void 0) {
850
+ if (floor > opening && !undercut.isExternal) {
851
+ depth = (floor - opening) / 2;
852
+ depthLine = `(facts.floorWidth \u2212 facts.topOpeningWidth) / 2 = (${mm(floor)} \u2212 ${mm(opening)}) / 2 = ${mm(depth)}`;
853
+ }
854
+ }
855
+ if (depth !== void 0) {
856
+ rows.push({
857
+ key: "undercutDepth",
858
+ label: "Undercut depth",
859
+ ...length(depth),
860
+ derivation: [
861
+ "How far the groove runs back from its opening: what the cutter\u2019s head reaches past its shaft.",
862
+ depthLine
863
+ ]
864
+ });
865
+ }
866
+ if (angle) {
867
+ rows.push({
868
+ key: "taper",
869
+ label: "Dovetail angle",
870
+ value: `${angle} from the tool axis`,
871
+ derivation: [`facts.taperDeg = ${angle}, between the overhanging wall and the tool axis`]
872
+ });
873
+ }
874
+ const cutter = (value, derivation, alt) => ({
875
+ key: "undercutCutter",
876
+ label: "Max cutting diameter",
877
+ milling: true,
878
+ value,
879
+ ...alt !== void 0 ? { alt } : {},
880
+ derivation
719
881
  });
720
- };
721
- var isSharpCorneredFillet = (featureType, datasheet) => {
722
- if (typeof featureType !== "string" || !featureType.toLowerCase().split("_").includes("fillet"))
723
- return false;
724
- const facts = recordOf2(recordOf2(datasheet).facts);
725
- return facts.hasSharpCorner === true || recordOf2(facts.three).hasSharpCorner === true;
726
- };
727
- var isNoReading = (featureType, datasheet) => isCutterlessTslot(datasheet) || isSharpCorneredFillet(featureType, datasheet);
728
- var featureSheet = (datasheet) => {
729
- const sheet = recordOf2(datasheet);
730
- const facts = recordOf2(sheet.facts);
731
- const kind = typeof facts.kind === "string" ? facts.kind : void 0;
732
- const band = recordOf2(kind === "Chamfer" ? recordOf2(facts.three).cd : facts.cd);
733
- const ignoreBand = recordOf2(band.ignore);
734
- const fit = clearanceFit(datasheet);
735
- const fields = {
736
- kind,
737
- zMin: asNumber2(sheet.zMin),
738
- zMax: asNumber2(sheet.zMax),
739
- extendedZMax: asNumber2(sheet.extendedZMax),
740
- maxTool: asNumber2(ignoreBand.min),
741
- diameter: kind === "Hole" ? asNumber2(facts.diameter) : void 0,
742
- tipAngle: kind === "Hole" ? asNumber2(facts.fullConeDeg) : void 0,
743
- filletRadius: asNumber2(facts.filletRadius),
744
- // A bevel's angle wherever the facts carry one: a chamfer's, and any other kind that bevels.
745
- chamferAngle: asNumber2(recordOf2(facts.bevel).angleDeg)
746
- };
747
- const toolFitPath = facts.toolFit !== void 0 ? "facts.toolFit" : "facts.three.toolFit";
748
- const toolFit = recordOf2(facts.toolFit ?? recordOf2(facts.three).toolFit);
749
- fields.fitToolDiameter = asNumber2(toolFit.toolDiameter);
750
- fields.fitCornerRadius = asNumber2(toolFit.cornerRadius);
751
- const ballOnly = (facts.useOnlyBallToolsForFinish ?? recordOf2(facts.three).useOnlyBallToolsForFinish) === true;
752
- const measured = Object.fromEntries(
753
- Object.entries(fields).filter(([, value]) => value !== void 0)
754
- );
755
- const cramped = Object.keys(ignoreBand).length > 0 && noToolFits(ignoreBand);
756
- const walled = kind !== "Hole" && sheet.hasWall === true;
757
- const clearance = walled ? fit : void 0;
758
- const cornerRadius = clearance ? insideCornerRadius(clearance) : void 0;
759
- const sharp = cornerRadius !== void 0 && cornerRadius < SHARP_RADIUS_MM && !cramped;
760
- const threading = kind === "Hole" ? threadingOf(facts) : void 0;
761
- const pinched = (readPinch(datasheet)?.discs ?? []).filter(
762
- (disc) => disc.diameter >= PINCH_TOOL_MM
763
- );
764
- return {
765
- ...measured,
766
- ...pinched.length > 0 ? {
767
- pinchDiameter: Math.min(...pinched.map((disc) => disc.diameter)),
768
- pinchPlaces: pinched.length
769
- } : {},
770
- ...cornerRadius !== void 0 ? { cornerRadius } : {},
771
- ...clearance && !clearance.resolved ? { cornerRadiusUnresolved: true } : {},
772
- ...clearance ? { clearance } : {},
773
- ...Object.keys(toolFit).length > 0 ? { toolFitPath } : {},
774
- ...sharp ? { sharpCorners: pinchCentres(sheet.pinchPoints) } : {},
775
- ...cramped ? { noToolFits: true } : {},
776
- ...ballOnly ? { ballOnly: true } : {},
777
- ...threading ? { threading } : {}
778
- };
882
+ if (undercut.unmeasured) {
883
+ return [
884
+ ...rows,
885
+ cutter("Not measured", [
886
+ "The Engine could not measure the groove (facts.isInvalidGeometry or facts.cd.measurementFailed), so its figures offer no cutter."
887
+ ])
888
+ ];
889
+ }
890
+ if (sheet.noToolFits) {
891
+ return [
892
+ ...rows,
893
+ cutter("None fits", [
894
+ "facts.cd.ignore.max is next to nothing: no cutter fits anywhere in the groove."
895
+ ])
896
+ ];
897
+ }
898
+ const band = sheet.maxTool !== void 0 && sheet.maxTool > 0 ? sheet.maxTool : void 0;
899
+ const dropIn = undercut.isClosed ? undercut.maxEntry : void 0;
900
+ const head = band === void 0 ? void 0 : Math.min(band, dropIn ?? Infinity);
901
+ const opensTo = undercut.maxEntry ?? opening;
902
+ const behind = head !== void 0 && depth !== void 0 ? head - 2 * depth : void 0;
903
+ const limits = [opensTo, behind].filter((each) => each !== void 0);
904
+ const shaft = limits.length > 0 ? Math.max(Math.min(...limits), 0) : void 0;
905
+ const shaftLines = [
906
+ "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.",
907
+ ...opensTo !== void 0 ? [`Opening: ${tslot ? "facts.maxEntryCd" : "facts.topOpeningWidth"} = ${mm(opensTo)}`] : [],
908
+ ...behind !== void 0 && head !== void 0 && depth !== void 0 ? [
909
+ `Reach: max cutting diameter \u2212 2 \xD7 undercut depth = ${mm(head)} \u2212 2 \xD7 ${mm(depth)} = ${mm(behind)}`
910
+ ] : [],
911
+ ...shaft !== void 0 ? [`Max shaft = ${mm(shaft)}`] : []
912
+ ];
913
+ if (head !== void 0 && band !== void 0) {
914
+ const headLines = [
915
+ "The biggest head that reaches every point of the groove.",
916
+ `facts.cd.ignore.min = ${mm(band)}`,
917
+ ...dropIn !== void 0 ? [
918
+ `A closed slot: the head comes down through the opening above it, facts.maxEntryCd = ${mm(dropIn)}`
919
+ ] : [],
920
+ `\u2300 = ${mm(head)}`
921
+ ];
922
+ if (head < SHARP_MM || shaft !== void 0 && shaft < SHARP_MM) {
923
+ return [
924
+ ...rows,
925
+ cutter("None fits", [
926
+ ...headLines,
927
+ ...shaft !== void 0 ? shaftLines : [],
928
+ "No head that fits the groove leaves room for a shaft to carry it."
929
+ ])
930
+ ];
931
+ }
932
+ const size = length(head);
933
+ const filletRadius = sheet.filletRadius !== void 0 && sheet.filletRadius > 0 ? sheet.filletRadius : void 0;
934
+ const fillet = filletRadius !== void 0 ? length(filletRadius) : void 0;
935
+ const describe = (unit2) => [`\u2300 ${size[unit2]}`, ...angle ? [angle] : [], ...fillet ? [`R ${fillet[unit2]}`] : []].join(
936
+ " \xB7 "
937
+ );
938
+ rows.push(
939
+ cutter(
940
+ describe("value"),
941
+ [
942
+ ...headLines,
943
+ ...angle ? [`Angle = facts.taperDeg = ${angle}, from the tool axis`] : [],
944
+ ...filletRadius !== void 0 ? [`R = facts.filletRadius = ${mm(filletRadius)}, the blend the head\u2019s corners cut`] : []
945
+ ],
946
+ describe("alt")
947
+ )
948
+ );
949
+ if (top !== null && zMin !== void 0) {
950
+ const reach = top - zMin;
951
+ const ratio = (reach / head).toFixed(3);
952
+ rows.push({
953
+ key: "topLd",
954
+ label: "L/D to top of part",
955
+ milling: true,
956
+ value: ratio,
957
+ derivation: [
958
+ `L = depth below top of part = ${mm(reach)}`,
959
+ `D = max cutting diameter = ${mm(head)}`,
960
+ `L / D = ${mm(reach)} / ${mm(head)} = ${ratio}`
961
+ ]
962
+ });
963
+ }
964
+ }
965
+ if (shaft !== void 0 || tslot) {
966
+ const size = shaft !== void 0 ? length(shaft) : void 0;
967
+ rows.push({
968
+ key: "maxShaft",
969
+ label: "Max shaft diameter",
970
+ milling: true,
971
+ ...size ? { value: `\u2300 ${size.value}`, alt: `\u2300 ${size.alt}` } : { value: "No limit" },
972
+ derivation: size ? shaftLines : [
973
+ "facts.maxEntryCd is not reported and the head or undercut depth is not known: nothing limits the shaft."
974
+ ]
975
+ });
976
+ }
977
+ return rows;
779
978
  };
780
979
 
781
980
  // src/model/pinch-mark.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolpath/dfm",
3
- "version": "0.3.1",
3
+ "version": "0.4.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/api": "^0.7.0",
68
- "@toolpath/ui": "^1.0.0"
67
+ "@toolpath/ui": "^1.0.0",
68
+ "@toolpath/api": "^0.7.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",