@toolpath/tool-drawing 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.
package/README.md CHANGED
@@ -45,6 +45,30 @@ chamfer. Where a number has to be assumed to draw at all — a drill point angle
45
45
  the vendor never published — the segment says so in its `provenance`, and a
46
46
  renderer is expected to show it.
47
47
 
48
+ ## Two kinds of holder
49
+
50
+ `holder` is a union, and the two members are different inputs rather than two
51
+ grades of one:
52
+
53
+ ```ts
54
+ // What a vendor's table states: a nose, a body, a flange.
55
+ holder: { noseDiameter: 28, noseLength: null, gaugeLength: 50, /* … */ }
56
+
57
+ // What the vendor's own CAD model measures: the silhouette, `[z, r]` in mm.
58
+ holder: { points: [[-48.4, 16], /* … */ [60, 8.5]], datum: 'gage-line', /* … */ }
59
+ ```
60
+
61
+ A measured profile is **not** projected onto the parametric fields — the
62
+ V-flange groove and the thread relief are the reason to measure at all, and a
63
+ nose diameter and a body length cannot carry them. It is drawn as measured,
64
+ vertex for vertex, with its nose face at the stickout. `isHolderProfile`
65
+ narrows the union for an adapter that holds both.
66
+
67
+ On a `gage-line` profile the drawing splits at `z = 0` — the spindle face — so
68
+ everything above it is shaded as the spindle connection, exactly as the
69
+ parametric flange is. A `nose`-datumed profile has no spindle face to split on
70
+ and no gauge length to state, and the note under the drawing says so.
71
+
48
72
  `geometry` keys are the scraper's own field names (`DC`, `SFDM`, `OAL`, `LCF`,
49
73
  `RE`, `SIG`, `NOF`, `shoulder-diameter`, `shoulder-length`). They are not
50
74
  renamed here: a translation table between two vocabularies is where an `SFDM`
@@ -80,7 +104,6 @@ unit a shop reads in is the application's.
80
104
 
