@toolpath/dfm 0.1.0 → 0.2.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,57 +1,76 @@
1
- import { d as DfmHit, U as Units, b as DfmFeature, F as FeatureSheets, e as FeatureSheet, D as DfmRule } from '../reach-CfmYU_yQ.js';
2
- export { C as CRITICAL_COLOR, f as ClearanceFit, a as DfmCheck, g as DfmMetric, h as DfmOp, i as DfmQuantity, j as DfmSources, k as FactsKind, l as FeatureAcross, m as FeaturePinch, c as FeatureProfile, n as FeatureThreading, P as PINCH_TOOL_MM, o as PinchDisc, p as RULE_COLORS, q as RangeHigh, r as RangeLow, R as ReachCurve, s as ReportFeature, t as RuleResult, W as WARNING_COLOR, u as checkPart, v as compareColors, w as describeRule, x as dfmFeatures, y as featureProfile, z as featureSheet, A as featureTypeLabel, B as isNoReading, E as makeRule, G as parseRules, H as peakHeight, I as readPinch, J as readReachCurve, K as runStart, L as trimmedCurve, M as withSurroundedFaces } from '../reach-CfmYU_yQ.js';
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';
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;
6
6
 
7
- /** One row of a feature's details. */
8
- interface Measurement {
9
- /** Stable, so a list can key its rows. */
10
- readonly key: string;
11
- readonly label: string;
12
- readonly value: string;
13
- /** The same number in the other unit. Absent on a ratio or a count. */
14
- readonly alt?: string;
15
- /**
16
- * How it was worked out from the datasheet, a line at a time: the fields it
17
- * reads, by their path in the raw record, and the arithmetic, in the
18
- * Engine's millimetres.
19
- */
20
- readonly derivation: readonly string[];
7
+ /** Whether two depths are one depth, as the quoting app counts them: within a twentieth, or a hundredth of a mm. */
8
+ declare const sameDepth: (a: number, b: number) => boolean;
9
+
10
+ /**
11
+ * A feature's pinch discs as the widest tool that fits stands in them: the
12
+ * datasheet's discs a tool can stand in, in its tool frame, mm, with how deep
13
+ * the feature runs and the corner the tool has.
14
+ */
15
+ interface PinchMark extends FeaturePinch {
21
16
  /**
22
- * A machining consideration rather than a measurement of the feature's own
23
- * shape: the tool that cuts it, and how far it has to reach. The panel lists
24
- * these under their own heading, for every kind of feature.
17
+ * The radius on the tool's corner, mm: a filleted pocket's floor fillet,
18
+ * which a bull nose of that radius cuts. Absent for a flat-bottomed tool.
25
19
  */
26
- readonly milling?: true;
27
- /** A row that stands for a tool the viewer can draw on the part, by its kind. */
28
- readonly tool?: SurfaceToolKind;
20
+ readonly corner?: number;
29
21
  }
30
- /** Which of a surface's finishing tools: a ball nose, or a bull nose with a flat between its corners. */
31
- type SurfaceToolKind = 'ball' | 'bull';
32
- /** Whether a feature type is read as a surface: see `SURFACE_TYPES`. */
33
- declare const isSurfaceType: (featureType: string) => boolean;
34
22
  /**
35
- * The biggest ball nose a surface admits, mm: a ball's corner radius is half
36
- * its diameter, so it is held under the tool fit's diameter and corner radius
37
- * at once. None where the surface reports no fit, or admits no corner at all.
23
+ * The pinch discs worth drawing a tool in: none for a feature with no discs,
24
+ * only a sharp corner's (no tool at all), or tools wider than
25
+ * {@link MAX_DRAWN_TOOL_MM}. A walled feature's floor fillet rounds the tool's
26
+ * corner; a surface's fillet radius is its own shape, and does not.
38
27
  */
39
- declare const maxBallDiameter: (sheet: Pick<FeatureSheet, "fitToolDiameter" | "fitCornerRadius">) => number | undefined;
28
+ declare const pinchMark: (datasheet: unknown, sheet: FeatureSheet | undefined, featureType: string) => PinchMark | null;
40
29
  /**
41
- * The rows a feature gets, in {@link ROW_ORDER}: its own geometry first, then
42
- * where it sits in the part and the reach that makes, then the tools a
43
- * surface admits. A row is left out rather than shown empty: "—" against a
44
- * field this kind never reports reads as a measurement that failed.
30
+ * What a pinch mark's tool is labelled: its diameter at the tightest disc —
31
+ * "≤" where the clearance was measured at one tolerance only — and, quieter,
32
+ * the kind of tool and how many places are that tight.
45
33
  */
46
- declare const featureMeasurements: ({ features, feature, sheets, units, }: {
47
- features: readonly DfmFeature[];
48
- feature: DfmFeature;
49
- sheets: FeatureSheets;
50
- units: Units;
51
- }) => Measurement[];
34
+ declare const pinchLabel: (mark: PinchMark, units: Units) => {
35
+ label: string;
36
+ note: string;
37
+ };
52
38
 
53
- /** Whether two depths are one depth, as the quoting app counts them: within a twentieth, or a hundredth of a mm. */
54
- declare const sameDepth: (a: number, b: number) => boolean;
39
+ /** A direction or a point. */
40
+ interface Vec3 {
41
+ readonly x: number;
42
+ readonly y: number;
43
+ readonly z: number;
44
+ }
45
+ /** A feature's faces as triangles: nine numbers each for the corners, three for the outward normal. */
46
+ interface FaceTriangles {
47
+ readonly positions: ArrayLike<number>;
48
+ readonly normals: ArrayLike<number>;
49
+ }
50
+ /** The widest tool a feature admits, placed on the part: what to draw, in the drawn coordinates. */
51
+ interface PlacedTool {
52
+ /** The centre of the tool's bottom, on the feature's floor. */
53
+ readonly base: Vec3;
54
+ /** Up the tool. */
55
+ readonly axis: Vec3;
56
+ readonly diameter: number;
57
+ readonly height: number;
58
+ /** The radius on its corner: a filleted floor's, no more than the tool's own radius. */
59
+ readonly cornerRadius: number;
60
+ /** Across the tool, toward where it touches the wall. */
61
+ readonly across: Vec3;
62
+ }
63
+ /**
64
+ * The widest tool a feature admits, standing on its tightest pinch disc, ready
65
+ * to draw: the datasheet's frame found against the feature's faces, the disc
66
+ * placed in it, and the tool turned toward the wall that pinches it.
67
+ *
68
+ * `origin` is where the CAD file's own zero sits in the coordinates the part
69
+ * is drawn in — moved by whatever centred it — since a datasheet is in the
70
+ * file's coordinates and the triangles and the answer are in the drawn ones.
71
+ * Null for a feature with no faces or no discs.
72
+ */
73
+ declare const placePinchTool: (mark: PinchMark, triangles: FaceTriangles, direction: Vec3, origin?: Vec3) => PlacedTool | null;
55
74
 
56
75
  /**
57
76
  * A set of DFM rules as a file: what Export writes, Import reads, and what
@@ -106,4 +125,4 @@ declare const parseRuleSetFile: (text: string) => {
106
125
  */
107
126
  declare const llmRulePrompt: (current?: readonly DfmRule[]) => string;
108
127
 
109
- export { DfmFeature, DfmHit, DfmRule, FILE_KIND, FeatureSheet, FeatureSheets, type Measurement, type RuleSetFile, type SurfaceToolKind, Units, featureMeasurements, hitFigure, isSurfaceType, llmRulePrompt, maxBallDiameter, parseRuleSetFile, sameDepth, serializeRuleSet };
128
+ export { DfmHit, DfmRule, FILE_KIND, type FaceTriangles, FeaturePinch, FeatureSheet, type PinchMark, type PlacedTool, type RuleSetFile, Units, type Vec3, hitFigure, llmRulePrompt, parseRuleSetFile, pinchLabel, pinchMark, placePinchTool, sameDepth, serializeRuleSet };
@@ -14,6 +14,7 @@ import {
14
14
  featureTypeLabel,
15
15
  hitFigure,
16
16
  isDrilled,
17
+ lengthValue,
17
18
  llmRulePrompt,
18
19
  makeRule,
19
20
  metricInfo,
@@ -26,9 +27,39 @@ import {
26
27
  sameDepth,
27
28
  serializeRuleSet,
28
29
  trimmedCurve,
30
+ unitLabel,
29
31
  worseUpward
30
32
  } from "../chunk-5MPHT7BC.js";
31
33
 
34
+ // src/model/broken-rules.ts
35
+ var byColor = (hits) => [...hits].sort((a, b) => compareColors(a.rule.color, b.rule.color));
36
+ var worstHits = (hits) => {
37
+ const seen = /* @__PURE__ */ new Set();
38
+ return byColor(hits).filter((hit) => {
39
+ if (seen.has(hit.metric.key)) return false;
40
+ seen.add(hit.metric.key);
41
+ return true;
42
+ });
43
+ };
44
+ var brokenRules = (check, tag, units) => worstHits(check.hitsByTag.get(tag.toLowerCase()) ?? []).map((hit) => {
45
+ const figure = hitFigure(hit, units);
46
+ return {
47
+ key: hit.rule.id,
48
+ color: hit.rule.color,
49
+ text: describeRule(hit.rule, units),
50
+ ...figure ? { figure } : {}
51
+ };
52
+ });
53
+ var featureColor = (check, tag, region = null) => {
54
+ const key = tag.toLowerCase();
55
+ const face = region === null ? void 0 : check.paintedFaces.get(region);
56
+ if (face && face.tag.toLowerCase() === key) return face.color;
57
+ const whole = check.paintedWhole.get(key);
58
+ if (whole) return whole.color;
59
+ if (!check.required.has(key)) return void 0;
60
+ return byColor(check.hitsByTag.get(key) ?? [])[0]?.rule.color;
61
+ };
62
+
32
63
  // src/model/dfm-flags.ts
33
64
  var howCut = (sheet) => {
34
65
  const drilled = isDrilled(sheet);
@@ -329,7 +360,7 @@ var clearanceDerivation = (fit, clearance) => {
329
360
  return lines;
330
361
  };
331
362
  var trim = (value, places) => String(Number(value.toFixed(places)));
332
- var formatLength = (mm2, units) => units === "inch" ? `${trim(mm2 / MM_PER_INCH, LENGTH_PLACES.inch)} in` : `${trim(mm2, LENGTH_PLACES.mm)} mm`;
363
+ var formatLength = (mm2, units, inchMark = false) => units === "inch" ? `${trim(mm2 / MM_PER_INCH, LENGTH_PLACES.inch)}${inchMark ? '"' : " in"}` : `${trim(mm2, LENGTH_PLACES.mm)} mm`;
333
364
  var SURFACE_TYPES = /* @__PURE__ */ new Set([
334
365
  "contour_surface",
335
366
  "inner_fillet",
@@ -365,10 +396,14 @@ var featureMeasurements = ({
365
396
  features,
366
397
  feature,
367
398
  sheets,
368
- units
399
+ units,
400
+ inchMark = false
369
401
  }) => {
370
402
  const other = units === "inch" ? "mm" : "inch";
371
- const length = (mm2) => ({ value: formatLength(mm2, units), alt: formatLength(mm2, other) });
403
+ const length = (mm2) => ({
404
+ value: formatLength(mm2, units, inchMark),
405
+ alt: formatLength(mm2, other, inchMark)
406
+ });
372
407
  const sheet = sheets[feature.tag.toLowerCase()];
373
408
  const rows = [];
374
409
  const top = sheet ? partTop(features, sheets, feature.machiningDirection) : null;
@@ -570,7 +605,7 @@ var featureMeasurements = ({
570
605
  rows.push({
571
606
  key: "thread",
572
607
  label: "Thread",
573
- value: `${formatLength(sheet.threading.basicDiameter, units)} \xD7 ${formatLength(sheet.threading.threadPitch, units)} pitch`,
608
+ value: `${formatLength(sheet.threading.basicDiameter, units, inchMark)} \xD7 ${formatLength(sheet.threading.threadPitch, units, inchMark)} pitch`,
574
609
  derivation: [
575
610
  `facts.threading.spec.basicDiameter = ${mm(sheet.threading.basicDiameter)}`,
576
611
  `facts.threading.spec.threadPitch = ${mm(sheet.threading.threadPitch)}`
@@ -742,16 +777,250 @@ var featureSheet = (datasheet) => {
742
777
  ...threading ? { threading } : {}
743
778
  };
744
779
  };
780
+
781
+ // src/model/pinch-mark.ts
782
+ import { MM_PER_INCH as MM_PER_INCH2 } from "@toolpath/tool-support";
783
+ var MAX_DRAWN_TOOL_MM = 3 * MM_PER_INCH2;
784
+ var pinchMark = (datasheet, sheet, featureType) => {
785
+ const pinch = readPinch(datasheet);
786
+ const discs = pinch?.discs.filter(
787
+ (disc) => disc.diameter >= PINCH_TOOL_MM && disc.diameter <= MAX_DRAWN_TOOL_MM
788
+ ) ?? [];
789
+ if (!pinch || discs.length === 0) return null;
790
+ const fillet = sheet?.filletRadius;
791
+ const corner = !isSurfaceType(featureType) && fillet !== void 0 && fillet > 0 ? fillet : void 0;
792
+ return { ...pinch, discs, ...corner !== void 0 ? { corner } : {} };
793
+ };
794
+ var pinchLabel = (mark, units) => {
795
+ const length = (mm2) => `${lengthValue(mm2, units)} ${unitLabel(units)}`;
796
+ const tightest = Math.min(...mark.discs.map((disc) => disc.diameter));
797
+ const tool = mark.corner !== void 0 ? `bull nose R ${length(mark.corner)}` : "widest tool";
798
+ const places = mark.discs.length;
799
+ return {
800
+ label: `${mark.unresolved ? "\u2264 " : ""}\u2300 ${length(tightest)}`,
801
+ note: places > 1 ? `${tool} \xB7 ${places} places` : tool
802
+ };
803
+ };
804
+
805
+ // src/model/tool-frame.ts
806
+ var extent = (values) => {
807
+ let low = Infinity;
808
+ let high = -Infinity;
809
+ for (const value of values) {
810
+ if (value < low) low = value;
811
+ if (value > high) high = value;
812
+ }
813
+ return { low, high };
814
+ };
815
+ var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
816
+ var cross = (a, b) => [
817
+ a[1] * b[2] - a[2] * b[1],
818
+ a[2] * b[0] - a[0] * b[2],
819
+ a[0] * b[1] - a[1] * b[0]
820
+ ];
821
+ var scale = (a, by) => [a[0] * by, a[1] * by, a[2] * by];
822
+ var add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
823
+ var unit = (a) => scale(a, 1 / Math.hypot(a[0], a[1], a[2]));
824
+ var ON_DEPTH_MM = 0.05;
825
+ var ON_CORNER_MM = 0.02;
826
+ var POINT_DISC_MM = 0.01;
827
+ var ON_WALL_MM = 0.1;
828
+ var ON_WALL_SHARE = 0.02;
829
+ var WALL_NORMAL_ALONG = 0.7;
830
+ var SAME_PLACE_MM = 0.05;
831
+ var MIN_HEIGHT_MM = 0.05;
832
+ var readTriangles = ({ positions, normals }, origin) => {
833
+ const triangles = [];
834
+ for (let at2 = 0; at2 + 8 < positions.length; at2 += 9) {
835
+ const corner = (k) => [
836
+ (positions[at2 + k] ?? 0) - origin[0],
837
+ (positions[at2 + k + 1] ?? 0) - origin[1],
838
+ (positions[at2 + k + 2] ?? 0) - origin[2]
839
+ ];
840
+ const n = at2 / 9 * 3;
841
+ triangles.push({
842
+ a: corner(0),
843
+ b: corner(3),
844
+ c: corner(6),
845
+ normal: [normals[n] ?? 0, normals[n + 1] ?? 0, normals[n + 2] ?? 0]
846
+ });
847
+ }
848
+ return triangles;
849
+ };
850
+ var pointsOf = (triangles) => triangles.flatMap(({ a, b, c }) => [a, b, c]);
851
+ var flatten = (point, { u, v }) => ({ x: dot(point, u), y: dot(point, v) });
852
+ var wallSegments = (triangles, frame) => triangles.filter(({ normal }) => Math.abs(dot(normal, frame.w)) < WALL_NORMAL_ALONG).flatMap(({ a, b, c, normal }) => {
853
+ const [fa, fb, fc] = [flatten(a, frame), flatten(b, frame), flatten(c, frame)];
854
+ const air = flatten(normal, frame);
855
+ return [
856
+ { p: fa, q: fb, air },
857
+ { p: fb, q: fc, air },
858
+ { p: fc, q: fa, air }
859
+ ];
860
+ });
861
+ var nearestOn = ({ p, q }, at2) => {
862
+ const dx = q.x - p.x;
863
+ const dy = q.y - p.y;
864
+ const length = dx * dx + dy * dy;
865
+ const t = length === 0 ? 0 : Math.min(1, Math.max(0, ((at2.x - p.x) * dx + (at2.y - p.y) * dy) / length));
866
+ return { x: p.x + t * dx, y: p.y + t * dy };
867
+ };
868
+ var nearestWall = (segments, at2) => segments.reduce((best, segment) => {
869
+ const point = nearestOn(segment, at2);
870
+ const distance2 = Math.hypot(point.x - at2.x, point.y - at2.y);
871
+ if (best && distance2 >= best.distance) return best;
872
+ const open = (at2.x - point.x) * segment.air.x + (at2.y - point.y) * segment.air.y >= 0;
873
+ return { point, distance: distance2, open };
874
+ }, null);
875
+ var fitsDiscs = (triangles, frame, discs) => {
876
+ const points = discs.filter((disc) => disc.diameter < POINT_DISC_MM);
877
+ const wide = discs.filter((disc) => disc.diameter >= POINT_DISC_MM);
878
+ if (points.length > 0) {
879
+ const flat = pointsOf(triangles).map((point) => flatten(point, frame));
880
+ const onCorners = points.every(
881
+ (disc) => flat.some((point) => Math.hypot(point.x - disc.x, point.y - disc.y) <= ON_CORNER_MM)
882
+ );
883
+ if (!onCorners) return false;
884
+ }
885
+ if (wide.length === 0) return points.length > 0;
886
+ const segments = wallSegments(triangles, frame);
887
+ if (segments.length === 0) return false;
888
+ let touches = false;
889
+ for (const disc of wide) {
890
+ const nearest = nearestWall(segments, disc);
891
+ if (!nearest || !nearest.open) return false;
892
+ const slack = ON_WALL_MM + ON_WALL_SHARE * disc.diameter;
893
+ const radius = disc.diameter / 2;
894
+ if (nearest.distance < radius - slack) return false;
895
+ if (nearest.distance <= radius + slack) touches = true;
896
+ }
897
+ return touches;
898
+ };
899
+ var placeInFrame = ({ u, v, w }, x, y, z) => add(add(scale(u, x), scale(v, y)), scale(w, z));
900
+ var AXES = [
901
+ [1, 0, 0],
902
+ [0, 1, 0],
903
+ [0, 0, 1]
904
+ ];
905
+ var candidateFrames = (points, zMin, zMax, direction, strict = true) => {
906
+ const along = unit(direction);
907
+ const frames = [];
908
+ for (const sign of [1, -1]) {
909
+ const w = scale(along, sign);
910
+ const { low, high } = extent(points.map((point) => dot(point, w)));
911
+ if (strict && (Math.abs(low - zMin) > ON_DEPTH_MM || Math.abs(high - zMax) > ON_DEPTH_MM)) {
912
+ continue;
913
+ }
914
+ const across = AXES.reduce(
915
+ (best, axis) => Math.abs(dot(axis, w)) < Math.abs(dot(best, w)) ? axis : best
916
+ );
917
+ const u0 = unit(add(across, scale(w, -dot(across, w))));
918
+ const v0 = cross(w, u0);
919
+ for (let quarter = 0; quarter < 4; quarter++) {
920
+ const angle = quarter * Math.PI / 2;
921
+ const u = add(scale(u0, Math.cos(angle)), scale(v0, Math.sin(angle)));
922
+ for (const hand of [1, -1]) frames.push({ u, v: scale(cross(w, u), hand), w });
923
+ }
924
+ }
925
+ return frames;
926
+ };
927
+ var distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
928
+ var exactFrame = (triangles, discs, zMin, zMax, direction) => {
929
+ const fits = candidateFrames(pointsOf(triangles), zMin, zMax, direction).filter(
930
+ (frame) => fitsDiscs(triangles, frame, discs)
931
+ );
932
+ const [first] = fits;
933
+ if (!first) return null;
934
+ const placed = (frame) => discs.map((disc) => placeInFrame(frame, disc.x, disc.y, zMin));
935
+ const firstPlaces = placed(first);
936
+ const agree = fits.every(
937
+ (frame) => placed(frame).every((at2, index) => distance(at2, firstPlaces[index] ?? at2) <= SAME_PLACE_MM)
938
+ );
939
+ return agree ? first : null;
940
+ };
941
+ var discMiss = (triangles, frame, discs) => {
942
+ let miss = 0;
943
+ const points = discs.filter((disc) => disc.diameter < POINT_DISC_MM);
944
+ if (points.length > 0) {
945
+ const flat = pointsOf(triangles).map((point) => flatten(point, frame));
946
+ for (const disc of points) {
947
+ miss += extent(flat.map((point) => Math.hypot(point.x - disc.x, point.y - disc.y))).low;
948
+ }
949
+ }
950
+ const wide = discs.filter((disc) => disc.diameter >= POINT_DISC_MM);
951
+ if (wide.length === 0) return miss;
952
+ const segments = wallSegments(triangles, frame);
953
+ let touch = Infinity;
954
+ for (const disc of wide) {
955
+ const radius = disc.diameter / 2;
956
+ const nearest = nearestWall(segments, disc);
957
+ if (!nearest) return Infinity;
958
+ if (!nearest.open) miss += disc.diameter * 4;
959
+ miss += Math.max(0, radius - nearest.distance);
960
+ touch = Math.min(touch, Math.abs(nearest.distance - radius));
961
+ }
962
+ return miss + touch;
963
+ };
964
+ var bestFrame = (triangles, discs, zMin, zMax, direction) => {
965
+ if (triangles.length === 0 || discs.length === 0) return null;
966
+ const exact = exactFrame(triangles, discs, zMin, zMax, direction);
967
+ if (exact) return { frame: exact, lift: 0 };
968
+ const points = pointsOf(triangles);
969
+ const fitting = candidateFrames(points, zMin, zMax, direction);
970
+ const frames = fitting.length > 0 ? fitting : candidateFrames(points, zMin, zMax, direction, false);
971
+ const best = frames.reduce((was, frame) => {
972
+ const miss = discMiss(triangles, frame, discs);
973
+ return !was || miss < was.miss ? { frame, miss } : was;
974
+ }, null);
975
+ if (!best) return null;
976
+ const { low } = extent(points.map((point) => dot(point, best.frame.w)));
977
+ return { frame: best.frame, lift: fitting.length > 0 ? 0 : low - zMin };
978
+ };
979
+ var asVec = ([x, y, z]) => ({ x, y, z });
980
+ var placePinchTool = (mark, triangles, direction, origin = { x: 0, y: 0, z: 0 }) => {
981
+ const shift = [origin.x, origin.y, origin.z];
982
+ const faces = readTriangles(triangles, shift);
983
+ const found = bestFrame(faces, mark.discs, mark.zMin, mark.zMax, [
984
+ direction.x,
985
+ direction.y,
986
+ direction.z
987
+ ]);
988
+ const disc = mark.discs.reduce(
989
+ (tightest, each) => !tightest || each.diameter < tightest.diameter ? each : tightest,
990
+ null
991
+ );
992
+ if (!found || !disc) return null;
993
+ const { frame, lift } = found;
994
+ const radius = disc.diameter / 2;
995
+ const base = add(placeInFrame(frame, disc.x, disc.y, mark.zMin + lift), shift);
996
+ const contact = nearestWall(wallSegments(faces, frame), disc)?.point;
997
+ const toward = contact ? { x: contact.x - disc.x, y: contact.y - disc.y } : { x: 1, y: 0 };
998
+ const length = Math.hypot(toward.x, toward.y);
999
+ const across = add(
1000
+ scale(frame.u, length > 1e-9 ? toward.x / length : 1),
1001
+ scale(frame.v, length > 1e-9 ? toward.y / length : 0)
1002
+ );
1003
+ return {
1004
+ base: asVec(base),
1005
+ axis: asVec(frame.w),
1006
+ diameter: disc.diameter,
1007
+ height: Math.max(mark.zMax - mark.zMin, MIN_HEIGHT_MM),
1008
+ cornerRadius: Math.min(Math.max(mark.corner ?? 0, 0), radius),
1009
+ across: asVec(across)
1010
+ };
1011
+ };
745
1012
  export {
746
1013
  CRITICAL_COLOR,
747
1014
  FILE_KIND,
748
1015
  PINCH_TOOL_MM,
749
1016
  RULE_COLORS,
750
1017
  WARNING_COLOR,
1018
+ brokenRules,
751
1019
  checkPart,
752
1020
  compareColors,
753
1021
  describeRule,
754
1022
  dfmFeatures,
1023
+ featureColor,
755
1024
  featureMeasurements,
756
1025
  featureProfile,
757
1026
  featureSheet,
@@ -765,6 +1034,9 @@ export {
765
1034
  parseRuleSetFile,
766
1035
  parseRules,
767
1036
  peakHeight,
1037
+ pinchLabel,
1038
+ pinchMark,
1039
+ placePinchTool,
768
1040
  readPinch,
769
1041
  readReachCurve,
770
1042
  runStart,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@toolpath/dfm",
3
- "version": "0.1.0",
4
- "description": "Toolpath DFM rules: the rule list component, the rules and the part checker",
3
+ "version": "0.2.0",
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": {
7
7
  "node": ">=20"