@toolpath/dfm 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1628 @@
1
+ import {
2
+ FEATURE_SUBJECTS,
3
+ OPS,
4
+ PRESENCE_METRIC,
5
+ QUICK_METRICS,
6
+ QUICK_SUBJECTS,
7
+ RANGE_HIGHS,
8
+ RANGE_LOWS,
9
+ RULE_COLORS,
10
+ featureRows,
11
+ featureTypeLabel,
12
+ formatMeasured,
13
+ fromField,
14
+ highOf,
15
+ hitFigure,
16
+ lengthValue,
17
+ llmRulePrompt,
18
+ lowOf,
19
+ makeRule,
20
+ metricInfo,
21
+ metricsForSubject,
22
+ opSymbol,
23
+ parseRuleSetFile,
24
+ peakHeight,
25
+ quantityUnit,
26
+ runStart,
27
+ serializeRuleSet,
28
+ subjectLabel,
29
+ toField,
30
+ trimmedCurve,
31
+ unitLabel,
32
+ upperOf,
33
+ withMetric,
34
+ withOp,
35
+ withSubject
36
+ } from "./chunk-5MPHT7BC.js";
37
+
38
+ // src/rule-list/rule-list.tsx
39
+ import { useMemo as useMemo2, useState as useState7 } from "react";
40
+ import { ScrollArea } from "@toolpath/ui";
41
+
42
+ // src/rule-list/list-keys.ts
43
+ var listKeys = (name, orientation = "vertical") => (event) => {
44
+ const [back, forward] = orientation === "vertical" ? ["ArrowUp", "ArrowDown"] : ["ArrowLeft", "ArrowRight"];
45
+ if (![back, forward, "Home", "End"].includes(event.key) || isField(event.target)) return;
46
+ const items = [
47
+ ...event.currentTarget.querySelectorAll(`[data-list-item="${name}"]`)
48
+ ];
49
+ if (items.length === 0) return;
50
+ const at = currentIndex(items, document.activeElement);
51
+ const chosen = items.findIndex((item) => item.getAttribute("aria-pressed") === "true");
52
+ const step = (from) => Math.min(Math.max(from + (event.key === forward ? 1 : -1), 0), items.length - 1);
53
+ 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;
54
+ const target = items[index];
55
+ if (!target) return;
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+ if (target === document.activeElement) return;
59
+ target.focus();
60
+ if (target.dataset.follow !== void 0) target.click();
61
+ };
62
+ var currentIndex = (items, active) => {
63
+ if (!active) return -1;
64
+ const exact = items.indexOf(active);
65
+ if (exact !== -1) return exact;
66
+ const row = active.closest("li");
67
+ return row ? items.findIndex((item) => item.closest("li") === row) : -1;
68
+ };
69
+ var isField = (target) => target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement && target.type !== "color" && target.type !== "button" || target instanceof HTMLElement && target.isContentEditable;
70
+
71
+ // src/rule-list/new-rule.tsx
72
+ import { useState } from "react";
73
+ import { cn as cn2 } from "@toolpath/ui";
74
+
75
+ // src/rule-list/options.tsx
76
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
77
+ var MORE = "__more";
78
+ var showMore = (select, setAll) => {
79
+ setAll(true);
80
+ requestAnimationFrame(() => {
81
+ try {
82
+ select.showPicker();
83
+ } catch {
84
+ select.focus();
85
+ }
86
+ });
87
+ };
88
+ var option = (value, label) => /* @__PURE__ */ jsx("option", { value, children: label }, value);
89
+ var SubjectOptions = ({
90
+ current,
91
+ all
92
+ }) => {
93
+ const quick = [...QUICK_SUBJECTS, ...QUICK_SUBJECTS.includes(current) ? [] : [current]];
94
+ const rest = FEATURE_SUBJECTS.filter((each) => !quick.includes(each.value));
95
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
96
+ quick.map((value) => option(value, subjectLabel(value))),
97
+ all ? /* @__PURE__ */ jsx("optgroup", { label: "More feature types", children: rest.map((each) => option(each.value, each.label)) }) : option(MORE, "More feature types\u2026")
98
+ ] });
99
+ };
100
+ var MetricOptions = ({
101
+ subject,
102
+ current,
103
+ all
104
+ }) => {
105
+ const offered = metricsForSubject(subject);
106
+ const quick = offered.filter((each) => QUICK_METRICS.includes(each.key) || each.key === current);
107
+ quick.sort((a, b) => rank(a.key) - rank(b.key));
108
+ const rest = offered.filter((each) => !quick.includes(each));
109
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
110
+ quick.map((each) => option(each.key, each.label)),
111
+ all ? grouped(rest).map(([group, metrics]) => /* @__PURE__ */ jsx("optgroup", { label: group, children: metrics.map((each) => option(each.key, each.label)) }, group)) : rest.length > 0 ? option(MORE, "More measures\u2026") : null
112
+ ] });
113
+ };
114
+ var rank = (key) => {
115
+ const at = QUICK_METRICS.indexOf(key);
116
+ return at === -1 ? QUICK_METRICS.length : at;
117
+ };
118
+ var grouped = (items) => {
119
+ const groups = /* @__PURE__ */ new Map();
120
+ for (const item of items) groups.set(item.group, [...groups.get(item.group) ?? [], item]);
121
+ return [...groups];
122
+ };
123
+
124
+ // src/rule-list/styles.ts
125
+ import { cn } from "@toolpath/ui";
126
+ var FOCUS_RING = "outline-none focus-visible:ring-2 focus-visible:ring-info/75";
127
+ var WORD = cn(
128
+ "cursor-pointer appearance-none rounded bg-transparent px-0.5 font-medium text-gray-700 field-sizing-content",
129
+ "underline decoration-gray-300 decoration-dotted underline-offset-4 hover:bg-gray-100",
130
+ "dark:text-zinc-100 dark:decoration-zinc-600 dark:hover:bg-zinc-700",
131
+ "outline-none focus-visible:bg-gray-100 focus-visible:ring-2 focus-visible:ring-info/75 dark:focus-visible:bg-zinc-700"
132
+ );
133
+
134
+ // src/rule-list/new-rule.tsx
135
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
136
+ var NewRule = ({ onAdd }) => {
137
+ const [all, setAll] = useState(false);
138
+ return /* @__PURE__ */ jsxs2("li", { className: "flex items-start gap-1.5 py-1 pr-1.5", children: [
139
+ /* @__PURE__ */ jsx2("span", { "aria-hidden": true, className: "size-7 shrink-0" }),
140
+ /* @__PURE__ */ jsx2(
141
+ "span",
142
+ {
143
+ "aria-hidden": true,
144
+ className: "mt-1.5 size-4 shrink-0 rounded-full border border-dashed border-gray-300 dark:border-zinc-600"
145
+ }
146
+ ),
147
+ /* @__PURE__ */ jsx2("p", { className: "min-w-0 flex-1 text-sm leading-7", children: /* @__PURE__ */ jsxs2(
148
+ "select",
149
+ {
150
+ "aria-label": "New rule",
151
+ className: cn2(WORD, "font-normal text-gray-400 dark:text-zinc-500"),
152
+ value: "",
153
+ onChange: (event) => {
154
+ if (event.target.value === MORE) showMore(event.currentTarget, setAll);
155
+ else if (event.target.value) onAdd(event.target.value);
156
+ },
157
+ children: [
158
+ /* @__PURE__ */ jsx2("option", { value: "", disabled: true, children: "+ New Rule" }),
159
+ /* @__PURE__ */ jsx2(SubjectOptions, { current: "any", all })
160
+ ]
161
+ }
162
+ ) })
163
+ ] });
164
+ };
165
+
166
+ // src/rule-list/rule-row.tsx
167
+ import { useRef as useRef2, useState as useState5 } from "react";
168
+ import { CaretRightIcon as CaretRightIcon2, TrashIcon } from "@phosphor-icons/react";
169
+ import { IconButton, cn as cn7 } from "@toolpath/ui";
170
+
171
+ // src/rule-list/feature-rows.tsx
172
+ import { useMemo } from "react";
173
+
174
+ // src/rule-list/direction-label.ts
175
+ var EPSILON = 1e-6;
176
+ var trim = (value) => Number.parseFloat(value.toFixed(3)).toString();
177
+ var directionLabel = (direction) => {
178
+ const axes = [
179
+ ["X", direction.x],
180
+ ["Y", direction.y],
181
+ ["Z", direction.z]
182
+ ];
183
+ const nonZero = axes.filter(([, value]) => Math.abs(value) > EPSILON);
184
+ const [axis] = nonZero;
185
+ if (nonZero.length === 1 && axis && Math.abs(Math.abs(axis[1]) - 1) < EPSILON) {
186
+ return `${axis[1] > 0 ? "+" : "\u2212"}${axis[0]}`;
187
+ }
188
+ return axes.map(([, value]) => trim(value)).join(", ");
189
+ };
190
+
191
+ // src/rule-list/feature-button.tsx
192
+ import { cn as cn3 } from "@toolpath/ui";
193
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
194
+ var FEATURE_ROW = cn3(
195
+ "flex w-full cursor-pointer items-baseline gap-2 rounded px-2 py-1 text-left text-xs",
196
+ "text-gray-500 hover:bg-gray-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
197
+ "aria-pressed:bg-gray-100 aria-pressed:font-semibold aria-pressed:text-gray-700",
198
+ "dark:aria-pressed:bg-zinc-800 dark:aria-pressed:text-zinc-100",
199
+ FOCUS_RING
200
+ );
201
+ var ruleHit = (check, rule, tag) => check.hitsByTag.get(tag.toLowerCase())?.find((each) => each.rule.id === rule.id);
202
+ var HitFigure = ({
203
+ hit,
204
+ units
205
+ }) => hit ? /* @__PURE__ */ jsx3("span", { className: "shrink-0 text-gray-400 tabular-nums dark:text-zinc-400", children: hitFigure(hit, units) }) : null;
206
+ var FeatureButton = ({
207
+ tag,
208
+ featureOf,
209
+ rule,
210
+ check,
211
+ units,
212
+ selected,
213
+ selectedGroup,
214
+ onInspect,
215
+ onHover,
216
+ onBack
217
+ }) => {
218
+ const feature = featureOf.get(tag);
219
+ return /* @__PURE__ */ jsxs3(
220
+ "button",
221
+ {
222
+ type: "button",
223
+ "data-list-item": "rules",
224
+ "data-follow": true,
225
+ "data-feature-row": true,
226
+ "aria-pressed": selected === tag && !selectedGroup,
227
+ onClick: () => onInspect(tag),
228
+ onKeyDown: (event) => {
229
+ if (event.key !== "ArrowLeft") return;
230
+ event.preventDefault();
231
+ onBack();
232
+ },
233
+ onPointerEnter: () => onHover(tag),
234
+ onPointerLeave: () => onHover(null),
235
+ className: FEATURE_ROW,
236
+ children: [
237
+ /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1 truncate", children: [
238
+ featureTypeLabel(feature?.featureType ?? "feature"),
239
+ " ",
240
+ /* @__PURE__ */ jsx3("span", { className: "font-normal text-gray-300 dark:text-zinc-500", children: feature ? `from ${directionLabel(feature.machiningDirection)}` : "" })
241
+ ] }),
242
+ /* @__PURE__ */ jsx3(HitFigure, { hit: ruleHit(check, rule, tag), units })
243
+ ]
244
+ }
245
+ );
246
+ };
247
+
248
+ // src/rule-list/hole-group-row.tsx
249
+ import { useRef, useState as useState2 } from "react";
250
+ import { CaretRightIcon } from "@phosphor-icons/react";
251
+ import { cn as cn4 } from "@toolpath/ui";
252
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
253
+ var HoleGroupRow = ({
254
+ row,
255
+ direction,
256
+ ...context
257
+ }) => {
258
+ const [open, setOpen] = useState2(false);
259
+ const groupRef = useRef(null);
260
+ const { rule, check, units, selectedGroup, featureOf, onInspect, onHover, onBack } = context;
261
+ const [first] = row.tags;
262
+ const chosen = selectedGroup?.[0] === first;
263
+ return /* @__PURE__ */ jsxs4("li", { children: [
264
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center", children: [
265
+ /* @__PURE__ */ jsx4(
266
+ "button",
267
+ {
268
+ type: "button",
269
+ "aria-label": open ? "Hide holes" : "Show holes",
270
+ "aria-expanded": open,
271
+ tabIndex: -1,
272
+ onClick: () => setOpen((was) => !was),
273
+ className: "-ml-5 flex size-5 shrink-0 cursor-pointer items-center justify-center text-gray-300 dark:text-zinc-500",
274
+ children: /* @__PURE__ */ jsx4(
275
+ CaretRightIcon,
276
+ {
277
+ weight: "bold",
278
+ className: cn4("size-3 transition-transform", open && "rotate-90")
279
+ }
280
+ )
281
+ }
282
+ ),
283
+ /* @__PURE__ */ jsxs4(
284
+ "button",
285
+ {
286
+ ref: groupRef,
287
+ type: "button",
288
+ "data-list-item": "rules",
289
+ "data-follow": true,
290
+ "data-feature-row": true,
291
+ "aria-pressed": chosen,
292
+ onClick: () => onInspect(first, row.tags),
293
+ onKeyDown: (event) => {
294
+ if (event.key === "ArrowRight" && !open) {
295
+ event.preventDefault();
296
+ setOpen(true);
297
+ } else if (event.key === "ArrowLeft") {
298
+ event.preventDefault();
299
+ if (open) setOpen(false);
300
+ else onBack();
301
+ }
302
+ },
303
+ onPointerEnter: () => onHover(row.tags),
304
+ onPointerLeave: () => onHover(null),
305
+ className: FEATURE_ROW,
306
+ children: [
307
+ /* @__PURE__ */ jsxs4("span", { className: "min-w-0 flex-1 truncate", children: [
308
+ featureTypeLabel(featureOf.get(first)?.featureType ?? "hole"),
309
+ " \u2300",
310
+ formatMeasured("length", row.diameter, units),
311
+ " ",
312
+ /* @__PURE__ */ jsxs4("span", { className: "font-normal text-gray-400 dark:text-zinc-400", children: [
313
+ "\xD7",
314
+ row.tags.length,
315
+ " \xB7 from ",
316
+ direction
317
+ ] })
318
+ ] }),
319
+ /* @__PURE__ */ jsx4(HitFigure, { hit: ruleHit(check, rule, first), units })
320
+ ]
321
+ }
322
+ )
323
+ ] }),
324
+ 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
325
+ ] });
326
+ };
327
+
328
+ // src/rule-list/feature-rows.tsx
329
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
330
+ var FeatureRows = ({
331
+ tags,
332
+ otherTags,
333
+ features,
334
+ sheets,
335
+ ...rest
336
+ }) => {
337
+ const featureOf = useMemo(
338
+ () => new Map(features.map((feature) => [feature.tag, feature])),
339
+ [features]
340
+ );
341
+ const required = useMemo(() => featureRows(tags, features, sheets), [tags, features, sheets]);
342
+ const others = useMemo(
343
+ () => featureRows(otherTags, features, sheets),
344
+ [otherTags, features, sheets]
345
+ );
346
+ const context = { ...rest, featureOf };
347
+ const list = (rows) => rows.map(
348
+ (row) => row.kind === "holes" ? /* @__PURE__ */ jsx5(
349
+ HoleGroupRow,
350
+ {
351
+ row,
352
+ direction: directionOf(featureOf.get(row.key)),
353
+ ...context
354
+ },
355
+ row.key
356
+ ) : /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(FeatureButton, { tag: row.tag, ...context }) }, row.tag)
357
+ );
358
+ return /* @__PURE__ */ jsxs5("ul", { className: "pb-1 pl-12", children: [
359
+ list(required),
360
+ otherTags.length > 0 ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
361
+ /* @__PURE__ */ jsx5(
362
+ "li",
363
+ {
364
+ className: "pt-1 pb-0.5 text-2xs font-semibold text-gray-400 uppercase dark:text-zinc-500",
365
+ title: "Readings of faces another feature also cuts: not counted or painted, since a plan may never cut them this way",
366
+ children: "Other readings, not required"
367
+ }
368
+ ),
369
+ /* @__PURE__ */ jsx5("div", { className: "opacity-70", children: list(others) })
370
+ ] }) : null
371
+ ] });
372
+ };
373
+ var directionOf = (feature) => feature ? directionLabel(feature.machiningDirection) : "";
374
+
375
+ // src/rule-list/rule-sentence.tsx
376
+ import { useState as useState4 } from "react";
377
+
378
+ // src/rule-list/ops.tsx
379
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
380
+ var OP_TITLES = {
381
+ gte: "at least",
382
+ gt: "more than",
383
+ lte: "at most",
384
+ lt: "less than",
385
+ eq: "equal to",
386
+ ne: "not equal to",
387
+ between: "a range, between two numbers"
388
+ };
389
+ var LOW_SYMBOLS = { gte: "\u2264", gt: "<" };
390
+ var ONE_SIDED = "one:";
391
+ var OneSidedOptions = () => /* @__PURE__ */ jsx6("optgroup", { label: "One-sided", children: OPS.filter((op) => op !== "between").map((op) => /* @__PURE__ */ jsxs6("option", { value: `${ONE_SIDED}${op}`, children: [
392
+ opSymbol(op),
393
+ " ",
394
+ OP_TITLES[op]
395
+ ] }, op)) });
396
+ var rangeEnd = (rule, chosen, hold) => chosen.startsWith(ONE_SIDED) ? withOp(rule, chosen.slice(ONE_SIDED.length)) : { ...rule, ...hold(chosen) };
397
+
398
+ // src/rule-list/comparison.tsx
399
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
400
+ var Comparison = ({ rule, ranged, onChange }) => ranged ? /* @__PURE__ */ jsxs7(
401
+ "select",
402
+ {
403
+ "aria-label": "High end",
404
+ title: OP_TITLES[highOf(rule)],
405
+ className: WORD,
406
+ value: highOf(rule),
407
+ onChange: (event) => onChange(rangeEnd(rule, event.target.value, (op) => ({ highOp: op }))),
408
+ children: [
409
+ /* @__PURE__ */ jsx7("optgroup", { label: "Range", children: RANGE_HIGHS.map((op) => /* @__PURE__ */ jsx7("option", { value: op, children: opSymbol(op) }, op)) }),
410
+ /* @__PURE__ */ jsx7(OneSidedOptions, {})
411
+ ]
412
+ }
413
+ ) : /* @__PURE__ */ jsx7(
414
+ "select",
415
+ {
416
+ "aria-label": "Comparison",
417
+ title: OP_TITLES[rule.op],
418
+ className: WORD,
419
+ value: rule.op,
420
+ onChange: (event) => onChange(withOp(rule, event.target.value)),
421
+ children: OPS.map((op) => /* @__PURE__ */ jsx7("option", { value: op, children: opSymbol(op) }, op))
422
+ }
423
+ );
424
+
425
+ // src/rule-list/value-field.tsx
426
+ import { useEffect, useState as useState3 } from "react";
427
+ import { cn as cn5 } from "@toolpath/ui";
428
+ import { jsx as jsx8 } from "react/jsx-runtime";
429
+ var ValueField = ({
430
+ label,
431
+ info,
432
+ value,
433
+ units,
434
+ onChange
435
+ }) => {
436
+ const shown = toField(info.quantity, value, units);
437
+ const [text, setText] = useState3(shown);
438
+ const [typing, setTyping] = useState3(false);
439
+ useEffect(() => {
440
+ if (!typing) setText(shown);
441
+ }, [shown, typing]);
442
+ const invalid = fromField(info, text, units) === null;
443
+ return /* @__PURE__ */ jsx8(
444
+ "input",
445
+ {
446
+ "aria-label": label,
447
+ "aria-invalid": invalid,
448
+ inputMode: "decimal",
449
+ className: cn5(
450
+ WORD,
451
+ "min-w-[2ch] cursor-text tabular-nums",
452
+ invalid && "text-danger decoration-danger"
453
+ ),
454
+ value: text,
455
+ onFocus: () => setTyping(true),
456
+ onBlur: () => setTyping(false),
457
+ onChange: (event) => {
458
+ setText(event.target.value);
459
+ const typed = fromField(info, event.target.value, units);
460
+ if (typed !== null) onChange(typed);
461
+ },
462
+ onKeyDown: (event) => {
463
+ if (event.key === "Enter") event.currentTarget.blur();
464
+ }
465
+ }
466
+ );
467
+ };
468
+
469
+ // src/rule-list/rule-sentence.tsx
470
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
471
+ var RuleSentence = ({
472
+ rule,
473
+ units,
474
+ onChange,
475
+ autoFocus
476
+ }) => {
477
+ const info = metricInfo(rule.metric);
478
+ const flag = info?.quantity === "flag" || info?.quantity === "presence";
479
+ const presence = info?.quantity === "presence";
480
+ const ranged = info !== void 0 && !flag && rule.op === "between";
481
+ const unit = info ? quantityUnit(info.quantity, units) : "";
482
+ const [allSubjects, setAllSubjects] = useState4(false);
483
+ const [allMetrics, setAllMetrics] = useState4(false);
484
+ const unitSpan = unit ? /* @__PURE__ */ jsx9("span", { className: unit === "\xB0" || unit === "%" ? "" : "pl-1", children: unit }) : null;
485
+ return /* @__PURE__ */ jsxs8("p", { className: "min-w-0 flex-1 text-sm leading-7 text-gray-500 dark:text-zinc-400", children: [
486
+ /* @__PURE__ */ jsx9(
487
+ "select",
488
+ {
489
+ "aria-label": "Features",
490
+ className: WORD,
491
+ value: rule.subject,
492
+ onChange: (event) => {
493
+ if (event.target.value === MORE) showMore(event.currentTarget, setAllSubjects);
494
+ else onChange(withSubject(rule, event.target.value, units));
495
+ },
496
+ children: /* @__PURE__ */ jsx9(SubjectOptions, { current: rule.subject, all: allSubjects })
497
+ }
498
+ ),
499
+ presence ? ", " : " with ",
500
+ /* @__PURE__ */ jsxs8("span", { className: ranged ? "whitespace-nowrap" : void 0, children: [
501
+ ranged ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
502
+ /* @__PURE__ */ jsxs8("span", { className: "whitespace-nowrap", children: [
503
+ /* @__PURE__ */ jsx9(
504
+ ValueField,
505
+ {
506
+ label: "From",
507
+ info,
508
+ value: rule.value,
509
+ units,
510
+ onChange: (value) => onChange({ ...rule, value })
511
+ }
512
+ ),
513
+ unitSpan
514
+ ] }),
515
+ " ",
516
+ /* @__PURE__ */ jsxs8(
517
+ "select",
518
+ {
519
+ "aria-label": "Low end",
520
+ title: OP_TITLES[lowOf(rule)],
521
+ className: WORD,
522
+ value: lowOf(rule),
523
+ onChange: (event) => onChange(rangeEnd(rule, event.target.value, (op) => ({ lowOp: op }))),
524
+ children: [
525
+ /* @__PURE__ */ jsx9("optgroup", { label: "Range", children: RANGE_LOWS.map((op) => /* @__PURE__ */ jsx9("option", { value: op, children: LOW_SYMBOLS[op] }, op)) }),
526
+ /* @__PURE__ */ jsx9(OneSidedOptions, {})
527
+ ]
528
+ }
529
+ ),
530
+ " "
531
+ ] }) : null,
532
+ /* @__PURE__ */ jsx9(
533
+ "select",
534
+ {
535
+ "aria-label": "Measure",
536
+ title: info?.hint,
537
+ className: WORD,
538
+ value: rule.metric,
539
+ onChange: (event) => {
540
+ if (event.target.value === MORE) showMore(event.currentTarget, setAllMetrics);
541
+ else onChange(withMetric(rule, event.target.value, units));
542
+ },
543
+ autoFocus,
544
+ children: /* @__PURE__ */ jsx9(MetricOptions, { subject: rule.subject, current: rule.metric, all: allMetrics })
545
+ }
546
+ ),
547
+ info && !flag ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
548
+ " ",
549
+ /* @__PURE__ */ jsx9(Comparison, { rule, ranged, onChange }),
550
+ " ",
551
+ /* @__PURE__ */ jsxs8("span", { className: "whitespace-nowrap", children: [
552
+ /* @__PURE__ */ jsx9(
553
+ ValueField,
554
+ {
555
+ label: ranged ? "Up to" : "Value",
556
+ info,
557
+ value: ranged ? upperOf(rule) : rule.value,
558
+ units,
559
+ onChange: (value) => onChange(ranged ? { ...rule, max: value } : { ...rule, value })
560
+ }
561
+ ),
562
+ unitSpan
563
+ ] })
564
+ ] }) : null
565
+ ] })
566
+ ] });
567
+ };
568
+
569
+ // src/rule-list/swatch.tsx
570
+ import { cn as cn6 } from "@toolpath/ui";
571
+ import { jsx as jsx10 } from "react/jsx-runtime";
572
+ var swatchKeys = listKeys("swatches", "horizontal");
573
+ var Swatch = ({
574
+ color,
575
+ label,
576
+ onClick,
577
+ onKeyDown,
578
+ listItem,
579
+ className,
580
+ ref
581
+ }) => {
582
+ const dot = cn6("size-4 shrink-0 rounded-full ring-1 ring-black/10 dark:ring-white/15", className);
583
+ return onClick ? /* @__PURE__ */ jsx10(
584
+ "button",
585
+ {
586
+ ref,
587
+ type: "button",
588
+ "aria-label": label,
589
+ "data-list-item": listItem,
590
+ onClick,
591
+ onKeyDown,
592
+ className: cn6(
593
+ dot,
594
+ "cursor-pointer hover:ring-2 hover:ring-gray-300 dark:hover:ring-zinc-500",
595
+ FOCUS_RING
596
+ ),
597
+ style: { backgroundColor: color }
598
+ }
599
+ ) : /* @__PURE__ */ jsx10("span", { "aria-hidden": true, className: dot, style: { backgroundColor: color } });
600
+ };
601
+ var ColorPicker = ({
602
+ value,
603
+ onChange
604
+ }) => /* @__PURE__ */ jsx10("div", { className: "flex flex-wrap items-center gap-1.5 py-1", onKeyDown: swatchKeys, children: RULE_COLORS.map((each) => /* @__PURE__ */ jsx10(
605
+ "button",
606
+ {
607
+ type: "button",
608
+ "data-list-item": "swatches",
609
+ "aria-label": each.name,
610
+ "aria-pressed": value === each.value,
611
+ title: each.name,
612
+ autoFocus: value === each.value,
613
+ onClick: () => onChange(each.value),
614
+ className: cn6(
615
+ "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",
616
+ value === each.value && "ring-2 ring-gray-600 ring-offset-1 dark:ring-zinc-100 dark:ring-offset-zinc-900"
617
+ ),
618
+ style: { backgroundColor: each.value }
619
+ },
620
+ each.value
621
+ )) });
622
+
623
+ // src/rule-list/rule-row.tsx
624
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
625
+ var isControl = (target) => target instanceof Element && target.closest("button, select, input, a") !== null;
626
+ var RuleRow = ({
627
+ rule,
628
+ result,
629
+ checking,
630
+ open,
631
+ focusOnMount,
632
+ onToggle,
633
+ onChange,
634
+ onRemove,
635
+ check,
636
+ features,
637
+ sheets,
638
+ units,
639
+ ...list
640
+ }) => {
641
+ const [picking, setPicking] = useState5(false);
642
+ const swatchRef = useRef2(null);
643
+ const count = result?.tags.length ?? 0;
644
+ const others = result?.otherTags.length ?? 0;
645
+ const canOpen = count + others > 0;
646
+ const onRowKey = (event) => {
647
+ if (event.key === "ArrowRight" && canOpen) {
648
+ event.preventDefault();
649
+ if (!open) onToggle();
650
+ else
651
+ event.currentTarget.closest("li")?.querySelector("[data-feature-row]")?.focus();
652
+ } else if (event.key === "ArrowLeft" && open) {
653
+ event.preventDefault();
654
+ onToggle();
655
+ }
656
+ };
657
+ return /* @__PURE__ */ jsxs9(
658
+ "li",
659
+ {
660
+ className: cn7(
661
+ "group/rule",
662
+ // Nothing on the part breaks it: dimmed, until pointed at or worked in.
663
+ !checking && count === 0 && "opacity-55 transition-opacity focus-within:opacity-100 hover:opacity-100"
664
+ ),
665
+ children: [
666
+ /* @__PURE__ */ jsxs9(
667
+ "div",
668
+ {
669
+ onClick: (event) => {
670
+ if (canOpen && !isControl(event.target)) onToggle();
671
+ },
672
+ className: cn7(
673
+ "flex items-start gap-1.5 rounded py-1 pr-1.5 hover:bg-gray-50 dark:hover:bg-zinc-800/60",
674
+ canOpen && "cursor-pointer"
675
+ ),
676
+ children: [
677
+ /* @__PURE__ */ jsx11(
678
+ "button",
679
+ {
680
+ type: "button",
681
+ tabIndex: -1,
682
+ "aria-label": open ? "Hide features" : "Show features",
683
+ disabled: !canOpen,
684
+ onClick: onToggle,
685
+ className: cn7(
686
+ "flex size-7 shrink-0 items-center justify-center rounded text-gray-400 dark:text-zinc-400",
687
+ canOpen ? "cursor-pointer hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-zinc-700 dark:hover:text-zinc-100" : "invisible"
688
+ ),
689
+ children: /* @__PURE__ */ jsx11(
690
+ CaretRightIcon2,
691
+ {
692
+ weight: "bold",
693
+ className: cn7("size-4 transition-transform duration-150", open && "rotate-90")
694
+ }
695
+ )
696
+ }
697
+ ),
698
+ /* @__PURE__ */ jsx11(
699
+ Swatch,
700
+ {
701
+ ref: swatchRef,
702
+ color: rule.color,
703
+ label: "Change colour",
704
+ onClick: () => setPicking((was) => !was),
705
+ onKeyDown: onRowKey,
706
+ listItem: "rules",
707
+ className: "mt-1.5"
708
+ }
709
+ ),
710
+ /* @__PURE__ */ jsx11(RuleSentence, { rule, units, onChange, autoFocus: focusOnMount }),
711
+ /* @__PURE__ */ jsxs9("div", { className: "relative mt-1 shrink-0", children: [
712
+ /* @__PURE__ */ jsx11(
713
+ IconButton,
714
+ {
715
+ "aria-label": "Remove rule",
716
+ variant: "muted",
717
+ size: "sm",
718
+ onClick: onRemove,
719
+ className: cn7(
720
+ "absolute top-1/2 right-full mr-1 -translate-y-1/2 bg-white dark:bg-zinc-800",
721
+ "pointer-events-none opacity-0 transition-opacity",
722
+ "group-focus-within/rule:pointer-events-auto group-focus-within/rule:opacity-100",
723
+ "group-hover/rule:pointer-events-auto group-hover/rule:opacity-100"
724
+ ),
725
+ children: /* @__PURE__ */ jsx11(TrashIcon, { weight: "bold" })
726
+ }
727
+ ),
728
+ /* @__PURE__ */ jsx11(
729
+ CountPill,
730
+ {
731
+ count,
732
+ others,
733
+ checking,
734
+ open,
735
+ canOpen,
736
+ onToggle
737
+ }
738
+ )
739
+ ] })
740
+ ]
741
+ }
742
+ ),
743
+ picking ? /* @__PURE__ */ jsx11("div", { className: "pb-1 pl-[34px]", children: /* @__PURE__ */ jsx11(
744
+ ColorPicker,
745
+ {
746
+ value: rule.color,
747
+ onChange: (color) => {
748
+ onChange({ ...rule, color });
749
+ setPicking(false);
750
+ swatchRef.current?.focus();
751
+ }
752
+ }
753
+ ) }) : null,
754
+ open && result && check && features && sheets ? /* @__PURE__ */ jsx11(
755
+ FeatureRows,
756
+ {
757
+ rule,
758
+ tags: result.tags,
759
+ otherTags: result.otherTags,
760
+ check,
761
+ features,
762
+ sheets,
763
+ units,
764
+ onBack: () => swatchRef.current?.focus(),
765
+ ...list
766
+ }
767
+ ) : null
768
+ ]
769
+ }
770
+ );
771
+ };
772
+ var CountPill = ({
773
+ count,
774
+ others,
775
+ checking,
776
+ open,
777
+ canOpen,
778
+ onToggle
779
+ }) => /* @__PURE__ */ jsx11(
780
+ "button",
781
+ {
782
+ type: "button",
783
+ "aria-label": checking ? void 0 : countLabel(count, others),
784
+ title: others > 0 ? `${count} required \xB7 ${others} other ${readings(others)} not counted` : void 0,
785
+ "aria-expanded": canOpen ? open : void 0,
786
+ disabled: !canOpen,
787
+ onClick: onToggle,
788
+ className: cn7(
789
+ "flex h-5 min-w-8 items-center justify-center rounded-full px-1.5 text-xs font-semibold tabular-nums",
790
+ 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",
791
+ FOCUS_RING
792
+ ),
793
+ children: checking ? "\u2026" : count
794
+ }
795
+ );
796
+ var readings = (count) => count === 1 ? "reading" : "readings";
797
+ var countLabel = (count, others) => {
798
+ const required = `${count} required ${count === 1 ? "feature breaks" : "features break"} it`;
799
+ return others > 0 ? `${required}, and ${others} other ${readings(others)}` : required;
800
+ };
801
+
802
+ // src/rule-list/rule-set-actions.tsx
803
+ import {
804
+ useCallback,
805
+ useEffect as useEffect2,
806
+ useRef as useRef3,
807
+ useState as useState6
808
+ } from "react";
809
+ import { CheckIcon, CopyIcon, DownloadSimpleIcon, UploadSimpleIcon } from "@phosphor-icons/react";
810
+ import { Button, cn as cn8 } from "@toolpath/ui";
811
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
812
+ var NOTE_MS = 4e3;
813
+ var COPIED_MS = 2e3;
814
+ var EXPORT_FILE_NAME = "dfm-rules.json";
815
+ var RuleSetActions = ({ rules }) => {
816
+ const [note, setNote] = useTimedValue(NOTE_MS);
817
+ const [copied, setCopied] = useTimedValue(COPIED_MS);
818
+ const fileInput = useRef3(null);
819
+ const importRules = async (file) => {
820
+ if (!file) return;
821
+ let text;
822
+ try {
823
+ text = await file.text();
824
+ } catch {
825
+ setNote("Could not import: the file could not be read.");
826
+ return;
827
+ }
828
+ const read = parseRuleSetFile(text);
829
+ if ("error" in read) {
830
+ setNote(`Could not import: ${read.error}`);
831
+ return;
832
+ }
833
+ rules.replace(read.rules);
834
+ setNote(importedNote(read.rules.length, read.skipped, read.name));
835
+ };
836
+ const copyPrompt = async () => {
837
+ try {
838
+ await navigator.clipboard.writeText(llmRulePrompt(rules.rules));
839
+ } catch {
840
+ setNote("Could not copy the prompt. Allow clipboard access and try again.");
841
+ return;
842
+ }
843
+ setCopied(true);
844
+ setNote("Paste the prompt into an LLM, then import the JSON it writes.");
845
+ };
846
+ return /* @__PURE__ */ jsxs10("div", { className: "flex flex-col gap-2 px-4 pt-2 pb-3", children: [
847
+ /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-center gap-2", children: [
848
+ /* @__PURE__ */ jsx12(Button, { variant: "secondary", onClick: () => fileInput.current?.click(), children: /* @__PURE__ */ jsx12(ActionLabel, { icon: /* @__PURE__ */ jsx12(UploadSimpleIcon, { weight: "bold" }), children: "Import\u2026" }) }),
849
+ /* @__PURE__ */ jsx12(Button, { variant: "secondary", onClick: () => downloadRuleSet(rules), children: /* @__PURE__ */ jsx12(ActionLabel, { icon: /* @__PURE__ */ jsx12(DownloadSimpleIcon, { weight: "bold" }), children: "Export" }) }),
850
+ /* @__PURE__ */ jsx12(
851
+ Button,
852
+ {
853
+ variant: "secondary",
854
+ title: "Copies a prompt that walks an LLM through writing a rule set for you",
855
+ onClick: () => void copyPrompt(),
856
+ children: /* @__PURE__ */ jsxs10("span", { className: "grid justify-items-center *:col-start-1 *:row-start-1", children: [
857
+ /* @__PURE__ */ jsx12(ActionLabel, { hidden: copied !== null, icon: /* @__PURE__ */ jsx12(CopyIcon, { weight: "bold" }), children: "Copy LLM prompt" }),
858
+ /* @__PURE__ */ jsx12(ActionLabel, { hidden: copied === null, icon: /* @__PURE__ */ jsx12(CheckIcon, { weight: "bold" }), children: "Copied" })
859
+ ] })
860
+ }
861
+ ),
862
+ /* @__PURE__ */ jsx12(
863
+ "input",
864
+ {
865
+ ref: fileInput,
866
+ type: "file",
867
+ "aria-label": "Rule set file",
868
+ accept: ".json,application/json",
869
+ className: "hidden",
870
+ onChange: (event) => {
871
+ void importRules(event.currentTarget.files?.[0]);
872
+ event.currentTarget.value = "";
873
+ }
874
+ }
875
+ )
876
+ ] }),
877
+ note ? /* @__PURE__ */ jsx12("p", { className: "text-xs text-gray-500 dark:text-zinc-400", role: "status", children: note }) : null
878
+ ] });
879
+ };
880
+ var ActionLabel = ({ icon, children, hidden = false }) => /* @__PURE__ */ jsxs10(
881
+ "span",
882
+ {
883
+ "aria-hidden": hidden || void 0,
884
+ className: cn8("flex items-center gap-1.5 [&>svg]:size-3.5", hidden && "invisible"),
885
+ children: [
886
+ icon,
887
+ children
888
+ ]
889
+ }
890
+ );
891
+ var useTimedValue = (ms) => {
892
+ const [box, setBox] = useState6(null);
893
+ useEffect2(() => {
894
+ if (box === null) return;
895
+ const timer = setTimeout(() => setBox(null), ms);
896
+ return () => clearTimeout(timer);
897
+ }, [box, ms]);
898
+ const set = useCallback((value) => setBox({ value }), []);
899
+ return [box?.value ?? null, set];
900
+ };
901
+ var downloadRuleSet = (rules) => {
902
+ const url = URL.createObjectURL(
903
+ new Blob([serializeRuleSet(rules.rules)], { type: "application/json" })
904
+ );
905
+ const link = document.createElement("a");
906
+ link.href = url;
907
+ link.download = EXPORT_FILE_NAME;
908
+ link.click();
909
+ URL.revokeObjectURL(url);
910
+ };
911
+ var importedNote = (count, skipped, name) => {
912
+ const rulesRead = `Imported ${count} rule${count === 1 ? "" : "s"}`;
913
+ const named = name ? ` (${name})` : "";
914
+ const skippedSome = skipped > 0 ? `, ${skipped} skipped` : "";
915
+ return `${rulesRead}${named}${skippedSome}`;
916
+ };
917
+
918
+ // src/rule-list/rule-list.tsx
919
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
920
+ var ruleListKeys = listKeys("rules");
921
+ var RuleList = ({
922
+ rules,
923
+ check,
924
+ features,
925
+ sheets,
926
+ units,
927
+ ...list
928
+ }) => {
929
+ const [openIds, setOpenIds] = useState7(/* @__PURE__ */ new Set());
930
+ const [added, setAdded] = useState7(null);
931
+ const toggleOpen = (id) => setOpenIds((was) => {
932
+ const next = new Set(was);
933
+ if (!next.delete(id)) next.add(id);
934
+ return next;
935
+ });
936
+ const byId = useMemo2(
937
+ () => new Map(check?.byRule.map((result) => [result.rule.id, result])),
938
+ [check]
939
+ );
940
+ const found = (rule) => {
941
+ const result = byId.get(rule.id);
942
+ return check === null || rule.id === added || (result?.tags.length ?? 0) + (result?.otherTags.length ?? 0) > 0;
943
+ };
944
+ const active = rules.rules.filter(found);
945
+ const none = rules.rules.filter((rule) => !found(rule));
946
+ const row = (rule) => /* @__PURE__ */ jsx13(
947
+ RuleRow,
948
+ {
949
+ rule,
950
+ result: byId.get(rule.id) ?? null,
951
+ checking: check === null,
952
+ open: openIds.has(rule.id),
953
+ focusOnMount: added === rule.id,
954
+ onToggle: () => toggleOpen(rule.id),
955
+ onChange: rules.update,
956
+ onRemove: () => rules.remove(rule.id),
957
+ check,
958
+ features,
959
+ sheets,
960
+ units,
961
+ ...list
962
+ },
963
+ rule.id
964
+ );
965
+ return (
966
+ // Grows with its rules, and scrolls once the panel can grow no more.
967
+ /* @__PURE__ */ jsxs11(ScrollArea, { className: "min-h-0 flex-auto", children: [
968
+ /* @__PURE__ */ jsxs11("ul", { className: "flex flex-col gap-0.5 pt-2 pr-4 pl-2", children: [
969
+ active.map(row),
970
+ none.map(row),
971
+ /* @__PURE__ */ jsx13(
972
+ NewRule,
973
+ {
974
+ onAdd: (subject) => {
975
+ const rule = makeRule(subject, PRESENCE_METRIC.key, units);
976
+ rules.add(rule);
977
+ setAdded(rule.id);
978
+ }
979
+ }
980
+ )
981
+ ] }),
982
+ /* @__PURE__ */ jsx13(RuleSetActions, { rules })
983
+ ] })
984
+ );
985
+ };
986
+
987
+ // src/reach/reach-chart.tsx
988
+ import { useMemo as useMemo4 } from "react";
989
+
990
+ // src/reach/reach-section.tsx
991
+ import { useId, useMemo as useMemo3, useState as useState8 } from "react";
992
+
993
+ // src/reach/reading.tsx
994
+ import { heightAt } from "@toolpath/tool-support";
995
+
996
+ // src/reach/section-layout.ts
997
+ var SECTION_PAD = { left: 12, right: 64, top: 24, bottom: 62 };
998
+ var SECTION_MAX_H = 380;
999
+ var SECTION_MIN_H = 120;
1000
+ var FEATURE_SHARE = 0.4;
1001
+ var FEATURE_SHARE_MIN = 0.06;
1002
+ var FEATURE_SHARE_MAX = 0.6;
1003
+ var LABEL_GAP_X = 40;
1004
+ var LABEL_GAP_Y = 14;
1005
+ var WALL_LABEL_GAP_X = 30;
1006
+ var READ_CHAR_PX = 6.4;
1007
+ var HEADING_CHAR_PX = 6.5;
1008
+ var READ_INSET = 8;
1009
+ var AXIS_LABEL_HALF_PX = 84;
1010
+ var FLOOR_STRIP = 6;
1011
+ var DEPTH_WORD_PX = 34;
1012
+ var BREAK_STEP = 8;
1013
+ var BREAK_SWAY = 3;
1014
+ var risersOf = (curve) => {
1015
+ const risers = [];
1016
+ let level = 0;
1017
+ curve.heights.forEach((height, index) => {
1018
+ if (height > level) risers.push({ at: runStart(curve, index), from: level, to: height });
1019
+ level = Math.max(level, height);
1020
+ });
1021
+ return risers;
1022
+ };
1023
+ var spaced = (items, position, gap) => {
1024
+ const kept = [];
1025
+ items.forEach((item, index) => {
1026
+ const last = kept[kept.length - 1];
1027
+ const clear = last === void 0 || Math.abs(position(item) - position(last)) >= gap;
1028
+ if (clear) kept.push(item);
1029
+ else if (index === items.length - 1) kept[kept.length - 1] = item;
1030
+ });
1031
+ return kept;
1032
+ };
1033
+ var breakSteps = (top, bottom) => Math.max(2, Math.round((bottom - top) / BREAK_STEP));
1034
+ var featureBreak = (left, top, bottom) => {
1035
+ const steps = breakSteps(top, bottom);
1036
+ let path = `M ${left} ${top}`;
1037
+ for (let step = 1; step <= steps; step += 1) {
1038
+ const sway = step === steps ? 0 : step % 2 === 1 ? BREAK_SWAY : -BREAK_SWAY;
1039
+ path += ` L ${left + sway} ${top + (bottom - top) * step / steps}`;
1040
+ }
1041
+ return path;
1042
+ };
1043
+ var wallBreak = (right, top, bottom) => {
1044
+ const steps = breakSteps(top, bottom);
1045
+ let path = "";
1046
+ for (let step = 1; step < steps; step += 1) {
1047
+ const sway = step % 2 === 1 ? -BREAK_SWAY : BREAK_SWAY;
1048
+ path += ` L ${right + sway} ${top + (bottom - top) * step / steps}`;
1049
+ }
1050
+ return path;
1051
+ };
1052
+
1053
+ // src/reach/styles.ts
1054
+ var LINE = "stroke-gray-500 dark:stroke-zinc-400";
1055
+ var TEXT = "fill-gray-600 dark:fill-zinc-300";
1056
+ var MATERIAL = "fill-neutral-300 stroke-neutral-400 dark:fill-neutral-600 dark:stroke-neutral-500";
1057
+ var FLOOR = "fill-neutral-300 dark:fill-neutral-600";
1058
+ var ARROW = "fill-gray-500 dark:fill-zinc-400";
1059
+ var FEATURE_FILL = "fill-blue-500/15 dark:fill-blue-400/15";
1060
+ var FEATURE_LINE = "stroke-blue-600 dark:stroke-blue-400";
1061
+ var FEATURE_TEXT = "fill-blue-600 dark:fill-blue-400";
1062
+ var READ = "stroke-sky-600 dark:stroke-sky-400";
1063
+ var READ_DOT = "fill-sky-600 dark:fill-sky-400";
1064
+
1065
+ // src/reach/reading.tsx
1066
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1067
+ var readingAt = ({ offsets, heights }, offset) => ({
1068
+ offset,
1069
+ height: heightAt({ horizontalOffset: offsets, verticalOffset: heights }, offset),
1070
+ past: offset > (offsets[offsets.length - 1] ?? 0)
1071
+ });
1072
+ var readingText = ({ offset, height, past }, units) => `${lengthValue(offset, units)} ${unitLabel(units)} \u2192 ${lengthValue(height, units)} ${unitLabel(units)} reach${past ? " \xB7 past the last reading" : ""}`;
1073
+ var offsetUnder = (clientX, box, geometry) => {
1074
+ const at = (clientX - box.left) * (geometry.width / box.width);
1075
+ const offset = (at - SECTION_PAD.left) / geometry.scale + geometry.x0;
1076
+ return offset >= 0 && at <= geometry.right ? offset : null;
1077
+ };
1078
+ var ReadingMark = ({
1079
+ geometry,
1080
+ reading,
1081
+ units
1082
+ }) => {
1083
+ const { x, y, width, floorY } = geometry;
1084
+ const text = readingText(reading, units);
1085
+ const textWidth = text.length * READ_CHAR_PX;
1086
+ const pointerX = x(reading.offset);
1087
+ const onRight = pointerX + READ_INSET + textWidth <= width - READ_INSET;
1088
+ const labelX = onRight ? pointerX + READ_INSET : Math.min(Math.max(pointerX - READ_INSET, READ_INSET + textWidth), width - READ_INSET);
1089
+ return /* @__PURE__ */ jsxs12("g", { pointerEvents: "none", children: [
1090
+ /* @__PURE__ */ jsx14(
1091
+ "line",
1092
+ {
1093
+ x1: pointerX,
1094
+ x2: pointerX,
1095
+ y1: SECTION_PAD.top,
1096
+ y2: floorY,
1097
+ strokeWidth: 1,
1098
+ strokeDasharray: "3 3",
1099
+ className: READ
1100
+ }
1101
+ ),
1102
+ /* @__PURE__ */ jsx14("circle", { cx: pointerX, cy: y(reading.height), r: 4, className: READ_DOT }),
1103
+ /* @__PURE__ */ jsx14(
1104
+ "text",
1105
+ {
1106
+ x: labelX,
1107
+ y: SECTION_PAD.top + 14,
1108
+ textAnchor: onRight ? "start" : "end",
1109
+ fontSize: 11,
1110
+ fontWeight: 600,
1111
+ className: TEXT,
1112
+ children: text
1113
+ }
1114
+ )
1115
+ ] });
1116
+ };
1117
+
1118
+ // src/reach/section-geometry.ts
1119
+ var materialPath = (curve, cut, x, y, floorY, right) => {
1120
+ let path = `M ${x(0)} ${floorY}`;
1121
+ let level = 0;
1122
+ curve.offsets.forEach((offset, index) => {
1123
+ level = curve.heights[index] ?? 0;
1124
+ path += ` L ${x(runStart(curve, index))} ${y(level)} L ${Math.min(x(offset), right)} ${y(level)}`;
1125
+ });
1126
+ path += ` L ${right} ${y(level)}`;
1127
+ if (cut) path += wallBreak(right, y(level), floorY);
1128
+ return `${path} L ${right} ${floorY} Z`;
1129
+ };
1130
+ var featureShare = (feature, x1) => {
1131
+ const trueShare = feature.across ? feature.across.width / (feature.across.width + x1) : FEATURE_SHARE;
1132
+ return {
1133
+ share: Math.min(Math.max(trueShare, FEATURE_SHARE_MIN), FEATURE_SHARE_MAX),
1134
+ cut: feature.across !== null && trueShare > FEATURE_SHARE_MAX
1135
+ };
1136
+ };
1137
+ var plotScale = (width, spanMm, topMm) => {
1138
+ const fitted = (width - SECTION_PAD.left - SECTION_PAD.right) / spanMm;
1139
+ if (topMm * fitted > SECTION_MAX_H) return { scale: SECTION_MAX_H / topMm, plotH: SECTION_MAX_H };
1140
+ return { scale: fitted, plotH: Math.max(topMm * fitted, SECTION_MIN_H) };
1141
+ };
1142
+ var heightLabels = (risers, depth, y) => spaced(
1143
+ [...new Set(risers.map((riser) => riser.to))].sort((a, b) => a - b),
1144
+ (h) => -y(h),
1145
+ LABEL_GAP_Y
1146
+ ).filter((h) => depth === null || Math.abs(y(h) - y(depth)) >= LABEL_GAP_Y);
1147
+ var offsetLabels = (risers, x) => spaced(
1148
+ risers.filter((riser) => x(riser.at) - x(0) >= WALL_LABEL_GAP_X),
1149
+ (riser) => x(riser.at),
1150
+ LABEL_GAP_X
1151
+ );
1152
+ var headingFor = (feature) => feature.through ? "THROUGH FEATURE" : "FEATURE";
1153
+ var headingMiddle = (heading, wallX) => Math.max(
1154
+ (SECTION_PAD.left + wallX) / 2,
1155
+ SECTION_PAD.left + heading.length * HEADING_CHAR_PX / 2
1156
+ );
1157
+ var sectionGeometry = (whole, feature, width) => {
1158
+ const { curve, cut } = trimmedCurve(whole);
1159
+ const last = curve.offsets[curve.offsets.length - 1] ?? 0;
1160
+ const peak = peakHeight(curve);
1161
+ const span = last > 0 ? last : Math.max(peak, 1);
1162
+ const x1 = span * 1.08;
1163
+ const { share, cut: featureCut } = featureShare(feature, x1);
1164
+ const x0 = -x1 * share / (1 - share);
1165
+ const top = Math.max(peak * 1.06, (feature.depth ?? 0) * 1.06, span * 0.08);
1166
+ const { scale, plotH } = plotScale(width, x1 - x0, top);
1167
+ const floorY = SECTION_PAD.top + plotH;
1168
+ const x = (mm) => SECTION_PAD.left + (mm - x0) * scale;
1169
+ const y = (mm) => floorY - mm * scale;
1170
+ const right = Math.min(x(x1), width - SECTION_PAD.right);
1171
+ const risers = risersOf(curve);
1172
+ const { depth } = feature;
1173
+ const heading = headingFor(feature);
1174
+ return {
1175
+ curve,
1176
+ cut,
1177
+ last,
1178
+ width,
1179
+ height: plotH + SECTION_PAD.top + SECTION_PAD.bottom,
1180
+ scale,
1181
+ x0,
1182
+ x,
1183
+ y,
1184
+ floorY,
1185
+ right,
1186
+ materialPath: materialPath(curve, cut, x, y, floorY, right),
1187
+ across: offsetLabels(risers, x),
1188
+ up: heightLabels(risers, depth, y),
1189
+ feature,
1190
+ featureCut,
1191
+ featureHeading: heading,
1192
+ featureHeadingX: headingMiddle(heading, x(0)),
1193
+ featureTopY: depth === null ? SECTION_PAD.top : y(depth),
1194
+ featureBottomY: feature.through ? floorY + FLOOR_STRIP : floorY,
1195
+ wallTop: y(curve.heights[0] ?? 0),
1196
+ datumX: right + 8,
1197
+ ordinateY: floorY + 22
1198
+ };
1199
+ };
1200
+
1201
+ // src/reach/section-parts.tsx
1202
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1203
+ var Arrows = ({ id }) => /* @__PURE__ */ jsx15("defs", { children: /* @__PURE__ */ jsx15(
1204
+ "marker",
1205
+ {
1206
+ id,
1207
+ viewBox: "0 0 10 10",
1208
+ refX: "10",
1209
+ refY: "5",
1210
+ markerWidth: "7",
1211
+ markerHeight: "7",
1212
+ orient: "auto-start-reverse",
1213
+ children: /* @__PURE__ */ jsx15("path", { d: "M 0 1.5 L 10 5 L 0 8.5 z", className: ARROW })
1214
+ }
1215
+ ) });
1216
+ var SectionShapes = ({ geometry }) => {
1217
+ const { x, floorY, right, featureTopY, featureBottomY, wallTop, feature } = geometry;
1218
+ const floorLeft = feature.through ? x(0) : SECTION_PAD.left;
1219
+ return /* @__PURE__ */ jsxs13(Fragment4, { children: [
1220
+ /* @__PURE__ */ jsx15(
1221
+ "rect",
1222
+ {
1223
+ x: SECTION_PAD.left,
1224
+ y: featureTopY,
1225
+ width: x(0) - SECTION_PAD.left,
1226
+ height: featureBottomY - featureTopY,
1227
+ className: FEATURE_FILL
1228
+ }
1229
+ ),
1230
+ /* @__PURE__ */ jsx15(
1231
+ "rect",
1232
+ {
1233
+ x: floorLeft,
1234
+ y: floorY,
1235
+ width: right - floorLeft,
1236
+ height: FLOOR_STRIP,
1237
+ className: FLOOR
1238
+ }
1239
+ ),
1240
+ /* @__PURE__ */ jsx15("path", { d: geometry.materialPath, strokeWidth: 1, className: MATERIAL }),
1241
+ /* @__PURE__ */ jsx15("line", { x1: floorLeft, x2: right, y1: floorY, y2: floorY, strokeWidth: 1, className: LINE }),
1242
+ feature.through ? null : /* @__PURE__ */ jsx15(
1243
+ "line",
1244
+ {
1245
+ x1: SECTION_PAD.left,
1246
+ x2: x(0),
1247
+ y1: floorY,
1248
+ y2: floorY,
1249
+ strokeWidth: 2,
1250
+ className: FEATURE_LINE
1251
+ }
1252
+ ),
1253
+ /* @__PURE__ */ jsx15(
1254
+ "line",
1255
+ {
1256
+ x1: x(0),
1257
+ x2: x(0),
1258
+ y1: featureBottomY,
1259
+ y2: wallTop,
1260
+ strokeWidth: 2,
1261
+ className: FEATURE_LINE
1262
+ }
1263
+ ),
1264
+ /* @__PURE__ */ jsx15(
1265
+ "line",
1266
+ {
1267
+ x1: x(0),
1268
+ x2: x(0),
1269
+ y1: wallTop,
1270
+ y2: SECTION_PAD.top,
1271
+ strokeWidth: 0.75,
1272
+ strokeDasharray: "3 2",
1273
+ className: FEATURE_LINE
1274
+ }
1275
+ )
1276
+ ] });
1277
+ };
1278
+ var SectionHeadings = ({ geometry }) => {
1279
+ const { right } = geometry;
1280
+ return /* @__PURE__ */ jsxs13(Fragment4, { children: [
1281
+ /* @__PURE__ */ jsx15(
1282
+ "text",
1283
+ {
1284
+ x: geometry.featureHeadingX,
1285
+ y: SECTION_PAD.top - 8,
1286
+ textAnchor: "middle",
1287
+ fontSize: 9,
1288
+ fontWeight: 600,
1289
+ className: FEATURE_TEXT,
1290
+ children: geometry.featureHeading
1291
+ }
1292
+ ),
1293
+ /* @__PURE__ */ jsx15(
1294
+ "text",
1295
+ {
1296
+ x: right,
1297
+ y: SECTION_PAD.top - 8,
1298
+ textAnchor: "end",
1299
+ fontSize: 9,
1300
+ className: TEXT,
1301
+ opacity: 0.7,
1302
+ children: "ADJACENT WALL HEIGHT"
1303
+ }
1304
+ )
1305
+ ] });
1306
+ };
1307
+ var FeatureBreak = ({ geometry }) => geometry.featureCut ? /* @__PURE__ */ jsx15(
1308
+ "path",
1309
+ {
1310
+ d: featureBreak(SECTION_PAD.left, geometry.featureTopY, geometry.featureBottomY),
1311
+ strokeWidth: 1,
1312
+ fill: "none",
1313
+ className: FEATURE_LINE
1314
+ }
1315
+ ) : null;
1316
+ var FeatureDepth = ({ geometry, units }) => {
1317
+ const { x, featureTopY, datumX } = geometry;
1318
+ const { depth } = geometry.feature;
1319
+ if (depth === null) return null;
1320
+ return /* @__PURE__ */ jsxs13("g", { children: [
1321
+ /* @__PURE__ */ jsx15("title", { children: "Feature depth" }),
1322
+ /* @__PURE__ */ jsx15(
1323
+ "line",
1324
+ {
1325
+ x1: SECTION_PAD.left,
1326
+ x2: x(0),
1327
+ y1: featureTopY,
1328
+ y2: featureTopY,
1329
+ strokeWidth: 2,
1330
+ className: FEATURE_LINE
1331
+ }
1332
+ ),
1333
+ /* @__PURE__ */ jsx15(
1334
+ "line",
1335
+ {
1336
+ x1: x(0),
1337
+ x2: datumX,
1338
+ y1: featureTopY,
1339
+ y2: featureTopY,
1340
+ strokeWidth: 0.75,
1341
+ strokeDasharray: "4 2",
1342
+ className: FEATURE_LINE
1343
+ }
1344
+ ),
1345
+ x(0) - SECTION_PAD.left >= DEPTH_WORD_PX ? /* @__PURE__ */ jsx15(
1346
+ "text",
1347
+ {
1348
+ x: SECTION_PAD.left + 4,
1349
+ y: featureTopY - 4,
1350
+ fontSize: 9,
1351
+ fontWeight: 600,
1352
+ className: FEATURE_TEXT,
1353
+ children: "depth"
1354
+ }
1355
+ ) : null,
1356
+ /* @__PURE__ */ jsx15(
1357
+ "text",
1358
+ {
1359
+ x: datumX + 4,
1360
+ y: featureTopY + 3.5,
1361
+ fontSize: 10,
1362
+ fontWeight: 600,
1363
+ className: FEATURE_TEXT,
1364
+ children: lengthValue(depth, units)
1365
+ }
1366
+ )
1367
+ ] });
1368
+ };
1369
+ var ReadRun = ({ geometry, units, arrowId }) => {
1370
+ const { x, last, cut, floorY, ordinateY } = geometry;
1371
+ if (last <= 0 || cut) return null;
1372
+ return /* @__PURE__ */ jsxs13("g", { children: [
1373
+ /* @__PURE__ */ jsx15(
1374
+ "line",
1375
+ {
1376
+ x1: x(0),
1377
+ x2: x(last),
1378
+ y1: ordinateY - 8,
1379
+ y2: ordinateY - 8,
1380
+ strokeWidth: 0.75,
1381
+ markerStart: `url(#${arrowId})`,
1382
+ markerEnd: `url(#${arrowId})`,
1383
+ className: LINE
1384
+ }
1385
+ ),
1386
+ /* @__PURE__ */ jsx15(
1387
+ "line",
1388
+ {
1389
+ x1: x(last),
1390
+ x2: x(last),
1391
+ y1: floorY,
1392
+ y2: ordinateY - 4,
1393
+ strokeWidth: 0.5,
1394
+ className: LINE
1395
+ }
1396
+ ),
1397
+ /* @__PURE__ */ jsxs13(
1398
+ "text",
1399
+ {
1400
+ x: (x(0) + x(last)) / 2,
1401
+ y: ordinateY - 11,
1402
+ textAnchor: "middle",
1403
+ fontSize: 10,
1404
+ className: TEXT,
1405
+ children: [
1406
+ lengthValue(last, units),
1407
+ " read"
1408
+ ]
1409
+ }
1410
+ )
1411
+ ] });
1412
+ };
1413
+ var OffsetOrdinates = (props) => {
1414
+ const { geometry, units } = props;
1415
+ const { x, y, right, width, floorY, ordinateY } = geometry;
1416
+ return /* @__PURE__ */ jsxs13(Fragment4, { children: [
1417
+ /* @__PURE__ */ jsx15(
1418
+ "text",
1419
+ {
1420
+ x: Math.min(
1421
+ Math.max((x(0) + right) / 2, AXIS_LABEL_HALF_PX + READ_INSET),
1422
+ width - AXIS_LABEL_HALF_PX - READ_INSET
1423
+ ),
1424
+ y: ordinateY + 32,
1425
+ textAnchor: "middle",
1426
+ fontSize: 9,
1427
+ className: TEXT,
1428
+ opacity: 0.7,
1429
+ children: "XY PLANE DISTANCE FROM FEATURE"
1430
+ }
1431
+ ),
1432
+ /* @__PURE__ */ jsx15(
1433
+ "line",
1434
+ {
1435
+ x1: x(0),
1436
+ x2: x(0),
1437
+ y1: floorY,
1438
+ y2: ordinateY + 4,
1439
+ strokeWidth: 0.75,
1440
+ className: LINE
1441
+ }
1442
+ ),
1443
+ /* @__PURE__ */ jsx15("text", { x: x(0), y: ordinateY + 15, textAnchor: "middle", fontSize: 10, className: TEXT, children: "0" }),
1444
+ geometry.across.map((riser) => /* @__PURE__ */ jsxs13("g", { children: [
1445
+ /* @__PURE__ */ jsx15(
1446
+ "line",
1447
+ {
1448
+ x1: x(riser.at),
1449
+ x2: x(riser.at),
1450
+ y1: y(riser.from) + 2,
1451
+ y2: ordinateY + 4,
1452
+ strokeWidth: 0.5,
1453
+ strokeDasharray: "2 2",
1454
+ className: LINE
1455
+ }
1456
+ ),
1457
+ /* @__PURE__ */ jsx15(
1458
+ "text",
1459
+ {
1460
+ x: x(riser.at),
1461
+ y: ordinateY + 15,
1462
+ textAnchor: "middle",
1463
+ fontSize: 10,
1464
+ className: TEXT,
1465
+ children: lengthValue(riser.at, units)
1466
+ }
1467
+ )
1468
+ ] }, `x:${riser.at}`)),
1469
+ /* @__PURE__ */ jsx15(ReadRun, { ...props })
1470
+ ] });
1471
+ };
1472
+ var HeightOrdinates = ({ geometry, units }) => {
1473
+ const { y, right, floorY, datumX } = geometry;
1474
+ return /* @__PURE__ */ jsxs13(Fragment4, { children: [
1475
+ /* @__PURE__ */ jsx15("text", { x: datumX + 4, y: floorY + 3.5, fontSize: 10, className: TEXT, children: "0" }),
1476
+ geometry.up.map((h) => /* @__PURE__ */ jsxs13("g", { children: [
1477
+ /* @__PURE__ */ jsx15(
1478
+ "line",
1479
+ {
1480
+ x1: right - 2,
1481
+ x2: datumX,
1482
+ y1: y(h),
1483
+ y2: y(h),
1484
+ strokeWidth: 0.5,
1485
+ strokeDasharray: "2 2",
1486
+ className: LINE
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ jsx15("text", { x: datumX + 4, y: y(h) + 3.5, fontSize: 10, className: TEXT, children: lengthValue(h, units) })
1490
+ ] }, `y:${h}`))
1491
+ ] });
1492
+ };
1493
+
1494
+ // src/reach/reach-section.tsx
1495
+ import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1496
+ var ReachSection = ({
1497
+ curve: whole,
1498
+ units,
1499
+ feature,
1500
+ width
1501
+ }) => {
1502
+ const [hover, setHover] = useState8(null);
1503
+ const geometry = useMemo3(() => sectionGeometry(whole, feature, width), [whole, feature, width]);
1504
+ const reactId = useId();
1505
+ const arrowId = `reach-dim-arrow-${reactId.replace(/[^\w-]/g, "")}`;
1506
+ const reading = hover === null ? null : readingAt(whole, hover);
1507
+ const drawing = useMemo3(
1508
+ () => /* @__PURE__ */ jsxs14(Fragment5, { children: [
1509
+ /* @__PURE__ */ jsx16(Arrows, { id: arrowId }),
1510
+ /* @__PURE__ */ jsx16(SectionShapes, { geometry }),
1511
+ /* @__PURE__ */ jsx16(SectionHeadings, { geometry }),
1512
+ /* @__PURE__ */ jsx16(FeatureBreak, { geometry }),
1513
+ /* @__PURE__ */ jsx16(FeatureDepth, { geometry, units }),
1514
+ /* @__PURE__ */ jsx16(OffsetOrdinates, { geometry, units, arrowId }),
1515
+ /* @__PURE__ */ jsx16(HeightOrdinates, { geometry, units })
1516
+ ] }),
1517
+ [arrowId, geometry, units]
1518
+ );
1519
+ return /* @__PURE__ */ jsxs14(
1520
+ "svg",
1521
+ {
1522
+ viewBox: `0 0 ${width} ${geometry.height}`,
1523
+ width,
1524
+ height: geometry.height,
1525
+ className: "select-none",
1526
+ role: "img",
1527
+ "aria-label": "Reach curve, dimensioned",
1528
+ onPointerMove: (event) => setHover(offsetUnder(event.clientX, event.currentTarget.getBoundingClientRect(), geometry)),
1529
+ onPointerLeave: () => setHover(null),
1530
+ children: [
1531
+ drawing,
1532
+ reading ? /* @__PURE__ */ jsx16(ReadingMark, { geometry, reading, units }) : null
1533
+ ]
1534
+ }
1535
+ );
1536
+ };
1537
+
1538
+ // src/reach/use-element-size.ts
1539
+ import { useLayoutEffect, useRef as useRef4, useState as useState9 } from "react";
1540
+ var useElementSize = () => {
1541
+ const ref = useRef4(null);
1542
+ const [size, setSize] = useState9({ width: 0, height: 0 });
1543
+ useLayoutEffect(() => {
1544
+ const element = ref.current;
1545
+ if (!element) return;
1546
+ const observer = new ResizeObserver(([entry]) => {
1547
+ if (!entry) return;
1548
+ const { width, height } = entry.contentRect;
1549
+ setSize(
1550
+ (was) => Math.abs(was.width - width) < 0.5 && Math.abs(was.height - height) < 0.5 ? was : { width, height }
1551
+ );
1552
+ });
1553
+ observer.observe(element);
1554
+ return () => observer.disconnect();
1555
+ }, []);
1556
+ return [ref, size];
1557
+ };
1558
+
1559
+ // src/reach/reach-chart.tsx
1560
+ import { Fragment as Fragment6, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
1561
+ var CHART_MIN_W = 200;
1562
+ var DRAWING_MIN_W = 240;
1563
+ var ReachChart = ({ curve, units, feature }) => {
1564
+ const [ref, size] = useElementSize();
1565
+ return /* @__PURE__ */ jsx17("div", { ref, className: "w-full", children: /* @__PURE__ */ jsx17(
1566
+ ReachSection,
1567
+ {
1568
+ curve,
1569
+ units,
1570
+ feature,
1571
+ width: Math.max(size.width, CHART_MIN_W)
1572
+ }
1573
+ ) });
1574
+ };
1575
+ var CutNote = ({
1576
+ curve,
1577
+ units
1578
+ }) => {
1579
+ const trimmed = useMemo4(() => trimmedCurve(curve), [curve]);
1580
+ if (!trimmed.cut) return null;
1581
+ const drawnTo = trimmed.curve.offsets[trimmed.curve.offsets.length - 1] ?? 0;
1582
+ const readTo = curve.offsets[curve.offsets.length - 1] ?? 0;
1583
+ return /* @__PURE__ */ jsxs15(Fragment6, { children: [
1584
+ " ",
1585
+ "It is at full height from ",
1586
+ lengthValue(drawnTo, units),
1587
+ " ",
1588
+ unitLabel(units),
1589
+ " out, so the drawing stops there; the Engine read it to ",
1590
+ lengthValue(readTo, units),
1591
+ " ",
1592
+ unitLabel(units),
1593
+ "."
1594
+ ] });
1595
+ };
1596
+ var ReachDrawing = ({ curve, units, feature }) => {
1597
+ const [ref, size] = useElementSize();
1598
+ return /* @__PURE__ */ jsxs15("div", { ref, className: "flex flex-col gap-3 p-4", children: [
1599
+ /* @__PURE__ */ jsxs15("figure", { className: "flex flex-col gap-1", children: [
1600
+ /* @__PURE__ */ jsxs15("figcaption", { className: "text-2xs font-semibold text-gray-400 uppercase dark:text-zinc-500", children: [
1601
+ "Section at the wall \xB7 to scale \xB7 ",
1602
+ unitLabel(units)
1603
+ ] }),
1604
+ /* @__PURE__ */ jsx17(
1605
+ ReachSection,
1606
+ {
1607
+ curve,
1608
+ units,
1609
+ feature,
1610
+ width: Math.max(size.width, DRAWING_MIN_W)
1611
+ }
1612
+ )
1613
+ ] }),
1614
+ /* @__PURE__ */ jsxs15("p", { className: "text-2xs text-gray-400 dark:text-zinc-500", children: [
1615
+ "Point at the walls to read them: the distance from the feature, then the reach a tool needs there.",
1616
+ /* @__PURE__ */ jsx17(CutNote, { curve, units })
1617
+ ] })
1618
+ ] });
1619
+ };
1620
+ export {
1621
+ ReachChart,
1622
+ ReachDrawing,
1623
+ RuleList,
1624
+ Swatch,
1625
+ isField,
1626
+ listKeys,
1627
+ ruleListKeys
1628
+ };