@toolpath/tool-drawing 0.1.0 → 0.3.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
@@ -14,6 +14,12 @@ in 3D; this one draws a catalog tool in 2D.
14
14
  npm install @toolpath/tool-drawing react react-dom
15
15
  ```
16
16
 
17
+ Its one runtime dependency is [`@toolpath/tool-support`](../tool-support), the
18
+ shared cutting-tool domain, which itself depends on nothing. `Provenance`,
19
+ `ViewerTool`, `ViewerHolder` and `ViewerHolderProfile` are aliases of its types
20
+ and are re-exported here, so a consumer that also uses `@toolpath/tool-support`
21
+ gets the same types and needs no adapter between them.
22
+
17
23
  ## Exports
18
24
 
19
25
  | Entry point | What it is |
@@ -24,6 +30,7 @@ npm install @toolpath/tool-drawing react react-dom
24
30
 
25
31
  `/geometry` is pure and server-safe: it touches no DOM and imports no React, so
26
32
  a Node server can measure an assembly without paying for a renderer.
33
+ `tests/subpaths.test.ts` asserts that from the import graph.
27
34
 
28
35
  ## Geometry
29
36
 
@@ -45,6 +52,30 @@ chamfer. Where a number has to be assumed to draw at all — a drill point angle
45
52
  the vendor never published — the segment says so in its `provenance`, and a
46
53
  renderer is expected to show it.
47
54
 
55
+ ## Two kinds of holder
56
+
57
+ `holder` is a union, and the two members are different inputs rather than two
58
+ grades of one:
59
+
60
+ ```ts
61
+ // What a vendor's table states: a nose, a body, a flange.
62
+ holder: { noseDiameter: 28, noseLength: null, gaugeLength: 50, /* … */ }
63
+
64
+ // What the vendor's own CAD model measures: the silhouette, `[z, r]` in mm.
65
+ holder: { points: [[-48.4, 16], /* … */ [60, 8.5]], datum: 'gage-line', /* … */ }
66
+ ```
67
+
68
+ A measured profile is **not** projected onto the parametric fields — the
69
+ V-flange groove and the thread relief are the reason to measure at all, and a
70
+ nose diameter and a body length cannot carry them. It is drawn as measured,
71
+ vertex for vertex, with its nose face at the stickout. `isHolderProfile`
72
+ narrows the union for an adapter that holds both.
73
+
74
+ On a `gage-line` profile the drawing splits at `z = 0` — the spindle face — so
75
+ everything above it is shaded as the spindle connection, exactly as the
76
+ parametric flange is. A `nose`-datumed profile has no spindle face to split on
77
+ and no gauge length to state, and the note under the drawing says so.
78
+
48
79
  `geometry` keys are the scraper's own field names (`DC`, `SFDM`, `OAL`, `LCF`,
49
80
  `RE`, `SIG`, `NOF`, `shoulder-diameter`, `shoulder-length`). They are not
50
81
  renamed here: a translation table between two vocabularies is where an `SFDM`
@@ -68,19 +99,52 @@ as a plausible cylinder.
68
99
  ## Dimensions
69
100
 
70
101
  ```tsx
