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