@toolpath/dfm 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,634 @@
1
- import { h as DfmHit, U as Units, i as FeaturePinch, j as FeatureSheet, g as RuleProcess, D as DfmRule, R as RuleVocabulary, b as DfmFeature, a as DfmCheck } from '../feature-record-CfjgDCJU.js';
2
- export { B as BrokenRule, C as CRITICAL_COLOR, k as ClearanceFit, l as DfmMetric, m as DfmOp, n as DfmQuantity, o as DfmSources, p as DfmSubject, q as FactsKind, r as FeatureAcross, d as FeatureProfile, e as FeatureRecord, F as FeatureSheets, s as FeatureThreading, t as FeatureUndercut, f as MILLING_RULES, M as Measurement, P as PINCH_TOOL_MM, u as PinchDisc, v as RULE_COLORS, w as RangeHigh, x as RangeLow, c as ReachCurve, y as ReportFeature, z as RuleResult, S as SurfaceToolKind, T as TURNING_RULES, A as TurnedFactsKind, W as WARNING_COLOR, E as brokenRules, G as checkPart, H as compareColors, I as describeRule, J as dfmFeatures, K as featureColor, L as featureMeasurements, N as featureProfile, O as featureSheet, Q as featureTypeLabel, V as isNoReading, X as isSurfaceType, Y as makeRule, Z as maxBallDiameter, _ as parseRules, $ as peakHeight, a0 as readPinch, a1 as readReachCurve, a2 as runStart, a3 as trimmedCurve, a4 as turnedTypeLabel, a5 as withSurroundedFaces } from '../feature-record-CfjgDCJU.js';
1
+ /**
2
+ * Where a feature is at its tightest (the datasheet's `pinchPoints`, API
3
+ * 1.12.1): one disc per stretch of the feature that reaches its minimum
4
+ * clearance, tightest first and never more than ten.
5
+ *
6
+ * As the API defines it, a disc fits in the space the clearance was measured
7
+ * in and touches the feature's boundary, so a cylinder of its diameter
8
+ * standing at its centre and spanning the datasheet's `zMin`..`zMax` is
9
+ * exactly the widest tool that reaches the boundary there. That is what makes
10
+ * them drawable: each is the tool the feature allows, standing where the
11
+ * feature stops a larger one.
12
+ *
13
+ * The Engine measures each disc to a tolerance (`toleranceBand.atolIgnore`),
14
+ * which lets it read a little wider than the gap it sits in. The discs here
15
+ * have that slack taken out (see {@link clearanceFit}), so each is the
16
+ * clearance itself: in a 2.5 mm corner, a 5 mm disc.
17
+ *
18
+ * Empty for the kinds whose clearance is not measured off a single medial
19
+ * axis (a hole, a facing pass, the undercut and layered kinds), so empty is
20
+ * "nowhere to point at", never "nowhere is tight".
21
+ *
22
+ * Millimetres, in the datasheet's tool frame: x and y across the tool, z up
23
+ * it.
24
+ */
25
+ /** One disc: its centre across the tool, and its diameter. */
26
+ interface PinchDisc {
27
+ x: number;
28
+ y: number;
29
+ diameter: number;
30
+ }
31
+ /** A feature's pinch discs, and the band along the tool they stand through. */
32
+ interface FeaturePinch {
33
+ discs: readonly PinchDisc[];
34
+ zMin: number;
35
+ zMax: number;
36
+ /**
37
+ * The Engine measured the clearance at one tolerance only, so the slack
38
+ * could not be taken out: each disc is as measured, the most the clearance
39
+ * can be.
40
+ */
41
+ unresolved?: true;
42
+ }
43
+ /**
44
+ * The figures a feature's clearance is run back from, as the datasheet has
45
+ * them: the widest tool that reaches every point (`min` of the cutter band,
46
+ * which is also its tightest pinch disc) at the strict tolerance, and at the
47
+ * looser one where the Engine reports it.
48
+ */
49
+ interface ClearanceFit {
50
+ /** `facts.cd`, or `facts.three.cd` for a chamfer. */
51
+ bandPath: string;
52
+ strict: number;
53
+ loose?: number;
54
+ atStrict?: number;
55
+ atLoose?: number;
56
+ /**
57
+ * How much wider, mm, the tolerance lets the widest tool read than the
58
+ * clearance it fits: what to take off a measured disc. Zero where the
59
+ * Engine measured at one tolerance only (`resolved` false), or where the
60
+ * tool does not grow with tolerance (a gap between walls, not a corner).
61
+ */
62
+ slack: number;
63
+ resolved: boolean;
64
+ }
65
+ /** A datasheet's pinch discs, the tolerance taken out of each, or null where it has none to draw. */
66
+ declare const readPinch: (datasheet: unknown) => FeaturePinch | null;
67
+
68
+ /**
69
+ * The few numbers of a feature's datasheet that the DFM rules and the
70
+ * feature details read. All millimetres, as the Engine reports them. Each is
71
+ * left out where this kind of feature does not report it, rather than sent as
72
+ * a zero that reads as measured.
73
+ *
74
+ * From the quoting app (quoting-ui `server/lib/engine.ts`). A datasheet is
75
+ * large, so the worker cuts each one down to this before it sends it.
76
+ */
77
+ interface FeatureSheet {
78
+ /** The Engine's family for it: `Hole`, `Pocket`, … */
79
+ kind?: string;
80
+ /** Along the machining direction, the feature's own bottom and top. */
81
+ zMin?: number;
82
+ zMax?: number;
83
+ /** Its top extended to the stock, which the highest of stands in for the part's top. */
84
+ extendedZMax?: number;
85
+ /** The widest cutter that still reaches the tightest corner. */
86
+ maxTool?: number;
87
+ diameter?: number;
88
+ /** For a hole: the full angle of the cone at its bottom, degrees; 180 for a flat bottom. */
89
+ tipAngle?: number;
90
+ filletRadius?: number;
91
+ chamferAngle?: number;
92
+ /**
93
+ * For a milled feature with walls: the radius of its tightest inside corner
94
+ * between walls, as the geometry has it rather than as a tool reaches it —
95
+ * zero for a sharp one. Half its minimum clearance: see {@link insideCornerRadius}.
96
+ */
97
+ cornerRadius?: number;
98
+ /**
99
+ * The Engine measured the clearance at one tolerance only, so neither
100
+ * `cornerRadius` nor `pinchDiameter` could be run back to none: each is as
101
+ * a tool reads it at that tolerance, the most the clearance can be.
102
+ */
103
+ cornerRadiusUnresolved?: true;
104
+ /** What the clearance was worked out from, so a reader can follow it. See {@link ClearanceFit}. */
105
+ clearance?: ClearanceFit;
106
+ /**
107
+ * A vertical inside corner between its walls with no radius, which no end
108
+ * mill makes; and where, in the datasheet's tool frame (x, y across the
109
+ * tool; its `zMin`..`zMax` along it).
110
+ */
111
+ sharpCorners?: readonly {
112
+ x: number;
113
+ y: number;
114
+ }[];
115
+ /**
116
+ * For a contour surface: the largest tool its shape admits (`facts.toolFit`)
117
+ * — its diameter, and the radius on its corner, no larger than the
118
+ * surface's tightest concave curve; half the diameter for a ball.
119
+ */
120
+ fitToolDiameter?: number;
121
+ fitCornerRadius?: number;
122
+ /** Where the tool fit was read: `facts.toolFit` or `facts.three.toolFit`. */
123
+ toolFitPath?: string;
124
+ /** For a contour surface: only a ball end mill can finish it, not a bull nose. */
125
+ ballOnly?: true;
126
+ /**
127
+ * The feature's minimum clearance: the smallest of its pinch discs
128
+ * (`pinchPoints`), the tolerance taken out, leaving out the zero-wide ones
129
+ * a sharp corner reports. The widest tool that fits where it is tightest,
130
+ * and what a milled feature's L/D is taken over.
131
+ */
132
+ pinchDiameter?: number;
133
+ /** How many places are that tight: the pinch discs counted in `pinchDiameter`. */
134
+ pinchPlaces?: number;
135
+ /** No cutter fits anywhere in the feature. */
136
+ noToolFits?: true;
137
+ /** For a hole the model threads: its thread. */
138
+ threading?: FeatureThreading;
139
+ /** For an undercut — a T-slot or a dovetail: what its cutter is held to. */
140
+ undercut?: FeatureUndercut;
141
+ }
142
+ /**
143
+ * An undercut's own figures, from its `facts`: a T-slot's or a dovetail's.
144
+ * Millimetres and degrees. Each is left out where the Engine did not measure
145
+ * it, or where nothing limits it.
146
+ */
147
+ interface FeatureUndercut {
148
+ /** T-slot: how far the groove runs back from its opening, radially (`facts.undercutDepth`). */
149
+ undercutDepth?: number;
150
+ /**
151
+ * T-slot: the widest tool that comes down through the opening above it
152
+ * (`facts.maxEntryCd`), which holds the cutter's shaft. Left out where
153
+ * nothing above limits it; zero where nothing fits.
154
+ */
155
+ maxEntry?: number;
156
+ /** Dovetail: between the overhanging wall and the tool axis, degrees (`facts.taperDeg`). */
157
+ taperDeg?: number;
158
+ /** Dovetail: the widest clearance over the floor, the width the groove cuts at. */
159
+ floorWidth?: number;
160
+ /** Dovetail: the clearance through the opening at the top of the groove. */
161
+ topOpeningWidth?: number;
162
+ /** Dovetail: the groove's widths vary from place to place — it runs out somewhere. */
163
+ isExternal?: true;
164
+ /** T-slot: its walls close on themselves in plan view, so a cutter's head comes down through its opening. */
165
+ isClosed?: true;
166
+ /**
167
+ * The Engine could not measure it (`facts.isInvalidGeometry` or
168
+ * `facts.cd.measurementFailed`), so its figures offer no cutter.
169
+ */
170
+ unmeasured?: true;
171
+ }
172
+ /** A modelled thread, from a hole's `facts.threading`. Millimetres. */
173
+ interface FeatureThreading {
174
+ basicDiameter: number;
175
+ threadPitch: number;
176
+ }
177
+ /** Feature tag, lower-cased → its sheet. */
178
+ type FeatureSheets = Record<string, FeatureSheet>;
179
+ /** Narrower than this, mm, a pinch disc is a sharp corner, which no tool fits: not a pinch tool. */
180
+ declare const PINCH_TOOL_MM = 0.01;
181
+ /** A feature that is no reading of its faces at all, which the analysis leaves out. */
182
+ declare const isNoReading: (featureType: unknown, datasheet: unknown) => boolean;
183
+ /** One datasheet, cut down to what the rules read. Read loosely: kinds report different fields. */
184
+ declare const featureSheet: (datasheet: unknown) => FeatureSheet;
185
+
186
+ type Direction = {
187
+ readonly x: number;
188
+ readonly y: number;
189
+ readonly z: number;
190
+ };
191
+ /**
192
+ * A feature as the Engine's report gives it, cut down to the fields the rules
193
+ * read. `@toolpath/api`'s `PartFeature` is one, so the package takes no
194
+ * dependency on the SDK for a type.
195
+ */
196
+ interface ReportFeature {
197
+ readonly featureTag: string;
198
+ readonly featureType: string;
199
+ readonly machiningDirection: Direction;
200
+ readonly regionIdxs: readonly number[];
201
+ }
202
+ /** A feature as the DFM rules read it: which it is, what kind, the way it is machined, and its faces. */
203
+ interface DfmFeature {
204
+ readonly tag: string;
205
+ readonly featureType: string;
206
+ readonly machiningDirection: Direction;
207
+ readonly regionIdxs: readonly number[];
208
+ /**
209
+ * What the panels call it, after its type: 1, 2, 3… across the part,
210
+ * numbered a machining direction at a time, so the features cut one way
211
+ * run together. Arbitrary otherwise, but steady for one analysis.
212
+ */
213
+ readonly number: number;
214
+ }
215
+ /** The report's features, cut down to what the rules read, and numbered by direction. */
216
+ declare const dfmFeatures: (features: readonly ReportFeature[]) => DfmFeature[];
217
+ /** `through_hole` as `Through hole`. The Engine's feature types are open-ended snake case. */
218
+ declare const featureTypeLabel: (featureType: string) => string;
219
+
220
+ /** A turning datasheet's `facts.kind`, and `Unknown` for a stretch the Engine could not name. */
221
+ type TurnedFactsKind = 'AxialHole' | 'Face' | 'FaceGroove' | 'FaceProfile' | 'IdGroove' | 'IdProfile' | 'IdThread' | 'OdGroove' | 'OdProfile' | 'OdThread' | 'Unknown';
222
+ /**
223
+ * `od_groove` as `OD groove`, `axial_hole` as `Axial hole`. The Engine's
224
+ * turned feature types are open-ended snake case, so a type it adds later
225
+ * reads as its words.
226
+ */
227
+ declare const turnedTypeLabel: (featureType: string) => string;
228
+ /** Turning rules: the measures and subjects of the Engine's turned features. */
229
+ declare const TURNING_RULES: RuleVocabulary;
230
+
231
+ /** The unit a length is shown in. The Engine's meshes are always millimetres. */
232
+ type Units = 'mm' | 'inch';
233
+ /** The number alone, for a line of several lengths that names the unit once. */
234
+ declare const lengthValue: (mm: number, units: Units) => string;
235
+ declare const unitLabel: (units: Units) => string;
236
+
237
+ /**
238
+ * At or above, above, at or below, below, equal, not equal, or between —
239
+ * equal meaning within half a percent, since figures are measured, and not
240
+ * equal anything further off; between from a low end up to, not including, a
241
+ * high one.
242
+ */
243
+ type DfmOp = 'gte' | 'gt' | 'lte' | 'lt' | 'eq' | 'ne' | 'between';
244
+ /**
245
+ * How a figure is read and typed: a length (mm), an area (mm²), an angle
246
+ * (degrees), a plain ratio, a count, a percentage, yes/no, or — for "any
247
+ * size" — nothing at all.
248
+ */
249
+ type DfmQuantity = 'length' | 'area' | 'angle' | 'ratio' | 'count' | 'percent' | 'flag' | 'presence';
250
+ /** A datasheet's `facts.kind`: which family of measurements the Engine took. */
251
+ type FactsKind = 'Hole' | 'Pocket' | 'Boss' | 'Wall' | 'Face' | 'Three' | 'Chamfer' | 'Profile' | 'Dovetail' | 'Tslot';
252
+ /** A datasheet's `facts.kind`, milled or turned. */
253
+ type AnyFactsKind = FactsKind | TurnedFactsKind;
254
+ interface DfmMetric {
255
+ key: string;
256
+ /** As the rule sentence reads it: "L/D", "depth below top". */
257
+ label: string;
258
+ /** The heading it is listed under when choosing. */
259
+ group: string;
260
+ quantity: DfmQuantity;
261
+ /** Which way is worse, for a new rule: deep is bad (≥), a tiny corner is bad (≤). */
262
+ op: DfmOp;
263
+ /** What a new rule starts at: millimetres and inches kept apart, so both read as round numbers. */
264
+ start: Record<Units, number>;
265
+ /** What it means, in a few plain words. */
266
+ hint: string;
267
+ /** The kinds of datasheet that report it; every kind when left out. */
268
+ kinds?: readonly AnyFactsKind[];
269
+ /**
270
+ * Where it sits in the datasheet, first found wins: a chamfer keeps its
271
+ * cutter band under its surface reading (`facts.three.cd`). Left out for a
272
+ * measure worked out rather than read (see `dfm-flags.ts`).
273
+ */
274
+ path?: readonly string[];
275
+ /** Whether a rule may name a value below zero: a height, or a signed radius. */
276
+ signed?: true;
277
+ }
278
+ declare const PRESENCE_METRIC: DfmMetric;
279
+ interface DfmSubject {
280
+ value: string;
281
+ /** Plural, as the rule sentence starts: "Holes", "Undercut T-slots". */
282
+ label: string;
283
+ /** "Groups" or "Feature types", for choosing. */
284
+ group: string;
285
+ /** The datasheet kinds its features report; every kind when left out. */
286
+ kinds?: readonly AnyFactsKind[];
287
+ }
288
+ /** The common subjects, one list — broad groups and the few types worth naming alone. */
289
+ declare const QUICK_SUBJECTS: readonly string[];
290
+ /** Which kind of machining a set of rules is about: a milled part's features, or a turned part's. */
291
+ type RuleProcess = 'milling' | 'turning';
292
+ /**
293
+ * Everything a set of rules can say, for one process: its measures, its
294
+ * subjects, the short lists offered first, and how a feature that breaks a
295
+ * rule is named in a list. Pure data, so the editor, the checker and the
296
+ * rule-set file read milling and turning rules the same way.
297
+ */
298
+ interface RuleVocabulary {
299
+ process: RuleProcess;
300
+ metrics: readonly DfmMetric[];
301
+ subjects: readonly DfmSubject[];
302
+ quickSubjects: readonly string[];
303
+ quickMetrics: readonly string[];
304
+ /** What a new or moved rule falls back on when its own measure is unknown: L/D for milling, "any size" for turning. */
305
+ fallbackMetric: DfmMetric;
306
+ /**
307
+ * A feature as a list or the feature panel names it: `{ name: 'Blind hole',
308
+ * detail: 'from +Z' }`, or for a turned one `{ name: 'OD groove', detail:
309
+ * 'from both ends' }`. The detail follows "Machined" in the feature panel.
310
+ */
311
+ featureLabel: (feature: Pick<DfmFeature, 'featureType' | 'machiningDirection'>) => {
312
+ name: string;
313
+ detail: string;
314
+ };
315
+ /** The feature panel's heading over what a feature asks of the machining: `Milling considerations`. */
316
+ considerationsTitle: string;
317
+ /** What an exported set is saved as. */
318
+ fileName: string;
319
+ }
320
+ /** Milling rules: the measures and subjects above, which the Engine's milled features report. */
321
+ declare const MILLING_RULES: RuleVocabulary;
322
+ declare const metricInfo: (key: string, vocabulary?: RuleVocabulary) => DfmMetric | undefined;
323
+ /**
324
+ * The measures a subject's features can have: those every kind reports, and
325
+ * those its kinds do. Any feature, of whatever kind, can have any of them.
326
+ */
327
+ declare const metricsForSubject: (subject: string, vocabulary?: RuleVocabulary) => readonly DfmMetric[];
328
+
329
+ declare const OPS: readonly DfmOp[];
330
+ /** How a range holds its low end: at or above it, or above it. */
331
+ type RangeLow = 'gte' | 'gt';
332
+ /** How a range holds its high end: below it, or at or below it. */
333
+ type RangeHigh = 'lt' | 'lte';
334
+ declare const RANGE_LOWS: readonly RangeLow[];
335
+ declare const RANGE_HIGHS: readonly RangeHigh[];
336
+ declare const opSymbol: (op: DfmOp) => string;
337
+ interface DfmRule {
338
+ /** Stable, so a list can key its rows and a feature can name the rule it broke. */
339
+ id: string;
340
+ /** Which features: a value from its vocabulary's subjects — `FEATURE_SUBJECTS` for milling. */
341
+ subject: string;
342
+ /** What is measured: a key from its vocabulary's metrics — `FEATURE_METRICS` for milling. */
343
+ metric: string;
344
+ op: DfmOp;
345
+ /** Canonical — mm or a ratio. Ignored by a yes/no metric. The low end of a range. */
346
+ value: number;
347
+ /** A range's high end. Only a `between` rule has one. */
348
+ max?: number;
349
+ /** How a range holds its low end; ≥ where unsaid. Only a `between` rule has one. */
350
+ lowOp?: RangeLow;
351
+ /** How a range holds its high end; < where unsaid. Only a `between` rule has one. */
352
+ highOp?: RangeHigh;
353
+ /** `#rrggbb`: what the features that break it are painted. */
354
+ color: string;
355
+ }
356
+ /**
357
+ * The colours a rule can wear: vivid, and far enough apart in hue and
358
+ * lightness to tell apart on the part at a glance (no two closer than ΔE 37,
359
+ * and none within 50 of the selection's blue). Warm first, since most
360
+ * rules call out trouble; then cool ones for a rule that marks something
361
+ * good.
362
+ */
363
+ declare const RULE_COLORS: readonly {
364
+ name: string;
365
+ value: string;
366
+ }[];
367
+ /**
368
+ * Which colour shows where a feature breaks rules of two: the one earliest in
369
+ * the palette, so red over yellow. Colours from outside it come after, in an
370
+ * order of their own, so a tie never falls back on the list.
371
+ */
372
+ declare const compareColors: (a: string, b: string) => number;
373
+ /** The quoting app's two severities, as the colours a rule starts in. */
374
+ declare const CRITICAL_COLOR = "#e5484d";
375
+ declare const WARNING_COLOR = "#ffd60a";
376
+ declare const subjectLabel: (value: string, vocabulary?: RuleVocabulary) => string;
377
+ /** The unit a value is typed in, beside its field. */
378
+ declare const quantityUnit: (quantity: DfmQuantity, units: Units) => string;
379
+ /** A canonical value as its field shows it. */
380
+ declare const toField: (quantity: DfmQuantity, value: number, units: Units) => string;
381
+ /**
382
+ * What a field says, back to canonical — or null when it is not a number,
383
+ * is below zero where the measure cannot be, or is a fraction of a count.
384
+ */
385
+ declare const fromField: (metric: DfmMetric, text: string, units: Units) => number | null;
386
+ /** A measured value, as a row says it: `7.5`, `1.25"`, `3.2 mm`, `1,200 mm²`. */
387
+ declare const formatMeasured: (quantity: DfmQuantity, value: number, units: Units) => string;
388
+ /**
389
+ * A rule as one plain line: "Holes with L/D ≥ 8", "Holes with 4 ≤ L/D < 8",
390
+ * "Any feature with a sharp inside corner", "Undercut T-slots, any size".
391
+ */
392
+ declare const describeRule: (rule: DfmRule, units: Units, vocabulary?: RuleVocabulary) => string;
393
+ /**
394
+ * A new rule for a subject and metric, at the metric's own starting value in
395
+ * the reader's units. A metric the vocabulary does not know is its fallback.
396
+ */
397
+ declare const makeRule: (subject: string, metricKey: string, units: Units, color?: string, vocabulary?: RuleVocabulary) => DfmRule;
398
+ /** A range's high end: its own, or as far again above its low one (at least 1) where it has none yet. */
399
+ declare const upperOf: (rule: DfmRule) => number;
400
+ /** How a range holds its low end. */
401
+ declare const lowOf: (rule: DfmRule) => RangeLow;
402
+ /** How a range holds its high end. */
403
+ declare const highOf: (rule: DfmRule) => RangeHigh;
404
+ /** A rule with its metric moved: the metric's own way and starting value, since the old ones meant something else. */
405
+ declare const withMetric: (rule: DfmRule, metricKey: string, units: Units, vocabulary?: RuleVocabulary) => DfmRule;
406
+ /** A rule with its comparison changed: a range gets a high end, and anything else loses it. */
407
+ declare const withOp: (rule: DfmRule, op: DfmOp) => DfmRule;
408
+ /** A rule with its subject moved, and its metric too when the new subject has no such measure. */
409
+ declare const withSubject: (rule: DfmRule, subject: string, units: Units, vocabulary?: RuleVocabulary) => DfmRule;
410
+ /**
411
+ * Stored rules, the ones that read in this vocabulary — a turning rule is
412
+ * dropped as milling reads it, and the other way round; null when what is
413
+ * stored is not a list at all.
414
+ */
415
+ declare const parseRules: (raw: unknown, vocabulary?: RuleVocabulary) => DfmRule[] | null;
416
+
417
+ /** What the checks read of the part, beyond its features. */
418
+ interface DfmSources {
419
+ /** Each feature's measurements, cut down (`feature-sheet.ts`), by lower-cased tag. */
420
+ sheets: FeatureSheets;
421
+ /** Each feature's datasheet as the API sent it, by lower-cased tag. */
422
+ datasheets: Readonly<Record<string, unknown>>;
423
+ /** Each face's area, mm², by region index. */
424
+ regionAreas: ReadonlyMap<number, number>;
425
+ }
426
+ /** One rule a feature broke, with the figure that broke it. */
427
+ interface DfmHit {
428
+ rule: DfmRule;
429
+ metric: DfmMetric;
430
+ /** What was measured, canonical. */
431
+ value: number;
432
+ /** For an L/D: against a drill's bore rather than the widest cutter. */
433
+ drilling?: boolean;
434
+ }
435
+ /** One rule and the features that break it. */
436
+ interface RuleResult {
437
+ rule: DfmRule;
438
+ /** The required features' tags, as the report spells them: the worst first, the furthest past the rule. */
439
+ tags: string[];
440
+ /**
441
+ * The other readings that break it, worst first: features some other
442
+ * reading could stand in for, so they are not counted or painted, but a
443
+ * reader opening the rule should see everything it caught.
444
+ */
445
+ otherTags: string[];
446
+ }
447
+ /** The part checked against the rules. */
448
+ interface DfmCheck {
449
+ /** Lower-cased tag → the rules the feature breaks, in the rules' order. Every feature, required or not. */
450
+ hitsByTag: ReadonlyMap<string, DfmHit[]>;
451
+ /** Lower-cased tags of the features a plan cannot do without. */
452
+ required: ReadonlySet<string>;
453
+ /** Each rule in order, with the required features that break it. */
454
+ byRule: RuleResult[];
455
+ /**
456
+ * Region → the colour a face is painted alone, and the reading it is painted for. See
457
+ * {@link unavoidableFaces}, and {@link withSurroundedFaces} for the faces painted with their surroundings.
458
+ */
459
+ paintedFaces: ReadonlyMap<number, {
460
+ tag: string;
461
+ color: string;
462
+ }>;
463
+ /** Lower-cased tag → a contour surface painted whole in its own worst colour. See {@link unavoidableFaces}. */
464
+ paintedWhole: ReadonlyMap<string, {
465
+ tag: string;
466
+ color: string;
467
+ }>;
468
+ }
469
+ /**
470
+ * The check with the faces its paint surrounds painted too. A face some
471
+ * reading cuts clean is left unpainted, but where that face is one of the
472
+ * feature that paints most of its painted neighbours, it is part of that
473
+ * feature, cut with it: a through hole in a flagged pocket's wall, say, read
474
+ * clean as a hole drilled from a floor far below that no plan would drill it
475
+ * from. Leaving it bare punches a hole in the feature's paint, so it takes
476
+ * the feature's colour. Where two features paint as many neighbours, the
477
+ * face takes the worse colour of those it is one of.
478
+ *
479
+ * One pass, over the paint as the check left it, so paint never spreads
480
+ * from a face that was itself only surrounded.
481
+ */
482
+ declare const withSurroundedFaces: (check: DfmCheck, features: readonly DfmFeature[], adjacency: ReadonlyMap<number, ReadonlySet<number>>) => DfmCheck;
483
+ /**
484
+ * Checks every feature against every rule. Only the required features count
485
+ * against a rule: a feature some other reading could stand in for may never
486
+ * be cut, so flagging it would bury the ones that will be.
487
+ */
488
+ declare const checkPart: (features: readonly DfmFeature[], sources: DfmSources, rules: readonly DfmRule[]) => DfmCheck;
489
+
490
+ /**
491
+ * A rule a feature breaks, as a row of its details says it: a coloured dot,
492
+ * the rule, and the figure that broke it. Neutral, so an app with rules of its
493
+ * own — the quoting app's, by severity — fills it from those, and an app on
494
+ * this package's checker fills it with {@link brokenRules}.
495
+ */
496
+ interface BrokenRule {
497
+ /** Unique among one feature's rows: the rule's id. */
498
+ readonly key: string;
499
+ /** The dot: the rule's colour, or the app's colour for its severity. */
500
+ readonly color: string;
501
+ /** The rule: `Holes with L/D to top of part ≥ 8`, or a short label where `limit` says the rest. */
502
+ readonly text: string;
503
+ /** What the feature measured: `9.8`. Absent for a yes/no. */
504
+ readonly figure?: string;
505
+ /** The line it crossed, where `text` does not already say it: `≥ 8`. */
506
+ readonly limit?: string;
507
+ /** On hover: the rule written out, and why it matters. */
508
+ readonly note?: string;
509
+ }
510
+ /**
511
+ * The rules a feature breaks, as its details list them: one a measure, the
512
+ * worst, palette first, each written in the vocabulary its rules are in.
513
+ */
514
+ declare const brokenRules: (check: DfmCheck, tag: string, units: Units, vocabulary?: RuleVocabulary) => BrokenRule[];
515
+ /**
516
+ * The colour a feature is painted on the part: the one earliest in the
517
+ * palette of the rules it breaks, for a required feature. Read from a face
518
+ * painted alone (`region`), that face's colour where it is painted for this
519
+ * feature; a feature painted whole, its colour. Undefined otherwise.
520
+ */
521
+ declare const featureColor: (check: DfmCheck, tag: string, region?: number | null) => string | undefined;
3
522
 