71
- <ToolDrawing assembly={assembly} dimensions dimensionSides="both" formatLength={inches} />
102
+ <ToolDrawing
103
+ assembly={assembly}
104
+ dimensions
105
+ dimensionSides="both"
106
+ highlight={hovered}
107
+ onDimensionHover={setHovered}
108
+ />
72
109
  ```
73
110
 
74
111
  Every stated length and width, each in its own lane, nested shortest-innermost
75
- so no two lines cross, with each figure in the band just outboard of its own
76
- lane. Only stated numbers are dimensioned. `formatLength` is yours, because the
77
- unit a shop reads in is the application's.
112
+ so no two lines cross. Only stated numbers are dimensioned.
113
+
114
+ **The drawing letters none of them.** The numbers belong in your own table,
115
+ where they can be read; six two-line figures fighting for the margin said the
116
+ same numbers a second time and worse. Which line is which is answered by
117
+ pointing at it instead:
118
+
119
+ - `highlight` names the dimension or dimensions to draw in the sheet's accent,
120
+ by ISO 13399 code — `DC`, `LCF`, `OAL`, `SFDM`, `LBH`, `stickout`, `SIG`, and
121
+ the two `shoulder-` codes. A code the tool does not dimension highlights
122
+ nothing.
123
+ - `onDimensionHover` is told the code under the pointer and `null` when it
124
+ leaves, so your table can be lit from the drawing as well as the other way
125
+ about. Passing it puts hit targets on the lines; leaving it off draws none.
126
+
127
+ Where two codes are one span the drawing carries one line, not two. A shop that
128
+ clamps to its own rule states the stickout and the below-holder length as the
129
+ same number, and a tool stood out to its flutes states it again as the flute
130
+ length; with nothing lettered, identical lines in two lanes cannot be told
131
+ apart. The first code named keeps the line — the tool's own number ahead of the
132
+ shop's — and the others light it too. Hover reports the code the line is drawn
133
+ under.
134
+
135
+ **The stickout is yours.** `assembly.stickout` is where the holder nose goes,
136
+ and nothing here derives it — pass `LBH` as the stickout if standing the tool
137
+ out to its below-holder length is the rule you want drawn. Where the two
138
+ disagree and `LBH` ends up inside the holder, it is not dimensioned: the
139
+ drawing will not run a line to a face it has drawn a holder over.
140
+
141
+ A highlighted line is drawn in the accent **and heavier**, so the highlight
142
+ survives a reader who cannot tell the two colours apart.
78
143
 
79
144
  ## Clearance overlay
80
145
 
81
146
  ```tsx
82
147
  import { ClearanceOverlay, tightestGaps, describeGaps } from '@toolpath/tool-drawing/clearance'
83
-
84
148
  ;<ToolDrawing assembly={assembly} collisions={collisions} verdict={{ clears, note }}>
85
149
  <ClearanceOverlay
86
150
  profile={profile}
@@ -1,4 +1,8 @@
1
+ // src/model/types.ts
2
+ import { isHolderProfile } from "@toolpath/tool-support";
3
+
1
4
  // src/model/outline.ts
5
+ import { hasNeck } from "@toolpath/tool-support";
2
6
  var EPSILON = 1e-6;
3
7
  var stated = (tool, code) => tool.provenance?.[code] ?? "vendor-stated";
