@toolpath/tool-drawing 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Toolpath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # Toolpath Tool Drawing
2
+
3
+ `@toolpath/tool-drawing` draws a cutting tool, and the holder it is clamped in,
4
+ as a 2D elevation: one silhouette turned about the tool axis, dimensioned, on
5
+ its own sheet. It is SVG and arithmetic — no runtime dependencies, React only as
6
+ a peer.
7
+
8
+ It is deliberately not `@toolpath/viewer`. That package shows a customer's part
9
+ in 3D; this one draws a catalog tool in 2D.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @toolpath/tool-drawing react react-dom
15
+ ```
16
+
17
+ ## Exports
18
+
19
+ | Entry point | What it is |
20
+ | ---------------------------------- | ------------------------------------------------------- |
21
+ | `@toolpath/tool-drawing` | The component, the input contract, the framing options |
22
+ | `@toolpath/tool-drawing/geometry` | `assemblyOutline` and the profile generators — no React |
23
+ | `@toolpath/tool-drawing/clearance` | The optional clearance overlay |
24
+
25
+ `/geometry` is pure and server-safe: it touches no DOM and imports no React, so
26
+ a Node server can measure an assembly without paying for a renderer.
27
+
28
+ ## Geometry
29
+
30
+ ```ts
31
+ import { assemblyOutline } from '@toolpath/tool-drawing/geometry'
32
+
33
+ const outline = assemblyOutline({
34
+ tool: { form: 'flat end mill', geometry: { DC: 6, LCF: 13, SFDM: 6, OAL: 57 } },
35
+ holder: null,
36
+ stickout: null,
37
+ })
38
+ ```
39
+
40
+ `assemblyOutline` returns `null` rather than a picture when it cannot draw the
41
+ tool honestly — an unrecognised form, or a tool with no stated cutting diameter
42
+ or flute length. **Every number in a generated profile comes off a vendor
43
+ field.** There is no default taper angle, no assumed neck, no invented lead
44
+ chamfer. Where a number has to be assumed to draw at all — a drill point angle
45
+ the vendor never published — the segment says so in its `provenance`, and a
46
+ renderer is expected to show it.
47
+
48
+ `geometry` keys are the scraper's own field names (`DC`, `SFDM`, `OAL`, `LCF`,
49
+ `RE`, `SIG`, `NOF`, `shoulder-diameter`, `shoulder-length`). They are not
50
+ renamed here: a translation table between two vocabularies is where an `SFDM`
51
+ silently becomes a `DC`.
52
+
53
+ ## Drawing
54
+
55
+ ```tsx
56
+ import { ToolDrawing } from '@toolpath/tool-drawing'
57
+ ;<ToolDrawing assembly={assembly} theme="dark" />
58
+ ```
59
+
60
+ The component measures its own panel, frames the assembly to fill it, and draws
61
+ along the panel's long axis — no orientation prop, no pan, no zoom. `theme` is a
62
+ prop rather than a hook because a package cannot reach the application's theme;
63
+ it defaults to `'dark'`.
64
+
65
+ A form the geometry has no shape for is **stated in words and named**, not drawn
66
+ as a plausible cylinder.
67
+
68
+ ## Dimensions
69
+
70
+ ```tsx
71
+ <ToolDrawing assembly={assembly} dimensions dimensionSides="both" formatLength={inches} />
72
+ ```
73
+
74
+ 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.
78
+
79
+ ## Clearance overlay
80
+
81
+ ```tsx
82
+ import { ClearanceOverlay, tightestGaps, describeGaps } from '@toolpath/tool-drawing/clearance'
83
+
84
+ ;<ToolDrawing assembly={assembly} collisions={collisions} verdict={{ clears, note }}>
85
+ <ClearanceOverlay
86
+ profile={profile}
87
+ gaps={gaps}
88
+ cuttingRadius={cuttingRadius}
89
+ formatLength={formatLength}
90
+ />
91
+ </ToolDrawing>
92
+ ```
93
+
94
+ The overlay draws in the drawing's own coordinates and is given them: the
95
+ frame, the outline and the sheet reach it from the `<ToolDrawing>` around it.
96
+ It could not work them out for itself — the panel is measured by a
97
+ `ResizeObserver` inside that component, on an `<svg>` you never hold — so
98
+ passing them is only for overriding the frame, as a test framing a fixture
99
+ does. Drawn outside a `<ToolDrawing>` with none supplied, it throws rather than
100
+ inventing one.
101
+
102
+ **The overlay draws a verdict; it does not reach one.** Whether an assembly
103
+ clears a feature is a tool-selection question with callers that never draw
104
+ anything, so it stays with them: this takes the material profile, the
105
+ collisions and the two tightest gaps as data and owns every line drawn from
106
+ them — the wall, the hatch, the interrupted-view breaks, the clearance
107
+ dimensions and their readouts, and the paint on a section that is in the metal.
108
+
109
+ It is optional in three senses: a subpath of its own, so a consumer that never
110
+ imports it never pays for it; no Toolpath schema dependency, because the
111
+ reach-curve shape is declared structurally here; and omitting the props draws
112
+ the tool alone.
113
+
114
+ ## Status
115
+
116
+ Geometry, layout, the renderer, the dimensions and the clearance overlay are
117
+ all in.
@@ -0,0 +1,34 @@
1
+ // src/render/drawing-context.tsx
2
+ import { createContext, useContext } from "react";
3
+ var Drawing = createContext(null);
4
+ var DrawingProvider = Drawing.Provider;
5
+ var useDrawingContext = () => useContext(Drawing);
6
+
7
+ // src/render/arrows.ts
8
+ var TOWARD_TIP = { dr: 0, dz: -1 };
9
+ var AWAY_FROM_TIP = { dr: 0, dz: 1 };
10
+ var TOWARD_PLUS = { dr: 1, dz: 0 };
11
+ var TOWARD_MINUS = { dr: -1, dz: 0 };
12
+ var at = (point, frame) => `${frame.toX(point.r, point.z).toFixed(2)},${frame.toY(point.r, point.z).toFixed(2)}`;
13
+ var arrowhead = (r, z, towards, size, frame) => {
14
+ const wing = size * 0.3;
15
+ const baseR = r - towards.dr * size;
16
+ const baseZ = z - towards.dz * size;
17
+ const acrossR = -towards.dz * wing;
18
+ const acrossZ = towards.dr * wing;
19
+ return [
20
+ at({ r, z }, frame),
21
+ at({ r: baseR + acrossR, z: baseZ + acrossZ }, frame),
22
+ at({ r: baseR - acrossR, z: baseZ - acrossZ }, frame)
23
+ ].join(" ");
24
+ };
25
+
26
+ export {
27
+ DrawingProvider,
28
+ useDrawingContext,
29
+ TOWARD_TIP,
30
+ AWAY_FROM_TIP,
31
+ TOWARD_PLUS,
32
+ TOWARD_MINUS,
33
+ arrowhead
34
+ };
@@ -0,0 +1,225 @@
1
+ // src/model/outline.ts
2
+ var EPSILON = 1e-6;
3
+ var stated = (tool, code) => tool.provenance?.[code] ?? "vendor-stated";
4
+ var arc = (centre, radius, fromDeg, toDeg, steps = 6) => Array.from({ length: steps + 1 }, (_, index) => {
5
+ const angle = (fromDeg + (toDeg - fromDeg) * index / steps) * Math.PI / 180;
6
+ const exact = (value) => Math.round(value * 1e9) / 1e9;
7
+ return {
8
+ r: exact(centre.r + radius * Math.cos(angle)),
9
+ z: exact(centre.z + radius * Math.sin(angle))
10
+ };
11
+ });
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
+ var cone = (tool, r, whenUnstated) => {
23
+ const angle = tool.geometry.SIG;
24
+ const half = (angle ?? whenUnstated) / 2 * (Math.PI / 180);
25
+ const top = r / Math.tan(half);
26
+ if (!Number.isFinite(top) || top < 0) {
27
+ return null;
28
+ }
29
+ return {
30
+ points: [
31
+ { r: 0, z: 0 },
32
+ { r, z: top }
33
+ ],
34
+ provenance: angle === void 0 ? "assumed" : stated(tool, "SIG"),
35
+ top
36
+ };
37
+ };
38
+ var tip = (tool, LCF) => {
39
+ const DC = tool.geometry.DC ?? 0;
40
+ const r = DC / 2;
41
+ const RE = tool.geometry.RE ?? 0;
42
+ const square = {
43
+ points: [
44
+ { r: 0, z: 0 },
45
+ { r, z: 0 }
46
+ ],
47
+ provenance: stated(tool, "DC"),
48
+ top: 0
49
+ };
50
+ switch (tool.form) {
51
+ case "flat end mill":
52
+ case "tap left hand":
53
+ case "tap right hand":
54
+ return square;
55
+ case "ball end mill":
56
+ return { points: arc({ r: 0, z: r }, r, -90, 0), provenance: stated(tool, "DC"), top: r };
57
+ case "bull nose end mill": {
58
+ const corner = Math.min(RE, r, LCF / 2);
59
+ if (corner <= EPSILON) {
60
+ return square;
61
+ }
62
+ return {
63
+ points: [
64
+ { r: 0, z: 0 },
65
+ { r: r - corner, z: 0 },
66
+ ...arc({ r: r - corner, z: corner }, corner, -90, 0)
67
+ ],
68
+ provenance: stated(tool, "RE"),
69
+ top: corner
70
+ };
71
+ }
72
+ case "slot mill": {
73
+ const corner = Math.min(RE, r, LCF / 2);
74
+ if (corner <= EPSILON) {
75
+ return square;
76
+ }
77
+ return {
78
+ points: [
79
+ { r: 0, z: 0 },
80
+ { r: r - corner, z: 0 },
81
+ ...arc({ r: r - corner, z: corner }, corner, -90, 0)
82
+ ],
83
+ provenance: stated(tool, "RE"),
84
+ top: corner,
85
+ crown: arc({ r: r - corner, z: LCF - corner }, corner, 0, 90)
86
+ };
87
+ }
88
+ case "drill":
89
+ case "spot drill":
90
+ case "center drill":
91
+ return cone(tool, r, 118);
92
+ case "chamfer mill":
93
+ case "counter sink":
94
+ return cone(tool, r, 90);
95
+ default:
96
+ return null;
97
+ }
98
+ };
99
+ var assemblyOutline = (assembly) => {
100
+ const { tool, holder, stickout } = assembly;
101
+ const { DC, LCF, SFDM } = tool.geometry;
102
+ if (DC === void 0 || LCF === void 0) {
103
+ return null;
104
+ }
105
+ const r = DC / 2;
106
+ const segments = [];
107
+ const point = tip(tool, LCF);
108
+ if (point === null) {
109
+ return null;
110
+ }
111
+ segments.push({ part: "tip", points: point.points, provenance: point.provenance });
112
+ const crown = point.crown ?? [];
113
+ const straightTop = crown[0]?.z ?? LCF;
114
+ segments.push({
115
+ part: "flutes",
116
+ points: [
117
+ { r, z: point.top },
118
+ { r, z: straightTop }
119
+ ],
120
+ provenance: stated(tool, "LCF")
121
+ });
122
+ if (crown.length > 0) {
123
+ segments.push({ part: "flutes", points: crown, provenance: point.provenance });
124
+ }
125
+ const neckDiameter = tool.geometry["shoulder-diameter"];
126
+ const shoulder = tool.geometry["shoulder-length"];
127
+ let top = LCF;
128
+ if (neckDiameter !== void 0 && shoulder !== void 0 && shoulder > LCF) {
129
+ const rn = neckDiameter / 2;
130
+ segments.push({
131
+ // A shoulder as wide as the cut is plain shank; only a narrower one is a neck.
132
+ part: hasNeck(tool) ? "neck" : "shank",
133
+ points: [
134
+ { r: rn, z: LCF },
135
+ { r: rn, z: shoulder }
136
+ ],
137
+ provenance: stated(tool, "shoulder-length")
138
+ });
139
+ top = shoulder;
140
+ }
141
+ const shankTop = stickout ?? tool.geometry.OAL ?? top;
142
+ if (SFDM !== void 0 && shankTop > top) {
143
+ const rs = SFDM / 2;
144
+ segments.push({
145
+ part: "shank",
146
+ points: [
147
+ { r: rs, z: top },
148
+ { r: rs, z: shankTop }
149
+ ],
150
+ provenance: stickout === null ? stated(tool, "SFDM") : "chosen"
151
+ });
152
+ top = shankTop;
153
+ }
154
+ if (stickout !== null && holder !== null && holder.noseDiameter !== null) {
155
+ const rh = holder.noseDiameter / 2;
156
+ const holderStated = (code) => holder.provenance?.[code] ?? "vendor-stated";
157
+ const series = /(\d+(?:\.\d+)?)/.exec(holder.colletSeries ?? "");
158
+ if (holder.colletProtrusion !== null && series) {
159
+ const rc = Number(series[1]) / 2;
160
+ segments.push({
161
+ part: "collet",
162
+ points: [
163
+ { r: rc, z: stickout - holder.colletProtrusion },
164
+ { r: rc, z: stickout }
165
+ ],
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")
214
+ });
215
+ top = flangeTop;
216
+ }
217
+ }
218
+ const radius = Math.max(...segments.flatMap((segment) => segment.points.map((point2) => point2.r)));
219
+ return { segments, height: top, radius };
220
+ };
221
+
222
+ export {
223
+ hasNeck,
224
+ assemblyOutline
225
+ };
@@ -0,0 +1,205 @@
1
+ import { OutlinePart, OutlineSegment, OutlinePoint } from '../geometry/index.js';
2
+ import * as react from 'react';
3
+ import { F as Frame, E as Extent, S as Sheet } from '../sheet-D0LSO7qP.js';
4
+
5
+ /**
6
+ * The feature's reach curve, and how the drawing reads it.
7
+ *
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.
13
+ */
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
+ }
25
+ /** Room the shop wants kept between the stack and the part, in millimetres. */
26
+ interface Margins {
27
+ readonly radial: number;
28
+ readonly axial: number;
29
+ }
30
+ 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
+ /**
46
+ * Where the wall face stands at a given height, as an offset from the cut:
47
+ * the start of the first run of the staircase that rises above that height.
48
+ * Null where nothing stands that tall — no wall to measure to.
49
+ */
50
+ declare const wallFaceAt: (curve: ReachCurve, z: number) => number | null;
51
+
52
+ interface Gap {
53
+ readonly part: OutlinePart;
54
+ /** Where on the stack it was measured: radius from the axis, height above the tip, mm. */
55
+ readonly r: number;
56
+ readonly z: number;
57
+ /** The room measured, mm — negative is into the material. */
58
+ readonly gap: number;
59
+ /** Whether that much meets the room the shop asked for. */
60
+ readonly clears: boolean;
61
+ }
62
+ /** The axial gap also carries the wall it was measured from. */
63
+ interface AxialGap extends Gap {
64
+ /** How high the material stands at this part's offset, mm. */
65
+ readonly wall: number;
66
+ }
67
+ interface Gaps {
68
+ /** Up from the material to the part above it. Null with nothing swept. */
69
+ readonly axial: AxialGap | null;
70
+ /** Sideways to a wall taller than the part. Null where nothing stands taller. */
71
+ readonly radial: Gap | null;
72
+ }
73
+ /**
74
+ * Both gaps, each at its own tightest point.
75
+ *
76
+ * Takes the outline's segments rather than an assembly, because by the time
77
+ * anything is drawn the outline already exists — and because an outline is
78
+ * this package's own vocabulary, where an assembly record is the consumer's.
79
+ */
80
+ declare const tightestGaps: (segments: ReadonlyArray<OutlineSegment>, curve: ReachCurve, cuttingRadius: number, margins: Margins) => Gaps;
81
+
82
+ /**
83
+ * The material beside the tool, as lines.
84
+ *
85
+ * All of it in the drawing's own space — radius from the axis, height above
86
+ * the tip — so the overlay maps through `toX`/`toY` like everything else and
87
+ * reads the same either way the tool is laid.
88
+ */
89
+ /**
90
+ * The wall's corners: **both ends of every run**, so a step draws as a step.
91
+ *
92
+ * A rise smaller than `noise` is float noise and makes no corner. Everything
93
+ * else is kept, including the far end of the run the rise interrupts — and
94
+ * that far end is the correction of 2026-08-30. Keeping only the point where
95
+ * a new height begins left consecutive corners that spanned a whole run *and*
96
+ * the rise after it, so the line drew a diagonal ramp across both: a square
97
+ * step read as a chamfer, and the material over the run looked taller than
98
+ * the sweep says it is. Paul's section view is the reference — a wall is
99
+ * vertical, a ledge is horizontal, and only a fillet is round.
100
+ *
101
+ * A sampled fillet still keeps every corner, which is what lets `wallPath`
102
+ * draw it as the arc it is (Paul, 2026-08-30: thinning to chords had turned a
103
+ * fillet into a chamfer).
104
+ */
105
+ declare const wallCorners: (profile: ReadonlyArray<OutlinePoint>, noise: number) => Array<OutlinePoint>;
106
+ /**
107
+ * Where the wall stops changing: the radius of the outermost rise. Beyond it
108
+ * the material is flat and drawing more of it says nothing.
109
+ */
110
+ declare const lastRise: (corners: ReadonlyArray<OutlinePoint>) => number;
111
+ /** The staircase as a polygon, clipped at the drawing's outer edge and its top. */
112
+ declare const clipped: (profile: ReadonlyArray<OutlinePoint>, edge: number, ceiling: number) => Array<OutlinePoint>;
113
+ /** The frame's two coordinate mappings: the only things that know the axis sense. */
114
+ interface Mapping {
115
+ readonly toX: (r: number, z: number) => number;
116
+ readonly toY: (r: number, z: number) => number;
117
+ }
118
+ /**
119
+ * The wall as an SVG path that looks like the geometry it came from.
120
+ *
121
+ * The reach curve samples a curved surface — a fillet, a draft — as a run of
122
+ * closely spaced rises; a vertical wall or a step is one big rise. A corner
123
+ * whose neighbours on both sides are within `smooth.run` across and
124
+ * `smooth.rise` up belongs to a curve and is passed through with a
125
+ * Catmull-Rom spline; any other corner stays a sharp line join. So a fillet
126
+ * reads as the arc it is and a wall as the wall it is (Paul, 2026-08-30:
127
+ * "more closely resemble the actual geometry", after chords made a fillet
128
+ * read as a chamfer).
129
+ */
130
+ declare const wallPath: (points: ReadonlyArray<OutlinePoint>, smooth: {
131
+ readonly run: number;
132
+ readonly rise: number;
133
+ }, frame: Mapping) => string;
134
+ /**
135
+ * A break, as the ragged edge of an interrupted view: the saw-tooth along an
136
+ * edge that says the material carries on past where the drawing stops. It is
137
+ * what lets the part be cut short at all.
138
+ *
139
+ * Runs at a constant radius, from one height to another, wandering across the
140
+ * axis by `amplitude` — so it is the same saw-tooth whichever way the tool is
141
+ * laid.
142
+ */
143
+ declare const zigzag: (atR: number, fromZ: number, toZ: number, amplitude: number, steps?: number) => Array<OutlinePoint>;
144
+
145
+ /**
146
+ * The material the sweep read, drawn beside the tool, with the two clearances
147
+ * that decided the verdict.
148
+ *
149
+ * **The verdict does not live here — only its drawing does.** Whether an
150
+ * assembly clears a feature is the catalog's tool-selection question, answered
151
+ * for a dozen callers that never draw anything; putting it behind a rendering
152
+ * package would put that engine behind a dependency on React. So this takes
153
+ * the answer as data — the material profile, the collisions, the two tightest
154
+ * gaps — and owns every line drawn from it.
155
+ *
156
+ * **The part is always secondary to the assembly** (Paul, 2026-08-30). The
157
+ * stack sets the frame and the scale; the material is drawn in the room left
158
+ * beside it and cut off at a **break** — the saw-tooth of an interrupted view
159
+ * — rather than pushing the stack smaller to fit a wall in. A dimension whose
160
+ * far face falls past the break is broken too, and carries the true number.
161
+ * The material is **hatched**, because it is a section through metal and
162
+ * nothing on the stack is.
163
+ */
164
+ interface ClearanceOverlayProps {
165
+ /**
166
+ * The material around the feature, as a staircase in the drawing's own
167
+ * space: radius from the axis, height above the tip.
168
+ *
169
+ * Taken rather than derived, because whatever turns a reach curve into this
170
+ * staircase has drawing consumers of its own and stays with them.
171
+ */
172
+ readonly profile: ReadonlyArray<OutlinePoint>;
173
+ /**
174
+ * The frame, the extent and the ink.
175
+ *
176
+ * All three are taken from the surrounding `<ToolDrawing>` and are only worth
177
+ * passing to override it — framing a fixture in a test, or composing a
178
+ * drawing by hand. A consumer drawing an overlay the ordinary way, as a child,
179
+ * supplies none of them: it cannot, because the panel is measured inside the
180
+ * component it is a child of.
181
+ */
182
+ readonly frame?: Frame;
183
+ readonly outline?: Extent;
184
+ /** Where the cut is: the radius the material is measured out from. */
185
+ readonly cuttingRadius: number;
186
+ /** The two tightest points, each measured at its own. */
187
+ readonly gaps: Gaps;
188
+ /** Room the shop wants kept, drawn as a dashed line outside the wall. */
189
+ readonly margins?: Margins;
190
+ readonly sheet?: Sheet;
191
+ /** How a length is written out, for the two readouts. */
192
+ readonly formatLength: (millimetres: number) => string;
193
+ }
194
+ declare const ClearanceOverlay: ({ profile, frame: framed, outline: extent, cuttingRadius, gaps, margins, sheet: ink, formatLength, }: ClearanceOverlayProps) => react.JSX.Element | null;
195
+
196
+ /**
197
+ * The caption's sentence for the tightest points, in the caller's own unit.
198
+ *
199
+ * **This is the drawing's half of the verdict, not the verdict.** It says what
200
+ * was measured and where; whether the assembly may be used is answered
201
+ * elsewhere, by the engine that answers it for everything else too.
202
+ */
203
+ declare const describeGaps: (gaps: Gaps, margins: Margins, formatLength: (millimetres: number) => string) => string | null;
204
+
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 };