4
523
  /** The figure that broke a rule, bare: `7.5`, `0.05"`. Empty for a yes/no or "any size". */
5
524
  declare const hitFigure: (hit: DfmHit, units: Units) => string;
6
525
 
526
+ /** One row of a feature's details. */
527
+ interface Measurement {
528
+ /** Stable, so a list can key its rows. */
529
+ readonly key: string;
530
+ readonly label: string;
531
+ readonly value: string;
532
+ /** The same number in the other unit. Absent on a ratio or a count. */
533
+ readonly alt?: string;
534
+ /**
535
+ * How it was worked out from the datasheet, a line at a time: the fields it
536
+ * reads, by their path in the raw record, and the arithmetic, in the
537
+ * Engine's millimetres.
538
+ */
539
+ readonly derivation: readonly string[];
540
+ /**
541
+ * A machining consideration rather than a measurement of the feature's own
542
+ * shape: the tool that cuts it, and how far it has to reach. The panel lists
543
+ * these under their own heading, for every kind of feature.
544
+ */
545
+ readonly milling?: true;
546
+ /** A row that stands for a tool the viewer can draw on the part, by its kind. */
547
+ readonly tool?: SurfaceToolKind;
548
+ }
549
+ /** Which of a surface's finishing tools: a ball nose, or a bull nose with a flat between its corners. */
550
+ type SurfaceToolKind = 'ball' | 'bull';
551
+ /** Whether a feature type is read as a surface: see `SURFACE_TYPES`. */
552
+ declare const isSurfaceType: (featureType: string) => boolean;
553
+ /**
554
+ * The biggest ball nose a surface admits, mm: a ball's corner radius is half
555
+ * its diameter, so it is held under the tool fit's diameter and corner radius
556
+ * at once. None where the surface reports no fit, or admits no corner at all.
557
+ */
558
+ declare const maxBallDiameter: (sheet: Pick<FeatureSheet, "fitToolDiameter" | "fitCornerRadius">) => number | undefined;
559
+ /**
560
+ * The rows a feature gets, in {@link ROW_ORDER}: its own geometry first, then
561
+ * where it sits in the part and the reach that makes, then the tools a
562
+ * surface admits. A row is left out rather than shown empty: "—" against a
563
+ * field this kind never reports reads as a measurement that failed.
564
+ */
565
+ declare const featureMeasurements: ({ features, feature, sheets, units, inchMark, }: {
566
+ features: readonly DfmFeature[];
567
+ feature: DfmFeature;
568
+ sheets: FeatureSheets;
569
+ units: Units;
570
+ /** Writes inches as `0.46"` rather than `0.46 in`. */
571
+ inchMark?: boolean;
572
+ }) => Measurement[];
573
+
574
+ /**
575
+ * Everything the API said about one feature: its entry in the part report and
576
+ * its datasheet, both as sent, so fields newer than any SDK are still there.
577
+ */
578
+ interface FeatureRecord {
579
+ readonly feature: unknown;
580
+ /** Null where the API sent no datasheet for the feature. */
581
+ readonly datasheet: unknown;
582
+ }
583
+ /** One leaf of a datasheet: where it sits — `facts.bevel.angleDeg` — and what it says. */
584
+ interface DatasheetField {
585
+ readonly path: string;
586
+ readonly value: string;
587
+ }
588
+ /** More rows than this and the list stops, rather than read a long curve out point by point. */
589
+ declare const MAX_DATASHEET_FIELDS = 500;
590
+ /**
591
+ * Every leaf of a datasheet by its path, as sent: millimetres and degrees,
592
+ * numbers to four places. A list of plain values is one row; a list of records
593
+ * is walked by index; an empty list or record is a row of its own. Stops at
594
+ * {@link MAX_DATASHEET_FIELDS}, and says so with `more`.
595
+ */
596
+ declare const datasheetFields: (datasheet: unknown) => {
597
+ fields: DatasheetField[];
598
+ more: boolean;
599
+ };
600
+
601
+ /**
602
+ * A machining direction as a reader names it: `+Z` along an axis, the vector
603
+ * otherwise. The same words `@toolpath/viewer` uses, kept here so the rule
604
+ * list does not pull in three.js for one label.
605
+ */
606
+ declare const directionLabel: (direction: {
607
+ readonly x: number;
608
+ readonly y: number;
609
+ readonly z: number;
610
+ }) => string;
611
+
7
612
  /** Whether two depths are one depth, as the quoting app counts them: within a twentieth, or a hundredth of a mm. */