4
8
  var arc = (centre, radius, fromDeg, toDeg, steps = 6) => Array.from({ length: steps + 1 }, (_, index) => {
@@ -9,16 +13,6 @@ var arc = (centre, radius, fromDeg, toDeg, steps = 6) => Array.from({ length: st
9
13
  z: exact(centre.z + radius * Math.sin(angle))
10
14
  };
11
15
  });
12
- var hasNeck = (tool) => {
13
- const { LCF, SFDM, DC } = tool.geometry;
14
- const shoulder = tool.geometry["shoulder-length"];
15
- const relief = tool.geometry["shoulder-diameter"];
16
- if (shoulder === void 0 || relief === void 0 || LCF === void 0 || shoulder <= LCF) {
17
- return false;
18
- }
19
- const shank = SFDM ?? DC;
20
- return shank === void 0 ? true : relief < shank - EPSILON;
21
- };
22
16
  var cone = (tool, r, whenUnstated) => {
23
17
  const angle = tool.geometry.SIG;
24
18
  const half = (angle ?? whenUnstated) / 2 * (Math.PI / 180);
@@ -96,6 +90,93 @@ var tip = (tool, LCF) => {
96
90
  return null;
97
91
  }
98
92
  };
93
+ var profileSegments = (holder, stickout) => {
94
+ const nose = holder.points[holder.points.length - 1];
95
+ if (holder.points.length < 2 || nose === void 0) {
96
+ return [];
97
+ }
98
+ const provenance = holder.provenance?.["points"] ?? "vendor-stated";
99
+ const gage = stickout + nose[0];
100
+ const up = [...holder.points].reverse().map(([z, r]) => ({ r, z: stickout + (nose[0] - z) }));
101
+ const cut = holder.datum === "gage-line" ? up.findIndex((point) => point.z > gage) : -1;
102
+ if (cut <= 0) {
103
+ return [{ part: "body", points: up, provenance }];
104
+ }
105
+ const below = up.slice(0, cut);
106
+ const above = up.slice(cut);
107
+ const last = below[below.length - 1];
108
+ const first = above[0];
109
+ const meet = last.z === gage ? last : {
110
+ r: last.r + (gage - last.z) / (first.z - last.z) * (first.r - last.r),
111
+ z: gage
112
+ };
113
+ if (meet !== last) {
114
+ below.push(meet);
115
+ }
116
+ above.unshift(meet);
117
+ return [
118
+ { part: "body", points: below, provenance },
119
+ { part: "flange", points: above, provenance }
120
+ ];
121
+ };
122
+ var parametricSegments = (holder, stickout, DC) => {
123
+ if (holder.noseDiameter === null) {
124
+ return [];
125
+ }
126
+ const segments = [];
127
+ const rh = holder.noseDiameter / 2;
128
+ const holderStated = (code) => holder.provenance?.[code] ?? "vendor-stated";
129
+ let top = stickout;
130
+ const noseLength = holder.noseLength ?? holder.gaugeLength ?? Math.max(20, DC * 3);
131
+ segments.push({
132
+ part: "nose",
133
+ points: [
134
+ { r: rh, z: stickout },
135
+ { r: rh, z: stickout + noseLength }
136
+ ],
137
+ provenance: holder.noseLength !== null ? holderStated("noseLength") : holder.gaugeLength === null ? "assumed" : holderStated("noseDiameter")
138
+ });
139
+ top = stickout + noseLength;
140
+ let radius = rh;
141
+ if (holder.bodyDiameter !== null && holder.bodyLength !== null) {
142
+ const rb = holder.bodyDiameter / 2;
143
+ segments.push({
144
+ part: "body",
145
+ points: [
146
+ { r: rb, z: top },
147
+ { r: rb, z: top + holder.bodyLength }
148
+ ],
149
+ provenance: holderStated("bodyDiameter")
150
+ });
151
+ top += holder.bodyLength;
152
+ radius = rb;
153
+ }
154
+ if (holder.projection !== null && holder.flangeDiameter !== null) {
155
+ const flangeAt = stickout + holder.projection;
156
+ const rf = holder.flangeDiameter / 2;
157
+ if (flangeAt > top) {
158
+ segments.push({
159
+ part: "body",
160
+ points: [
161
+ { r: radius, z: top },
162
+ { r: radius, z: flangeAt }
163
+ ],
164
+ provenance: "assumed"
165
+ });
166
+ }
167
+ const flangeTop = holder.gaugeLength !== null && holder.gaugeLength > holder.projection ? stickout + holder.gaugeLength : flangeAt + 20;
168
+ segments.push({
169
+ part: "flange",
170
+ points: [
171
+ { r: rf, z: flangeAt },
172
+ { r: rf, z: flangeTop }
173
+ ],
174
+ provenance: holder.gaugeLength === null ? "assumed" : holderStated("flangeDiameter")
175
+ });
176
+ top = flangeTop;
177
+ }
178
+ return segments;
179
+ };
99
180
  var assemblyOutline = (assembly) => {
100
181
  const { tool, holder, stickout } = assembly;
101
182
  const { DC, LCF, SFDM } = tool.geometry;
@@ -151,9 +232,8 @@ var assemblyOutline = (assembly) => {
151
232
  });
152
233
  top = shankTop;
153
234
  }
154
- if (stickout !== null && holder !== null && holder.noseDiameter !== null) {
155
- const rh = holder.noseDiameter / 2;
156
- const holderStated = (code) => holder.provenance?.[code] ?? "vendor-stated";
235
+ const holderSegments = stickout === null || holder === null ? [] : isHolderProfile(holder) ? profileSegments(holder, stickout) : parametricSegments(holder, stickout, DC);
236
+ if (stickout !== null && holder !== null && holderSegments.length > 0) {
157
237
  const series = /(\d+(?:\.\d+)?)/.exec(holder.colletSeries ?? "");
158
238
  if (holder.colletProtrusion !== null && series) {
159
239
  const rc = Number(series[1]) / 2;
@@ -163,63 +243,35 @@ var assemblyOutline = (assembly) => {
163
243
  { r: rc, z: stickout - holder.colletProtrusion },
164
244
  { r: rc, z: stickout }
165
245
  ],
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")
246
+ provenance: holder.provenance?.["colletProtrusion"] ?? "vendor-stated"
189
247
  });
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")
214
- });
215
- top = flangeTop;
216
248
  }
