@toolpath/dfm 0.4.0 → 1.0.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
@@ -1,2628 +1,132 @@
1
1
  import {
2
- FEATURE_SUBJECTS,
2
+ CRITICAL_COLOR,
3
+ FILE_KIND,
4
+ MAX_DATASHEET_FIELDS,
5
+ MILLING_RULES,
3
6
  OPS,
7
+ PINCH_TOOL_MM,
4
8
  PRESENCE_METRIC,
5
- QUICK_METRICS,
6
9
  QUICK_SUBJECTS,
7
10
  RANGE_HIGHS,
8
11
  RANGE_LOWS,
9
12
  RULE_COLORS,
13
+ TURNING_RULES,
14
+ WARNING_COLOR,
15
+ brokenRules,
16
+ checkPart,
17
+ checkTurnedPart,
18
+ compareColors,
19
+ datasheetFields,
20
+ describeRule,
21
+ dfmFeatures,
22
+ directionLabel,
23
+ featureColor,
24
+ featureMeasurements,
25
+ featureProfile,
10
26
  featureRows,
27
+ featureSheet,
11
28
  featureTypeLabel,
12
29
  formatMeasured,
13
30
  fromField,
14
31
  highOf,
15
32
  hitFigure,
33
+ isNoReading,
34
+ isSurfaceType,
16
35
  lengthValue,
17
36
  llmRulePrompt,
18
37
  lowOf,
19
38
  makeRule,
39
+ maxBallDiameter,
20
40
  metricInfo,
21
41
  metricsForSubject,
22
42
  opSymbol,
23
43
  parseRuleSetFile,
44
+ parseRules,
24
45
  peakHeight,
46
+ pinchLabel,
47
+ pinchMark,
48
+ placePinchTool,
25
49
  quantityUnit,
50
+ readPinch,
26
51
  readReachCurve,
27
52
  runStart,
53
+ sameDepth,
28
54
  serializeRuleSet,
29
55
  subjectLabel,
30
56
  toField,
31
57
  trimmedCurve,
58
+ turnedStretches,
59
+ turnedTypeLabel,
32
60
  unitLabel,
33
61
  upperOf,
34
62
  withMetric,
35
63
  withOp,
36
- withSubject
37
- } from "./chunk-5MPHT7BC.js";
38
-
39
- // src/rule-list/rule-list.tsx
40
- import { useMemo as useMemo2, useState as useState8 } from "react";
41
- import { ScrollArea } from "@toolpath/ui";
42
-
43
- // src/rule-list/list-keys.ts
44
- var listKeys = (name, orientation = "vertical") => (event) => {
45
- const [back, forward] = orientation === "vertical" ? ["ArrowUp", "ArrowDown"] : ["ArrowLeft", "ArrowRight"];
46
- if (![back, forward, "Home", "End"].includes(event.key) || isField(event.target)) return;
47
- const items = [
48
- ...event.currentTarget.querySelectorAll(`[data-list-item="${name}"]`)
49
- ];
50
- if (items.length === 0) return;
51
- const at = currentIndex(items, document.activeElement);
52
- const chosen = items.findIndex((item) => item.getAttribute("aria-pressed") === "true");
53
- const step = (from) => Math.min(Math.max(from + (event.key === forward ? 1 : -1), 0), items.length - 1);
54
- const index = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : at !== -1 ? step(at) : chosen !== -1 ? items[chosen]?.dataset.follow !== void 0 ? step(chosen) : chosen : event.key === forward ? 0 : items.length - 1;
55
- const target = items[index];
56
- if (!target) return;
57
- event.preventDefault();
58
- event.stopPropagation();
59
- if (target === document.activeElement) return;
60
- target.focus();
61
- if (target.dataset.follow !== void 0) target.click();
62
- };
63
- var currentIndex = (items, active) => {
64
- if (!active) return -1;
65
- const exact = items.indexOf(active);
66
- if (exact !== -1) return exact;
67
- const row = active.closest("li");
68
- return row ? items.findIndex((item) => item.closest("li") === row) : -1;
69
- };
70
- var isField = (target) => target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement && target.type !== "color" && target.type !== "button" || target instanceof HTMLElement && target.isContentEditable;
71
-
72
- // src/rule-list/new-rule.tsx
73
- import { useState as useState2 } from "react";
74
-
75
- // src/rule-list/options.ts
76
- var subjectChoices = (current, all) => {
77
- const quick = [...QUICK_SUBJECTS, ...QUICK_SUBJECTS.includes(current) ? [] : [current]];
78
- const first = { choices: quick.map((value) => ({ value, label: subjectLabel(value) })) };
79
- if (!all) return { groups: [first], more: "More feature types\u2026" };
80
- const rest = FEATURE_SUBJECTS.filter((each) => !quick.includes(each.value));
81
- return { groups: [first, { heading: "More feature types", choices: rest }] };
82
- };
83
- var metricChoices = (subject, current, all) => {
84
- const offered = metricsForSubject(subject);
85
- const quick = offered.filter((each) => QUICK_METRICS.includes(each.key) || each.key === current);
86
- quick.sort((a, b) => rank(a.key) - rank(b.key));
87
- const rest = offered.filter((each) => !quick.includes(each));
88
- const first = { choices: quick.map(choice) };
89
- if (!all) return { groups: [first], more: rest.length > 0 ? "More measures\u2026" : void 0 };
90
- return {
91
- groups: [
92
- first,
93
- ...grouped(rest).map(([heading, metrics]) => ({ heading, choices: metrics.map(choice) }))
94
- ]
95
- };
96
- };
97
- var choice = ({ key, label }) => ({ value: key, label });
98
- var rank = (key) => {
99
- const at = QUICK_METRICS.indexOf(key);
100
- return at === -1 ? QUICK_METRICS.length : at;
101
- };
102
- var grouped = (items) => {
103
- const groups = /* @__PURE__ */ new Map();
104
- for (const item of items) groups.set(item.group, [...groups.get(item.group) ?? [], item]);
105
- return [...groups];
106
- };
107
-
108
- // src/rule-list/picker.tsx
109
- import {
110
- Fragment,
111
- useEffect,
112
- useId,
113
- useRef,
114
- useState
115
- } from "react";
116
- import { CheckIcon } from "@phosphor-icons/react";
117
- import { Menu, cn as cn2 } from "@toolpath/ui";
118
-
119
- // src/rule-list/styles.ts
120
- import { cn } from "@toolpath/ui";
121
- var FOCUS_RING = "outline-none focus-visible:ring-2 focus-visible:ring-info/75";
122
- var WORD = cn(
123
- "cursor-pointer appearance-none rounded bg-transparent px-0.5 font-medium text-gray-700 field-sizing-content",
124
- "underline decoration-gray-300 decoration-dotted underline-offset-4 hover:bg-gray-100",
125
- "dark:text-zinc-100 dark:decoration-zinc-600 dark:hover:bg-zinc-700",
126
- "outline-none focus-visible:bg-gray-100 focus-visible:ring-2 focus-visible:ring-info/75 dark:focus-visible:bg-zinc-700"
127
- );
128
-
129
- // src/rule-list/picker.tsx
130
- import { jsx, jsxs } from "react/jsx-runtime";
131
- var HEADING = "px-2 pt-1.5 pb-1 text-[10px] font-semibold tracking-wider text-gray-300 uppercase font-body dark:text-zinc-500";
132
- var Picker = ({
133
- label,
134
- value,
135
- choices,
136
- onChange,
137
- onMore,
138
- placeholder,
139
- title: title2,
140
- className,
141
- autoFocus
142
- }) => {
143
- const id = useId();
144
- const held = useRef(null);
145
- const [holding, setHolding] = useState(0);
146
- const hold = (next) => {
147
- held.current = next;
148
- if (next) setHolding((count) => count + 1);
149
- };
150
- const flat = choices.groups.flatMap((group) => group.choices);
151
- const at = flat.findIndex((choice2) => choice2.value === value);
152
- const shown = flat[at]?.label ?? placeholder ?? value;
153
- const starts = choices.groups.map(
154
- (_, g) => choices.groups.slice(0, g).reduce((count, group) => count + group.choices.length, 0)
155
- );
156
- useEffect(() => {
157
- if (holding === 0) return;
158
- const frame = requestAnimationFrame(() => focusHeld(id, held.current));
159
- return () => cancelAnimationFrame(frame);
160
- }, [holding, id]);
161
- return (
162
- // A click on the word, in its menu or on the menu's backdrop is the picker's: React would carry
163
- // it up through the portal to the row, which opens on a click.
164
- /* @__PURE__ */ jsx("span", { onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsxs(Menu, { onOpenChange: (open) => hold(open && at !== -1 ? { index: at, reveal: false } : null), children: [
165
- /* @__PURE__ */ jsx(
166
- Menu.Trigger,
167
- {
168
- nativeButton: true,
169
- render: /* @__PURE__ */ jsx(
170
- "button",
171
- {
172
- type: "button",
173
- "aria-label": at === -1 ? label : `${label}: ${shown}`,
174
- title: title2,
175
- className: cn2(
176
- WORD,
177
- "data-[popup-open]:bg-gray-100 dark:data-[popup-open]:bg-zinc-700",
178
- className
179
- ),
180
- autoFocus
181
- }
182
- ),
183
- children: shown
184
- }
185
- ),
186
- /* @__PURE__ */ jsxs(
187
- Menu.Popover,
188
- {
189
- align: "start",
190
- sideOffset: 4,
191
- collisionPadding: 8,
192
- className: "max-h-[min(24rem,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto py-1 shadow-lg",
193
- onFocus: (event) => {
194
- const item = held.current && document.getElementById(itemId(id, held.current.index));
195
- if (item && event.target !== item) focusHeld(id, held.current);
196
- },
197
- onPointerMove: () => hold(null),
198
- onKeyDownCapture: () => hold(null),
199
- onKeyDown: keepKeys,
200
- children: [
201
- choices.groups.map((group, g) => /* @__PURE__ */ jsxs(Fragment, { children: [
202
- g > 0 ? /* @__PURE__ */ jsx(Menu.Divider, { className: "my-1" }) : null,
203
- /* @__PURE__ */ jsxs(
204
- "div",
205
- {
206
- role: "group",
207
- "aria-labelledby": group.heading ? `${id}-group-${g}` : void 0,
208
- className: "flex flex-col",
209
- children: [
210
- group.heading ? /* @__PURE__ */ jsx("div", { id: `${id}-group-${g}`, className: HEADING, children: group.heading }) : null,
211
- group.choices.map((choice2, c) => {
212
- const chosen = choice2.value === value;
213
- return /* @__PURE__ */ jsxs(
214
- Menu.Item,
215
- {
216
- id: itemId(id, (starts[g] ?? 0) + c),
217
- label: choice2.label,
218
- onClick: () => {
219
- if (!chosen) onChange(choice2.value);
220
- },
221
- className: "gap-3",
222
- children: [
223
- /* @__PURE__ */ jsx("span", { className: "flex-1", children: choice2.label }),
224
- /* @__PURE__ */ jsx(
225
- CheckIcon,
226
- {
227
- "aria-hidden": true,
228
- weight: "bold",
229
- className: cn2(
230
- "size-3.5 text-gray-400 dark:text-zinc-300",
231
- !chosen && "invisible"
232
- )
233
- }
234
- )
235
- ]
236
- },
237
- choice2.value
238
- );
239
- })
240
- ]
241
- }
242
- )
243
- ] }, group.heading ?? g)),
244
- choices.more && onMore ? /* @__PURE__ */ jsx(
245
- Menu.Item,
246
- {
247
- closeOnClick: false,
248
- onClick: () => {
249
- hold({ index: flat.length, reveal: true });
250
- onMore();
251
- },
252
- className: "text-gray-400 dark:text-zinc-400",
253
- children: choices.more
254
- }
255
- ) : null
256
- ]
257
- }
258
- )
259
- ] }) })
260
- );
261
- };
262
- var focusHeld = (picker, held) => {
263
- const item = held && document.getElementById(itemId(picker, held.index));
264
- if (!item) return;
265
- item.focus({ preventScroll: held.reveal });
266
- const menu = item.closest('[role="menu"]');
267
- const group = item.closest('[role="group"]');
268
- if (held.reveal && menu && group)
269
- menu.scrollTop += group.getBoundingClientRect().top - menu.getBoundingClientRect().top - 8;
270
- };
271
- var itemId = (picker, index) => `${picker}-${index}`;
272
- var keepKeys = (event) => {
273
- if (event.key !== "Tab") event.stopPropagation();
274
- };
275
-
276
- // src/rule-list/new-rule.tsx
277
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
278
- var NewRule = ({ onAdd }) => {
279
- const [all, setAll] = useState2(false);
280
- return /* @__PURE__ */ jsxs2("li", { className: "flex items-start gap-1.5 py-1 pr-1.5", children: [
281
- /* @__PURE__ */ jsx2("span", { "aria-hidden": true, className: "size-7 shrink-0" }),
282
- /* @__PURE__ */ jsx2(
283
- "span",
284
- {
285
- "aria-hidden": true,
286
- className: "mt-1.5 size-4 shrink-0 rounded-full border border-dashed border-gray-300 dark:border-zinc-600"
287
- }
288
- ),
289
- /* @__PURE__ */ jsx2("p", { className: "min-w-0 flex-1 text-sm leading-7", children: /* @__PURE__ */ jsx2(
290
- Picker,
291
- {
292
- label: "New rule",
293
- placeholder: "+ New Rule",
294
- value: "",
295
- choices: subjectChoices("any", all),
296
- onChange: onAdd,
297
- onMore: () => setAll(true),
298
- className: "font-normal text-gray-400 dark:text-zinc-500"
299
- }
300
- ) })
301
- ] });
302
- };
303
-
304
- // src/rule-list/rule-row.tsx
305
- import { useRef as useRef3, useState as useState6 } from "react";
306
- import { CaretRightIcon as CaretRightIcon2, TrashIcon } from "@phosphor-icons/react";
307
- import { IconButton, cn as cn7 } from "@toolpath/ui";
308
-
309
- // src/rule-list/feature-rows.tsx
310
- import { useMemo } from "react";
311
-
312
- // src/rule-list/direction-label.ts
313
- var EPSILON = 1e-6;
314
- var trim = (value) => Number.parseFloat(value.toFixed(3)).toString();
315
- var directionLabel = (direction) => {
316
- const axes = [
317
- ["X", direction.x],
318
- ["Y", direction.y],
319
- ["Z", direction.z]
320
- ];
321
- const nonZero = axes.filter(([, value]) => Math.abs(value) > EPSILON);
322
- const [axis] = nonZero;
323
- if (nonZero.length === 1 && axis && Math.abs(Math.abs(axis[1]) - 1) < EPSILON) {
324
- return `${axis[1] > 0 ? "+" : "\u2212"}${axis[0]}`;
325
- }
326
- return axes.map(([, value]) => trim(value)).join(", ");
327
- };
328
-
329
- // src/rule-list/feature-button.tsx
330
- import { cn as cn3 } from "@toolpath/ui";
331
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
332
- var FEATURE_ROW = cn3(
333
- "flex w-full cursor-pointer items-baseline gap-2 rounded px-2 py-1 text-left text-xs",
334
- "text-gray-500 hover:bg-gray-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
335
- "aria-pressed:bg-gray-100 aria-pressed:font-semibold aria-pressed:text-gray-700",
336
- "dark:aria-pressed:bg-zinc-800 dark:aria-pressed:text-zinc-100",
337
- FOCUS_RING
338
- );
339
- var ruleHit = (check, rule, tag) => check.hitsByTag.get(tag.toLowerCase())?.find((each) => each.rule.id === rule.id);
340
- var HitFigure = ({
341
- hit,
342
- units
343
- }) => hit ? /* @__PURE__ */ jsx3("span", { className: "shrink-0 text-gray-400 tabular-nums dark:text-zinc-400", children: hitFigure(hit, units) }) : null;
344
- var FeatureButton = ({
345
- tag,
346
- featureOf,
347
- rule,
348
- check,
349
- units,
350
- selected,
351
- selectedGroup,
352
- onInspect,
353
- onHover,
354
- onBack
355
- }) => {
356
- const feature = featureOf.get(tag);
357
- return /* @__PURE__ */ jsxs3(
358
- "button",
359
- {
360
- type: "button",
361
- "data-list-item": "rules",
362
- "data-follow": true,
363
- "data-feature-row": true,
364
- "aria-pressed": selected === tag && !selectedGroup,
365
- onClick: () => onInspect(tag),
366
- onKeyDown: (event) => {
367
- if (event.key !== "ArrowLeft") return;
368
- event.preventDefault();
369
- onBack();
370
- },
371
- onPointerEnter: () => onHover(tag),
372
- onPointerLeave: () => onHover(null),
373
- className: FEATURE_ROW,
374
- children: [
375
- /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1 truncate", children: [
376
- featureTypeLabel(feature?.featureType ?? "feature"),
377
- " ",
378
- /* @__PURE__ */ jsx3("span", { className: "font-normal text-gray-300 dark:text-zinc-500", children: feature ? `from ${directionLabel(feature.machiningDirection)}` : "" })
379
- ] }),
380
- /* @__PURE__ */ jsx3(HitFigure, { hit: ruleHit(check, rule, tag), units })
381
- ]
382
- }
383
- );
384
- };
385
-
386
- // src/rule-list/hole-group-row.tsx
387
- import { useRef as useRef2, useState as useState3 } from "react";
388
- import { CaretRightIcon } from "@phosphor-icons/react";
389
- import { cn as cn4 } from "@toolpath/ui";
390
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
391
- var HoleGroupRow = ({
392
- row,
393
- direction,
394
- ...context
395
- }) => {
396
- const [open, setOpen] = useState3(false);
397
- const groupRef = useRef2(null);
398
- const { rule, check, units, selectedGroup, featureOf, onInspect, onHover, onBack } = context;
399
- const [first] = row.tags;
400
- const chosen = selectedGroup?.[0] === first;
401
- return /* @__PURE__ */ jsxs4("li", { children: [
402
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center", children: [
403
- /* @__PURE__ */ jsx4(
404
- "button",
405
- {
406
- type: "button",
407
- "aria-label": open ? "Hide holes" : "Show holes",
408
- "aria-expanded": open,
409
- tabIndex: -1,
410
- onClick: () => setOpen((was) => !was),
411
- className: "-ml-5 flex size-5 shrink-0 cursor-pointer items-center justify-center text-gray-300 dark:text-zinc-500",
412
- children: /* @__PURE__ */ jsx4(
413
- CaretRightIcon,
414
- {
415
- weight: "bold",
416
- className: cn4("size-3 transition-transform", open && "rotate-90")
417
- }
418
- )
419
- }
420
- ),
421
- /* @__PURE__ */ jsxs4(
422
- "button",
423
- {
424
- ref: groupRef,
425
- type: "button",
426
- "data-list-item": "rules",
427
- "data-follow": true,
428
- "data-feature-row": true,
429
- "aria-pressed": chosen,
430
- onClick: () => onInspect(first, row.tags),
431
- onKeyDown: (event) => {
432
- if (event.key === "ArrowRight" && !open) {
433
- event.preventDefault();
434
- setOpen(true);
435
- } else if (event.key === "ArrowLeft") {
436
- event.preventDefault();
437
- if (open) setOpen(false);
438
- else onBack();
439
- }
440
- },
441
- onPointerEnter: () => onHover(row.tags),
442
- onPointerLeave: () => onHover(null),
443
- className: FEATURE_ROW,
444
- children: [
445
- /* @__PURE__ */ jsxs4("span", { className: "min-w-0 flex-1 truncate", children: [
446
- featureTypeLabel(featureOf.get(first)?.featureType ?? "hole"),
447
- " \u2300",
448
- formatMeasured("length", row.diameter, units),
449
- " ",
450
- /* @__PURE__ */ jsxs4("span", { className: "font-normal text-gray-400 dark:text-zinc-400", children: [
451
- "\xD7",
452
- row.tags.length,
453
- " \xB7 from ",
454
- direction
455
- ] })
456
- ] }),
457
- /* @__PURE__ */ jsx4(HitFigure, { hit: ruleHit(check, rule, first), units })
458
- ]
459
- }
460
- )
461
- ] }),
462
- open ? /* @__PURE__ */ jsx4("ul", { className: "pl-4", children: row.tags.map((tag) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(FeatureButton, { ...context, tag, onBack: () => groupRef.current?.focus() }) }, tag)) }) : null
463
- ] });
464
- };
465
-
466
- // src/rule-list/feature-rows.tsx
467
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
468
- var FeatureRows = ({
469
- tags,
470
- otherTags,
471
- features,
472
- sheets,
473
- ...rest
474
- }) => {
475
- const featureOf = useMemo(
476
- () => new Map(features.map((feature) => [feature.tag, feature])),
477
- [features]
478
- );
479
- const required = useMemo(() => featureRows(tags, features, sheets), [tags, features, sheets]);
480
- const others = useMemo(
481
- () => featureRows(otherTags, features, sheets),
482
- [otherTags, features, sheets]
483
- );
484
- const context = { ...rest, featureOf };
485
- const list = (rows) => rows.map(
486
- (row) => row.kind === "holes" ? /* @__PURE__ */ jsx5(
487
- HoleGroupRow,
488
- {
489
- row,
490
- direction: directionOf(featureOf.get(row.key)),
491
- ...context
492
- },
493
- row.key
494
- ) : /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(FeatureButton, { tag: row.tag, ...context }) }, row.tag)
495
- );
496
- return /* @__PURE__ */ jsxs5("ul", { className: "pb-1 pl-12", children: [
497
- list(required),
498
- otherTags.length > 0 ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
499
- /* @__PURE__ */ jsx5(
500
- "li",
501
- {
502
- className: "pt-1 pb-0.5 text-2xs font-semibold text-gray-400 uppercase dark:text-zinc-500",
503
- title: "Readings of faces another feature also cuts: not counted or painted, since a plan may never cut them this way",
504
- children: "Other readings, not required"
505
- }
506
- ),
507
- /* @__PURE__ */ jsx5("div", { className: "opacity-70", children: list(others) })
508
- ] }) : null
509
- ] });
510
- };
511
- var directionOf = (feature) => feature ? directionLabel(feature.machiningDirection) : "";
512
-
513
- // src/rule-list/rule-sentence.tsx
514
- import { useState as useState5 } from "react";
515
-
516
- // src/rule-list/ops.ts
517
- var OP_TITLES = {
518
- gte: "at least",
519
- gt: "more than",
520
- lte: "at most",
521
- lt: "less than",
522
- eq: "equal to",
523
- ne: "not equal to",
524
- between: "a range, between two numbers"
525
- };
526
- var LOW_SYMBOLS = { gte: "\u2264", gt: "<" };
527
- var ONE_SIDED = "one:";
528
- var ONE_SIDED_CHOICES = {
529
- heading: "One-sided",
530
- choices: OPS.filter((op) => op !== "between").map((op) => ({
531
- value: `${ONE_SIDED}${op}`,
532
- label: `${opSymbol(op)} ${OP_TITLES[op]}`
533
- }))
534
- };
535
- var rangeEnd = (rule, chosen, hold) => chosen.startsWith(ONE_SIDED) ? withOp(rule, chosen.slice(ONE_SIDED.length)) : { ...rule, ...hold(chosen) };
536
-
537
- // src/rule-list/comparison.tsx
538
- import { jsx as jsx6 } from "react/jsx-runtime";
539
- var COMPARISONS = {
540
- groups: [{ choices: OPS.map((op) => ({ value: op, label: opSymbol(op) })) }]
541
- };
542
- var RANGE_HIGH = {
543
- groups: [
544
- { heading: "Range", choices: RANGE_HIGHS.map((op) => ({ value: op, label: opSymbol(op) })) },
545
- ONE_SIDED_CHOICES
546
- ]
547
- };
548
- var Comparison = ({ rule, ranged, onChange }) => ranged ? /* @__PURE__ */ jsx6(
549
- Picker,
550
- {
551
- label: "High end",
552
- title: OP_TITLES[highOf(rule)],
553
- value: highOf(rule),
554
- choices: RANGE_HIGH,
555
- onChange: (value) => onChange(rangeEnd(rule, value, (op) => ({ highOp: op })))
556
- }
557
- ) : /* @__PURE__ */ jsx6(
558
- Picker,
559
- {
560
- label: "Comparison",
561
- title: OP_TITLES[rule.op],
562
- value: rule.op,
563
- choices: COMPARISONS,
564
- onChange: (value) => onChange(withOp(rule, value))
565
- }
566
- );
567
-
568
- // src/rule-list/value-field.tsx
569
- import { useEffect as useEffect2, useState as useState4 } from "react";
570
- import { cn as cn5 } from "@toolpath/ui";
571
- import { jsx as jsx7 } from "react/jsx-runtime";
572
- var ValueField = ({
573
- label,
574
- info,
575
- value,
576
- units,
577
- onChange
578
- }) => {
579
- const shown = toField(info.quantity, value, units);
580
- const [text, setText] = useState4(shown);
581
- const [typing, setTyping] = useState4(false);
582
- useEffect2(() => {
583
- if (!typing) setText(shown);
584
- }, [shown, typing]);
585
- const invalid = fromField(info, text, units) === null;
586
- return /* @__PURE__ */ jsx7(
587
- "input",
588
- {
589
- "aria-label": label,
590
- "aria-invalid": invalid,
591
- inputMode: "decimal",
592
- className: cn5(
593
- WORD,
594
- "min-w-[2ch] cursor-text tabular-nums",
595
- invalid && "text-danger decoration-danger"
596
- ),
597
- value: text,
598
- onFocus: () => setTyping(true),
599
- onBlur: () => setTyping(false),
600
- onChange: (event) => {
601
- setText(event.target.value);
602
- const typed = fromField(info, event.target.value, units);
603
- if (typed !== null) onChange(typed);
604
- },
605
- onKeyDown: (event) => {
606
- if (event.key === "Enter") event.currentTarget.blur();
607
- }
608
- }
609
- );
610
- };
611
-
612
- // src/rule-list/rule-sentence.tsx
613
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
614
- var RANGE_LOW = {
615
- groups: [
616
- { heading: "Range", choices: RANGE_LOWS.map((op) => ({ value: op, label: LOW_SYMBOLS[op] })) },
617
- ONE_SIDED_CHOICES
618
- ]
619
- };
620
- var RuleSentence = ({
621
- rule,
622
- units,
623
- onChange,
624
- autoFocus
625
- }) => {
626
- const info = metricInfo(rule.metric);
627
- const flag = info?.quantity === "flag" || info?.quantity === "presence";
628
- const presence = info?.quantity === "presence";
629
- const ranged = info !== void 0 && !flag && rule.op === "between";
630
- const unit = info ? quantityUnit(info.quantity, units) : "";
631
- const [allSubjects, setAllSubjects] = useState5(false);
632
- const [allMetrics, setAllMetrics] = useState5(false);
633
- const unitSpan = unit ? /* @__PURE__ */ jsx8("span", { className: unit === "\xB0" || unit === "%" ? "" : "pl-1", children: unit }) : null;
634
- return /* @__PURE__ */ jsxs6("p", { className: "min-w-0 flex-1 text-sm leading-7 text-gray-500 dark:text-zinc-400", children: [
635
- /* @__PURE__ */ jsx8(
636
- Picker,
637
- {
638
- label: "Features",
639
- value: rule.subject,
640
- choices: subjectChoices(rule.subject, allSubjects),
641
- onChange: (value) => onChange(withSubject(rule, value, units)),
642
- onMore: () => setAllSubjects(true)
643
- }
644
- ),
645
- presence ? ", " : " with ",
646
- /* @__PURE__ */ jsxs6("span", { className: ranged ? "whitespace-nowrap" : void 0, children: [
647
- ranged ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
648
- /* @__PURE__ */ jsxs6("span", { className: "whitespace-nowrap", children: [
649
- /* @__PURE__ */ jsx8(
650
- ValueField,
651
- {
652
- label: "From",
653
- info,
654
- value: rule.value,
655
- units,
656
- onChange: (value) => onChange({ ...rule, value })
657
- }
658
- ),
659
- unitSpan
660
- ] }),
661
- " ",
662
- /* @__PURE__ */ jsx8(
663
- Picker,
664
- {
665
- label: "Low end",
666
- title: OP_TITLES[lowOf(rule)],
667
- value: lowOf(rule),
668
- choices: RANGE_LOW,
669
- onChange: (value) => onChange(rangeEnd(rule, value, (op) => ({ lowOp: op })))
670
- }
671
- ),
672
- " "
673
- ] }) : null,
674
- /* @__PURE__ */ jsx8(
675
- Picker,
676
- {
677
- label: "Measure",
678
- title: info?.hint,
679
- value: rule.metric,
680
- choices: metricChoices(rule.subject, rule.metric, allMetrics),
681
- onChange: (value) => onChange(withMetric(rule, value, units)),
682
- onMore: () => setAllMetrics(true),
683
- autoFocus
684
- }
685
- ),
686
- info && !flag ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
687
- " ",
688
- /* @__PURE__ */ jsx8(Comparison, { rule, ranged, onChange }),
689
- " ",
690
- /* @__PURE__ */ jsxs6("span", { className: "whitespace-nowrap", children: [
691
- /* @__PURE__ */ jsx8(
692
- ValueField,
693
- {
694
- label: ranged ? "Up to" : "Value",
695
- info,
696
- value: ranged ? upperOf(rule) : rule.value,
697
- units,
698
- onChange: (value) => onChange(ranged ? { ...rule, max: value } : { ...rule, value })
699
- }
700
- ),
701
- unitSpan
702
- ] })
703
- ] }) : null
704
- ] })
705
- ] });
706
- };
707
-
708
- // src/rule-list/swatch.tsx
709
- import { cn as cn6 } from "@toolpath/ui";
710
- import { jsx as jsx9 } from "react/jsx-runtime";
711
- var swatchKeys = listKeys("swatches", "horizontal");
712
- var Swatch = ({
713
- color,
714
- label,
715
- onClick,
716
- onKeyDown,
717
- listItem,
718
- className,
719
- ref
720
- }) => {
721
- const dot = cn6("size-4 shrink-0 rounded-full ring-1 ring-black/10 dark:ring-white/15", className);
722
- return onClick ? /* @__PURE__ */ jsx9(
723
- "button",
724
- {
725
- ref,
726
- type: "button",
727
- "aria-label": label,
728
- "data-list-item": listItem,
729
- onClick,
730
- onKeyDown,
731
- className: cn6(
732
- dot,
733
- "cursor-pointer hover:ring-2 hover:ring-gray-300 dark:hover:ring-zinc-500",
734
- FOCUS_RING
735
- ),
736
- style: { backgroundColor: color }
737
- }
738
- ) : /* @__PURE__ */ jsx9("span", { "aria-hidden": true, className: dot, style: { backgroundColor: color } });
739
- };
740
- var ColorPicker = ({
741
- value,
742
- onChange
743
- }) => /* @__PURE__ */ jsx9("div", { className: "flex flex-wrap items-center gap-1.5 py-1", onKeyDown: swatchKeys, children: RULE_COLORS.map((each) => /* @__PURE__ */ jsx9(
744
- "button",
745
- {
746
- type: "button",
747
- "data-list-item": "swatches",
748
- "aria-label": each.name,
749
- "aria-pressed": value === each.value,
750
- title: each.name,
751
- autoFocus: value === each.value,
752
- onClick: () => onChange(each.value),
753
- className: cn6(
754
- "size-5 cursor-pointer rounded-full ring-1 ring-black/10 outline-none focus-visible:ring-2 focus-visible:ring-info dark:ring-white/15",
755
- value === each.value && "ring-2 ring-gray-600 ring-offset-1 dark:ring-zinc-100 dark:ring-offset-zinc-900"
756
- ),
757
- style: { backgroundColor: each.value }
758
- },
759
- each.value
760
- )) });
761
-
762
- // src/rule-list/rule-row.tsx
763
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
764
- var isControl = (target) => target instanceof Element && target.closest("button, select, input, a") !== null;
765
- var RuleRow = ({
766
- rule,
767
- result,
768
- checking,
769
- open,
770
- focusOnMount,
771
- onToggle,
772
- onChange,
773
- onRemove,
774
- check,
775
- features,
776
- sheets,
777
- units,
778
- ...list
779
- }) => {
780
- const [picking, setPicking] = useState6(false);
781
- const swatchRef = useRef3(null);
782
- const count = result?.tags.length ?? 0;
783
- const others = result?.otherTags.length ?? 0;
784
- const canOpen = count + others > 0;
785
- const onRowKey = (event) => {
786
- if (event.key === "ArrowRight" && canOpen) {
787
- event.preventDefault();
788
- if (!open) onToggle();
789
- else
790
- event.currentTarget.closest("li")?.querySelector("[data-feature-row]")?.focus();
791
- } else if (event.key === "ArrowLeft" && open) {
792
- event.preventDefault();
793
- onToggle();
794
- }
795
- };
796
- return /* @__PURE__ */ jsxs7(
797
- "li",
798
- {
799
- className: cn7(
800
- "group/rule",
801
- // Nothing on the part breaks it: dimmed, until pointed at or worked in.
802
- !checking && count === 0 && "opacity-55 transition-opacity focus-within:opacity-100 hover:opacity-100"
803
- ),
804
- children: [
805
- /* @__PURE__ */ jsxs7(
806
- "div",
807
- {
808
- onClick: (event) => {
809
- if (canOpen && !isControl(event.target)) onToggle();
810
- },
811
- className: cn7(
812
- "flex items-start gap-1.5 rounded py-1 pr-1.5 hover:bg-gray-50 dark:hover:bg-zinc-800/60",
813
- canOpen && "cursor-pointer"
814
- ),
815
- children: [
816
- /* @__PURE__ */ jsx10(
817
- "button",
818
- {
819
- type: "button",
820
- tabIndex: -1,
821
- "aria-label": open ? "Hide features" : "Show features",
822
- disabled: !canOpen,
823
- onClick: onToggle,
824
- className: cn7(
825
- "flex size-7 shrink-0 items-center justify-center rounded text-gray-400 dark:text-zinc-400",
826
- canOpen ? "cursor-pointer hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-zinc-700 dark:hover:text-zinc-100" : "invisible"
827
- ),
828
- children: /* @__PURE__ */ jsx10(
829
- CaretRightIcon2,
830
- {
831
- weight: "bold",
832
- className: cn7("size-4 transition-transform duration-150", open && "rotate-90")
833
- }
834
- )
835
- }
836
- ),
837
- /* @__PURE__ */ jsx10(
838
- Swatch,
839
- {
840
- ref: swatchRef,
841
- color: rule.color,
842
- label: "Change colour",
843
- onClick: () => setPicking((was) => !was),
844
- onKeyDown: onRowKey,
845
- listItem: "rules",
846
- className: "mt-1.5"
847
- }
848
- ),
849
- /* @__PURE__ */ jsx10(RuleSentence, { rule, units, onChange, autoFocus: focusOnMount }),
850
- /* @__PURE__ */ jsxs7("div", { className: "relative mt-1 shrink-0", children: [
851
- /* @__PURE__ */ jsx10(
852
- IconButton,
853
- {
854
- "aria-label": "Remove rule",
855
- variant: "muted",
856
- size: "sm",
857
- onClick: onRemove,
858
- className: cn7(
859
- "absolute top-1/2 right-full mr-1 -translate-y-1/2 bg-white dark:bg-zinc-800",
860
- "pointer-events-none opacity-0 transition-opacity",
861
- "group-focus-within/rule:pointer-events-auto group-focus-within/rule:opacity-100",
862
- "group-hover/rule:pointer-events-auto group-hover/rule:opacity-100"
863
- ),
864
- children: /* @__PURE__ */ jsx10(TrashIcon, { weight: "bold" })
865
- }
866
- ),
867
- /* @__PURE__ */ jsx10(
868
- CountPill,
869
- {
870
- count,
871
- others,
872
- checking,
873
- open,
874
- canOpen,
875
- onToggle
876
- }
877
- )
878
- ] })
879
- ]
880
- }
881
- ),
882
- picking ? /* @__PURE__ */ jsx10("div", { className: "pb-1 pl-[34px]", children: /* @__PURE__ */ jsx10(
883
- ColorPicker,
884
- {
885
- value: rule.color,
886
- onChange: (color) => {
887
- onChange({ ...rule, color });
888
- setPicking(false);
889
- swatchRef.current?.focus();
890
- }
891
- }
892
- ) }) : null,
893
- open && result && check && features && sheets ? /* @__PURE__ */ jsx10(
894
- FeatureRows,
895
- {
896
- rule,
897
- tags: result.tags,
898
- otherTags: result.otherTags,
899
- check,
900
- features,
901
- sheets,
902
- units,
903
- onBack: () => swatchRef.current?.focus(),
904
- ...list
905
- }
906
- ) : null
907
- ]
908
- }
909
- );
910
- };
911
- var CountPill = ({
912
- count,
913
- others,
914
- checking,
915
- open,
916
- canOpen,
917
- onToggle
918
- }) => /* @__PURE__ */ jsx10(
919
- "button",
920
- {
921
- type: "button",
922
- "aria-label": checking ? void 0 : countLabel(count, others),
923
- title: others > 0 ? `${count} required \xB7 ${others} other ${readings(others)} not counted` : void 0,
924
- "aria-expanded": canOpen ? open : void 0,
925
- disabled: !canOpen,
926
- onClick: onToggle,
927
- className: cn7(
928
- "flex h-5 min-w-8 items-center justify-center rounded-full px-1.5 text-xs font-semibold tabular-nums",
929
- canOpen ? "cursor-pointer bg-gray-100 text-gray-500 hover:bg-gray-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700" : "text-gray-300 dark:text-zinc-600",
930
- FOCUS_RING
931
- ),
932
- children: checking ? "\u2026" : count
933
- }
934
- );
935
- var readings = (count) => count === 1 ? "reading" : "readings";
936
- var countLabel = (count, others) => {
937
- const required = `${count} required ${count === 1 ? "feature breaks" : "features break"} it`;
938
- return others > 0 ? `${required}, and ${others} other ${readings(others)}` : required;
939
- };
940
-
941
- // src/rule-list/rule-set-actions.tsx
942
- import { useRef as useRef4 } from "react";
943
- import { CheckIcon as CheckIcon2, CopyIcon, DownloadSimpleIcon, UploadSimpleIcon } from "@phosphor-icons/react";
944
- import { Button, cn as cn8 } from "@toolpath/ui";
945
-
946
- // src/rule-list/use-timed-value.ts
947
- import { useCallback, useEffect as useEffect3, useState as useState7 } from "react";
948
- var useTimedValue = (ms) => {
949
- const [box, setBox] = useState7(null);
950
- useEffect3(() => {
951
- if (box === null) return;
952
- const timer = setTimeout(() => setBox(null), ms);
953
- return () => clearTimeout(timer);
954
- }, [box, ms]);
955
- const set = useCallback((value) => setBox({ value }), []);
956
- return [box?.value ?? null, set];
957
- };
958
-
959
- // src/rule-list/rule-set-actions.tsx
960
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
961
- var NOTE_MS = 4e3;
962
- var COPIED_MS = 2e3;
963
- var EXPORT_FILE_NAME = "dfm-rules.json";
964
- var RuleSetActions = ({
965
- rules,
966
- actions
967
- }) => {
968
- const [note, setNote] = useTimedValue(NOTE_MS);
969
- const [copied, setCopied] = useTimedValue(COPIED_MS);
970
- const fileInput = useRef4(null);
971
- const importRules = async (file) => {
972
- if (!file) return;
973
- let text;
974
- try {
975
- text = await file.text();
976
- } catch {
977
- setNote("Could not import: the file could not be read.");
978
- return;
979
- }
980
- const read = parseRuleSetFile(text);
981
- if ("error" in read) {
982
- setNote(`Could not import: ${read.error}`);
983
- return;
984
- }
985
- rules.replace(read.rules);
986
- setNote(importedNote(read.rules.length, read.skipped, read.name));
987
- };
988
- const copyPrompt = async () => {
989
- try {
990
- await navigator.clipboard.writeText(llmRulePrompt(rules.rules));
991
- } catch {
992
- setNote("Could not copy the prompt. Allow clipboard access and try again.");
993
- return;
994
- }
995
- setCopied(true);
996
- setNote("Paste the prompt into an LLM, then import the JSON it writes.");
997
- };
998
- return /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2 px-4 pt-2 pb-3", children: [
999
- /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-center gap-2", children: [
1000
- /* @__PURE__ */ jsx11(Button, { variant: "secondary", onClick: () => fileInput.current?.click(), children: /* @__PURE__ */ jsx11(ActionLabel, { icon: /* @__PURE__ */ jsx11(UploadSimpleIcon, { weight: "bold" }), children: "Import\u2026" }) }),
1001
- /* @__PURE__ */ jsx11(Button, { variant: "secondary", onClick: () => downloadRuleSet(rules), children: /* @__PURE__ */ jsx11(ActionLabel, { icon: /* @__PURE__ */ jsx11(DownloadSimpleIcon, { weight: "bold" }), children: "Export" }) }),
1002
- /* @__PURE__ */ jsx11(
1003
- Button,
1004
- {
1005
- variant: "secondary",
1006
- title: "Copies a prompt that walks an LLM through writing a rule set for you",
1007
- onClick: () => void copyPrompt(),
1008
- children: /* @__PURE__ */ jsxs8("span", { className: "grid justify-items-center *:col-start-1 *:row-start-1", children: [
1009
- /* @__PURE__ */ jsx11(ActionLabel, { hidden: copied !== null, icon: /* @__PURE__ */ jsx11(CopyIcon, { weight: "bold" }), children: "Copy LLM prompt" }),
1010
- /* @__PURE__ */ jsx11(ActionLabel, { hidden: copied === null, icon: /* @__PURE__ */ jsx11(CheckIcon2, { weight: "bold" }), children: "Copied" })
1011
- ] })
1012
- }
1013
- ),
1014
- actions,
1015
- /* @__PURE__ */ jsx11(
1016
- "input",
1017
- {
1018
- ref: fileInput,
1019
- type: "file",
1020
- "aria-label": "Rule set file",
1021
- accept: ".json,application/json",
1022
- className: "hidden",
1023
- onChange: (event) => {
1024
- void importRules(event.currentTarget.files?.[0]);
1025
- event.currentTarget.value = "";
1026
- }
1027
- }
1028
- )
1029
- ] }),
1030
- note ? /* @__PURE__ */ jsx11("p", { className: "text-xs text-gray-500 dark:text-zinc-400", role: "status", children: note }) : null
1031
- ] });
1032
- };
1033
- var ActionLabel = ({ icon, children, hidden = false }) => /* @__PURE__ */ jsxs8(
1034
- "span",
1035
- {
1036
- "aria-hidden": hidden || void 0,
1037
- className: cn8("flex items-center gap-1.5 [&>svg]:size-3.5", hidden && "invisible"),
1038
- children: [
1039
- icon,
1040
- children
1041
- ]
1042
- }
1043
- );
1044
- var downloadRuleSet = (rules) => {
1045
- const url = URL.createObjectURL(
1046
- new Blob([serializeRuleSet(rules.rules)], { type: "application/json" })
1047
- );
1048
- const link = document.createElement("a");
1049
- link.href = url;
1050
- link.download = EXPORT_FILE_NAME;
1051
- link.click();
1052
- URL.revokeObjectURL(url);
1053
- };
1054
- var importedNote = (count, skipped, name) => {
1055
- const rulesRead = `Imported ${count} rule${count === 1 ? "" : "s"}`;
1056
- const named = name ? ` (${name})` : "";
1057
- const skippedSome = skipped > 0 ? `, ${skipped} skipped` : "";
1058
- return `${rulesRead}${named}${skippedSome}`;
1059
- };
1060
-
1061
- // src/rule-list/rule-list.tsx
1062
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1063
- var ruleListKeys = listKeys("rules");
1064
- var RuleList = ({
1065
- rules,
1066
- check,
1067
- features,
1068
- sheets,
1069
- units,
1070
- actions,
1071
- ...list
1072
- }) => {
1073
- const [openIds, setOpenIds] = useState8(/* @__PURE__ */ new Set());
1074
- const [added, setAdded] = useState8(null);
1075
- const toggleOpen = (id) => setOpenIds((was) => {
1076
- const next = new Set(was);
1077
- if (!next.delete(id)) next.add(id);
1078
- return next;
1079
- });
1080
- const byId = useMemo2(
1081
- () => new Map(check?.byRule.map((result) => [result.rule.id, result])),
1082
- [check]
1083
- );
1084
- const found = (rule) => {
1085
- const result = byId.get(rule.id);
1086
- return check === null || rule.id === added || (result?.tags.length ?? 0) + (result?.otherTags.length ?? 0) > 0;
1087
- };
1088
- const active = rules.rules.filter(found);
1089
- const none = rules.rules.filter((rule) => !found(rule));
1090
- const row = (rule) => /* @__PURE__ */ jsx12(
1091
- RuleRow,
1092
- {
1093
- rule,
1094
- result: byId.get(rule.id) ?? null,
1095
- checking: check === null,
1096
- open: openIds.has(rule.id),
1097
- focusOnMount: added === rule.id,
1098
- onToggle: () => toggleOpen(rule.id),
1099
- onChange: rules.update,
1100
- onRemove: () => rules.remove(rule.id),
1101
- check,
1102
- features,
1103
- sheets,
1104
- units,
1105
- ...list
1106
- },
1107
- rule.id
1108
- );
1109
- return (
1110
- // Grows with its rules, and scrolls once the panel can grow no more.
1111
- /* @__PURE__ */ jsxs9(ScrollArea, { className: "min-h-0 flex-auto", children: [
1112
- /* @__PURE__ */ jsxs9("ul", { className: "flex flex-col gap-0.5 pt-2 pr-4 pl-2", children: [
1113
- active.map(row),
1114
- none.map(row),
1115
- /* @__PURE__ */ jsx12(
1116
- NewRule,
1117
- {
1118
- onAdd: (subject) => {
1119
- const rule = makeRule(subject, PRESENCE_METRIC.key, units);
1120
- rules.add(rule);
1121
- setAdded(rule.id);
1122
- }
1123
- }
1124
- )
1125
- ] }),
1126
- /* @__PURE__ */ jsx12(RuleSetActions, { rules, actions })
1127
- ] })
1128
- );
1129
- };
1130
-
1131
- // src/reach/reach-chart.tsx
1132
- import { useMemo as useMemo4 } from "react";
1133
-
1134
- // src/reach/reach-section.tsx
1135
- import { useId as useId2, useMemo as useMemo3, useState as useState9 } from "react";
1136
-
1137
- // src/reach/reading.tsx
1138
- import { heightAt } from "@toolpath/tool-support";
1139
-
1140
- // src/reach/section-layout.ts
1141
- var SECTION_PAD = { left: 12, right: 64, top: 24, bottom: 62 };
1142
- var SECTION_MAX_H = 380;
1143
- var SECTION_MIN_H = 120;
1144
- var FEATURE_SHARE = 0.4;
1145
- var FEATURE_SHARE_MIN = 0.06;
1146
- var FEATURE_SHARE_MAX = 0.6;
1147
- var LABEL_GAP_X = 40;
1148
- var LABEL_GAP_Y = 14;
1149
- var WALL_LABEL_GAP_X = 30;
1150
- var READ_CHAR_PX = 6.4;
1151
- var HEADING_CHAR_PX = 6.5;
1152
- var READ_INSET = 8;
1153
- var AXIS_LABEL_HALF_PX = 84;
1154
- var FLOOR_STRIP = 6;
1155
- var DEPTH_WORD_PX = 34;
1156
- var BREAK_STEP = 8;
1157
- var BREAK_SWAY = 3;
1158
- var risersOf = (curve) => {
1159
- const risers = [];
1160
- let level = 0;
1161
- curve.heights.forEach((height, index) => {
1162
- if (height > level) risers.push({ at: runStart(curve, index), from: level, to: height });
1163
- level = Math.max(level, height);
1164
- });
1165
- return risers;
1166
- };
1167
- var spaced = (items, position, gap) => {
1168
- const kept = [];
1169
- items.forEach((item, index) => {
1170
- const last = kept[kept.length - 1];
1171
- const clear = last === void 0 || Math.abs(position(item) - position(last)) >= gap;
1172
- if (clear) kept.push(item);
1173
- else if (index === items.length - 1) kept[kept.length - 1] = item;
1174
- });
1175
- return kept;
1176
- };
1177
- var breakSteps = (top, bottom) => Math.max(2, Math.round((bottom - top) / BREAK_STEP));
1178
- var featureBreak = (left, top, bottom) => {
1179
- const steps = breakSteps(top, bottom);
1180
- let path = `M ${left} ${top}`;
1181
- for (let step = 1; step <= steps; step += 1) {
1182
- const sway = step === steps ? 0 : step % 2 === 1 ? BREAK_SWAY : -BREAK_SWAY;
1183
- path += ` L ${left + sway} ${top + (bottom - top) * step / steps}`;
1184
- }
1185
- return path;
1186
- };
1187
- var wallBreak = (right, top, bottom) => {
1188
- const steps = breakSteps(top, bottom);
1189
- let path = "";
1190
- for (let step = 1; step < steps; step += 1) {
1191
- const sway = step % 2 === 1 ? -BREAK_SWAY : BREAK_SWAY;
1192
- path += ` L ${right + sway} ${top + (bottom - top) * step / steps}`;
1193
- }
1194
- return path;
1195
- };
1196
-
1197
- // src/reach/styles.ts
1198
- var LINE = "stroke-gray-500 dark:stroke-zinc-400";
1199
- var TEXT = "fill-gray-600 dark:fill-zinc-300";
1200
- var MATERIAL = "fill-neutral-300 stroke-neutral-400 dark:fill-neutral-600 dark:stroke-neutral-500";
1201
- var FLOOR = "fill-neutral-300 dark:fill-neutral-600";
1202
- var ARROW = "fill-gray-500 dark:fill-zinc-400";
1203
- var FEATURE_FILL = "fill-blue-500/15 dark:fill-blue-400/15";
1204
- var FEATURE_LINE = "stroke-blue-600 dark:stroke-blue-400";
1205
- var FEATURE_TEXT = "fill-blue-600 dark:fill-blue-400";
1206
- var READ = "stroke-sky-600 dark:stroke-sky-400";
1207
- var READ_DOT = "fill-sky-600 dark:fill-sky-400";
1208
-
1209
- // src/reach/reading.tsx
1210
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1211
- var readingAt = ({ offsets, heights }, offset) => ({
1212
- offset,
1213
- height: heightAt({ horizontalOffset: offsets, verticalOffset: heights }, offset),
1214
- past: offset > (offsets[offsets.length - 1] ?? 0)
1215
- });
1216
- var readingText = ({ offset, height, past }, units) => `${lengthValue(offset, units)} ${unitLabel(units)} \u2192 ${lengthValue(height, units)} ${unitLabel(units)} reach${past ? " \xB7 past the last reading" : ""}`;
1217
- var offsetUnder = (clientX, box, geometry) => {
1218
- const at = (clientX - box.left) * (geometry.width / box.width);
1219
- const offset = (at - SECTION_PAD.left) / geometry.scale + geometry.x0;
1220
- return offset >= 0 && at <= geometry.right ? offset : null;
1221
- };
1222
- var ReadingMark = ({
1223
- geometry,
1224
- reading,
1225
- units
1226
- }) => {
1227
- const { x, y, width, floorY } = geometry;
1228
- const text = readingText(reading, units);
1229
- const textWidth = text.length * READ_CHAR_PX;
1230
- const pointerX = x(reading.offset);
1231
- const onRight = pointerX + READ_INSET + textWidth <= width - READ_INSET;
1232
- const labelX = onRight ? pointerX + READ_INSET : Math.min(Math.max(pointerX - READ_INSET, READ_INSET + textWidth), width - READ_INSET);
1233
- return /* @__PURE__ */ jsxs10("g", { pointerEvents: "none", children: [
1234
- /* @__PURE__ */ jsx13(
1235
- "line",
1236
- {
1237
- x1: pointerX,
1238
- x2: pointerX,
1239
- y1: SECTION_PAD.top,
1240
- y2: floorY,
1241
- strokeWidth: 1,
1242
- strokeDasharray: "3 3",
1243
- className: READ
1244
- }
1245
- ),
1246
- /* @__PURE__ */ jsx13("circle", { cx: pointerX, cy: y(reading.height), r: 4, className: READ_DOT }),
1247
- /* @__PURE__ */ jsx13(
1248
- "text",
1249
- {
1250
- x: labelX,
1251
- y: SECTION_PAD.top + 14,
1252
- textAnchor: onRight ? "start" : "end",
1253
- fontSize: 11,
1254
- fontWeight: 600,
1255
- className: TEXT,
1256
- children: text
1257
- }
1258
- )
1259
- ] });
1260
- };
1261
-
1262
- // src/reach/section-geometry.ts
1263
- var materialPath = (curve, cut, x, y, floorY, right) => {
1264
- let path = `M ${x(0)} ${floorY}`;
1265
- let level = 0;
1266
- curve.offsets.forEach((offset, index) => {
1267
- level = curve.heights[index] ?? 0;
1268
- path += ` L ${x(runStart(curve, index))} ${y(level)} L ${Math.min(x(offset), right)} ${y(level)}`;
1269
- });
1270
- path += ` L ${right} ${y(level)}`;
1271
- if (cut) path += wallBreak(right, y(level), floorY);
1272
- return `${path} L ${right} ${floorY} Z`;
1273
- };
1274
- var featureShare = (feature, x1) => {
1275
- const trueShare = feature.across ? feature.across.width / (feature.across.width + x1) : FEATURE_SHARE;
1276
- return {
1277
- share: Math.min(Math.max(trueShare, FEATURE_SHARE_MIN), FEATURE_SHARE_MAX),
1278
- cut: feature.across !== null && trueShare > FEATURE_SHARE_MAX
1279
- };
1280
- };
1281
- var plotScale = (width, spanMm, topMm) => {
1282
- const fitted = (width - SECTION_PAD.left - SECTION_PAD.right) / spanMm;
1283
- if (topMm * fitted > SECTION_MAX_H) return { scale: SECTION_MAX_H / topMm, plotH: SECTION_MAX_H };
1284
- return { scale: fitted, plotH: Math.max(topMm * fitted, SECTION_MIN_H) };
1285
- };
1286
- var heightLabels = (risers, depth, y) => spaced(
1287
- [...new Set(risers.map((riser) => riser.to))].sort((a, b) => a - b),
1288
- (h) => -y(h),
1289
- LABEL_GAP_Y
1290
- ).filter((h) => depth === null || Math.abs(y(h) - y(depth)) >= LABEL_GAP_Y);
1291
- var offsetLabels = (risers, x) => spaced(
1292
- risers.filter((riser) => x(riser.at) - x(0) >= WALL_LABEL_GAP_X),
1293
- (riser) => x(riser.at),
1294
- LABEL_GAP_X
1295
- );
1296
- var headingFor = (feature) => feature.through ? "THROUGH FEATURE" : "FEATURE";
1297
- var headingMiddle = (heading, wallX) => Math.max(
1298
- (SECTION_PAD.left + wallX) / 2,
1299
- SECTION_PAD.left + heading.length * HEADING_CHAR_PX / 2
1300
- );
1301
- var sectionGeometry = (whole, feature, width) => {
1302
- const { curve, cut } = trimmedCurve(whole);
1303
- const last = curve.offsets[curve.offsets.length - 1] ?? 0;
1304
- const peak = peakHeight(curve);
1305
- const span = last > 0 ? last : Math.max(peak, 1);
1306
- const x1 = span * 1.08;
1307
- const { share, cut: featureCut } = featureShare(feature, x1);
1308
- const x0 = -x1 * share / (1 - share);
1309
- const top = Math.max(peak * 1.06, (feature.depth ?? 0) * 1.06, span * 0.08);
1310
- const { scale, plotH } = plotScale(width, x1 - x0, top);
1311
- const floorY = SECTION_PAD.top + plotH;
1312
- const x = (mm) => SECTION_PAD.left + (mm - x0) * scale;
1313
- const y = (mm) => floorY - mm * scale;
1314
- const right = Math.min(x(x1), width - SECTION_PAD.right);
1315
- const risers = risersOf(curve);
1316
- const { depth } = feature;
1317
- const heading = headingFor(feature);
1318
- return {
1319
- curve,
1320
- cut,
1321
- last,
1322
- width,
1323
- height: plotH + SECTION_PAD.top + SECTION_PAD.bottom,
1324
- scale,
1325
- x0,
1326
- x,
1327
- y,
1328
- floorY,
1329
- right,
1330
- materialPath: materialPath(curve, cut, x, y, floorY, right),
1331
- across: offsetLabels(risers, x),
1332
- up: heightLabels(risers, depth, y),
1333
- feature,
1334
- featureCut,
1335
- featureHeading: heading,
1336
- featureHeadingX: headingMiddle(heading, x(0)),
1337
- featureTopY: depth === null ? SECTION_PAD.top : y(depth),
1338
- featureBottomY: feature.through ? floorY + FLOOR_STRIP : floorY,
1339
- wallTop: y(curve.heights[0] ?? 0),
1340
- datumX: right + 8,
1341
- ordinateY: floorY + 22
1342
- };
1343
- };
1344
-
1345
- // src/reach/section-parts.tsx
1346
- import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1347
- var Arrows = ({ id }) => /* @__PURE__ */ jsx14("defs", { children: /* @__PURE__ */ jsx14(
1348
- "marker",
1349
- {
1350
- id,
1351
- viewBox: "0 0 10 10",
1352
- refX: "10",
1353
- refY: "5",
1354
- markerWidth: "7",
1355
- markerHeight: "7",
1356
- orient: "auto-start-reverse",
1357
- children: /* @__PURE__ */ jsx14("path", { d: "M 0 1.5 L 10 5 L 0 8.5 z", className: ARROW })
1358
- }
1359
- ) });
1360
- var SectionShapes = ({ geometry }) => {
1361
- const { x, floorY, right, featureTopY, featureBottomY, wallTop, feature } = geometry;
1362
- const floorLeft = feature.through ? x(0) : SECTION_PAD.left;
1363
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1364
- /* @__PURE__ */ jsx14(
1365
- "rect",
1366
- {
1367
- x: SECTION_PAD.left,
1368
- y: featureTopY,
1369
- width: x(0) - SECTION_PAD.left,
1370
- height: featureBottomY - featureTopY,
1371
- className: FEATURE_FILL
1372
- }
1373
- ),
1374
- /* @__PURE__ */ jsx14(
1375
- "rect",
1376
- {
1377
- x: floorLeft,
1378
- y: floorY,
1379
- width: right - floorLeft,
1380
- height: FLOOR_STRIP,
1381
- className: FLOOR
1382
- }
1383
- ),
1384
- /* @__PURE__ */ jsx14("path", { d: geometry.materialPath, strokeWidth: 1, className: MATERIAL }),
1385
- /* @__PURE__ */ jsx14("line", { x1: floorLeft, x2: right, y1: floorY, y2: floorY, strokeWidth: 1, className: LINE }),
1386
- feature.through ? null : /* @__PURE__ */ jsx14(
1387
- "line",
1388
- {
1389
- x1: SECTION_PAD.left,
1390
- x2: x(0),
1391
- y1: floorY,
1392
- y2: floorY,
1393
- strokeWidth: 2,
1394
- className: FEATURE_LINE
1395
- }
1396
- ),
1397
- /* @__PURE__ */ jsx14(
1398
- "line",
1399
- {
1400
- x1: x(0),
1401
- x2: x(0),
1402
- y1: featureBottomY,
1403
- y2: wallTop,
1404
- strokeWidth: 2,
1405
- className: FEATURE_LINE
1406
- }
1407
- ),
1408
- /* @__PURE__ */ jsx14(
1409
- "line",
1410
- {
1411
- x1: x(0),
1412
- x2: x(0),
1413
- y1: wallTop,
1414
- y2: SECTION_PAD.top,
1415
- strokeWidth: 0.75,
1416
- strokeDasharray: "3 2",
1417
- className: FEATURE_LINE
1418
- }
1419
- )
1420
- ] });
1421
- };
1422
- var SectionHeadings = ({ geometry }) => {
1423
- const { right } = geometry;
1424
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1425
- /* @__PURE__ */ jsx14(
1426
- "text",
1427
- {
1428
- x: geometry.featureHeadingX,
1429
- y: SECTION_PAD.top - 8,
1430
- textAnchor: "middle",
1431
- fontSize: 9,
1432
- fontWeight: 600,
1433
- className: FEATURE_TEXT,
1434
- children: geometry.featureHeading
1435
- }
1436
- ),
1437
- /* @__PURE__ */ jsx14(
1438
- "text",
1439
- {
1440
- x: right,
1441
- y: SECTION_PAD.top - 8,
1442
- textAnchor: "end",
1443
- fontSize: 9,
1444
- className: TEXT,
1445
- opacity: 0.7,
1446
- children: "ADJACENT WALL HEIGHT"
1447
- }
1448
- )
1449
- ] });
1450
- };
1451
- var FeatureBreak = ({ geometry }) => geometry.featureCut ? /* @__PURE__ */ jsx14(
1452
- "path",
1453
- {
1454
- d: featureBreak(SECTION_PAD.left, geometry.featureTopY, geometry.featureBottomY),
1455
- strokeWidth: 1,
1456
- fill: "none",
1457
- className: FEATURE_LINE
1458
- }
1459
- ) : null;
1460
- var FeatureDepth = ({ geometry, units }) => {
1461
- const { x, featureTopY, datumX } = geometry;
1462
- const { depth } = geometry.feature;
1463
- if (depth === null) return null;
1464
- return /* @__PURE__ */ jsxs11("g", { children: [
1465
- /* @__PURE__ */ jsx14("title", { children: "Feature depth" }),
1466
- /* @__PURE__ */ jsx14(
1467
- "line",
1468
- {
1469
- x1: SECTION_PAD.left,
1470
- x2: x(0),
1471
- y1: featureTopY,
1472
- y2: featureTopY,
1473
- strokeWidth: 2,
1474
- className: FEATURE_LINE
1475
- }
1476
- ),
1477
- /* @__PURE__ */ jsx14(
1478
- "line",
1479
- {
1480
- x1: x(0),
1481
- x2: datumX,
1482
- y1: featureTopY,
1483
- y2: featureTopY,
1484
- strokeWidth: 0.75,
1485
- strokeDasharray: "4 2",
1486
- className: FEATURE_LINE
1487
- }
1488
- ),
1489
- x(0) - SECTION_PAD.left >= DEPTH_WORD_PX ? /* @__PURE__ */ jsx14(
1490
- "text",
1491
- {
1492
- x: SECTION_PAD.left + 4,
1493
- y: featureTopY - 4,
1494
- fontSize: 9,
1495
- fontWeight: 600,
1496
- className: FEATURE_TEXT,
1497
- children: "depth"
1498
- }
1499
- ) : null,
1500
- /* @__PURE__ */ jsx14(
1501
- "text",
1502
- {
1503
- x: datumX + 4,
1504
- y: featureTopY + 3.5,
1505
- fontSize: 10,
1506
- fontWeight: 600,
1507
- className: FEATURE_TEXT,
1508
- children: lengthValue(depth, units)
1509
- }
1510
- )
1511
- ] });
1512
- };
1513
- var ReadRun = ({ geometry, units, arrowId }) => {
1514
- const { x, last, cut, floorY, ordinateY } = geometry;
1515
- if (last <= 0 || cut) return null;
1516
- return /* @__PURE__ */ jsxs11("g", { children: [
1517
- /* @__PURE__ */ jsx14(
1518
- "line",
1519
- {
1520
- x1: x(0),
1521
- x2: x(last),
1522
- y1: ordinateY - 8,
1523
- y2: ordinateY - 8,
1524
- strokeWidth: 0.75,
1525
- markerStart: `url(#${arrowId})`,
1526
- markerEnd: `url(#${arrowId})`,
1527
- className: LINE
1528
- }
1529
- ),
1530
- /* @__PURE__ */ jsx14(
1531
- "line",
1532
- {
1533
- x1: x(last),
1534
- x2: x(last),
1535
- y1: floorY,
1536
- y2: ordinateY - 4,
1537
- strokeWidth: 0.5,
1538
- className: LINE
1539
- }
1540
- ),
1541
- /* @__PURE__ */ jsxs11(
1542
- "text",
1543
- {
1544
- x: (x(0) + x(last)) / 2,
1545
- y: ordinateY - 11,
1546
- textAnchor: "middle",
1547
- fontSize: 10,
1548
- className: TEXT,
1549
- children: [
1550
- lengthValue(last, units),
1551
- " read"
1552
- ]
1553
- }
1554
- )
1555
- ] });
1556
- };
1557
- var OffsetOrdinates = (props) => {
1558
- const { geometry, units } = props;
1559
- const { x, y, right, width, floorY, ordinateY } = geometry;
1560
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1561
- /* @__PURE__ */ jsx14(
1562
- "text",
1563
- {
1564
- x: Math.min(
1565
- Math.max((x(0) + right) / 2, AXIS_LABEL_HALF_PX + READ_INSET),
1566
- width - AXIS_LABEL_HALF_PX - READ_INSET
1567
- ),
1568
- y: ordinateY + 32,
1569
- textAnchor: "middle",
1570
- fontSize: 9,
1571
- className: TEXT,
1572
- opacity: 0.7,
1573
- children: "XY PLANE DISTANCE FROM FEATURE"
1574
- }
1575
- ),
1576
- /* @__PURE__ */ jsx14(
1577
- "line",
1578
- {
1579
- x1: x(0),
1580
- x2: x(0),
1581
- y1: floorY,
1582
- y2: ordinateY + 4,
1583
- strokeWidth: 0.75,
1584
- className: LINE
1585
- }
1586
- ),
1587
- /* @__PURE__ */ jsx14("text", { x: x(0), y: ordinateY + 15, textAnchor: "middle", fontSize: 10, className: TEXT, children: "0" }),
1588
- geometry.across.map((riser) => /* @__PURE__ */ jsxs11("g", { children: [
1589
- /* @__PURE__ */ jsx14(
1590
- "line",
1591
- {
1592
- x1: x(riser.at),
1593
- x2: x(riser.at),
1594
- y1: y(riser.from) + 2,
1595
- y2: ordinateY + 4,
1596
- strokeWidth: 0.5,
1597
- strokeDasharray: "2 2",
1598
- className: LINE
1599
- }
1600
- ),
1601
- /* @__PURE__ */ jsx14(
1602
- "text",
1603
- {
1604
- x: x(riser.at),
1605
- y: ordinateY + 15,
1606
- textAnchor: "middle",
1607
- fontSize: 10,
1608
- className: TEXT,
1609
- children: lengthValue(riser.at, units)
1610
- }
1611
- )
1612
- ] }, `x:${riser.at}`)),
1613
- /* @__PURE__ */ jsx14(ReadRun, { ...props })
1614
- ] });
1615
- };
1616
- var HeightOrdinates = ({ geometry, units }) => {
1617
- const { y, right, floorY, datumX } = geometry;
1618
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1619
- /* @__PURE__ */ jsx14("text", { x: datumX + 4, y: floorY + 3.5, fontSize: 10, className: TEXT, children: "0" }),
1620
- geometry.up.map((h) => /* @__PURE__ */ jsxs11("g", { children: [
1621
- /* @__PURE__ */ jsx14(
1622
- "line",
1623
- {
1624
- x1: right - 2,
1625
- x2: datumX,
1626
- y1: y(h),
1627
- y2: y(h),
1628
- strokeWidth: 0.5,
1629
- strokeDasharray: "2 2",
1630
- className: LINE
1631
- }
1632
- ),
1633
- /* @__PURE__ */ jsx14("text", { x: datumX + 4, y: y(h) + 3.5, fontSize: 10, className: TEXT, children: lengthValue(h, units) })
1634
- ] }, `y:${h}`))
1635
- ] });
1636
- };
1637
-
1638
- // src/reach/reach-section.tsx
1639
- import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1640
- var ReachSection = ({
1641
- curve: whole,
1642
- units,
1643
- feature,
1644
- width
1645
- }) => {
1646
- const [hover, setHover] = useState9(null);
1647
- const geometry = useMemo3(() => sectionGeometry(whole, feature, width), [whole, feature, width]);
1648
- const reactId = useId2();
1649
- const arrowId = `reach-dim-arrow-${reactId.replace(/[^\w-]/g, "")}`;
1650
- const reading = hover === null ? null : readingAt(whole, hover);
1651
- const drawing = useMemo3(
1652
- () => /* @__PURE__ */ jsxs12(Fragment5, { children: [
1653
- /* @__PURE__ */ jsx15(Arrows, { id: arrowId }),
1654
- /* @__PURE__ */ jsx15(SectionShapes, { geometry }),
1655
- /* @__PURE__ */ jsx15(SectionHeadings, { geometry }),
1656
- /* @__PURE__ */ jsx15(FeatureBreak, { geometry }),
1657
- /* @__PURE__ */ jsx15(FeatureDepth, { geometry, units }),
1658
- /* @__PURE__ */ jsx15(OffsetOrdinates, { geometry, units, arrowId }),
1659
- /* @__PURE__ */ jsx15(HeightOrdinates, { geometry, units })
1660
- ] }),
1661
- [arrowId, geometry, units]
1662
- );
1663
- return /* @__PURE__ */ jsxs12(
1664
- "svg",
1665
- {
1666
- viewBox: `0 0 ${width} ${geometry.height}`,
1667
- width,
1668
- height: geometry.height,
1669
- className: "select-none",
1670
- role: "img",
1671
- "aria-label": "Reach curve, dimensioned",
1672
- onPointerMove: (event) => setHover(offsetUnder(event.clientX, event.currentTarget.getBoundingClientRect(), geometry)),
1673
- onPointerLeave: () => setHover(null),
1674
- children: [
1675
- drawing,
1676
- reading ? /* @__PURE__ */ jsx15(ReadingMark, { geometry, reading, units }) : null
1677
- ]
1678
- }
1679
- );
1680
- };
1681
-
1682
- // src/reach/use-element-size.ts
1683
- import { useLayoutEffect, useRef as useRef5, useState as useState10 } from "react";
1684
- var useElementSize = () => {
1685
- const ref = useRef5(null);
1686
- const [size, setSize] = useState10({ width: 0, height: 0 });
1687
- useLayoutEffect(() => {
1688
- const element = ref.current;
1689
- if (!element) return;
1690
- const observer = new ResizeObserver(([entry]) => {
1691
- if (!entry) return;
1692
- const { width, height } = entry.contentRect;
1693
- setSize(
1694
- (was) => Math.abs(was.width - width) < 0.5 && Math.abs(was.height - height) < 0.5 ? was : { width, height }
1695
- );
1696
- });
1697
- observer.observe(element);
1698
- return () => observer.disconnect();
1699
- }, []);
1700
- return [ref, size];
1701
- };
1702
-
1703
- // src/reach/reach-chart.tsx
1704
- import { Fragment as Fragment6, jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
1705
- var CHART_MIN_W = 200;
1706
- var DRAWING_MIN_W = 240;
1707
- var ReachChart = ({ curve, units, feature }) => {
1708
- const [ref, size] = useElementSize();
1709
- return /* @__PURE__ */ jsx16("div", { ref, className: "w-full", children: /* @__PURE__ */ jsx16(
1710
- ReachSection,
1711
- {
1712
- curve,
1713
- units,
1714
- feature,
1715
- width: Math.max(size.width, CHART_MIN_W)
1716
- }
1717
- ) });
1718
- };
1719
- var CutNote = ({
1720
- curve,
1721
- units
1722
- }) => {
1723
- const trimmed = useMemo4(() => trimmedCurve(curve), [curve]);
1724
- if (!trimmed.cut) return null;
1725
- const drawnTo = trimmed.curve.offsets[trimmed.curve.offsets.length - 1] ?? 0;
1726
- const readTo = curve.offsets[curve.offsets.length - 1] ?? 0;
1727
- return /* @__PURE__ */ jsxs13(Fragment6, { children: [
1728
- " ",
1729
- "It is at full height from ",
1730
- lengthValue(drawnTo, units),
1731
- " ",
1732
- unitLabel(units),
1733
- " out, so the drawing stops there; the Engine read it to ",
1734
- lengthValue(readTo, units),
1735
- " ",
1736
- unitLabel(units),
1737
- "."
1738
- ] });
1739
- };
1740
- var ReachDrawing = ({ curve, units, feature }) => {
1741
- const [ref, size] = useElementSize();
1742
- return /* @__PURE__ */ jsxs13("div", { ref, className: "flex flex-col gap-3 p-4", children: [
1743
- /* @__PURE__ */ jsxs13("figure", { className: "flex flex-col gap-1", children: [
1744
- /* @__PURE__ */ jsxs13("figcaption", { className: "text-2xs font-semibold text-gray-400 uppercase dark:text-zinc-500", children: [
1745
- "Section at the wall \xB7 to scale \xB7 ",
1746
- unitLabel(units)
1747
- ] }),
1748
- /* @__PURE__ */ jsx16(
1749
- ReachSection,
1750
- {
1751
- curve,
1752
- units,
1753
- feature,
1754
- width: Math.max(size.width, DRAWING_MIN_W)
1755
- }
1756
- )
1757
- ] }),
1758
- /* @__PURE__ */ jsxs13("p", { className: "text-2xs text-gray-400 dark:text-zinc-500", children: [
1759
- "Point at the walls to read them: the distance from the feature, then the reach a tool needs there.",
1760
- /* @__PURE__ */ jsx16(CutNote, { curve, units })
1761
- ] })
1762
- ] });
1763
- };
1764
-
1765
- // src/feature-panel/fold-store.ts
1766
- var storageFolds = (storage, prefix) => ({
1767
- read: (id) => {
1768
- try {
1769
- const stored = storage?.getItem(`${prefix}${id}`);
1770
- return stored === "open" ? true : stored === "shut" ? false : void 0;
1771
- } catch {
1772
- return void 0;
1773
- }
1774
- },
1775
- write: (id, open) => {
1776
- try {
1777
- storage?.setItem(`${prefix}${id}`, open ? "open" : "shut");
1778
- } catch {
1779
- }
1780
- }
1781
- });
1782
-
1783
- // src/feature-panel/feature-details.tsx
1784
- import { useCallback as useCallback2, useMemo as useMemo7, useState as useState13 } from "react";
1785
- import { CrosshairIcon, InfoIcon } from "@phosphor-icons/react";
1786
- import { Button as Button3, Tooltip, cn as cn12 } from "@toolpath/ui";
1787
-
1788
- // src/feature-panel/feature-name.tsx
1789
- import { cn as cn9 } from "@toolpath/ui";
1790
-
1791
- // src/feature-panel/look.ts
1792
- var resolveLook = (look) => ({
1793
- rules: look?.rules ?? "sentence",
1794
- selected: look?.selected ?? "grey",
1795
- muted: look?.muted ?? "pale"
1796
- });
1797
- var secondaryText = (look) => look.muted === "strong" ? "text-gray dark:text-zinc-300" : "text-gray-400 dark:text-zinc-400";
1798
- var faintText = (look) => look.muted === "strong" ? "text-gray dark:text-zinc-300" : "text-gray-300 dark:text-zinc-500";
1799
-
1800
- // src/feature-panel/feature-name.tsx
1801
- import { Fragment as Fragment7, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
1802
- var FeatureName = ({ feature, label, look }) => /* @__PURE__ */ jsxs14(Fragment7, { children: [
1803
- label ?? featureTypeLabel(feature.featureType),
1804
- " ",
1805
- /* @__PURE__ */ jsxs14("span", { className: cn9("font-normal", faintText(look)), children: [
1806
- "from ",
1807
- directionLabel(feature.machiningDirection)
1808
- ] })
1809
- ] });
1810
- var RequiredBadge = () => /* @__PURE__ */ jsx17("span", { className: "rounded-full bg-gray-100 px-1.5 text-2xs font-semibold text-gray-500 uppercase dark:bg-zinc-800 dark:text-zinc-300", children: "Required" });
1811
-
1812
- // src/feature-panel/fold-section.tsx
1813
- import { useState as useState11 } from "react";
1814
- import { CaretRightIcon as CaretRightIcon3 } from "@phosphor-icons/react";
1815
- import { cn as cn10 } from "@toolpath/ui";
1816
- import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1817
- var FoldSection = ({
1818
- id,
1819
- title: title2,
1820
- action,
1821
- defaultOpen = true,
1822
- folds,
1823
- className,
1824
- children
1825
- }) => {
1826
- const [open, setOpen] = useState11(() => folds?.read(id) ?? defaultOpen);
1827
- return /* @__PURE__ */ jsxs15("section", { className: cn10("flex flex-col gap-1", className), children: [
1828
- /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", children: [
1829
- /* @__PURE__ */ jsxs15(
1830
- "button",
1831
- {
1832
- type: "button",
1833
- "aria-expanded": open,
1834
- onClick: () => {
1835
- setOpen(!open);
1836
- folds?.write(id, !open);
1837
- },
1838
- className: cn10(
1839
- "flex min-w-0 flex-1 cursor-pointer items-center gap-1 rounded text-left text-2xs font-semibold text-gray-400 uppercase",
1840
- "hover:text-gray-600 dark:text-zinc-500 dark:hover:text-zinc-300",
1841
- "outline-none focus-visible:ring-2 focus-visible:ring-info/75"
1842
- ),
1843
- children: [
1844
- /* @__PURE__ */ jsx18(
1845
- CaretRightIcon3,
1846
- {
1847
- weight: "bold",
1848
- className: cn10("size-3 shrink-0 transition-transform duration-150", open && "rotate-90")
1849
- }
1850
- ),
1851
- /* @__PURE__ */ jsx18("span", { className: "truncate", children: title2 })
1852
- ]
1853
- }
1854
- ),
1855
- action
1856
- ] }),
1857
- open ? children : null
1858
- ] });
1859
- };
1860
-
1861
- // src/feature-panel/read-state.tsx
1862
- import { useEffect as useEffect4, useState as useState12 } from "react";
1863
- import { Button as Button2 } from "@toolpath/ui";
1864
- import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
1865
- var Reading = ({ children }) => /* @__PURE__ */ jsx19("p", { className: "text-sm text-gray-400 dark:text-zinc-400", role: "status", children });
1866
- var ReadFailure = ({
1867
- failure
1868
- }) => {
1869
- const waiting = useWaiting(failure.retryAt);
1870
- return /* @__PURE__ */ jsxs16("div", { className: "flex flex-col items-start gap-2 text-sm", children: [
1871
- /* @__PURE__ */ jsx19("p", { className: "text-danger", role: "alert", children: failure.message }),
1872
- failure.retry ? /* @__PURE__ */ jsx19(Button2, { variant: "secondary", size: "sm", disabled: waiting, onClick: failure.retry, children: "Retry" }) : null
1873
- ] });
1874
- };
1875
- var useWaiting = (until) => {
1876
- const [passed, setPassed] = useState12(void 0);
1877
- useEffect4(() => {
1878
- if (until === void 0) return;
1879
- const timer = setTimeout(() => setPassed(until), Math.max(until - Date.now(), 0));
1880
- return () => clearTimeout(timer);
1881
- }, [until]);
1882
- return until !== void 0 && passed !== until;
1883
- };
1884
-
1885
- // src/feature-panel/pop-outs.tsx
1886
- import { useMemo as useMemo6 } from "react";
1887
- import {
1888
- ArrowSquareOutIcon,
1889
- BracketsCurlyIcon,
1890
- ChartLineIcon,
1891
- TableIcon
1892
- } from "@phosphor-icons/react";
1893
- import { IconButton as IconButton3 } from "@toolpath/ui";
1894
-
1895
- // src/feature-panel/record-sections.tsx
1896
- import { useMemo as useMemo5 } from "react";
1897
- import { CheckIcon as CheckIcon3, CopyIcon as CopyIcon2 } from "@phosphor-icons/react";
1898
- import { IconButton as IconButton2, cn as cn11 } from "@toolpath/ui";
1899
-
1900
- // src/model/feature-record.ts
1901
- var MAX_DATASHEET_FIELDS = 500;
1902
- var formatLeaf = (value) => typeof value === "number" ? String(Number(value.toFixed(4))) : typeof value === "string" ? value : value === null ? "null" : String(value);
1903
- var datasheetFields = (datasheet) => {
1904
- const fields = [];
1905
- let more = false;
1906
- const walk = (value, path) => {
1907
- if (fields.length >= MAX_DATASHEET_FIELDS) {
1908
- more = true;
1909
- return;
1910
- }
1911
- if (Array.isArray(value)) {
1912
- if (value.every((each) => typeof each !== "object" || each === null)) {
1913
- fields.push({ path, value: value.length === 0 ? "[]" : value.map(formatLeaf).join(", ") });
1914
- } else {
1915
- value.forEach((each, index) => walk(each, `${path}[${index}]`));
1916
- }
1917
- } else if (typeof value === "object" && value !== null) {
1918
- const entries = Object.entries(value);
1919
- if (entries.length === 0) fields.push({ path, value: "{}" });
1920
- for (const [key, each] of entries) walk(each, path ? `${path}.${key}` : key);
1921
- } else {
1922
- fields.push({ path, value: formatLeaf(value) });
1923
- }
1924
- };
1925
- if (datasheet !== null && datasheet !== void 0) walk(datasheet, "");
1926
- return { fields, more };
1927
- };
1928
-
1929
- // src/feature-panel/record-sections.tsx
1930
- import { Fragment as Fragment8, jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
1931
- var COPIED_MS2 = 1500;
1932
- var RecordSections = ({
1933
- record,
1934
- look,
1935
- folds,
1936
- popOut
1937
- }) => {
1938
- const value = record.status === "ready" ? record.value : null;
1939
- const { fields, more } = useMemo5(() => datasheetFields(value?.datasheet), [value]);
1940
- const json = useMemo5(() => value ? JSON.stringify(value, null, 2) : "", [value]);
1941
- if (record.status === "ready" && !value) return null;
1942
- const waiting = record.status === "loading" ? /* @__PURE__ */ jsx20(Reading, { children: "Reading the feature\u2019s record\u2026" }) : record.status === "error" ? /* @__PURE__ */ jsx20(ReadFailure, { failure: record }) : null;
1943
- const count = more ? `${MAX_DATASHEET_FIELDS}+` : String(fields.length);
1944
- return /* @__PURE__ */ jsxs17(Fragment8, { children: [
1945
- /* @__PURE__ */ jsx20(
1946
- FoldSection,
1947
- {
1948
- id: "datasheet",
1949
- defaultOpen: false,
1950
- title: value ? `All datasheet fields (${count})` : "All datasheet fields",
1951
- action: value ? popOut?.("datasheet") : null,
1952
- folds,
1953
- children: waiting ?? /* @__PURE__ */ jsx20(DatasheetFields, { record: value, look })
1954
- }
1955
- ),
1956
- /* @__PURE__ */ jsx20(
1957
- FoldSection,
1958
- {
1959
- id: "raw",
1960
- defaultOpen: false,
1961
- title: "Raw API record",
1962
- action: value ? /* @__PURE__ */ jsxs17(Fragment8, { children: [
1963
- /* @__PURE__ */ jsx20(CopyButton, { text: json }),
1964
- popOut?.("raw")
1965
- ] }) : null,
1966
- folds,
1967
- children: waiting ?? /* @__PURE__ */ jsx20(RawRecord, { json })
1968
- }
1969
- )
1970
- ] });
1971
- };
1972
- var DatasheetFields = ({ record, look, wide = false }) => {
1973
- const { fields } = useMemo5(() => datasheetFields(record?.datasheet), [record]);
1974
- return record?.datasheet ? /* @__PURE__ */ jsx20(
1975
- "dl",
1976
- {
1977
- className: cn11(
1978
- "grid grid-cols-[minmax(0,1fr)_auto] gap-y-0.5 text-xs",
1979
- wide ? "gap-x-4 p-4" : "gap-x-3"
1980
- ),
1981
- children: fields.map((field) => /* @__PURE__ */ jsxs17("div", { className: "contents", children: [
1982
- /* @__PURE__ */ jsx20("dt", { className: cn11("truncate font-mono", secondaryText(look)), title: field.path, children: field.path }),
1983
- /* @__PURE__ */ jsx20(
1984
- "dd",
1985
- {
1986
- className: cn11(
1987
- "truncate text-right font-mono text-gray-600 tabular-nums select-text dark:text-zinc-100",
1988
- wide ? "max-w-60" : "max-w-40"
1989
- ),
1990
- title: field.value,
1991
- children: field.value
1992
- }
1993
- )
1994
- ] }, field.path))
1995
- }
1996
- ) : /* @__PURE__ */ jsx20("p", { className: cn11("text-sm", secondaryText(look), wide && "p-4"), children: "The API sent no datasheet for this feature." });
1997
- };
1998
- var RawRecord = ({
1999
- json,
2000
- wide = false
2001
- }) => /* @__PURE__ */ jsx20(
2002
- "pre",
2003
- {
2004
- className: cn11(
2005
- "font-mono text-2xs leading-relaxed text-gray-600 select-text dark:text-zinc-200",
2006
- wide ? "p-4" : "max-h-80 overflow-auto rounded bg-gray-50 p-2 dark:bg-zinc-800/60"
2007
- ),
2008
- children: json
2009
- }
2010
- );
2011
- var CopyButton = ({ text }) => {
2012
- const [copied, setCopied] = useTimedValue(COPIED_MS2);
2013
- return /* @__PURE__ */ jsx20(
2014
- IconButton2,
2015
- {
2016
- "aria-label": copied ? "Copied" : "Copy raw record",
2017
- title: "Copy",
2018
- variant: "muted",
2019
- size: "sm",
2020
- onClick: () => {
2021
- void navigator.clipboard?.writeText(text).then(() => setCopied(true)).catch(() => void 0);
2022
- },
2023
- children: copied ? /* @__PURE__ */ jsx20(CheckIcon3, { weight: "bold" }) : /* @__PURE__ */ jsx20(CopyIcon2, { weight: "bold" })
2024
- }
2025
- );
2026
- };
2027
-
2028
- // src/feature-panel/pop-outs.tsx
2029
- import { Fragment as Fragment9, jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
2030
- var TITLES = {
2031
- reach: "Reach",
2032
- datasheet: "All datasheet fields",
2033
- raw: "Raw API record"
2034
- };
2035
- var LABELS = {
2036
- reach: "Pop out the reach drawing",
2037
- datasheet: "Pop out the datasheet fields",
2038
- raw: "Pop out the raw record"
2039
- };
2040
- var PopOutButton = ({
2041
- id,
2042
- onOpen
2043
- }) => /* @__PURE__ */ jsx21(
2044
- IconButton3,
2045
- {
2046
- "aria-label": LABELS[id],
2047
- title: "Pop out, larger",
2048
- variant: "muted",
2049
- size: "sm",
2050
- onClick: () => onOpen(id),
2051
- children: /* @__PURE__ */ jsx21(ArrowSquareOutIcon, { weight: "bold" })
2052
- }
2053
- );
2054
- var PopOuts = ({
2055
- open,
2056
- onClose,
2057
- Window,
2058
- subtitle,
2059
- record,
2060
- units,
2061
- profile,
2062
- look
2063
- }) => {
2064
- const curve = useMemo6(() => readReachCurve(record?.datasheet), [record]);
2065
- const json = useMemo6(() => record ? JSON.stringify(record, null, 2) : "", [record]);
2066
- if (!record) return null;
2067
- return /* @__PURE__ */ jsxs18(Fragment9, { children: [
2068
- open.has("reach") && curve ? /* @__PURE__ */ jsx21(
2069
- Window,
2070
- {
2071
- id: "reach",
2072
- title: TITLES.reach,
2073
- subtitle,
2074
- icon: /* @__PURE__ */ jsx21(ChartLineIcon, { weight: "bold" }),
2075
- onClose: () => onClose("reach"),
2076
- children: /* @__PURE__ */ jsx21(ReachDrawing, { curve, units, feature: profile })
2077
- }
2078
- ) : null,
2079
- open.has("datasheet") ? /* @__PURE__ */ jsx21(
2080
- Window,
2081
- {
2082
- id: "datasheet",
2083
- title: TITLES.datasheet,
2084
- subtitle,
2085
- icon: /* @__PURE__ */ jsx21(TableIcon, { weight: "bold" }),
2086
- onClose: () => onClose("datasheet"),
2087
- children: /* @__PURE__ */ jsx21(DatasheetFields, { record, look, wide: true })
2088
- }
2089
- ) : null,
2090
- open.has("raw") ? /* @__PURE__ */ jsx21(
2091
- Window,
2092
- {
2093
- id: "raw",
2094
- title: TITLES.raw,
2095
- subtitle,
2096
- icon: /* @__PURE__ */ jsx21(BracketsCurlyIcon, { weight: "bold" }),
2097
- action: /* @__PURE__ */ jsx21(CopyButton, { text: json }),
2098
- onClose: () => onClose("raw"),
2099
- children: /* @__PURE__ */ jsx21(RawRecord, { json, wide: true })
2100
- }
2101
- ) : null
2102
- ] });
2103
- };
2104
-
2105
- // src/feature-panel/feature-details.tsx
2106
- import { Fragment as Fragment10, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
2107
- var FeatureDetails = ({
2108
- feature,
2109
- units,
2110
- directionColor,
2111
- profile,
2112
- label,
2113
- required,
2114
- onFrame,
2115
- actions,
2116
- status,
2117
- children,
2118
- rules,
2119
- measurements,
2120
- record,
2121
- PopOut,
2122
- look: lookOption,
2123
- folds
2124
- }) => {
2125
- const look = resolveLook(lookOption);
2126
- const name = /* @__PURE__ */ jsx22(FeatureName, { feature, label, look });
2127
- const [out, setOut] = useState13(/* @__PURE__ */ new Set());
2128
- const openOut = useCallback2((id) => setOut((was) => new Set(was).add(id)), []);
2129
- const closeOut = useCallback2(
2130
- (id) => setOut((was) => {
2131
- const next = new Set(was);
2132
- next.delete(id);
2133
- return next;
2134
- }),
2135
- []
2136
- );
2137
- const popOut = PopOut ? (id) => /* @__PURE__ */ jsx22(PopOutButton, { id, onOpen: openOut }) : void 0;
2138
- return /* @__PURE__ */ jsxs19("div", { className: "flex flex-col gap-4 p-4", children: [
2139
- /* @__PURE__ */ jsxs19("div", { className: "flex flex-col gap-2", children: [
2140
- /* @__PURE__ */ jsxs19("div", { className: "flex items-start gap-2", children: [
2141
- /* @__PURE__ */ jsxs19("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
2142
- /* @__PURE__ */ jsx22("h3", { className: "truncate text-sm font-semibold text-gray-600 dark:text-zinc-100", children: name }),
2143
- /* @__PURE__ */ jsxs19("p", { className: cn12("flex flex-wrap items-center gap-1.5 text-xs", secondaryText(look)), children: [
2144
- /* @__PURE__ */ jsx22(Swatch, { color: directionColor, className: "size-2.5" }),
2145
- "Machined from ",
2146
- directionLabel(feature.machiningDirection),
2147
- required ? /* @__PURE__ */ jsx22(RequiredBadge, {}) : null,
2148
- status
2149
- ] })
2150
- ] }),
2151
- actions,
2152
- onFrame ? /* @__PURE__ */ jsx22(Button3, { variant: "secondary", size: "sm", onClick: onFrame, className: "shrink-0", children: /* @__PURE__ */ jsxs19("span", { className: "flex items-center gap-1 whitespace-nowrap", children: [
2153
- /* @__PURE__ */ jsx22(CrosshairIcon, { weight: "bold", className: "size-3.5" }),
2154
- "Show this feature"
2155
- ] }) }) : null
2156
- ] }),
2157
- children
2158
- ] }),
2159
- /* @__PURE__ */ jsx22(FoldSection, { id: "rules", title: "Rules broken", folds, children: /* @__PURE__ */ jsx22(BrokenRuleList, { rules, look }) }),
2160
- /* @__PURE__ */ jsx22(MeasurementSections, { measurements, look, folds }),
2161
- /* @__PURE__ */ jsx22(
2162
- ReachSection2,
2163
- {
2164
- record,
2165
- units,
2166
- profile,
2167
- folds,
2168
- action: popOut?.("reach")
2169
- }
2170
- ),
2171
- /* @__PURE__ */ jsx22(RecordSections, { record, look, folds, popOut }),
2172
- PopOut ? /* @__PURE__ */ jsx22(
2173
- PopOuts,
2174
- {
2175
- open: out,
2176
- onClose: closeOut,
2177
- Window: PopOut,
2178
- subtitle: name,
2179
- record: record.status === "ready" ? record.value : null,
2180
- units,
2181
- profile,
2182
- look
2183
- }
2184
- ) : null
2185
- ] });
2186
- };
2187
- var BrokenRuleList = ({
2188
- rules,
2189
- look
2190
- }) => rules.length > 0 ? /* @__PURE__ */ jsx22("ul", { className: "flex flex-col gap-1", children: rules.map((rule) => /* @__PURE__ */ jsxs19(
2191
- "li",
2192
- {
2193
- className: "flex items-start gap-2 text-sm text-gray-600 dark:text-zinc-100",
2194
- children: [
2195
- /* @__PURE__ */ jsx22(Swatch, { color: rule.color, className: "mt-0.5" }),
2196
- /* @__PURE__ */ jsx22(Explained, { note: rule.note, children: /* @__PURE__ */ jsx22("span", { className: "min-w-0 flex-1", children: rule.text }) }),
2197
- rule.figure ? /* @__PURE__ */ jsxs19(
2198
- "span",
2199
- {
2200
- className: cn12(
2201
- "shrink-0 tabular-nums",
2202
- look.rules === "result" ? "font-semibold" : secondaryText(look)
2203
- ),
2204
- style: look.rules === "result" ? { color: rule.color } : void 0,
2205
- children: [
2206
- rule.figure,
2207
- look.rules === "result" && rule.limit ? /* @__PURE__ */ jsx22("span", { className: "ml-1 font-normal opacity-60", children: rule.limit }) : null
2208
- ]
2209
- }
2210
- ) : null
2211
- ]
2212
- },
2213
- rule.key
2214
- )) }) : /* @__PURE__ */ jsx22(None, { look });
2215
- var Explained = ({ note, children }) => note ? /* @__PURE__ */ jsx22(Tooltip, { tip: note, side: "left", children: /* @__PURE__ */ jsx22(
2216
- "span",
2217
- {
2218
- tabIndex: 0,
2219
- className: "min-w-0 flex-1 rounded outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2220
- children
2221
- }
2222
- ) }) : children;
2223
- var None = ({ look }) => /* @__PURE__ */ jsx22("p", { className: cn12("text-sm", secondaryText(look)), children: "None" });
2224
- var MeasurementSections = ({ measurements, look, folds }) => {
2225
- if (measurements.status !== "ready") {
2226
- return /* @__PURE__ */ jsx22(Fragment10, { children: /* @__PURE__ */ jsx22(FoldSection, { id: "measurements", title: "Measurements", folds, children: measurements.status === "loading" ? /* @__PURE__ */ jsx22(Reading, { children: "Reading the feature\u2019s datasheet\u2026" }) : /* @__PURE__ */ jsx22(ReadFailure, { failure: measurements }) }) });
2227
- }
2228
- const measured = measurements.value.filter((row) => !row.milling);
2229
- const milling = measurements.value.filter((row) => row.milling);
2230
- return /* @__PURE__ */ jsxs19(Fragment10, { children: [
2231
- /* @__PURE__ */ jsx22(FoldSection, { id: "measurements", title: "Measurements", folds, children: /* @__PURE__ */ jsx22(MeasurementList, { rows: measured, look }) }),
2232
- milling.length > 0 ? /* @__PURE__ */ jsx22(FoldSection, { id: "milling", title: "Milling considerations", folds, children: /* @__PURE__ */ jsx22(MeasurementList, { rows: milling, look }) }) : null
2233
- ] });
2234
- };
2235
- var MeasurementList = ({ rows, look }) => rows.length > 0 ? /* @__PURE__ */ jsx22("dl", { className: "grid grid-cols-[1fr_auto] items-center gap-x-3 gap-y-1 text-sm", children: rows.map((row) => /* @__PURE__ */ jsxs19("div", { className: "contents", children: [
2236
- /* @__PURE__ */ jsxs19("dt", { className: cn12("flex items-center gap-1", secondaryText(look)), children: [
2237
- row.label,
2238
- /* @__PURE__ */ jsx22(Derivation, { lines: row.derivation, look })
2239
- ] }),
2240
- /* @__PURE__ */ jsx22("dd", { className: "text-right text-gray-600 tabular-nums dark:text-zinc-100", title: row.alt, children: row.value })
2241
- ] }, row.key)) }) : /* @__PURE__ */ jsx22(None, { look });
2242
- var Derivation = ({
2243
- lines,
2244
- look
2245
- }) => lines.length > 0 ? /* @__PURE__ */ jsx22(
2246
- Tooltip,
2247
- {
2248
- side: "left",
2249
- hoverable: true,
2250
- tip: /* @__PURE__ */ jsx22("div", { className: "flex max-w-sm flex-col gap-0.5 font-mono text-2xs leading-snug whitespace-pre-wrap", children: lines.map((line, index) => /* @__PURE__ */ jsx22("span", { children: line }, index)) }),
2251
- children: /* @__PURE__ */ jsx22(
2252
- "button",
2253
- {
2254
- type: "button",
2255
- "aria-label": "How this was measured",
2256
- className: cn12(
2257
- "shrink-0 rounded outline-none hover:text-gray-500 focus-visible:ring-2 focus-visible:ring-info/75 dark:hover:text-zinc-300",
2258
- faintText(look)
2259
- ),
2260
- children: /* @__PURE__ */ jsx22(InfoIcon, { weight: "bold", className: "size-3" })
2261
- }
2262
- )
2263
- }
2264
- ) : null;
2265
- var ReachSection2 = ({ record, units, profile, folds, action }) => {
2266
- const curve = useMemo7(
2267
- () => record.status === "ready" ? readReachCurve(record.value?.datasheet) : null,
2268
- [record]
2269
- );
2270
- if (record.status === "ready" && !curve) return null;
2271
- return /* @__PURE__ */ jsx22(FoldSection, { id: "reach", title: "Reach", folds, action: curve ? action : null, children: record.status === "loading" ? /* @__PURE__ */ jsx22(Reading, { children: "Reading the reach curve\u2026" }) : record.status === "error" ? /* @__PURE__ */ jsx22(ReadFailure, { failure: record }) : curve ? /* @__PURE__ */ jsx22(ReachChart, { curve, units, feature: profile }) : null });
2272
- };
2273
-
2274
- // src/feature-panel/candidate-list.tsx
2275
- import { XIcon } from "@phosphor-icons/react";
2276
- import { IconButton as IconButton4, cn as cn13 } from "@toolpath/ui";
2277
- import { jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
2278
- var candidateListKeys = listKeys("candidates");
2279
- var faceLetter = (index) => String.fromCharCode(65 + index % 26);
2280
- var FaceLetter = ({
2281
- index,
2282
- active
2283
- }) => /* @__PURE__ */ jsx23(
2284
- "span",
2285
- {
2286
- className: cn13(
2287
- "inline-flex size-4 shrink-0 items-center justify-center rounded-sm text-2xs font-bold",
2288
- active ? "bg-info text-white" : "bg-gray-100 text-gray-500 dark:bg-zinc-800 dark:text-zinc-300"
2289
- ),
2290
- children: faceLetter(index)
2291
- }
2292
- );
2293
- var CandidateList = ({
2294
- rows,
2295
- selected,
2296
- onSelect,
2297
- onHover,
2298
- face,
2299
- look: lookOption,
2300
- folds
2301
- }) => {
2302
- const look = resolveLook(lookOption);
2303
- const count = `Candidate features (${rows.length})`;
2304
- return /* @__PURE__ */ jsx23("div", { className: "border-b border-gray-100 p-2 dark:border-zinc-800", children: /* @__PURE__ */ jsx23(
2305
- FoldSection,
2306
- {
2307
- id: face ? `candidates-${faceLetter(face.index)}` : "candidates",
2308
- title: face ? /* @__PURE__ */ jsxs20("span", { className: "flex items-center gap-1.5", children: [
2309
- /* @__PURE__ */ jsx23(FaceLetter, { index: face.index, active: face.active }),
2310
- count
2311
- ] }) : count,
2312
- action: face?.onStop ? /* @__PURE__ */ jsx23(
2313
- IconButton4,
2314
- {
2315
- "aria-label": `Stop comparing face ${faceLetter(face.index)}`,
2316
- variant: "muted",
2317
- size: "sm",
2318
- onClick: face.onStop,
2319
- children: /* @__PURE__ */ jsx23(XIcon, { weight: "bold" })
2320
- }
2321
- ) : null,
2322
- folds,
2323
- className: "px-2",
2324
- children: /* @__PURE__ */ jsx23("ul", { className: "-mx-2 flex flex-col gap-0.5", children: rows.map((row) => /* @__PURE__ */ jsx23(
2325
- Row,
2326
- {
2327
- row,
2328
- chosen: row.feature.tag === selected,
2329
- onSelect,
2330
- onHover,
2331
- look
2332
- },
2333
- row.feature.tag
2334
- )) })
2335
- }
2336
- ) });
2337
- };
2338
- var Row = ({ row, chosen, onSelect, onHover, look }) => {
2339
- const { feature } = row;
2340
- const info = look.selected === "info";
2341
- return /* @__PURE__ */ jsxs20(
2342
- "li",
2343
- {
2344
- className: cn13(
2345
- "flex items-center gap-1",
2346
- info && "rounded border pr-1",
2347
- info && (chosen ? "border-info/60 bg-info/15" : "border-transparent")
2348
- ),
2349
- children: [
2350
- /* @__PURE__ */ jsxs20(
2351
- "button",
2352
- {
2353
- type: "button",
2354
- "data-list-item": "candidates",
2355
- "data-follow": true,
2356
- "aria-pressed": chosen,
2357
- onClick: () => onSelect(feature.tag),
2358
- onPointerEnter: onHover ? () => onHover(feature.tag) : void 0,
2359
- onPointerLeave: onHover ? () => onHover(null) : void 0,
2360
- className: cn13(
2361
- "flex min-w-0 flex-1 cursor-pointer flex-col rounded px-2 py-1.5 text-left text-sm",
2362
- "outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2363
- "text-gray-600 dark:text-zinc-200",
2364
- info ? "hover:bg-gray-50 dark:hover:bg-zinc-800/60" : "hover:bg-gray-100 dark:hover:bg-zinc-800",
2365
- chosen && (info ? "font-medium" : "bg-gray-100 font-semibold dark:bg-zinc-800")
2366
- ),
2367
- children: [
2368
- /* @__PURE__ */ jsxs20("span", { className: "flex w-full items-center gap-2", children: [
2369
- row.color ? /* @__PURE__ */ jsx23(Swatch, { color: row.color }) : /* @__PURE__ */ jsx23("span", { "aria-hidden": true, className: "size-4 shrink-0" }),
2370
- /* @__PURE__ */ jsx23("span", { className: "min-w-0 flex-1 truncate", children: /* @__PURE__ */ jsx23(FeatureName, { feature, label: row.label, look }) }),
2371
- row.ruleCount ? /* @__PURE__ */ jsxs20("span", { className: cn13("text-2xs tabular-nums", secondaryText(look)), children: [
2372
- row.ruleCount,
2373
- " ",
2374
- row.ruleCount === 1 ? "rule" : "rules"
2375
- ] }) : null,
2376
- row.required ? /* @__PURE__ */ jsx23(RequiredBadge, {}) : null,
2377
- row.detail ? /* @__PURE__ */ jsx23("span", { className: cn13("max-w-32 shrink-0 truncate text-xs", secondaryText(look)), children: row.detail }) : null
2378
- ] }),
2379
- row.note ? /* @__PURE__ */ jsx23("span", { className: "pl-6 text-2xs text-warning-darken", children: row.note }) : null
2380
- ]
2381
- }
2382
- ),
2383
- row.action
2384
- ]
2385
- }
2386
- );
2387
- };
2388
-
2389
- // src/feature-panel/comparison-table.tsx
2390
- import { useMemo as useMemo8 } from "react";
2391
- import { cn as cn14 } from "@toolpath/ui";
2392
- import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
2393
- var DASH = "\u2014";
2394
- var ComparisonTable = ({
2395
- faces,
2396
- active,
2397
- onSelect,
2398
- onHover,
2399
- look: lookOption,
2400
- folds
2401
- }) => {
2402
- const look = resolveLook(lookOption);
2403
- const rows = useMemo8(() => {
2404
- const row = (key, label, read) => {
2405
- const values = faces.map((face) => face.feature ? read(face, face.feature) : null);
2406
- const texts = values.map((value) => value?.text ?? DASH);
2407
- return {
2408
- key,
2409
- label,
2410
- cells: values.map((value) => value?.node ?? value?.text ?? DASH),
2411
- differs: new Set(texts).size > 1
2412
- };
2413
- };
2414
- const fixed = [
2415
- row("type", "Type", (_, feature) => ({ text: featureTypeLabel(feature.featureType) })),
2416
- row("direction", "Machined from", (face, feature) => {
2417
- const text = directionLabel(feature.machiningDirection);
2418
- return {
2419
- text,
2420
- node: /* @__PURE__ */ jsxs21("span", { className: "inline-flex items-center gap-1", children: [
2421
- /* @__PURE__ */ jsx24(Swatch, { color: face.directionColor, className: "size-2.5" }),
2422
- text
2423
- ] })
2424
- };
2425
- }),
2426
- ...faces.some((face) => face.required !== void 0) ? [row("required", "Required", (face) => ({ text: face.required ? "Yes" : "No" }))] : [],
2427
- row("rules", "Rules broken", (face) => ({
2428
- text: face.rules.map((rule) => rule.key).join(",") || "None",
2429
- node: face.rules.length > 0 ? /* @__PURE__ */ jsxs21(
2430
- "span",
2431
- {
2432
- className: "inline-flex items-center gap-0.5",
2433
- title: face.rules.map((rule) => rule.text).join("\n"),
2434
- children: [
2435
- face.rules.map((rule) => /* @__PURE__ */ jsx24(Swatch, { color: rule.color, className: "size-2.5" }, rule.key)),
2436
- /* @__PURE__ */ jsx24("span", { className: "ml-0.5", children: face.rules.length })
2437
- ]
2438
- }
2439
- ) : "None"
2440
- }))
2441
- ];
2442
- const labels = /* @__PURE__ */ new Map();
2443
- for (const face of faces)
2444
- for (const each of face.measurements) labels.set(each.key, labels.get(each.key) ?? each.label);
2445
- const measured = [...labels].map(([key, label]) => {
2446
- const values = faces.map((face) => face.measurements.find((each) => each.key === key));
2447
- return {
2448
- key,
2449
- label,
2450
- cells: values.map((value) => value ? /* @__PURE__ */ jsx24("span", { title: value.alt, children: value.value }) : DASH),
2451
- differs: new Set(values.map((value) => value?.value ?? DASH)).size > 1
2452
- };
2453
- });
2454
- return [...fixed, ...measured];
2455
- }, [faces]);
2456
- return /* @__PURE__ */ jsx24("div", { className: "border-b border-gray-100 p-4 dark:border-zinc-800", children: /* @__PURE__ */ jsx24(FoldSection, { id: "comparison", title: "Comparison", folds, children: /* @__PURE__ */ jsx24("div", { className: "-mx-1 overflow-x-auto", children: /* @__PURE__ */ jsxs21("table", { className: "w-full border-separate border-spacing-x-1 border-spacing-y-0.5 text-sm", children: [
2457
- /* @__PURE__ */ jsx24("thead", { children: /* @__PURE__ */ jsxs21("tr", { children: [
2458
- /* @__PURE__ */ jsx24("th", { className: "w-0" }),
2459
- faces.map(({ feature }, index) => /* @__PURE__ */ jsx24("th", { className: "text-left align-bottom font-normal", children: /* @__PURE__ */ jsxs21(
2460
- "button",
2461
- {
2462
- type: "button",
2463
- disabled: !feature,
2464
- onClick: () => feature && onSelect(feature.tag, index),
2465
- onPointerEnter: onHover ? () => onHover(feature?.tag ?? null) : void 0,
2466
- onPointerLeave: onHover ? () => onHover(null) : void 0,
2467
- className: cn14(
2468
- "flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded px-1 py-1 text-left text-xs",
2469
- "outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2470
- "text-gray-600 hover:bg-gray-100 dark:text-zinc-200 dark:hover:bg-zinc-800",
2471
- index === active && "bg-gray-100 font-semibold dark:bg-zinc-800"
2472
- ),
2473
- children: [
2474
- /* @__PURE__ */ jsx24(FaceLetter, { index, active: index === active }),
2475
- /* @__PURE__ */ jsx24("span", { className: "min-w-0 truncate", children: feature ? /* @__PURE__ */ jsx24(FeatureName, { feature, look }) : "No feature" })
2476
- ]
2477
- }
2478
- ) }, index))
2479
- ] }) }),
2480
- /* @__PURE__ */ jsx24("tbody", { children: rows.map((row) => /* @__PURE__ */ jsxs21("tr", { "data-differs": row.differs || void 0, children: [
2481
- /* @__PURE__ */ jsx24(
2482
- "th",
2483
- {
2484
- scope: "row",
2485
- className: cn14(
2486
- "pr-2 text-left align-top font-normal whitespace-nowrap",
2487
- secondaryText(look)
2488
- ),
2489
- children: row.label
2490
- }
2491
- ),
2492
- row.cells.map((cell, index) => /* @__PURE__ */ jsx24(
2493
- "td",
2494
- {
2495
- className: cn14(
2496
- "px-1 align-top tabular-nums whitespace-nowrap",
2497
- row.differs ? "text-gray-600 dark:text-zinc-100" : "text-gray-400 dark:text-zinc-500"
2498
- ),
2499
- children: cell
2500
- },
2501
- index
2502
- ))
2503
- ] }, row.key)) })
2504
- ] }) }) }) });
2505
- };
2506
-
2507
- // src/feature-panel/feature-panel.tsx
2508
- import { CubeIcon, XIcon as XIcon2 } from "@phosphor-icons/react";
2509
- import { IconButton as IconButton5, ScrollArea as ScrollArea2, cn as cn15 } from "@toolpath/ui";
2510
- import { Fragment as Fragment11, jsx as jsx25, jsxs as jsxs22 } from "react/jsx-runtime";
2511
- var title = (faces, active, group) => {
2512
- if (faces.length > 1) return `Comparing ${faces.length} faces`;
2513
- const clicked = faces[active]?.region !== null && faces[active]?.region !== void 0;
2514
- if (!clicked && group !== void 0) return `${group} identical holes`;
2515
- return "Feature details";
2516
- };
2517
- var FeaturePanel = ({
2518
- faces,
2519
- active,
2520
- group,
2521
- comparison,
2522
- onSelect,
2523
- onDropFace,
2524
- onHover,
2525
- onClose,
2526
- headerProps,
2527
- className,
2528
- look: lookOption,
2529
- folds,
2530
- children
2531
- }) => {
2532
- const look = resolveLook(lookOption);
2533
- const comparing = faces.length > 1;
2534
- const face = faces[active];
2535
- const heading = title(faces, active, group);
2536
- return /* @__PURE__ */ jsxs22(
2537
- "section",
2538
- {
2539
- "aria-label": heading,
2540
- "data-list-root": true,
2541
- onKeyDown: (event) => {
2542
- if (event.defaultPrevented || !event.currentTarget.contains(event.target)) return;
2543
- if (event.key === "Escape") onClose();
2544
- else candidateListKeys(event);
2545
- },
2546
- className: cn15(
2547
- "flex h-full min-h-0 flex-col overflow-hidden rounded bg-white/90 shadow-lg ring-1 ring-gray-200/70 backdrop-blur-md",
2548
- "dark:bg-zinc-900/90 dark:ring-zinc-800",
2549
- className
2550
- ),
2551
- children: [
2552
- /* @__PURE__ */ jsxs22(
2553
- "header",
2554
- {
2555
- ...headerProps,
2556
- className: cn15(
2557
- "flex items-center gap-2 border-b border-gray-100 py-2 pr-2 pl-4 select-none dark:border-zinc-800",
2558
- headerProps?.className
2559
- ),
2560
- children: [
2561
- /* @__PURE__ */ jsx25(CubeIcon, { weight: "bold", className: cn15("size-5 shrink-0", secondaryText(look)) }),
2562
- /* @__PURE__ */ jsx25("h2", { className: "flex-1 text-sm font-semibold text-gray-600 dark:text-zinc-100", children: heading }),
2563
- /* @__PURE__ */ jsx25(IconButton5, { "aria-label": "Close", variant: "muted", onClick: onClose, children: /* @__PURE__ */ jsx25(XIcon2, { weight: "bold" }) })
2564
- ]
2565
- }
2566
- ),
2567
- /* @__PURE__ */ jsxs22(ScrollArea2, { className: "min-h-0 flex-1", children: [
2568
- comparing ? /* @__PURE__ */ jsxs22(Fragment11, { children: [
2569
- faces.map((each, index) => /* @__PURE__ */ jsx25(
2570
- CandidateList,
2571
- {
2572
- rows: each.rows,
2573
- selected: each.selected,
2574
- onSelect: (tag) => onSelect(tag, index),
2575
- onHover,
2576
- face: {
2577
- index,
2578
- active: index === active,
2579
- ...onDropFace ? { onStop: () => onDropFace(index) } : {}
2580
- },
2581
- look: lookOption,
2582
- folds
2583
- },
2584
- `${each.region ?? "list"}-${index}`
2585
- )),
2586
- comparison ? /* @__PURE__ */ jsx25(
2587
- ComparisonTable,
2588
- {
2589
- faces: comparison,
2590
- active,
2591
- onSelect,
2592
- onHover,
2593
- look: lookOption,
2594
- folds
2595
- }
2596
- ) : null
2597
- ] }) : face && face.region !== null ? /* @__PURE__ */ jsx25(
2598
- CandidateList,
2599
- {
2600
- rows: face.rows,
2601
- selected: face.selected,
2602
- onSelect: (tag) => onSelect(tag, active),
2603
- onHover,
2604
- look: lookOption,
2605
- folds
2606
- }
2607
- ) : null,
2608
- face?.selected ? children : /* @__PURE__ */ jsx25("p", { className: cn15("px-4 py-3 text-sm", secondaryText(look)), children: face && face.rows.length > 0 ? "Choose a feature to read its details." : "No feature owns this face." })
2609
- ] })
2610
- ]
2611
- }
2612
- );
2613
- };
64
+ withSubject,
65
+ withSurroundedFaces
66
+ } from "./chunk-XHYPKN4E.js";
2614
67
  export {
2615
- CandidateList,
2616
- ComparisonTable,
2617
- FeatureDetails,
2618
- FeaturePanel,
2619
- ReachChart,
2620
- ReachDrawing,
2621
- RuleList,
2622
- Swatch,
2623
- candidateListKeys,
2624
- isField,
2625
- listKeys,
2626
- ruleListKeys,
2627
- storageFolds
68
+ CRITICAL_COLOR,
69
+ FILE_KIND,
70
+ MAX_DATASHEET_FIELDS,
71
+ MILLING_RULES,
72
+ OPS,
73
+ PINCH_TOOL_MM,
74
+ PRESENCE_METRIC,
75
+ QUICK_SUBJECTS,
76
+ RANGE_HIGHS,
77
+ RANGE_LOWS,
78
+ RULE_COLORS,
79
+ TURNING_RULES,
80
+ WARNING_COLOR,
81
+ brokenRules,
82
+ checkPart,
83
+ checkTurnedPart,
84
+ compareColors,
85
+ datasheetFields,
86
+ describeRule,
87
+ dfmFeatures,
88
+ directionLabel,
89
+ featureColor,
90
+ featureMeasurements,
91
+ featureProfile,
92
+ featureRows,
93
+ featureSheet,
94
+ featureTypeLabel,
95
+ formatMeasured,
96
+ fromField,
97
+ highOf,
98
+ hitFigure,
99
+ isNoReading,
100
+ isSurfaceType,
101
+ lengthValue,
102
+ llmRulePrompt,
103
+ lowOf,
104
+ makeRule,
105
+ maxBallDiameter,
106
+ metricInfo,
107
+ metricsForSubject,
108
+ opSymbol,
109
+ parseRuleSetFile,
110
+ parseRules,
111
+ peakHeight,
112
+ pinchLabel,
113
+ pinchMark,
114
+ placePinchTool,
115
+ quantityUnit,
116
+ readPinch,
117
+ readReachCurve,
118
+ runStart,
119
+ sameDepth,
120
+ serializeRuleSet,
121
+ subjectLabel,
122
+ toField,
123
+ trimmedCurve,
124
+ turnedStretches,
125
+ turnedTypeLabel,
126
+ unitLabel,
127
+ upperOf,
128
+ withMetric,
129
+ withOp,
130
+ withSubject,
131
+ withSurroundedFaces
2628
132
  };