8
613
  declare const sameDepth: (a: number, b: number) => boolean;
614
+ /** A row in a list of features: one feature, or a group of the same hole. */
615
+ type FeatureRow = {
616
+ kind: 'feature';
617
+ tag: string;
618
+ } | {
619
+ kind: 'holes';
620
+ /** Stable: the first hole's tag. */
621
+ key: string;
622
+ /** In list order, the first standing for them all. */
623
+ tags: [string, ...string[]];
624
+ diameter: number;
625
+ };
626
+ /**
627
+ * These features as a list's rows, identical holes gathered into one where
628
+ * the first of them was. Holes the Engine did not measure, and anything not
629
+ * a hole, stay a row each.
630
+ */
631
+ declare const featureRows: (tags: readonly string[], features: readonly DfmFeature[], sheets: FeatureSheets) => FeatureRow[];
9
632
 
10
633
  /**
11
634
  * A feature's pinch discs as the widest tool that fits stands in them: the
@@ -72,6 +695,64 @@ interface PlacedTool {
72
695
  */
73
696
  declare const placePinchTool: (mark: PinchMark, triangles: FaceTriangles, direction: Vec3, origin?: Vec3) => PlacedTool | null;
74
697
 
698
+ /**
699
+ * A feature's reach curve (the datasheet's `reachCurve`, API 1.12.1): how
700
+ * high the material around a feature stands, by how far out from it.
701
+ *
702
+ * As the API defines it: material within `horizontalOffset[i]` of the
703
+ * feature rises to `verticalOffset[i]` above it. Both lists ascend, the
704
+ * curve is a step that never falls, and past its last knot it holds. So the
705
+ * rise comes at the *start* of each run, not at its knot: between two knots
706
+ * the material could stand anywhere out to the next one, and the next one's
707
+ * height is the bound — as the tool catalog's sweep reads it
708
+ * (`@toolpath/tool-support` `heightAt`), so a stack clears here where it
709
+ * clears there. Offsets
710
+ * are measured from the feature — zero is the wall of the cut — so material
711
+ * at offset `d` meets a tool at radius `d` plus the cutter's radius. Each
712
+ * reading is the worst case over the whole feature, with no direction to it.
713
+ *
714
+ * Millimetres throughout.
715
+ */
716
+
717
+ interface ReachCurve {
718
+ /** Outward from the feature's wall, ascending. */
719
+ offsets: readonly number[];
720
+ /** How high the material within each offset rises above the feature, ascending. */
721
+ heights: readonly number[];
722
+ }
723
+ /** A datasheet's reach curve, or null where it has none worth drawing. */
724
+ declare const readReachCurve: (datasheet: unknown) => ReachCurve | null;
725
+ /** Where a knot's run starts, and so the nearest its material may stand: just past the knot before. */
726
+ declare const runStart: ({ offsets }: ReachCurve, index: number) => number;
727
+ /** The tallest the material stands anywhere the curve reaches. */
728
+ declare const peakHeight: ({ heights }: ReachCurve) => number;
729
+ /**
730
+ * The curve as the drawings show it: whole, where it keeps rising nearly to
731
+ * its end, and otherwise cut off {@link TAIL_MM} past where it reaches its
732
+ * full height, the height there held to the cut. `cut` says which, so the
733
+ * drawing can mark the break.
734
+ */
735
+ declare const trimmedCurve: (curve: ReachCurve) => {
736
+ curve: ReachCurve;
737
+ cut: boolean;
738
+ };
739
+ /** How wide the feature itself is, across the tool, and what that width is. */
740
+ interface FeatureAcross {
741
+ width: number;
742
+ /** A hole's bore, or a milled feature's tightest clearance (its smallest pinch disc). */
743
+ kind: 'diameter' | 'clearance';
744
+ }
745
+ /** The feature as the reach section draws it, left of its wall. */
746
+ interface FeatureProfile {
747
+ /** Its width, to draw it to scale; null draws it at a set share. */
748
+ across: FeatureAcross | null;
749
+ /** Its own depth, top to bottom (`zMax − zMin`), over the same floor the curve stands on. */
750
+ depth: number | null;
751
+ /** It runs out through the part, so there is no floor under it. */
752
+ through: boolean;
753
+ }
754
+ declare const featureProfile: (sheet: FeatureSheet | undefined, featureType: string) => FeatureProfile;
755
+
75
756
  /**
76
757
  * A set of DFM rules as a file: what Export writes, Import reads, and what
77
758
  * an LLM is asked to produce. One JSON object, so a shop can keep a set per
@@ -171,4 +852,4 @@ declare const turnedStretches: (features: readonly TurnedReportFeature[]) => Tur
171
852
  */