249
+ segments.push(...holderSegments);
250
+ top = holderSegments[holderSegments.length - 1].points.at(-1)?.z ?? top;
217
251
  }
218
252
  const radius = Math.max(...segments.flatMap((segment) => segment.points.map((point2) => point2.r)));
219
253
  return { segments, height: top, radius };
220
254
  };
255
+ var edgeRadius = (from, to, z) => {
256
+ const low = Math.min(from.z, to.z);
257
+ const high = Math.max(from.z, to.z);
258
+ if (z < low - EPSILON || z > high + EPSILON) {
259
+ return null;
260
+ }
261
+ if (high - low <= EPSILON) {
262
+ return Math.max(from.r, to.r);
263
+ }
264
+ return from.r + (z - from.z) / (to.z - from.z) * (to.r - from.r);
265
+ };
266
+ var radiusAt = (outline, z) => Math.max(
267
+ 0,
268
+ ...outline.segments.flatMap(
269
+ (segment) => segment.points.slice(1).map((point, index) => edgeRadius(segment.points[index], point, z)).filter((radius) => radius !== null)
270
+ )
271
+ );
221
272
 
222
273
  export {
223
- hasNeck,
224
- assemblyOutline
274
+ isHolderProfile,
275
+ assemblyOutline,
276
+ radiusAt
225
277
  };
@@ -1,47 +1,25 @@
1
+ import { ReachCurve } from '@toolpath/tool-support';
2
+ export { ReachCurve, heightAt } from '@toolpath/tool-support';
1
3
  import { OutlinePart, OutlineSegment, OutlinePoint } from '../geometry/index.js';
2
4
  import * as react from 'react';
3
- import { F as Frame, E as Extent, S as Sheet } from '../sheet-D0LSO7qP.js';
5
+ import { F as Frame, E as Extent, S as Sheet } from '../sheet-DicKQYfF.js';
4
6
 
5
7
  /**
6
8
  * The feature's reach curve, and how the drawing reads it.
7
9
  *
8
- * **Declared structurally, on purpose.** The shape is exactly what
9
- * `@toolpath/part-contracts` calls a `ReachCurve`, and naming it here rather
10
- * than importing it is one of the three senses in which this overlay stays
11
- * optional: a consumer that draws a tool alone pulls in no Toolpath schema.
12
- * A `ReachCurve` from the API satisfies this by structure, with no adapter.
10
+ * **Still declared structurally, one package up.** `@toolpath/tool-support`
11
+ * names the shape and imports no Toolpath schema to do it, which is one of the
12
+ * three senses in which this overlay stays optional: a consumer that draws a
13
+ * tool alone pulls in no OpenAPI contract. A `ReachCurve` off a report
14
+ * satisfies it by structure, with no adapter, exactly as before.
13
15
  */
14
- /**
15
- * The worst-case material around a feature, as a staircase.
16
- *
17
- * Read as "material within `horizontalOffset[i]` of the cut rises to
18
- * `verticalOffset[i]`". Both are in millimetres; the offsets run outward from
19
- * the cutting edge and the heights up from the bottom of the feature.
20
- */
21
- interface ReachCurve {
22
- readonly horizontalOffset: ReadonlyArray<number>;
23
- readonly verticalOffset: ReadonlyArray<number>;
24
- }
16
+
25
17
  /** Room the shop wants kept between the stack and the part, in millimetres. */
26
18
  interface Margins {
27
19
  readonly radial: number;
28
20
  readonly axial: number;
29
21
  }
30
22
  declare const NO_MARGINS: Margins;
31
- /**
32
- * The tallest material within `offset` mm of the cut, above the feature's bottom.
33
- *
34
- * **The one piece of the verdict's own arithmetic that had to travel.** The
35
- * decision — whether an assembly clears — stays with the catalog's
36
- * tool-selection engine, which has a dozen callers that never draw anything.
37
- * This is not that decision: it is the reading of the curve that the drawn
38
- * staircase is drawn from, and the gaps this overlay dimensions are measured
39
- * against. It is here because {@link tightestGaps} cannot be written without
40
- * it, and it must keep agreeing with whatever draws the material profile — the
41
- * rise comes at the *start* of each run, so everything out to a knot is
42
- * already as tall as that knot says.
43
- */
44
- declare const heightAt: (curve: ReachCurve, offset: number) => number;
45
23
  /**
46
24
  * Where the wall face stands at a given height, as an offset from the cut:
47
25
  * the start of the first run of the staircase that rises above that height.
@@ -202,4 +180,4 @@ declare const ClearanceOverlay: ({ profile, frame: framed, outline: extent, cutt
202
180
  */
