@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/LICENSE +21 -0
- package/README.md +97 -2
- package/dfm.css +9 -0
- package/dist/chunk-5MPHT7BC.js +1599 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +1628 -0
- package/dist/model/index.d.ts +109 -0
- package/dist/model/index.js +775 -0
- package/dist/reach-CfmYU_yQ.d.ts +415 -0
- package/package.json +72 -3
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
|
+
};
|