172
853
  declare const checkTurnedPart: (stretches: readonly TurnedStretch[], rules: readonly DfmRule[]) => DfmCheck;
173
854
 
174
- export { DfmCheck, DfmFeature, DfmHit, DfmRule, FILE_KIND, type FaceTriangles, FeaturePinch, FeatureSheet, type PinchMark, type PlacedTool, RuleProcess, type RuleSetFile, RuleVocabulary, type TurnedReportFeature, type TurnedStretch, Units, type Vec3, checkTurnedPart, hitFigure, llmRulePrompt, parseRuleSetFile, pinchLabel, pinchMark, placePinchTool, sameDepth, serializeRuleSet, turnedStretches };
855
+ export { type BrokenRule, CRITICAL_COLOR, type ClearanceFit, type DfmCheck, type DfmFeature, type DfmHit, type DfmMetric, type DfmOp, type DfmQuantity, type DfmRule, type DfmSources, type DfmSubject, FILE_KIND, type FaceTriangles, type FactsKind, type FeatureAcross, type FeaturePinch, type FeatureProfile, type FeatureRecord, type FeatureRow, type FeatureSheet, type FeatureSheets, type FeatureThreading, type FeatureUndercut, MAX_DATASHEET_FIELDS, MILLING_RULES, type Measurement, OPS, PINCH_TOOL_MM, PRESENCE_METRIC, type PinchDisc, type PinchMark, type PlacedTool, QUICK_SUBJECTS, RANGE_HIGHS, RANGE_LOWS, RULE_COLORS, type RangeHigh, type RangeLow, type ReachCurve, type ReportFeature, type RuleProcess, type RuleResult, type RuleSetFile, type RuleVocabulary, type SurfaceToolKind, TURNING_RULES, type TurnedFactsKind, type TurnedReportFeature, type TurnedStretch, type Units, type Vec3, WARNING_COLOR, brokenRules, checkPart, checkTurnedPart, compareColors, datasheetFields, describeRule, dfmFeatures, directionLabel, featureColor, featureMeasurements, featureProfile, featureRows, featureSheet, featureTypeLabel, formatMeasured, fromField, highOf, hitFigure, isNoReading, isSurfaceType, lengthValue, llmRulePrompt, lowOf, makeRule, maxBallDiameter, metricInfo, metricsForSubject, opSymbol, parseRuleSetFile, parseRules, peakHeight, pinchLabel, pinchMark, placePinchTool, quantityUnit, readPinch, readReachCurve, runStart, sameDepth, serializeRuleSet, subjectLabel, toField, trimmedCurve, turnedStretches, turnedTypeLabel, unitLabel, upperOf, withMetric, withOp, withSubject, withSurroundedFaces };