@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
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AWAY_FROM_TIP,
|
|
3
|
+
TOWARD_MINUS,
|
|
4
|
+
TOWARD_PLUS,
|
|
5
|
+
TOWARD_TIP,
|
|
6
|
+
arrowhead,
|
|
7
|
+
useDrawingContext
|
|
8
|
+
} from "../chunk-7ZID4QRO.js";
|
|
9
|
+
|
|
10
|
+
// src/clearance/model/curve.ts
|
|
11
|
+
var NO_MARGINS = { radial: 0, axial: 0 };
|
|
12
|
+
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
|
+
var wallFaceAt = (curve, z) => {
|
|
22
|
+
let from = 0;
|
|
23
|
+
for (let index = 0; index < curve.horizontalOffset.length; index += 1) {
|
|
24
|
+
if ((curve.verticalOffset[index] ?? 0) > z + GAP_TOLERANCE) {
|
|
25
|
+
return from;
|
|
26
|
+
}
|
|
27
|
+
from = curve.horizontalOffset[index] ?? from;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/clearance/model/gaps.ts
|
|
33
|
+
var SWEPT = /* @__PURE__ */ new Set([
|
|
34
|
+
"neck",
|
|
35
|
+
"shank",
|
|
36
|
+
"collet",
|
|
37
|
+
"nose",
|
|
38
|
+
"body",
|
|
39
|
+
"flange"
|
|
40
|
+
]);
|
|
41
|
+
var axialGap = (segments, curve, cuttingRadius, margins) => {
|
|
42
|
+
let best = null;
|
|
43
|
+
for (const segment of segments) {
|
|
44
|
+
if (!SWEPT.has(segment.part)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (const point of segment.points) {
|
|
48
|
+
const offset = point.r + margins.radial - cuttingRadius;
|
|
49
|
+
if (offset <= 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const wall = heightAt(curve, offset);
|
|
53
|
+
const gap = point.z - wall;
|
|
54
|
+
if (best === null || gap < best.gap) {
|
|
55
|
+
best = {
|
|
56
|
+
part: segment.part,
|
|
57
|
+
r: point.r,
|
|
58
|
+
z: point.z,
|
|
59
|
+
wall,
|
|
60
|
+
gap,
|
|
61
|
+
clears: gap + GAP_TOLERANCE >= margins.axial
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return best;
|
|
67
|
+
};
|
|
68
|
+
var radialGap = (segments, curve, cuttingRadius, margins) => {
|
|
69
|
+
let best = null;
|
|
70
|
+
for (const segment of segments) {
|
|
71
|
+
if (!SWEPT.has(segment.part)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const point of segment.points) {
|
|
75
|
+
const face = wallFaceAt(curve, point.z);
|
|
76
|
+
if (face === null) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const gap = cuttingRadius + face - point.r;
|
|
80
|
+
if (best === null || gap < best.gap) {
|
|
81
|
+
best = {
|
|
82
|
+
part: segment.part,
|
|
83
|
+
r: point.r,
|
|
84
|
+
z: point.z,
|
|
85
|
+
gap,
|
|
86
|
+
clears: gap + GAP_TOLERANCE >= margins.radial
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return best;
|
|
92
|
+
};
|
|
93
|
+
var tightestGaps = (segments, curve, cuttingRadius, margins) => {
|
|
94
|
+
const radial = radialGap(segments, curve, cuttingRadius, margins);
|
|
95
|
+
return {
|
|
96
|
+
axial: axialGap(segments, curve, cuttingRadius, margins),
|
|
97
|
+
// Nothing standing beside it is not "no room": it is nothing to measure.
|
|
98
|
+
radial: radial && radial.gap > GAP_TOLERANCE ? radial : null
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// src/clearance/model/wall.ts
|
|
103
|
+
var wallCorners = (profile, noise) => {
|
|
104
|
+
const corners = [];
|
|
105
|
+
let run = null;
|
|
106
|
+
for (const point of profile) {
|
|
107
|
+
const last = corners[corners.length - 1];
|
|
108
|
+
if (!last) {
|
|
109
|
+
corners.push({ r: point.r, z: point.z });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (Math.abs(point.z - last.z) < noise) {
|
|
113
|
+
run = point;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (run && run.r !== last.r) {
|
|
117
|
+
corners.push({ r: run.r, z: last.z });
|
|
118
|
+
}
|
|
119
|
+
corners.push({ r: point.r, z: point.z });
|
|
120
|
+
run = null;
|
|
121
|
+
}
|
|
122
|
+
const kept = corners[corners.length - 1];
|
|
123
|
+
if (run && kept && run.r !== kept.r) {
|
|
124
|
+
corners.push({ r: run.r, z: kept.z });
|
|
125
|
+
}
|
|
126
|
+
return corners;
|
|
127
|
+
};
|
|
128
|
+
var lastRise = (corners) => {
|
|
129
|
+
for (let index = corners.length - 1; index > 0; index -= 1) {
|
|
130
|
+
if (corners[index].z !== corners[index - 1].z) {
|
|
131
|
+
return corners[index].r;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return corners[0]?.r ?? 0;
|
|
135
|
+
};
|
|
136
|
+
var clipped = (profile, edge, ceiling) => {
|
|
137
|
+
const points = [];
|
|
138
|
+
for (const point of profile) {
|
|
139
|
+
if (point.r >= edge) {
|
|
140
|
+
points.push({ r: edge, z: Math.min(point.z, ceiling) });
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
points.push({ r: point.r, z: Math.min(point.z, ceiling) });
|
|
144
|
+
}
|
|
145
|
+
const last = points[points.length - 1];
|
|
146
|
+
if (last && last.r < edge) {
|
|
147
|
+
points.push({ r: edge, z: last.z });
|
|
148
|
+
}
|
|
149
|
+
return points;
|
|
150
|
+
};
|
|
151
|
+
var at = (point, frame) => `${frame.toX(point.r, point.z).toFixed(2)},${frame.toY(point.r, point.z).toFixed(2)}`;
|
|
152
|
+
var wallPath = (points, smooth, frame) => {
|
|
153
|
+
if (points.length === 0) {
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
const close = (a, b) => Math.abs(a.r - b.r) < smooth.run && Math.abs(a.z - b.z) < smooth.rise;
|
|
157
|
+
const isSmooth = (index) => {
|
|
158
|
+
const previous = points[index - 1];
|
|
159
|
+
const here = points[index];
|
|
160
|
+
const next = points[index + 1];
|
|
161
|
+
return previous !== void 0 && next !== void 0 && close(previous, here) && close(here, next);
|
|
162
|
+
};
|
|
163
|
+
let d = `M${at(points[0], frame)}`;
|
|
164
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
165
|
+
const from = points[index - 1];
|
|
166
|
+
const to = points[index];
|
|
167
|
+
if (isSmooth(index - 1) && isSmooth(index)) {
|
|
168
|
+
const before = points[index - 2] ?? from;
|
|
169
|
+
const after = points[index + 1] ?? to;
|
|
170
|
+
const c1 = { r: from.r + (to.r - before.r) / 6, z: from.z + (to.z - before.z) / 6 };
|
|
171
|
+
const c2 = { r: to.r - (after.r - from.r) / 6, z: to.z - (after.z - from.z) / 6 };
|
|
172
|
+
d += ` C${at(c1, frame)} ${at(c2, frame)} ${at(to, frame)}`;
|
|
173
|
+
} else {
|
|
174
|
+
d += ` L${at(to, frame)}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return d;
|
|
178
|
+
};
|
|
179
|
+
var zigzag = (atR, fromZ, toZ, amplitude, steps = 8) => Array.from({ length: steps + 1 }, (_, index) => {
|
|
180
|
+
const end = index === 0 || index === steps;
|
|
181
|
+
return {
|
|
182
|
+
r: atR + (end ? 0 : index % 2 === 1 ? amplitude : -amplitude),
|
|
183
|
+
z: fromZ + (toZ - fromZ) * index / steps
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
var polyline = (points, frame) => points.map((point) => at(point, frame)).join(" ");
|
|
187
|
+
|
|
188
|
+
// src/clearance/render/overlay.tsx
|
|
189
|
+
import { useId } from "react";
|
|
190
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
191
|
+
var PASS = "#6ee7b7";
|
|
192
|
+
var FAIL = "#f87171";
|
|
193
|
+
var ClearanceOverlay = ({
|
|
194
|
+
profile,
|
|
195
|
+
frame: framed,
|
|
196
|
+
outline: extent,
|
|
197
|
+
cuttingRadius,
|
|
198
|
+
gaps,
|
|
199
|
+
margins = NO_MARGINS,
|
|
200
|
+
sheet: ink,
|
|
201
|
+
formatLength
|
|
202
|
+
}) => {
|
|
203
|
+
const hatchId = `hatch-${useId().replace(/:/g, "")}`;
|
|
204
|
+
const drawing = useDrawingContext();
|
|
205
|
+
const frame = framed ?? drawing?.frame;
|
|
206
|
+
const outline = extent ?? drawing?.outline;
|
|
207
|
+
const sheet = ink ?? drawing?.sheet;
|
|
208
|
+
if (frame === void 0 || outline === void 0 || sheet === void 0) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"ClearanceOverlay needs a frame, an outline and a sheet: draw it inside <ToolDrawing>, or pass all three."
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (profile.length === 0) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const { fontSize, scale } = frame;
|
|
217
|
+
const head = fontSize * 0.9;
|
|
218
|
+
const top = outline.height;
|
|
219
|
+
const noise = top * 5e-4;
|
|
220
|
+
const corners = wallCorners(profile, noise);
|
|
221
|
+
const deciding = gaps.axial;
|
|
222
|
+
const sideways = gaps.radial;
|
|
223
|
+
const sheetEdge = outline.radius + frame.padding.plus / scale;
|
|
224
|
+
const wanted = corners.length === 0 ? 0 : Math.max(lastRise(corners), sideways ? sideways.r + sideways.gap : 0, cuttingRadius) + 2;
|
|
225
|
+
const wallEdge = Math.max(Math.min(wanted, sheetEdge - fontSize * 0.5), cuttingRadius + 1);
|
|
226
|
+
const wall = clipped(corners, wallEdge, top);
|
|
227
|
+
const smooth = { run: (wallEdge - cuttingRadius) * 0.06, rise: top * 0.06 };
|
|
228
|
+
const room = margins.radial > 0 || margins.axial > 0 ? clipped(
|
|
229
|
+
wallCorners(
|
|
230
|
+
profile.map((point) => ({
|
|
231
|
+
r: Math.max(cuttingRadius, point.r - margins.radial),
|
|
232
|
+
z: point.z + margins.axial
|
|
233
|
+
})),
|
|
234
|
+
noise
|
|
235
|
+
),
|
|
236
|
+
wallEdge,
|
|
237
|
+
top
|
|
238
|
+
) : null;
|
|
239
|
+
const outerBreak = zigzag(wallEdge, wall[wall.length - 1]?.z ?? 0, 0, fontSize * 0.3);
|
|
240
|
+
const wallAtCut = Math.min(profile[1]?.z ?? 0, top);
|
|
241
|
+
const hatch = fontSize * 0.85;
|
|
242
|
+
const readout = (key, r, z, text, tone) => {
|
|
243
|
+
const width = (text.length + 1) * fontSize * 0.6;
|
|
244
|
+
const one = { x: frame.toX(r, z), y: frame.toY(r, z) };
|
|
245
|
+
const two = {
|
|
246
|
+
x: frame.toX(r + width, z + fontSize * 1.45),
|
|
247
|
+
y: frame.toY(r + width, z + fontSize * 1.45)
|
|
248
|
+
};
|
|
249
|
+
const box = {
|
|
250
|
+
x: Math.min(one.x, two.x),
|
|
251
|
+
y: Math.min(one.y, two.y),
|
|
252
|
+
width: Math.abs(one.x - two.x),
|
|
253
|
+
height: Math.abs(one.y - two.y)
|
|
254
|
+
};
|
|
255
|
+
return /* @__PURE__ */ jsxs("g", { "data-readout": key, children: [
|
|
256
|
+
/* @__PURE__ */ jsx(
|
|
257
|
+
"rect",
|
|
258
|
+
{
|
|
259
|
+
x: box.x,
|
|
260
|
+
y: box.y,
|
|
261
|
+
width: box.width,
|
|
262
|
+
height: box.height,
|
|
263
|
+
rx: fontSize * 0.2,
|
|
264
|
+
fill: sheet.ground,
|
|
265
|
+
fillOpacity: 0.9
|
|
266
|
+
}
|
|
267
|
+
),
|
|
268
|
+
/* @__PURE__ */ jsx(
|
|
269
|
+
"text",
|
|
270
|
+
{
|
|
271
|
+
x: box.x + box.width / 2,
|
|
272
|
+
y: box.y + box.height / 2 + fontSize * 0.35,
|
|
273
|
+
fontSize,
|
|
274
|
+
textAnchor: "middle",
|
|
275
|
+
fill: tone,
|
|
276
|
+
fontFamily: "ui-monospace, monospace",
|
|
277
|
+
children: text
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
] }, key);
|
|
281
|
+
};
|
|
282
|
+
return /* @__PURE__ */ jsxs("g", { "data-clearance": true, children: [
|
|
283
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs(
|
|
284
|
+
"pattern",
|
|
285
|
+
{
|
|
286
|
+
id: hatchId,
|
|
287
|
+
width: hatch,
|
|
288
|
+
height: hatch,
|
|
289
|
+
patternUnits: "userSpaceOnUse",
|
|
290
|
+
patternTransform: "rotate(45)",
|
|
291
|
+
children: [
|
|
292
|
+
/* @__PURE__ */ jsx("rect", { width: hatch, height: hatch, fill: sheet.ground }),
|
|
293
|
+
/* @__PURE__ */ jsx(
|
|
294
|
+
"line",
|
|
295
|
+
{
|
|
296
|
+
x1: 0,
|
|
297
|
+
y1: 0,
|
|
298
|
+
x2: 0,
|
|
299
|
+
y2: hatch,
|
|
300
|
+
stroke: sheet.dimension,
|
|
301
|
+
strokeWidth: hatch * 0.09
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
) }),
|
|
307
|
+
/* @__PURE__ */ jsx(
|
|
308
|
+
"path",
|
|
309
|
+
{
|
|
310
|
+
"data-part": "material",
|
|
311
|
+
d: `${wallPath(wall, smooth, frame)} ${outerBreak.map(
|
|
312
|
+
(point) => `L${frame.toX(point.r, point.z).toFixed(2)},${frame.toY(point.r, point.z).toFixed(2)}`
|
|
313
|
+
).join(
|
|
314
|
+
" "
|
|
315
|
+
)} L${frame.toX(cuttingRadius, 0).toFixed(2)},${frame.toY(cuttingRadius, 0).toFixed(2)} Z`,
|
|
316
|
+
fill: `url(#${hatchId})`,
|
|
317
|
+
stroke: "none"
|
|
318
|
+
}
|
|
319
|
+
),
|
|
320
|
+
/* @__PURE__ */ jsx(
|
|
321
|
+
"path",
|
|
322
|
+
{
|
|
323
|
+
"data-surface": "material",
|
|
324
|
+
d: wallPath(wall, smooth, frame),
|
|
325
|
+
fill: "none",
|
|
326
|
+
stroke: sheet.ink,
|
|
327
|
+
strokeWidth: fontSize * 0.06,
|
|
328
|
+
strokeLinejoin: "round"
|
|
329
|
+
}
|
|
330
|
+
),
|
|
331
|
+
/* @__PURE__ */ jsx(
|
|
332
|
+
"line",
|
|
333
|
+
{
|
|
334
|
+
x1: frame.toX(cuttingRadius, 0),
|
|
335
|
+
y1: frame.toY(cuttingRadius, 0),
|
|
336
|
+
x2: frame.toX(wallEdge, 0),
|
|
337
|
+
y2: frame.toY(wallEdge, 0),
|
|
338
|
+
stroke: sheet.dimension,
|
|
339
|
+
strokeWidth: fontSize * 0.05
|
|
340
|
+
}
|
|
341
|
+
),
|
|
342
|
+
/* @__PURE__ */ jsx(
|
|
343
|
+
"polyline",
|
|
344
|
+
{
|
|
345
|
+
"data-break": "material",
|
|
346
|
+
points: polyline(outerBreak, frame),
|
|
347
|
+
fill: "none",
|
|
348
|
+
stroke: sheet.dimension,
|
|
349
|
+
strokeWidth: fontSize * 0.05
|
|
350
|
+
}
|
|
351
|
+
),
|
|
352
|
+
/* @__PURE__ */ jsx(
|
|
353
|
+
"line",
|
|
354
|
+
{
|
|
355
|
+
"data-wall": "cut",
|
|
356
|
+
x1: frame.toX(cuttingRadius, 0),
|
|
357
|
+
y1: frame.toY(cuttingRadius, 0),
|
|
358
|
+
x2: frame.toX(cuttingRadius, wallAtCut),
|
|
359
|
+
y2: frame.toY(cuttingRadius, wallAtCut),
|
|
360
|
+
stroke: sheet.centre,
|
|
361
|
+
strokeWidth: fontSize * 0.08
|
|
362
|
+
}
|
|
363
|
+
),
|
|
364
|
+
room ? /* @__PURE__ */ jsx(
|
|
365
|
+
"path",
|
|
366
|
+
{
|
|
367
|
+
"data-part": "room",
|
|
368
|
+
d: wallPath(room, smooth, frame),
|
|
369
|
+
fill: "none",
|
|
370
|
+
stroke: sheet.dimension,
|
|
371
|
+
strokeWidth: fontSize * 0.05,
|
|
372
|
+
strokeLinejoin: "round",
|
|
373
|
+
strokeDasharray: `${(fontSize * 0.6).toFixed(2)} ${(fontSize * 0.4).toFixed(2)}`
|
|
374
|
+
}
|
|
375
|
+
) : null,
|
|
376
|
+
deciding ? (() => {
|
|
377
|
+
const tone = deciding.clears ? PASS : FAIL;
|
|
378
|
+
const lane = deciding.r + fontSize * 1.6;
|
|
379
|
+
const low = Math.min(deciding.z, deciding.wall);
|
|
380
|
+
const high = Math.max(deciding.z, deciding.wall);
|
|
381
|
+
const inside = high - low >= head * 2.6;
|
|
382
|
+
const reach = head * 2.2;
|
|
383
|
+
const text = `${deciding.gap < 0 ? "\u2212" : ""}${formatLength(Math.abs(deciding.gap))} up`;
|
|
384
|
+
return /* @__PURE__ */ jsxs("g", { "data-clearance-dimension": "axial", children: [
|
|
385
|
+
/* @__PURE__ */ jsx(
|
|
386
|
+
"line",
|
|
387
|
+
{
|
|
388
|
+
x1: frame.toX(lane, inside ? low : low - reach),
|
|
389
|
+
y1: frame.toY(lane, inside ? low : low - reach),
|
|
390
|
+
x2: frame.toX(lane, inside ? high : high + reach),
|
|
391
|
+
y2: frame.toY(lane, inside ? high : high + reach),
|
|
392
|
+
stroke: tone,
|
|
393
|
+
strokeWidth: fontSize * 0.07
|
|
394
|
+
}
|
|
395
|
+
),
|
|
396
|
+
/* @__PURE__ */ jsx(
|
|
397
|
+
"polygon",
|
|
398
|
+
{
|
|
399
|
+
points: arrowhead(lane, low, inside ? TOWARD_TIP : AWAY_FROM_TIP, head, frame),
|
|
400
|
+
fill: tone
|
|
401
|
+
}
|
|
402
|
+
),
|
|
403
|
+
/* @__PURE__ */ jsx(
|
|
404
|
+
"polygon",
|
|
405
|
+
{
|
|
406
|
+
points: arrowhead(lane, high, inside ? AWAY_FROM_TIP : TOWARD_TIP, head, frame),
|
|
407
|
+
fill: tone
|
|
408
|
+
}
|
|
409
|
+
),
|
|
410
|
+
readout("axial", lane + fontSize * 0.5, (low + high) / 2, text, tone)
|
|
411
|
+
] });
|
|
412
|
+
})() : null,
|
|
413
|
+
sideways ? (() => {
|
|
414
|
+
const tone = sideways.clears ? PASS : FAIL;
|
|
415
|
+
const face = sideways.r + sideways.gap;
|
|
416
|
+
const cutOff = face > wallEdge;
|
|
417
|
+
const to = Math.min(face, wallEdge);
|
|
418
|
+
const inside = to - sideways.r >= head * 2.6;
|
|
419
|
+
const text = `${formatLength(sideways.gap)} across${cutOff ? " \u27E8" : ""}`;
|
|
420
|
+
return /* @__PURE__ */ jsxs("g", { "data-clearance-dimension": "radial", "data-broken": cutOff ? "true" : "false", children: [
|
|
421
|
+
/* @__PURE__ */ jsx(
|
|
422
|
+
"line",
|
|
423
|
+
{
|
|
424
|
+
x1: frame.toX(sideways.r, sideways.z),
|
|
425
|
+
y1: frame.toY(sideways.r, sideways.z),
|
|
426
|
+
x2: frame.toX(to, sideways.z),
|
|
427
|
+
y2: frame.toY(to, sideways.z),
|
|
428
|
+
stroke: tone,
|
|
429
|
+
strokeWidth: fontSize * 0.07
|
|
430
|
+
}
|
|
431
|
+
),
|
|
432
|
+
/* @__PURE__ */ jsx(
|
|
433
|
+
"polygon",
|
|
434
|
+
{
|
|
435
|
+
points: arrowhead(
|
|
436
|
+
sideways.r,
|
|
437
|
+
sideways.z,
|
|
438
|
+
inside ? TOWARD_MINUS : TOWARD_PLUS,
|
|
439
|
+
head,
|
|
440
|
+
frame
|
|
441
|
+
),
|
|
442
|
+
fill: tone
|
|
443
|
+
}
|
|
444
|
+
),
|
|
445
|
+
/* @__PURE__ */ jsx(
|
|
446
|
+
"polygon",
|
|
447
|
+
{
|
|
448
|
+
points: arrowhead(
|
|
449
|
+
to,
|
|
450
|
+
sideways.z,
|
|
451
|
+
inside ? TOWARD_PLUS : TOWARD_MINUS,
|
|
452
|
+
head,
|
|
453
|
+
frame
|
|
454
|
+
),
|
|
455
|
+
fill: tone
|
|
456
|
+
}
|
|
457
|
+
),
|
|
458
|
+
readout("radial", sideways.r, sideways.z + fontSize * 0.4, text, tone)
|
|
459
|
+
] });
|
|
460
|
+
})() : null
|
|
461
|
+
] });
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/clearance/model/describe.ts
|
|
465
|
+
var describeGaps = (gaps, margins, formatLength) => {
|
|
466
|
+
const deciding = gaps.axial;
|
|
467
|
+
if (deciding === null) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
const up = deciding.gap < 0 ? `${formatLength(-deciding.gap)} into the wall at the ${deciding.part}` : `${formatLength(deciding.gap)} above the wall at the ${deciding.part}`;
|
|
471
|
+
const sideways = gaps.radial;
|
|
472
|
+
const side = sideways === null || sideways.gap <= GAP_TOLERANCE ? "" : ` \xB7 ${formatLength(sideways.gap)} from the wall at the ${sideways.part}`;
|
|
473
|
+
return `tightest: ${up}${side} \u2014 ${formatLength(margins.axial)} up and ${formatLength(margins.radial)} sideways wanted`;
|
|
474
|
+
};
|
|
475
|
+
export {
|
|
476
|
+
ClearanceOverlay,
|
|
477
|
+
NO_MARGINS,
|
|
478
|
+
clipped,
|
|
479
|
+
describeGaps,
|
|
480
|
+
heightAt,
|
|
481
|
+
lastRise,
|
|
482
|
+
tightestGaps,
|
|
483
|
+
wallCorners,
|
|
484
|
+
wallFaceAt,
|
|
485
|
+
wallPath,
|
|
486
|
+
zigzag
|
|
487
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this package needs to draw an assembly, and nothing more.
|
|
3
|
+
*
|
|
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.
|
|
8
|
+
*
|
|
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`.
|
|
13
|
+
*
|
|
14
|
+
* All lengths are in millimetres and all angles in degrees, and every generator
|
|
15
|
+
* works in whatever unit system it is handed: an inch tool yields an inch
|
|
16
|
+
* outline. Converting is the caller's, at the seam where a tool meets a holder.
|
|
17
|
+
*/
|
|
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
|
+
}
|
|
39
|
+
interface ViewerAssembly {
|
|
40
|
+
readonly tool: ViewerTool;
|
|
41
|
+
readonly holder: ViewerHolder | null;
|
|
42
|
+
/**
|
|
43
|
+
* How far the tool stands out of the holder nose, in millimetres — the
|
|
44
|
+
* shop's number, nobody's else. Null draws the tool alone.
|
|
45
|
+
*/
|
|
46
|
+
readonly stickout: number | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* An assembly as a drawing: its outline in the plane of its own axis.
|
|
51
|
+
*
|
|
52
|
+
* A tool, a collet and a holder are all bodies of revolution, so the whole
|
|
53
|
+
* stack is one polyline of (radius, height) pairs, from the tip up. That is
|
|
54
|
+
* what a catalog draws, what a lathe would turn, and everything a picture needs
|
|
55
|
+
* — a renderer mirrors it about the axis and it is a silhouette.
|
|
56
|
+
*
|
|
57
|
+
* **Drawn from stated dimensions, and marked where they are not.** Every
|
|
58
|
+
* segment says which part it is and where its numbers came from, so a renderer
|
|
59
|
+
* can dash what was derived or assumed. What nobody states is not drawn: a
|
|
60
|
+
* holder the catalog knows only as a nose diameter and a gauge length is a
|
|
61
|
+
* cylinder of that size and nothing above it, rather than a body invented to
|
|
62
|
+
* look like one.
|
|
63
|
+
*
|
|
64
|
+
* **And what cannot be drawn honestly is not drawn at all.** There is no
|
|
65
|
+
* fallback cylinder for a form without a generator. A silent cylinder is how a
|
|
66
|
+
* made-up shape ships: it renders, it looks plausible, and it is wrong — a
|
|
67
|
+
* keyseat cutter drawn flat is a featureless sliver two pixels tall, and a
|
|
68
|
+
* reader has no way to tell that from a measurement. {@link assemblyOutline}
|
|
69
|
+
* returns `null` instead and the caller says so in words.
|
|
70
|
+
*/
|
|
71
|
+
type OutlinePart = 'tip' | 'flutes' | 'neck' | 'shank' | 'collet' | 'nose' | 'body' | 'flange';
|
|
72
|
+
/** Radius out from the axis and height above the tip, both in millimetres. */
|
|
73
|
+
interface OutlinePoint {
|
|
74
|
+
readonly r: number;
|
|
75
|
+
readonly z: number;
|
|
76
|
+
}
|
|
77
|
+
interface OutlineSegment {
|
|
78
|
+
readonly part: OutlinePart;
|
|
79
|
+
/** From the lower end up; the last point of one segment is the first of the next. */
|
|
80
|
+
readonly points: ReadonlyArray<OutlinePoint>;
|
|
81
|
+
/** Where the dimensions came from — `chosen` for the stickout, which is nobody's but the shop's. */
|
|
82
|
+
readonly provenance: Provenance | 'chosen';
|
|
83
|
+
}
|
|
84
|
+
interface Outline {
|
|
85
|
+
readonly segments: ReadonlyArray<OutlineSegment>;
|
|
86
|
+
/** The tallest point drawn, above the tip. */
|
|
87
|
+
readonly height: number;
|
|
88
|
+
/** The widest radius drawn. */
|
|
89
|
+
readonly radius: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The outline of one assembly at its stickout.
|
|
93
|
+
*
|
|
94
|
+
* Flutes to the flute length, a neck where a shoulder is stated, the shank up
|
|
95
|
+
* to the stickout, and the holder nose above that for its gauge length.
|
|
96
|
+
*
|
|
97
|
+
* `null` where there is no honest picture: a form without a generator, or a
|
|
98
|
+
* tool that states neither a cutting diameter nor a flute length.
|
|
99
|
+
*/
|
|
100
|
+
declare const assemblyOutline: (assembly: ViewerAssembly) => Outline | null;
|
|
101
|
+
|
|
102
|
+
export { type Outline, type OutlinePart, type OutlinePoint, type OutlineSegment, type Provenance, type ViewerAssembly, type ViewerHolder, type ViewerTool, assemblyOutline };
|