@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/dist/index.js CHANGED
@@ -9,8 +9,9 @@ import {
9
9
  } from "./chunk-7ZID4QRO.js";
10
10
  import {
11
11
  assemblyOutline,
12
- hasNeck
13
- } from "./chunk-OUMNS4RI.js";
12
+ isHolderProfile,
13
+ radiusAt
14
+ } from "./chunk-WYHJGR7P.js";
14
15
 
15
16
  // src/model/frame.ts
16
17
  var DEFAULT_PADDING = 16;
@@ -79,7 +80,21 @@ var frameFor = (outline, box, options = {}) => {
79
80
  };
80
81
 
81
82
  // src/model/dimensions.ts
83
+ import { hasNeck } from "@toolpath/tool-support";
84
+ var SAME = 1e-6;
82
85
  var EMPTY = { lengths: [], widths: [], angles: [], cornerRadius: null };
86
+ var oneLinePerSpan = (spans) => spans.reduce((lines, span) => {
87
+ const index = lines.findIndex(
88
+ (each) => Math.abs(each.from - span.from) < SAME && Math.abs(each.to - span.to) < SAME
89
+ );
90
+ if (index < 0) {
91
+ return [...lines, span];
92
+ }
93
+ const same = lines[index];
94
+ return lines.map(
95
+ (each, at2) => at2 === index ? { ...same, aliases: [...same.aliases ?? [], span.code] } : each
96
+ );
97
+ }, []);
83
98
  var dimensionsFor = (assembly) => {
84
99
  const { tool, holder, stickout } = assembly;
85
100
  const { DC, LCF, OAL, SFDM, RE } = tool.geometry;
@@ -102,21 +117,21 @@ var dimensionsFor = (assembly) => {
102
117
  }
103
118
  if (SFDM !== void 0) {
104
119
  const shankFrom = necked && shoulderLength !== void 0 ? shoulderLength : LCF;
105
- const shankTo = stickout ?? OAL ?? shankFrom;
120
+ const seated = holder?.colletProtrusion ?? null;
121
+ const shankTo = stickout !== null && seated !== null ? stickout - seated : stickout ?? OAL ?? shankFrom;
106
122
  widths.push({
107
123
  code: "SFDM",
108
124
  radius: SFDM / 2,
109
125
  at: (shankFrom + Math.max(shankFrom, shankTo)) / 2
110
126
  });
111
127
  }
112
- const spans = [
113
- { code: "LCF", from: 0, to: LCF }
114
- ];
128
+ const spans = [{ code: "LCF", from: 0, to: LCF }];
115
129
  if (necked && shoulderLength !== void 0) {
116
130
  spans.push({ code: "shoulder-length", from: 0, to: shoulderLength });
117
131
  }
118
- const below = tool.geometry.LBH;
119
- if (below !== void 0 && below > 0) {
132
+ const below = tool.geometry.LBH !== void 0 && tool.geometry.LBH > 0 ? tool.geometry.LBH : null;
133
+ const buried = below !== null && holder !== null && stickout !== null && below > stickout + SAME;
134
+ if (below !== null && !buried) {
120
135
  spans.push({ code: "LBH", from: 0, to: below });
121
136
  }
122
137
  if (holder === null) {
@@ -126,7 +141,7 @@ var dimensionsFor = (assembly) => {
126
141
  } else if (stickout !== null) {
127
142
  spans.push({ code: "stickout", from: 0, to: stickout });
128
143
  }
129
- const lengths = spans.slice().sort((a, b) => a.to - a.from - (b.to - b.from)).map((span, index) => ({ ...span, lane: index }));
144
+ const lengths = oneLinePerSpan(spans).sort((a, b) => a.to - a.from - (b.to - b.from)).map((span, index) => ({ ...span, lane: index }));
130
145
  const SIG = tool.geometry.SIG;
131
146
  const pointed = tool.form === "drill" || tool.form === "spot drill" || tool.form === "center drill";
132
147
  const angles = pointed && SIG !== void 0 && SIG > 0 ? [
@@ -143,104 +158,17 @@ var dimensionsFor = (assembly) => {
143
158
  cornerRadius: RE !== void 0 && RE > 0 ? RE : null
144
159
  };
145
160
  };
146
- var DIMENSION_LABEL = {
147
- LBH: "below holder",
148
- "shoulder-length": "shoulder",
149
- "shoulder-diameter": "shoulder \u2300",
150
- stickout: "stickout",
151
- // A drill's included angle. "Tip angle" is what the table beside it calls
152
- // the same number (Paul, 2026-09-01).
153
- SIG: "tip angle"
154
- };
155
- var dimensionLabel = (code) => DIMENSION_LABEL[code] ?? code;
156
- var formatMillimetres = (millimetres) => `${String(Math.round(millimetres * 100) / 100)} mm`;
157
- var stackLabels = (boxes, gap = 0, fixed = []) => {
158
- const placed = [...fixed];
159
- const moved = /* @__PURE__ */ new Map();
160
- const clashes = (one, two) => one.across < two.across + two.width && two.across < one.across + one.width && one.along < two.along + two.height + gap && two.along < one.along + one.height + gap;
161
- for (const box of [...boxes].sort((a, b) => a.along - b.along)) {
162
- let along = box.along;
163
- let clash = true;
164
- while (clash) {
165
- clash = false;
166
- for (const other of placed) {
167
- if (clashes({ ...box, along }, other)) {
168
- along = other.along + other.height + gap;
169
- clash = true;
170
- break;
171
- }
172
- }
173
- }
174
- placed.push({ ...box, along });
175
- moved.set(box.key, along);
176
- }
177
- return moved;
178
- };
179
- var figureType = (fontSize) => fontSize * 0.85;
180
- var figureWidth = (lines, type) => lines.length === 0 ? 0 : (Math.max(...lines.map((each) => each.length)) + 1) * type * 0.56;
181
- var figureHeight = (lines, type) => type * 0.45 * 2 + type * 1.15 * lines;
182
- var bandOffset = (bands, band, room) => {
183
- let at2 = room.arrow + room.gap;
184
- for (let index = 0; index < band; index += 1) {
185
- at2 += (bands[index] ?? 0) + room.gap * 2;
186
- }
187
- return at2;
188
- };
189
- var laneOffset = (bands, lane, room) => bandOffset(bands, lane, room) + (bands[lane] ?? 0) + room.gap;
190
- var bandRoom = (bands, room) => bands.length === 0 ? 0 : bandOffset(bands, bands.length - 1, room) + (bands[bands.length - 1] ?? 0);
191
- var linesOf = (model, format) => [
192
- ...model.lengths.map((each) => ({
193
- code: each.code,
194
- lines: [dimensionLabel(each.code), format(each.to - each.from)]
195
- })),
196
- ...model.angles.map((each) => ({
197
- code: each.code,
198
- lines: [dimensionLabel(each.code), `${String(each.degrees)}\xB0`]
199
- })),
200
- ...model.widths.map((each) => ({
161
+ var laneLayout = (model, sides = "one") => {
162
+ const lanes = model.lengths.map((each) => ({
201
163
  code: each.code,
202
- // Some labels carry the diameter sign already; none carries it twice.
203
- lines: [
204
- dimensionLabel(each.code).includes("\u2300") ? dimensionLabel(each.code) : `${dimensionLabel(each.code)} \u2300`,
205
- format(each.radius * 2),
206
- // The corner radius belongs to the tip's diameter, and reads under it
207
- // rather than beside it.
208
- ...each.at === 0 && model.cornerRadius !== null ? [`RE ${format(model.cornerRadius)}`] : []
209
- ]
210
- }))
211
- ];
212
- var dimensionLayout = (model, format, fontSize, sides = "one", textAlongAxis = false) => {
213
- const type = figureType(fontSize);
214
- const said = new Map(linesOf(model, format).map((each) => [each.code, each.lines]));
215
- const extents = (lines) => {
216
- const wide = figureWidth(lines, type);
217
- const deep = figureHeight(lines.length, type);
218
- return textAlongAxis ? { across: deep, along: wide } : { across: wide, along: deep };
219
- };
220
- const across = [...model.widths, ...model.angles];
221
- const figures = [
222
- ...across.map((each, index) => {
223
- const side = sides === "both" && index % 2 === 1 ? "plus" : "minus";
224
- const lines = said.get(each.code) ?? [];
225
- return { code: each.code, side, band: 0, lane: null, lines, ...extents(lines) };
226
- }),
227
- ...model.lengths.map((each) => {
228
- const side = sides === "both" && each.lane % 2 === 1 ? "plus" : "minus";
229
- const lane = sides === "both" ? Math.floor(each.lane / 2) : each.lane;
230
- const lines = said.get(each.code) ?? [];
231
- return { code: each.code, side, band: lane + 1, lane, lines, ...extents(lines) };
232
- })
233
- ];
234
- const bandsOn = (side) => {
235
- const mine = figures.filter((each) => each.side === side);
236
- const count = Math.max(0, ...mine.map((each) => each.band)) + 1;
237
- return Array.from(
238
- { length: mine.length === 0 ? 0 : count },
239
- (_, band) => Math.max(0, ...mine.filter((each) => each.band === band).map((each) => each.across))
240
- );
241
- };
242
- return { figures, bands: { minus: bandsOn("minus"), plus: bandsOn("plus") } };
164
+ side: sides === "both" && each.lane % 2 === 1 ? "plus" : "minus",
165
+ lane: sides === "both" ? Math.floor(each.lane / 2) : each.lane
166
+ }));
167
+ const on = (side) => lanes.filter((each) => each.side === side).length;
168
+ return { lanes, count: { minus: on("minus"), plus: on("plus") } };
243
169
  };
170
+ var laneOffset = (lane, room) => room.arrow + room.gap + lane * room.step;
171
+ var laneRoom = (lanes, room) => lanes === 0 ? room.arrow : laneOffset(lanes - 1, room);
244
172
 
245
173
  // src/render/sheet.ts
246
174
  var SHEETS = {
@@ -249,6 +177,15 @@ var SHEETS = {
249
177
  ink: "#3f4650",
250
178
  centre: "#15181c",
251
179
  dimension: "#606a76",
180
+ /**
181
+ * The dimension the reader is pointing at.
182
+ *
183
+ * **Not a colour the tool is already drawn in** (Paul, 2026-09-02): the
184
+ * flutes are gold and a struck section is red, so a highlight in either
185
+ * would read as a property of the metal rather than as a pointer. Blue is
186
+ * on neither the tool nor the holder, and it carries on both sheets.
187
+ */
188
+ accent: "#0284c7",
252
189
  body: "#c4c8ce",
253
190
  flutes: "#e6bf59",
254
191
  holder: "#9aa2ad",
@@ -260,6 +197,8 @@ var SHEETS = {
260
197
  ink: "#c7cdd6",
261
198
  centre: "#e8ebef",
262
199
  dimension: "#8d97a4",
200
+ // Lighter than the light sheet's, to hold its own against the dark ground.
201
+ accent: "#38bdf8",
263
202
  body: "#5b626c",
264
203
  // The gold reads as gold on either ground, a shade deeper here so it does
265
204
  // not glare against the greys around it.
@@ -333,6 +272,7 @@ var joins = (segments) => {
333
272
 
334
273
  // src/render/dimension-lines.tsx
335
274
  import { jsx, jsxs } from "react/jsx-runtime";
275
+ var LIT_WEIGHT = 1.8;
336
276
  var DimensionLines = ({
337
277
  model,
338
278
  layout,
@@ -341,13 +281,12 @@ var DimensionLines = ({
341
281
  room,
342
282
  requested,
343
283
  ink,
344
- ground
284
+ accent,
285
+ highlight,
286
+ onHover
345
287
  }) => {
346
288
  const { fontSize, scale } = frame;
347
289
  const head = fontSize * 0.9;
348
- const type = figureType(fontSize);
349
- const lineHeight = type * 1.15;
350
- const inset = type * 0.45;
351
290
  const mm = (pixels) => pixels / scale;
352
291
  const sign = (side) => side === "minus" ? -1 : 1;
353
292
  const granted = (side) => {
@@ -356,235 +295,239 @@ var DimensionLines = ({
356
295
  return asked > 0 ? got / asked : 1;
357
296
  };
358
297
  const at2 = (r, z) => `${frame.toX(r, z).toFixed(2)},${frame.toY(r, z).toFixed(2)}`;
359
- const edgeAt = (side) => sign(side) * outline.radius;
360
- const laneAt = (lane, side) => sign(side) * (outline.radius + mm(laneOffset(layout.bands[side], lane, room) * granted(side)));
361
- const bandAt = (band, side) => sign(side) * (outline.radius + mm(bandOffset(layout.bands[side], band, room) * granted(side)));
362
- const placeOf = new Map(layout.figures.map((each) => [each.code, each]));
363
- const wants = (figure) => {
364
- if (figure.lane === null) {
365
- const angle = model.angles.find((each) => each.code === figure.code);
366
- if (angle) {
367
- return { r: angle.at.r * sign(figure.side), z: angle.at.z };
368
- }
369
- const width = model.widths.find((each) => each.code === figure.code);
370
- return { r: (width?.radius ?? 0) * sign(figure.side), z: width?.at ?? 0 };
371
- }
372
- const length = model.lengths.find((each) => each.code === figure.code);
373
- return {
374
- r: laneAt(figure.lane, figure.side),
375
- /**
376
- * **At the top of its dimension, not the middle of it** (Paul,
377
- * 2026-09-01). A figure halfway down a 50 mm dimension is level with
378
- * nothing on the tool; at the top it is level with the arrow it belongs
379
- * to.
380
- */
381
- z: Math.max(length?.from ?? 0, length?.to ?? 0)
382
- };
383
- };
384
- const boxOf = (figure) => {
385
- const inboard = bandAt(figure.band, figure.side);
386
- const across = mm(figure.across * granted(figure.side));
387
- const along = mm(figure.along);
388
- return {
389
- // Outward from the band's inboard edge, on this flank.
390
- from: inboard,
391
- to: inboard + sign(figure.side) * across,
392
- across,
393
- along
394
- };
298
+ const clearance = fontSize * 0.35;
299
+ const edgeAt = (side, z) => sign(side) * (radiusAt(outline, z) + clearance);
300
+ const laneAt = (lane) => sign(lane.side) * (outline.radius + mm(laneOffset(lane.lane, room) * granted(lane.side)));
301
+ const standFor = (radius) => Math.max(head * 0.9, Math.min(head * 2.4, outline.radius - radius + mm(laneOffset(0, room))));
302
+ const laneOf = new Map(layout.lanes.map((each) => [each.code, each]));
303
+ const litCodes = /* @__PURE__ */ new Set([
304
+ ...highlight,
305
+ ...model.lengths.filter((each) => (each.aliases ?? []).some((code) => highlight.has(code))).map((each) => each.code)
306
+ ]);
307
+ const lit = (code) => litCodes.has(code);
308
+ const inkFor = (code) => lit(code) ? accent : ink;
309
+ const weightFor = (code, weight) => fontSize * weight * (lit(code) ? LIT_WEIGHT : 1);
310
+ const fadeFor = (code, opacity) => lit(code) ? 1 : opacity;
311
+ const pointing = (code) => onHover === void 0 ? {} : {
312
+ onPointerEnter: () => {
313
+ onHover(code);
314
+ },
315
+ onPointerLeave: () => {
316
+ onHover(null);
317
+ },
318
+ style: { cursor: "pointer" }
395
319
  };
396
- const obstacles = model.lengths.flatMap((each) => {
397
- const place = placeOf.get(each.code);
398
- if (place === void 0 || place.lane === null) {
399
- return [];
320
+ const hit = fontSize * 1.3;
321
+ const grab = (points) => onHover === void 0 ? null : /* @__PURE__ */ jsx(
322
+ "polyline",
323
+ {
324
+ points,
325
+ fill: "none",
326
+ stroke: "transparent",
327
+ strokeWidth: hit,
328
+ strokeLinecap: "round"
400
329
  }
401
- const lane = laneAt(place.lane, place.side);
402
- const edge = edgeAt(place.side);
403
- const thickness = mm(figureType(fontSize) * 0.7 * scale);
404
- return [each.from, each.to].map((height, index) => ({
405
- key: `line-${each.code}-${String(index)}`,
406
- across: Math.min(lane, edge),
407
- width: Math.abs(lane - edge),
408
- along: height - thickness / 2,
409
- height: thickness
410
- }));
411
- });
412
- const placed = stackLabels(
413
- layout.figures.map((figure) => {
414
- const box = boxOf(figure);
415
- const start = wants(figure);
416
- return {
417
- key: figure.code,
418
- across: Math.min(box.from, box.to),
419
- width: box.across,
420
- along: start.z,
421
- height: box.along
422
- };
423
- }),
424
- mm(type * 0.5 * scale),
425
- obstacles
426
330
  );
331
+ const byLight = (each) => [...each].sort((one, two) => Number(lit(one.code)) - Number(lit(two.code)));
427
332
  return /* @__PURE__ */ jsxs("g", { "data-dimensions": true, children: [
428
- model.lengths.map((each) => {
429
- const place = placeOf.get(each.code);
430
- if (place === void 0 || place.lane === null) {
333
+ byLight(model.lengths).map((each) => {
334
+ const place = laneOf.get(each.code);
335
+ if (place === void 0) {
431
336
  return null;
432
337
  }
433
- const lane = laneAt(place.lane, place.side);
434
- const edge = edgeAt(place.side);
338
+ const lane = laneAt(place);
339
+ const edge = { from: edgeAt(place.side, each.from), to: edgeAt(place.side, each.to) };
435
340
  const inside = Math.abs(each.to - each.from) >= head * 2.6;
436
341
  const reach = head * 2.2;
437
- return /* @__PURE__ */ jsxs("g", { "data-dimension": each.code, children: [
438
- /* @__PURE__ */ jsx(
439
- "line",
440
- {
441
- x1: frame.toX(lane, each.from),
442
- y1: frame.toY(lane, each.from),
443
- x2: frame.toX(edge, each.from),
444
- y2: frame.toY(edge, each.from),
445
- stroke: ink,
446
- strokeOpacity: 0.4,
447
- strokeWidth: fontSize * 0.05
448
- }
449
- ),
450
- /* @__PURE__ */ jsx(
451
- "line",
452
- {
453
- x1: frame.toX(lane, each.to),
454
- y1: frame.toY(lane, each.to),
455
- x2: frame.toX(edge, each.to),
456
- y2: frame.toY(edge, each.to),
457
- stroke: ink,
458
- strokeOpacity: 0.4,
459
- strokeWidth: fontSize * 0.05
460
- }
461
- ),
462
- /* @__PURE__ */ jsx(
463
- "line",
464
- {
465
- x1: frame.toX(lane, inside ? each.from : each.from - reach),
466
- y1: frame.toY(lane, inside ? each.from : each.from - reach),
467
- x2: frame.toX(lane, inside ? each.to : each.to + reach),
468
- y2: frame.toY(lane, inside ? each.to : each.to + reach),
469
- stroke: ink,
470
- strokeWidth: fontSize * 0.07
471
- }
472
- ),
473
- /* @__PURE__ */ jsx(
474
- "polygon",
475
- {
476
- points: arrowhead(lane, each.from, inside ? TOWARD_TIP : AWAY_FROM_TIP, head, frame),
477
- fill: ink
478
- }
479
- ),
480
- /* @__PURE__ */ jsx(
481
- "polygon",
482
- {
483
- points: arrowhead(lane, each.to, inside ? AWAY_FROM_TIP : TOWARD_TIP, head, frame),
484
- fill: ink
485
- }
486
- )
487
- ] }, each.code);
342
+ const from = inside ? each.from : each.from - reach;
343
+ const to = inside ? each.to : each.to + reach;
344
+ return /* @__PURE__ */ jsxs(
345
+ "g",
346
+ {
347
+ "data-dimension": each.code,
348
+ ...lit(each.code) ? { "data-lit": "true" } : {},
349
+ ...pointing(each.code),
350
+ children: [
351
+ grab(`${at2(lane, from)} ${at2(lane, to)}`),
352
+ /* @__PURE__ */ jsx(
353
+ "line",
354
+ {
355
+ x1: frame.toX(lane, each.from),
356
+ y1: frame.toY(lane, each.from),
357
+ x2: frame.toX(edge.from, each.from),
358
+ y2: frame.toY(edge.from, each.from),
359
+ stroke: inkFor(each.code),
360
+ strokeOpacity: fadeFor(each.code, 0.4),
361
+ strokeWidth: weightFor(each.code, 0.05)
362
+ }
363
+ ),
364
+ /* @__PURE__ */ jsx(
365
+ "line",
366
+ {
367
+ x1: frame.toX(lane, each.to),
368
+ y1: frame.toY(lane, each.to),
369
+ x2: frame.toX(edge.to, each.to),
370
+ y2: frame.toY(edge.to, each.to),
371
+ stroke: inkFor(each.code),
372
+ strokeOpacity: fadeFor(each.code, 0.4),
373
+ strokeWidth: weightFor(each.code, 0.05)
374
+ }
375
+ ),
376
+ /* @__PURE__ */ jsx(
377
+ "line",
378
+ {
379
+ x1: frame.toX(lane, from),
380
+ y1: frame.toY(lane, from),
381
+ x2: frame.toX(lane, to),
382
+ y2: frame.toY(lane, to),
383
+ stroke: inkFor(each.code),
384
+ strokeWidth: weightFor(each.code, 0.07)
385
+ }
386
+ ),
387
+ /* @__PURE__ */ jsx(
388
+ "polygon",
389
+ {
390
+ points: arrowhead(lane, each.from, inside ? TOWARD_TIP : AWAY_FROM_TIP, head, frame),
391
+ fill: inkFor(each.code)
392
+ }
393
+ ),
394
+ /* @__PURE__ */ jsx(
395
+ "polygon",
396
+ {
397
+ points: arrowhead(lane, each.to, inside ? AWAY_FROM_TIP : TOWARD_TIP, head, frame),
398
+ fill: inkFor(each.code)
399
+ }
400
+ )
401
+ ]
402
+ },
403
+ each.code
404
+ );
488
405
  }),
489
- model.widths.map((each) => {
490
- const stand = head * 2.4;
491
- return /* @__PURE__ */ jsxs("g", { "data-dimension": each.code, children: [
492
- /* @__PURE__ */ jsx(
493
- "line",
494
- {
495
- x1: frame.toX(-each.radius - stand, each.at),
496
- y1: frame.toY(-each.radius - stand, each.at),
497
- x2: frame.toX(-each.radius, each.at),
498
- y2: frame.toY(-each.radius, each.at),
499
- stroke: ink,
500
- strokeWidth: fontSize * 0.07
501
- }
502
- ),
503
- /* @__PURE__ */ jsx(
504
- "polygon",
505
- {
506
- points: arrowhead(-each.radius, each.at, TOWARD_PLUS, head, frame),
507
- fill: ink
508
- }
509
- ),
510
- /* @__PURE__ */ jsx(
511
- "line",
512
- {
513
- x1: frame.toX(each.radius, each.at),
514
- y1: frame.toY(each.radius, each.at),
515
- x2: frame.toX(each.radius + stand, each.at),
516
- y2: frame.toY(each.radius + stand, each.at),
517
- stroke: ink,
518
- strokeWidth: fontSize * 0.07
519
- }
520
- ),
521
- /* @__PURE__ */ jsx(
522
- "polygon",
523
- {
524
- points: arrowhead(each.radius, each.at, TOWARD_MINUS, head, frame),
525
- fill: ink
526
- }
527
- )
528
- ] }, each.code);
406
+ byLight(model.widths).map((each) => {
407
+ const stand = standFor(each.radius);
408
+ return /* @__PURE__ */ jsxs(
409
+ "g",
410
+ {
411
+ "data-dimension": each.code,
412
+ ...lit(each.code) ? { "data-lit": "true" } : {},
413
+ ...pointing(each.code),
414
+ children: [
415
+ grab(`${at2(-each.radius - stand, each.at)} ${at2(-each.radius, each.at)}`),
416
+ grab(`${at2(each.radius, each.at)} ${at2(each.radius + stand, each.at)}`),
417
+ /* @__PURE__ */ jsx(
418
+ "line",
419
+ {
420
+ x1: frame.toX(-each.radius - stand, each.at),
421
+ y1: frame.toY(-each.radius - stand, each.at),
422
+ x2: frame.toX(-each.radius, each.at),
423
+ y2: frame.toY(-each.radius, each.at),
424
+ stroke: inkFor(each.code),
425
+ strokeWidth: weightFor(each.code, 0.07)
426
+ }
427
+ ),
428
+ /* @__PURE__ */ jsx(
429
+ "polygon",
430
+ {
431
+ points: arrowhead(-each.radius, each.at, TOWARD_PLUS, head, frame),
432
+ fill: inkFor(each.code)
433
+ }
434
+ ),
435
+ /* @__PURE__ */ jsx(
436
+ "line",
437
+ {
438
+ x1: frame.toX(each.radius, each.at),
439
+ y1: frame.toY(each.radius, each.at),
440
+ x2: frame.toX(each.radius + stand, each.at),
441
+ y2: frame.toY(each.radius + stand, each.at),
442
+ stroke: inkFor(each.code),
443
+ strokeWidth: weightFor(each.code, 0.07)
444
+ }
445
+ ),
446
+ /* @__PURE__ */ jsx(
447
+ "polygon",
448
+ {
449
+ points: arrowhead(each.radius, each.at, TOWARD_MINUS, head, frame),
450
+ fill: inkFor(each.code)
451
+ }
452
+ )
453
+ ]
454
+ },
455
+ each.code
456
+ );
529
457
  }),
530
- layout.figures.map((figure) => {
531
- const box = boxOf(figure);
532
- const start = wants(figure);
533
- const low = placed.get(figure.code) ?? start.z;
534
- const one = { x: frame.toX(box.from, low), y: frame.toY(box.from, low) };
535
- const two = {
536
- x: frame.toX(box.to, low + box.along),
537
- y: frame.toY(box.to, low + box.along)
538
- };
539
- const rect = {
540
- x: Math.min(one.x, two.x),
541
- y: Math.min(one.y, two.y),
542
- width: Math.abs(one.x - two.x),
543
- height: Math.abs(one.y - two.y)
544
- };
545
- const turn = box.from - sign(figure.side) * mm(type * 0.5 * scale);
546
- const middleZ = low + box.along / 2;
547
- const anchorX = frame.toX(box.from, middleZ);
548
- const axisX = frame.toX(0, middleZ);
549
- const apart = Math.abs(anchorX - axisX) > mm(1);
550
- const textAnchor = apart ? anchorX < axisX ? "end" : "start" : "middle";
551
- const textX = apart ? anchorX : rect.x + rect.width / 2;
552
- return /* @__PURE__ */ jsxs("g", { "data-figure": figure.code, children: [
553
- /* @__PURE__ */ jsx(
554
- "polyline",
555
- {
556
- points: `${at2(start.r, start.z)} ${at2(turn, start.z)} ${at2(turn, middleZ)}`,
557
- fill: "none",
558
- stroke: ink,
559
- strokeOpacity: 0.35,
560
- strokeWidth: fontSize * 0.05
561
- }
562
- ),
563
- /* @__PURE__ */ jsx(
564
- "rect",
565
- {
566
- x: rect.x,
567
- y: rect.y,
568
- width: rect.width,
569
- height: rect.height,
570
- fill: ground,
571
- rx: type * 0.2
572
- }
573
- ),
574
- figure.lines.map((line, index) => /* @__PURE__ */ jsx(
575
- "text",
576
- {
577
- x: textX,
578
- y: rect.y + inset + type * 0.85 + lineHeight * index,
579
- fontSize: type,
580
- textAnchor,
581
- fill: ink,
582
- fontFamily: "ui-monospace, monospace",
583
- children: line
584
- },
585
- line
586
- ))
587
- ] }, `figure-${figure.code}`);
458
+ byLight(model.angles).map((each) => {
459
+ const half = each.degrees / 2 * Math.PI / 180;
460
+ const flank = Math.hypot(each.at.r * 2, each.at.z * 2);
461
+ const reach = Math.max(flank * 1.5, head * 3);
462
+ const ray = (towards) => ({
463
+ r: towards * reach * Math.sin(half),
464
+ z: reach * Math.cos(half)
465
+ });
466
+ const minus = ray(-1);
467
+ const plus = ray(1);
468
+ const radius = reach * 0.62;
469
+ const arc = Array.from({ length: 17 }, (_, index) => {
470
+ const angle = -half + 2 * half * index / 16;
471
+ return at2(radius * Math.sin(angle), radius * Math.cos(angle));
472
+ }).join(" ");
473
+ return /* @__PURE__ */ jsxs(
474
+ "g",
475
+ {
476
+ "data-dimension": each.code,
477
+ ...lit(each.code) ? { "data-lit": "true" } : {},
478
+ ...pointing(each.code),
479
+ children: [
480
+ grab(arc),
481
+ grab(`${at2(minus.r, minus.z)} ${at2(0, 0)} ${at2(plus.r, plus.z)}`),
482
+ /* @__PURE__ */ jsx(
483
+ "polyline",
484
+ {
485
+ points: `${at2(minus.r, minus.z)} ${at2(0, 0)} ${at2(plus.r, plus.z)}`,
486
+ fill: "none",
487
+ stroke: inkFor(each.code),
488
+ strokeOpacity: fadeFor(each.code, 0.5),
489
+ strokeWidth: weightFor(each.code, 0.05)
490
+ }
491
+ ),
492
+ /* @__PURE__ */ jsx(
493
+ "polyline",
494
+ {
495
+ points: arc,
496
+ fill: "none",
497
+ stroke: inkFor(each.code),
498
+ strokeWidth: weightFor(each.code, 0.07)
499
+ }
500
+ ),
501
+ /* @__PURE__ */ jsx(
502
+ "polygon",
503
+ {
504
+ points: arrowhead(
505
+ radius * Math.sin(-half),
506
+ radius * Math.cos(half),
507
+ { dr: -Math.cos(half), dz: -Math.sin(half) },
508
+ head * 0.8,
509
+ frame
510
+ ),
511
+ fill: inkFor(each.code)
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsx(
515
+ "polygon",
516
+ {
517
+ points: arrowhead(
518
+ radius * Math.sin(half),
519
+ radius * Math.cos(half),
520
+ { dr: Math.cos(half), dz: -Math.sin(half) },
521
+ head * 0.8,
522
+ frame
523
+ ),
524
+ fill: inkFor(each.code)
525
+ }
526
+ )
527
+ ]
528
+ },
529
+ each.code
530
+ );
588
531
  })
589
532
  ] });
590
533
  };
@@ -600,7 +543,8 @@ var ToolDrawing = ({
600
543
  caption,
601
544
  dimensions = false,
602
545
  dimensionSides = "one",
603
- formatLength = formatMillimetres,
546
+ highlight = null,
547
+ onDimensionHover,
604
548
  padding = DEFAULT_PADDING2,
605
549
  collisions,
606
550
  verdict,
@@ -629,6 +573,7 @@ var ToolDrawing = ({
629
573
  const { tool, holder } = assembly;
630
574
  const name = caption ?? tool.label ?? tool.form;
631
575
  const outline = assemblyOutline(assembly);
576
+ const unstatedLength = holder !== null && (isHolderProfile(holder) ? holder.datum !== "gage-line" : holder.gaugeLength === null);
632
577
  if (outline === null) {
633
578
  const missing = tool.geometry.DC === void 0 || tool.geometry.LCF === void 0;
634
579
  return /* @__PURE__ */ jsx2(
@@ -643,13 +588,15 @@ var ToolDrawing = ({
643
588
  }
644
589
  const typePx = typeSizeFor(box);
645
590
  const model = dimensions ? dimensionsFor(assembly) : null;
646
- const room = { arrow: typePx * 0.9 * 2.4, gap: typePx * 0.5 };
647
- const layout = model === null ? null : dimensionLayout(
648
- model,
649
- formatLength,
650
- typePx,
651
- dimensionSides,
652
- orientationFor(box) === "horizontal"
591
+ const widthsReachEdge = model !== null && model.widths.some((each) => each.radius >= outline.radius - 1e-6);
592
+ const room = {
593
+ arrow: widthsReachEdge ? typePx * 0.9 * 2.4 : 0,
594
+ gap: typePx * 0.6,
595
+ step: typePx * 1.1
596
+ };
597
+ const layout = model === null ? null : laneLayout(model, dimensionSides);
598
+ const lit = new Set(
599
+ highlight === null ? [] : typeof highlight === "string" ? [highlight] : highlight
653
600
  );
654
601
  const asked = typeof padding === "number" ? { minus: padding, plus: padding, along: padding } : {
655
602
  minus: padding.minus ?? DEFAULT_PADDING2,
@@ -657,12 +604,12 @@ var ToolDrawing = ({
657
604
  along: padding.along ?? DEFAULT_PADDING2
658
605
  };
659
606
  const chrome = layout === null ? asked : {
660
- minus: asked.minus + bandRoom(layout.bands.minus, room) + room.gap,
661
- plus: asked.plus + bandRoom(layout.bands.plus, room) + room.gap,
662
- // Headroom for one figure's depth past each end: the stacker moves a
663
- // clash away from the tip, so the outermost figure of a crowded tool
664
- // ends up past the top of what it measures.
665
- along: asked.along + Math.max(0, ...layout.figures.map((each) => each.along))
607
+ minus: asked.minus + laneRoom(layout.count.minus, room) + room.gap,
608
+ plus: asked.plus + laneRoom(layout.count.plus, room) + room.gap,
609
+ // Headroom for the arrows of a dimension too short to hold them:
610
+ // those stand outside the line and point back in, so they reach past
611
+ // the end of what they measure.
612
+ along: asked.along + typePx * 0.9 * 3.2
666
613
  };
667
614
  const frame = frameFor(outline, box, { padding: chrome });
668
615
  const { fontSize } = frame;
@@ -785,7 +732,9 @@ var ToolDrawing = ({
785
732
  room,
786
733
  requested: chrome,
787
734
  ink: sheet.dimension,
788
- ground: sheet.ground
735
+ accent: sheet.accent,
736
+ highlight: lit,
737
+ ...onDimensionHover === void 0 ? {} : { onHover: onDimensionHover }
789
738
  }
790
739
  ) : null,
791
740
  /* @__PURE__ */ jsx2(
@@ -818,7 +767,7 @@ var ToolDrawing = ({
818
767
  "Drawn from stated dimensions",
819
768
  assumed.length > 0 ? `; ${assumed.join(", ")} assumed` : "",
820
769
  ".",
821
- holder !== null && holder.gaugeLength === null ? " The holder length is not stated." : ""
770
+ unstatedLength ? " The holder length is not stated." : ""
822
771
  ]
823
772
  }
824
773
  )
@@ -829,18 +778,13 @@ var ToolDrawing = ({
829
778
  export {
830
779
  SHEETS,
831
780
  ToolDrawing,
832
- bandOffset,
833
- bandRoom,
834
- dimensionLabel,
835
- dimensionLayout,
836
781
  dimensionsFor,
837
- figureHeight,
838
- figureType,
839
- formatMillimetres,
840
782
  frameFor,
783
+ isHolderProfile,
784
+ laneLayout,
841
785
  laneOffset,
786
+ laneRoom,
842
787
  orientationFor,
843
- stackLabels,
844
788
  typeSizeFor,
845
789
  useDrawingContext
846
790
  };