81
105
  ```tsx
82
106
  import { ClearanceOverlay, tightestGaps, describeGaps } from '@toolpath/tool-drawing/clearance'
83
-
84
107
  ;<ToolDrawing assembly={assembly} collisions={collisions} verdict={{ clears, note }}>
85
108
  <ClearanceOverlay
86
109
  profile={profile}
@@ -1,3 +1,6 @@
1
+ // src/model/types.ts
2
+ var isHolderProfile = (holder) => "points" in holder;
3
+
1
4
  // src/model/outline.ts
2
5
  var EPSILON = 1e-6;
3
6
  var stated = (tool, code) => tool.provenance?.[code] ?? "vendor-stated";
@@ -96,6 +99,93 @@ var tip = (tool, LCF) => {
96
99
  return null;
97
100
  }
98
101
  };
102
+ var profileSegments = (holder, stickout) => {
103
+ const nose = holder.points[holder.points.length - 1];
104
+ if (holder.points.length < 2 || nose === void 0) {
105
+ return [];
106
+ }
107
+ const provenance = holder.provenance?.["points"] ?? "vendor-stated";
108
+ const gage = stickout + nose[0];
109
+ const up = [...holder.points].reverse().map(([z, r]) => ({ r, z: stickout + (nose[0] - z) }));
110
+ const cut = holder.datum === "gage-line" ? up.findIndex((point) => point.z > gage) : -1;
111
+ if (cut <= 0) {
112
+ return [{ part: "body", points: up, provenance }];
113
+ }
114
+ const below = up.slice(0, cut);
115
+ const above = up.slice(cut);
116
+ const last = below[below.length - 1];
117
+ const first = above[0];
118
+ const meet = last.z === gage ? last : {
119
+ r: last.r + (gage - last.z) / (first.z - last.z) * (first.r - last.r),
120
+ z: gage
121
+ };
122
+ if (meet !== last) {
123
+ below.push(meet);
124
+ }
125
+ above.unshift(meet);
126
+ return [
127
+ { part: "body", points: below, provenance },
128
+ { part: "flange", points: above, provenance }
129
+ ];
130
+ };
131
+ var parametricSegments = (holder, stickout, DC) => {
132
+ if (holder.noseDiameter === null) {
133
+ return [];
134
+ }
135
+ const segments = [];
136
+ const rh = holder.noseDiameter / 2;
137
+ const holderStated = (code) => holder.provenance?.[code] ?? "vendor-stated";
138
+ let top = stickout;
139
+ const noseLength = holder.noseLength ?? holder.gaugeLength ?? Math.max(20, DC * 3);
140
+ segments.push({
141
+ part: "nose",
142
+ points: [
143
+ { r: rh, z: stickout },
144
+ { r: rh, z: stickout + noseLength }
145
+ ],
146
+ provenance: holder.noseLength !== null ? holderStated("noseLength") : holder.gaugeLength === null ? "assumed" : holderStated("noseDiameter")
147
+ });
148
+ top = stickout + noseLength;
149
+ let radius = rh;
150
+ if (holder.bodyDiameter !== null && holder.bodyLength !== null) {
151
+ const rb = holder.bodyDiameter / 2;
152
+ segments.push({
153
+ part: "body",
154
+ points: [
155
+ { r: rb, z: top },
156
+ { r: rb, z: top + holder.bodyLength }
157
+ ],
158
+ provenance: holderStated("bodyDiameter")
159
+ });
160
+ top += holder.bodyLength;
161
+ radius = rb;
162
+ }
163
+ if (holder.projection !== null && holder.flangeDiameter !== null) {
164
+ const flangeAt = stickout + holder.projection;
165
+ const rf = holder.flangeDiameter / 2;
166
+ if (flangeAt > top) {
167
+ segments.push({
168
+ part: "body",
169
+ points: [
170
+ { r: radius, z: top },
171
+ { r: radius, z: flangeAt }
172
+ ],
173
+ provenance: "assumed"
174
+ });
175
+ }
176
+ const flangeTop = holder.gaugeLength !== null && holder.gaugeLength > holder.projection ? stickout + holder.gaugeLength : flangeAt + 20;
177
+ segments.push({
178
+ part: "flange",
179
+ points: [
180
+ { r: rf, z: flangeAt },
181
+ { r: rf, z: flangeTop }
182
+ ],
183
+ provenance: holder.gaugeLength === null ? "assumed" : holderStated("flangeDiameter")
184
+ });
185
+ top = flangeTop;
186
+ }
187
+ return segments;
188
+ };
99
189
  var assemblyOutline = (assembly) => {
100
190
  const { tool, holder, stickout } = assembly;
101
191
  const { DC, LCF, SFDM } = tool.geometry;
@@ -151,9 +241,8 @@ var assemblyOutline = (assembly) => {
151
241
  });
152
242
  top = shankTop;
153
243
  }
154
- if (stickout !== null && holder !== null && holder.noseDiameter !== null) {
155
- const rh = holder.noseDiameter / 2;
156
- const holderStated = (code) => holder.provenance?.[code] ?? "vendor-stated";
244
+ const holderSegments = stickout === null || holder === null ? [] : isHolderProfile(holder) ? profileSegments(holder, stickout) : parametricSegments(holder, stickout, DC);
245
+ if (stickout !== null && holder !== null && holderSegments.length > 0) {
157
246
  const series = /(\d+(?:\.\d+)?)/.exec(holder.colletSeries ?? "");
158
247
  if (holder.colletProtrusion !== null && series) {
159
248
  const rc = Number(series[1]) / 2;
@@ -163,63 +252,18 @@ var assemblyOutline = (assembly) => {
163
252
  { r: rc, z: stickout - holder.colletProtrusion },
164
253
  { r: rc, z: stickout }
165
254
  ],
166
- provenance: holderStated("colletProtrusion")
167
- });
168
- }
169
- const noseLength = holder.noseLength ?? holder.gaugeLength ?? Math.max(20, DC * 3);
170
- segments.push({
171
- part: "nose",
172
- points: [
173
- { r: rh, z: stickout },
174
- { r: rh, z: stickout + noseLength }
175
- ],
176
- provenance: holder.noseLength !== null ? holderStated("noseLength") : holder.gaugeLength === null ? "assumed" : holderStated("noseDiameter")
177
- });
178
- top = stickout + noseLength;
179
- let radius2 = rh;
180
- if (holder.bodyDiameter !== null && holder.bodyLength !== null) {
181
- const rb = holder.bodyDiameter / 2;
182
- segments.push({
183
- part: "body",
184
- points: [
185
- { r: rb, z: top },
186
- { r: rb, z: top + holder.bodyLength }
187
- ],
188
- provenance: holderStated("bodyDiameter")
189
- });
190
- top += holder.bodyLength;
191
- radius2 = rb;
192
- }
193
- if (holder.projection !== null && holder.flangeDiameter !== null) {
194
- const flangeAt = stickout + holder.projection;
195
- const rf = holder.flangeDiameter / 2;
196
- if (flangeAt > top) {
197
- segments.push({
198
- part: "body",
199
- points: [
200
- { r: radius2, z: top },
201
- { r: radius2, z: flangeAt }
202
- ],
203
- provenance: "assumed"
204
- });
205
- }
206
- const flangeTop = holder.gaugeLength !== null && holder.gaugeLength > holder.projection ? stickout + holder.gaugeLength : flangeAt + 20;
207
- segments.push({
208
- part: "flange",
209
- points: [
210
- { r: rf, z: flangeAt },
211
- { r: rf, z: flangeTop }
212
- ],
213
- provenance: holder.gaugeLength === null ? "assumed" : holderStated("flangeDiameter")
255
+ provenance: holder.provenance?.["colletProtrusion"] ?? "vendor-stated"
214
256
  });
215
- top = flangeTop;
216
257
  }
258
+ segments.push(...holderSegments);
259
+ top = holderSegments[holderSegments.length - 1].points.at(-1)?.z ?? top;
217
260
  }
218
261
  const radius = Math.max(...segments.flatMap((segment) => segment.points.map((point2) => point2.r)));
219
262
  return { segments, height: top, radius };
220
263
  };
221
264
 
222
265
  export {
266
+ isHolderProfile,
223
267
  hasNeck,
224
268
  assemblyOutline
225
269
  };
@@ -36,9 +36,65 @@ interface ViewerHolder {
36
36
  readonly colletProtrusion: number | null;
37
37
  readonly provenance?: Readonly<Record<string, Provenance>>;
38
38
  }
39
+ /**
40
+ * A holder as its own CAD model measures it: the silhouette, and nothing
41
+ * parametric.
42
+ *
43
+ * A {@link ViewerHolder} is a handful of numbers a vendor publishes in a table,
44
+ * and a drawing built from them is a stylised holder. This is the other thing a
45
+ * catalog can have — the envelope measured off the vendor's STEP model, a
46
+ * hundred-odd vertices carrying the V-flange groove and the thread relief that
47
+ * a machinist actually looks for. **It is not a refinement of the parametric
48
+ * form and does not project onto it**: reducing it to a nose and a body throws
49
+ * away the only reason to measure.
50
+ *
51
+ * So the two are a union rather than one shape with optional extras, and
52
+ * {@link isHolderProfile} tells them apart. A consumer that has both picks one;
53
+ * a consumer that has neither passes `null` and the tool is drawn alone.
54
+ */
55
+ interface ViewerHolderProfile {
56
+ /**
57
+ * The silhouette as `[z, r]` in millimetres, `z` ascending.
58
+ *
59
+ * Two vertices share a `z` where the solid steps, so this is a polyline and
60
+ * not a function of `z`. Fewer than two vertices is no holder and draws none.
61
+ */
62
+ readonly points: ReadonlyArray<readonly [z: number, r: number]>;
63
+ /**
64
+ * What `z = 0` means.
65
+ *
66
+ * `gage-line` is the spindle face, with `z` increasing toward the cutting end
67
+ * — so the taper is negative, the nose positive, and the holder's gauge
68
+ * length is the last vertex's `z`. `nose` is the frame a holder with no taper
69
+ * to solve a gauge plane on is measured in, and it is stated rather than
70
+ * silently referenced to an arbitrary end: there is no gauge length to read
71
+ * off it, and the drawing says so instead of printing one.
72
+ */
73
+ readonly datum: 'gage-line' | 'nose';
74
+ /** The series the holder takes, as {@link ViewerHolder} means it. */
75
+ readonly colletSeries: string | null;
76
+ /** How far the seated collet stands proud of the nose, in millimetres. */
77
+ readonly colletProtrusion: number | null;
78
+ /**
79
+ * Keyed as {@link ViewerHolder}'s is, plus `points` for the measurement
80
+ * itself — which is `vendor-stated` unless a caller says otherwise, because
81
+ * the shape measured is the vendor's own model rather than a derivation from
82
+ * its table.
83
+ */
84
+ readonly provenance?: Readonly<Record<string, Provenance>>;
85
+ }
86
+ /**
87
+ * Which of the two holder forms this is.
88
+ *
89
+ * On the presence of `points` rather than on a `kind` tag, because a tag would
90
+ * have to be added to {@link ViewerHolder} as well and every existing adapter
91
+ * would stop compiling to gain nothing a structural check does not already
92
+ * give.
93
+ */
94
+ declare const isHolderProfile: (holder: ViewerHolder | ViewerHolderProfile) => holder is ViewerHolderProfile;
39
95
  interface ViewerAssembly {
40
96
  readonly tool: ViewerTool;
41
- readonly holder: ViewerHolder | null;
97
+ readonly holder: ViewerHolder | ViewerHolderProfile | null;
42
98
  /**
43
99
  * How far the tool stands out of the holder nose, in millimetres — the
44
100
  * shop's number, nobody's else. Null draws the tool alone.
@@ -99,4 +155,4 @@ interface Outline {
99
155
  */
100
156
  declare const assemblyOutline: (assembly: ViewerAssembly) => Outline | null;
101
157
 
102
- export { type Outline, type OutlinePart, type OutlinePoint, type OutlineSegment, type Provenance, type ViewerAssembly, type ViewerHolder, type ViewerTool, assemblyOutline };
158
+ export { type Outline, type OutlinePart, type OutlinePoint, type OutlineSegment, type Provenance, type ViewerAssembly, type ViewerHolder, type ViewerHolderProfile, type ViewerTool, assemblyOutline, isHolderProfile };
@@ -1,6 +1,8 @@
1
1
  import {
2
- assemblyOutline
3
- } from "../chunk-OUMNS4RI.js";
2
+ assemblyOutline,
3
+ isHolderProfile
4
+ } from "../chunk-UBENO6GN.js";
4
5
  export {
5
- assemblyOutline
6
+ assemblyOutline,
7
+ isHolderProfile
6
8
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ViewerAssembly, Outline } from './geometry/index.js';
2
- export { Provenance, ViewerHolder, ViewerTool } from './geometry/index.js';
2
+ export { Provenance, ViewerHolder, ViewerHolderProfile, ViewerTool, isHolderProfile } from './geometry/index.js';
3
3
  import { F as Frame, S as Sheet, T as Theme, P as Padding } from './sheet-D0LSO7qP.js';
4
4
  export { B as Box, E as Extent, a as FrameOptions, O as Orientation, b as SHEETS, f as frameFor, o as orientationFor, t as typeSizeFor } from './sheet-D0LSO7qP.js';
5
5
  import * as react from 'react';
package/dist/index.js CHANGED
@@ -9,8 +9,9 @@ import {
9
9
  } from "./chunk-7ZID4QRO.js";
10
10
  import {
11
11
  assemblyOutline,
12
- hasNeck
13
- } from "./chunk-OUMNS4RI.js";
12
+ hasNeck,
13
+ isHolderProfile
14
+ } from "./chunk-UBENO6GN.js";
14
15
 
15
16
  // src/model/frame.ts
16
17
  var DEFAULT_PADDING = 16;
@@ -629,6 +630,7 @@ var ToolDrawing = ({
629
630
  const { tool, holder } = assembly;
630
631
  const name = caption ?? tool.label ?? tool.form;
631
632
  const outline = assemblyOutline(assembly);
633
+ const unstatedLength = holder !== null && (isHolderProfile(holder) ? holder.datum !== "gage-line" : holder.gaugeLength === null);
632
634
  if (outline === null) {
633
635
  const missing = tool.geometry.DC === void 0 || tool.geometry.LCF === void 0;
634
636
  return /* @__PURE__ */ jsx2(
@@ -818,7 +820,7 @@ var ToolDrawing = ({
818
820
  "Drawn from stated dimensions",
819
821
  assumed.length > 0 ? `; ${assumed.join(", ")} assumed` : "",
820
822
  ".",
821
- holder !== null && holder.gaugeLength === null ? " The holder length is not stated." : ""
823
+ unstatedLength ? " The holder length is not stated." : ""
822
824
  ]
823
825
  }
824
826
  )
@@ -838,6 +840,7 @@ export {
838
840
  figureType,
839
841
  formatMillimetres,
840
842
  frameFor,
843
+ isHolderProfile,
841
844
  laneOffset,
842
845
  orientationFor,
843
846
  stackLabels,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolpath/tool-drawing",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "2D elevation drawing of a cutting tool and its holder",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -43,11 +43,6 @@
43
43
  "README.md"
44
44
  ],
45
45
  "sideEffects": false,
46
- "scripts": {
47
- "build": "tsup src/index.ts src/geometry/index.ts src/clearance/index.ts --format esm --dts --clean --external react --external react-dom",
48
- "check-types": "tsc --noEmit",
49
- "test": "vitest run"
50
- },
51
46
  "peerDependencies": {
52
47
  "react": "^19.0.0",
53
48
  "react-dom": "^19.0.0"
@@ -63,5 +58,10 @@
63
58
  "tsup": "8.5.1",
64
59
  "typescript": "5.9.3",
65
60
  "vitest": "4.1.10"
61
+ },
62
+ "scripts": {
63
+ "build": "tsup src/index.ts src/geometry/index.ts src/clearance/index.ts --format esm --dts --clean --external react --external react-dom",
64
+ "check-types": "tsc --noEmit",
65
+ "test": "vitest run"
66
66
  }
67
- }
67
+ }