203
181
  declare const describeGaps: (gaps: Gaps, margins: Margins, formatLength: (millimetres: number) => string) => string | null;
204
182
 
205
- export { type AxialGap, ClearanceOverlay, type ClearanceOverlayProps, type Gap, type Gaps, type Margins, NO_MARGINS, type ReachCurve, clipped, describeGaps, heightAt, lastRise, tightestGaps, wallCorners, wallFaceAt, wallPath, zigzag };
183
+ export { type AxialGap, ClearanceOverlay, type ClearanceOverlayProps, type Gap, type Gaps, type Margins, NO_MARGINS, clipped, describeGaps, lastRise, tightestGaps, wallCorners, wallFaceAt, wallPath, zigzag };
@@ -8,16 +8,9 @@ import {
8
8
  } from "../chunk-7ZID4QRO.js";
9
9
 
10
10
  // src/clearance/model/curve.ts
11
+ import { heightAt } from "@toolpath/tool-support";
11
12
  var NO_MARGINS = { radial: 0, axial: 0 };
12
13
  var GAP_TOLERANCE = 1e-6;
13
- var heightAt = (curve, offset) => {
14
- for (let index = 0; index < curve.horizontalOffset.length; index += 1) {
15
- if ((curve.horizontalOffset[index] ?? 0) >= offset) {
16
- return curve.verticalOffset[index] ?? 0;
17
- }
18
- }
19
- return curve.verticalOffset[curve.verticalOffset.length - 1] ?? 0;
20
- };
21
14
  var wallFaceAt = (curve, z) => {
22
15
  let from = 0;
23
16
  for (let index = 0; index < curve.horizontalOffset.length; index += 1) {
@@ -1,44 +1,66 @@
1
+ import { Tool, Holder, HolderProfile, Provenance } from '@toolpath/tool-support';
2
+ export { Provenance, isHolderProfile } from '@toolpath/tool-support';
3
+
1
4
  /**
2
- * What this package needs to draw an assembly, and nothing more.
5
+ * What this package needs to draw an assembly which is the shared cutting-tool
6
+ * domain, under the names this package has always published.
7
+ *
8
+ * These four were declared here until `@toolpath/tool-support` existed, and the
9
+ * declarations were three-way duplicates: the same provenance strings, the same
10
+ * geometry codes and three shapes called "holder" of which no two agreed on
11
+ * which fields exist. A drawing and the number printed beside it were free to
12
+ * disagree about one tool, and once did — the stickout the details table showed
13
+ * was not the stickout the dimension line drew.
14
+ *
15
+ * So the shapes now come from the one declaration and these are **aliases**,
16
+ * exported from `.` and from `/geometry` exactly as before. A consumer's adapter
17
+ * does not move: `ViewerTool` is `Tool`, and a catalog record that satisfied one
18
+ * satisfies the other by structure.
19
+ *
20
+ * ## Why the alias and not a rename
3
21
  *
4
- * The contract is the package's own, so nothing here depends on a particular
5
- * catalog's record types. A consumer writes one adapter that projects its own
6
- * tool onto {@link ViewerTool}; the adapter is the single file that fails
7
- * loudly when either side moves.
22
+ * A consumer's adapter is the single file that fails loudly when either side
23
+ * moves, and renaming the type it targets would be that failure for no gain. The
24
+ * `Viewer*` names are also what the two entry points publish, so dropping them
25
+ * is a major bump this change has no reason to spend. `@toolpath/tool-support`
26
+ * is where the domain is documented; this file is the seam.
8
27
  *
9
- * `geometry` keeps the scraper's own field names — `DC`, `SFDM`, `OAL`, `LCF`,
10
- * `RE`, `SIG`, `NOF`, `shoulder-diameter`, `shoulder-length`. Renaming one here
11
- * would put a translation table between two vocabularies, which is where an
12
- * `SFDM` silently becomes a `DC`.
28
+ * `geometry` still keeps the scraper's own field names — `DC`, `SFDM`, `OAL`,
29
+ * `LCF`, `RE`, `SIG`, `NOF`, `shoulder-diameter`, `shoulder-length` and
30
+ * `@toolpath/tool-support`'s `GEOMETRY_FIELDS` is now the dictionary behind
31
+ * them.
13
32
  *
14
33
  * All lengths are in millimetres and all angles in degrees, and every generator
15
34
  * works in whatever unit system it is handed: an inch tool yields an inch
16
35
  * outline. Converting is the caller's, at the seam where a tool meets a holder.
17
36
  */
18
- /** Where a stated number came from. */
19
- type Provenance = 'vendor-stated' | 'derived' | 'assumed';
20
- interface ViewerTool {
21
- /** The CAM-library name for what the tool is: `flat end mill`, `drill`, `slot mill`. */
22
- readonly form: string;
23
- readonly label?: string;
24
- readonly geometry: Readonly<Record<string, number | undefined>>;
25
- readonly provenance?: Readonly<Record<string, Provenance>>;
26
- }
27
- interface ViewerHolder {
28
- readonly noseDiameter: number | null;
29
- readonly noseLength: number | null;
30
- readonly bodyDiameter: number | null;
31
- readonly bodyLength: number | null;
32
- readonly projection: number | null;
33
- readonly flangeDiameter: number | null;
34
- readonly gaugeLength: number | null;
35
- readonly colletSeries: string | null;
36
- readonly colletProtrusion: number | null;
37
- readonly provenance?: Readonly<Record<string, Provenance>>;
38
- }
37
+
38
+ /** A cutting tool, as the drawing needs one. */
39
+ type ViewerTool = Tool;
40
+ /** A holder as its vendor publishes it: a nose, a body, a projection. */
41
+ type ViewerHolder = Holder;
42
+ /**
43
+ * A holder as its own CAD model measures it: the silhouette, and nothing
44
+ * parametric.
45
+ *
46
+ * Not a refinement of {@link ViewerHolder} and it does not project onto one —
47
+ * reducing a measured envelope to a nose and a body throws away the only reason
48
+ * to measure. The two are a union and {@link isHolderProfile} tells them apart;
49
+ * a consumer that has neither passes `null` and the tool is drawn alone.
50
+ */
51
+ type ViewerHolderProfile = HolderProfile;
52
+
53
+ /**
54
+ * A tool, the holder it is clamped in, and how far it stands out.
55
+ *
56
+ * Deliberately **not** `@toolpath/tool-support`'s `Assembly`, which also carries
57
+ * the collet: a drawing reads a collet only through the holder's own series and
58
+ * protrusion, and taking one it never reads would be a field every adapter has
59
+ * to fill for nothing.
60
+ */
39
61
  interface ViewerAssembly {
40
62
  readonly tool: ViewerTool;
41
- readonly holder: ViewerHolder | null;
63
+ readonly holder: ViewerHolder | ViewerHolderProfile | null;
42
64
  /**
43
65
  * How far the tool stands out of the holder nose, in millimetres — the
44
66
  * shop's number, nobody's else. Null draws the tool alone.
@@ -99,4 +121,4 @@ interface Outline {
99
121
  */
100
122
  declare const assemblyOutline: (assembly: ViewerAssembly) => Outline | null;
101
123
 
102
- export { type Outline, type OutlinePart, type OutlinePoint, type OutlineSegment, type Provenance, type ViewerAssembly, type ViewerHolder, type ViewerTool, assemblyOutline };
124
+ export { type Outline, type OutlinePart, type OutlinePoint, type OutlineSegment, type ViewerAssembly, type ViewerHolder, type ViewerHolderProfile, type ViewerTool, assemblyOutline };
@@ -1,6 +1,8 @@
1
1
  import {
2
- assemblyOutline
3
- } from "../chunk-OUMNS4RI.js";
2
+ assemblyOutline,
3
+ isHolderProfile
4
+ } from "../chunk-WYHJGR7P.js";
4
5
  export {
5
- assemblyOutline
6
+ assemblyOutline,
7
+ isHolderProfile
6
8
  };