@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 +21 -0
- package/README.md +117 -0
- package/dist/chunk-7ZID4QRO.js +34 -0
- package/dist/chunk-OUMNS4RI.js +225 -0
- package/dist/clearance/index.d.ts +205 -0
- package/dist/clearance/index.js +487 -0
- package/dist/geometry/index.d.ts +102 -0
- package/dist/geometry/index.js +6 -0
- package/dist/index.d.ts +353 -0
- package/dist/index.js +846 -0
- package/dist/sheet-D0LSO7qP.d.ts +183 -0
- package/package.json +67 -0
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 };
|