@toolpath/dfm 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  parseRuleSetFile,
24
24
  peakHeight,
25
25
  quantityUnit,
26
+ readReachCurve,
26
27
  runStart,
27
28
  serializeRuleSet,
28
29
  subjectLabel,
@@ -800,14 +801,24 @@ var countLabel = (count, others) => {
800
801
  };
801
802
 
802
803
  // 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";
804
+ import { useRef as useRef3 } from "react";
809
805
  import { CheckIcon, CopyIcon, DownloadSimpleIcon, UploadSimpleIcon } from "@phosphor-icons/react";
810
806
  import { Button, cn as cn8 } from "@toolpath/ui";
807
+
808
+ // src/rule-list/use-timed-value.ts
809
+ import { useCallback, useEffect as useEffect2, useState as useState6 } from "react";
810
+ var useTimedValue = (ms) => {
811
+ const [box, setBox] = useState6(null);
812
+ useEffect2(() => {
813
+ if (box === null) return;
814
+ const timer = setTimeout(() => setBox(null), ms);
815
+ return () => clearTimeout(timer);
816
+ }, [box, ms]);
817
+ const set = useCallback((value) => setBox({ value }), []);
818
+ return [box?.value ?? null, set];
819
+ };
820
+
821
+ // src/rule-list/rule-set-actions.tsx
811
822
  import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
812
823
  var NOTE_MS = 4e3;
813
824
  var COPIED_MS = 2e3;
@@ -888,16 +899,6 @@ var ActionLabel = ({ icon, children, hidden = false }) => /* @__PURE__ */ jsxs10
888
899
  ]
889
900
  }
890
901
  );
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
902
  var downloadRuleSet = (rules) => {
902
903
  const url = URL.createObjectURL(
903
904
  new Blob([serializeRuleSet(rules.rules)], { type: "application/json" })
@@ -1617,12 +1618,869 @@ var ReachDrawing = ({ curve, units, feature }) => {
1617
1618
  ] })
1618
1619
  ] });
1619
1620
  };
1621
+
1622
+ // src/feature-panel/fold-store.ts
1623
+ var storageFolds = (storage, prefix) => ({
1624
+ read: (id) => {
1625
+ try {
1626
+ const stored = storage?.getItem(`${prefix}${id}`);
1627
+ return stored === "open" ? true : stored === "shut" ? false : void 0;
1628
+ } catch {
1629
+ return void 0;
1630
+ }
1631
+ },
1632
+ write: (id, open) => {
1633
+ try {
1634
+ storage?.setItem(`${prefix}${id}`, open ? "open" : "shut");
1635
+ } catch {
1636
+ }
1637
+ }
1638
+ });
1639
+
1640
+ // src/feature-panel/feature-details.tsx
1641
+ import { useCallback as useCallback2, useMemo as useMemo7, useState as useState12 } from "react";
1642
+ import { CrosshairIcon, InfoIcon } from "@phosphor-icons/react";
1643
+ import { Button as Button3, Tooltip, cn as cn12 } from "@toolpath/ui";
1644
+
1645
+ // src/feature-panel/feature-name.tsx
1646
+ import { cn as cn9 } from "@toolpath/ui";
1647
+
1648
+ // src/feature-panel/look.ts
1649
+ var resolveLook = (look) => ({
1650
+ rules: look?.rules ?? "sentence",
1651
+ selected: look?.selected ?? "grey",
1652
+ muted: look?.muted ?? "pale"
1653
+ });
1654
+ var secondaryText = (look) => look.muted === "strong" ? "text-gray dark:text-zinc-300" : "text-gray-400 dark:text-zinc-400";
1655
+ var faintText = (look) => look.muted === "strong" ? "text-gray dark:text-zinc-300" : "text-gray-300 dark:text-zinc-500";
1656
+
1657
+ // src/feature-panel/feature-name.tsx
1658
+ import { Fragment as Fragment7, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
1659
+ var FeatureName = ({ feature, label, look }) => /* @__PURE__ */ jsxs16(Fragment7, { children: [
1660
+ label ?? featureTypeLabel(feature.featureType),
1661
+ " ",
1662
+ /* @__PURE__ */ jsxs16("span", { className: cn9("font-normal", faintText(look)), children: [
1663
+ "from ",
1664
+ directionLabel(feature.machiningDirection)
1665
+ ] })
1666
+ ] });
1667
+ var RequiredBadge = () => /* @__PURE__ */ jsx18("span", { className: "rounded-full bg-gray-100 px-1.5 text-2xs font-semibold text-gray-500 uppercase dark:bg-zinc-800 dark:text-zinc-300", children: "Required" });
1668
+
1669
+ // src/feature-panel/fold-section.tsx
1670
+ import { useState as useState10 } from "react";
1671
+ import { CaretRightIcon as CaretRightIcon3 } from "@phosphor-icons/react";
1672
+ import { cn as cn10 } from "@toolpath/ui";
1673
+ import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
1674
+ var FoldSection = ({
1675
+ id,
1676
+ title: title2,
1677
+ action,
1678
+ defaultOpen = true,
1679
+ folds,
1680
+ className,
1681
+ children
1682
+ }) => {
1683
+ const [open, setOpen] = useState10(() => folds?.read(id) ?? defaultOpen);
1684
+ return /* @__PURE__ */ jsxs17("section", { className: cn10("flex flex-col gap-1", className), children: [
1685
+ /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-1", children: [
1686
+ /* @__PURE__ */ jsxs17(
1687
+ "button",
1688
+ {
1689
+ type: "button",
1690
+ "aria-expanded": open,
1691
+ onClick: () => {
1692
+ setOpen(!open);
1693
+ folds?.write(id, !open);
1694
+ },
1695
+ className: cn10(
1696
+ "flex min-w-0 flex-1 cursor-pointer items-center gap-1 rounded text-left text-2xs font-semibold text-gray-400 uppercase",
1697
+ "hover:text-gray-600 dark:text-zinc-500 dark:hover:text-zinc-300",
1698
+ "outline-none focus-visible:ring-2 focus-visible:ring-info/75"
1699
+ ),
1700
+ children: [
1701
+ /* @__PURE__ */ jsx19(
1702
+ CaretRightIcon3,
1703
+ {
1704
+ weight: "bold",
1705
+ className: cn10("size-3 shrink-0 transition-transform duration-150", open && "rotate-90")
1706
+ }
1707
+ ),
1708
+ /* @__PURE__ */ jsx19("span", { className: "truncate", children: title2 })
1709
+ ]
1710
+ }
1711
+ ),
1712
+ action
1713
+ ] }),
1714
+ open ? children : null
1715
+ ] });
1716
+ };
1717
+
1718
+ // src/feature-panel/read-state.tsx
1719
+ import { useEffect as useEffect3, useState as useState11 } from "react";
1720
+ import { Button as Button2 } from "@toolpath/ui";
1721
+ import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
1722
+ var Reading = ({ children }) => /* @__PURE__ */ jsx20("p", { className: "text-sm text-gray-400 dark:text-zinc-400", role: "status", children });
1723
+ var ReadFailure = ({
1724
+ failure
1725
+ }) => {
1726
+ const waiting = useWaiting(failure.retryAt);
1727
+ return /* @__PURE__ */ jsxs18("div", { className: "flex flex-col items-start gap-2 text-sm", children: [
1728
+ /* @__PURE__ */ jsx20("p", { className: "text-danger", role: "alert", children: failure.message }),
1729
+ failure.retry ? /* @__PURE__ */ jsx20(Button2, { variant: "secondary", size: "sm", disabled: waiting, onClick: failure.retry, children: "Retry" }) : null
1730
+ ] });
1731
+ };
1732
+ var useWaiting = (until) => {
1733
+ const [passed, setPassed] = useState11(void 0);
1734
+ useEffect3(() => {
1735
+ if (until === void 0) return;
1736
+ const timer = setTimeout(() => setPassed(until), Math.max(until - Date.now(), 0));
1737
+ return () => clearTimeout(timer);
1738
+ }, [until]);
1739
+ return until !== void 0 && passed !== until;
1740
+ };
1741
+
1742
+ // src/feature-panel/pop-outs.tsx
1743
+ import { useMemo as useMemo6 } from "react";
1744
+ import {
1745
+ ArrowSquareOutIcon,
1746
+ BracketsCurlyIcon,
1747
+ ChartLineIcon,
1748
+ TableIcon
1749
+ } from "@phosphor-icons/react";
1750
+ import { IconButton as IconButton3 } from "@toolpath/ui";
1751
+
1752
+ // src/feature-panel/record-sections.tsx
1753
+ import { useMemo as useMemo5 } from "react";
1754
+ import { CheckIcon as CheckIcon2, CopyIcon as CopyIcon2 } from "@phosphor-icons/react";
1755
+ import { IconButton as IconButton2, cn as cn11 } from "@toolpath/ui";
1756
+
1757
+ // src/model/feature-record.ts
1758
+ var MAX_DATASHEET_FIELDS = 500;
1759
+ var formatLeaf = (value) => typeof value === "number" ? String(Number(value.toFixed(4))) : typeof value === "string" ? value : value === null ? "null" : String(value);
1760
+ var datasheetFields = (datasheet) => {
1761
+ const fields = [];
1762
+ let more = false;
1763
+ const walk = (value, path) => {
1764
+ if (fields.length >= MAX_DATASHEET_FIELDS) {
1765
+ more = true;
1766
+ return;
1767
+ }
1768
+ if (Array.isArray(value)) {
1769
+ if (value.every((each) => typeof each !== "object" || each === null)) {
1770
+ fields.push({ path, value: value.length === 0 ? "[]" : value.map(formatLeaf).join(", ") });
1771
+ } else {
1772
+ value.forEach((each, index) => walk(each, `${path}[${index}]`));
1773
+ }
1774
+ } else if (typeof value === "object" && value !== null) {
1775
+ const entries = Object.entries(value);
1776
+ if (entries.length === 0) fields.push({ path, value: "{}" });
1777
+ for (const [key, each] of entries) walk(each, path ? `${path}.${key}` : key);
1778
+ } else {
1779
+ fields.push({ path, value: formatLeaf(value) });
1780
+ }
1781
+ };
1782
+ if (datasheet !== null && datasheet !== void 0) walk(datasheet, "");
1783
+ return { fields, more };
1784
+ };
1785
+
1786
+ // src/feature-panel/record-sections.tsx
1787
+ import { Fragment as Fragment8, jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
1788
+ var COPIED_MS2 = 1500;
1789
+ var RecordSections = ({
1790
+ record,
1791
+ look,
1792
+ folds,
1793
+ popOut
1794
+ }) => {
1795
+ const value = record.status === "ready" ? record.value : null;
1796
+ const { fields, more } = useMemo5(() => datasheetFields(value?.datasheet), [value]);
1797
+ const json = useMemo5(() => value ? JSON.stringify(value, null, 2) : "", [value]);
1798
+ if (record.status === "ready" && !value) return null;
1799
+ const waiting = record.status === "loading" ? /* @__PURE__ */ jsx21(Reading, { children: "Reading the feature\u2019s record\u2026" }) : record.status === "error" ? /* @__PURE__ */ jsx21(ReadFailure, { failure: record }) : null;
1800
+ const count = more ? `${MAX_DATASHEET_FIELDS}+` : String(fields.length);
1801
+ return /* @__PURE__ */ jsxs19(Fragment8, { children: [
1802
+ /* @__PURE__ */ jsx21(
1803
+ FoldSection,
1804
+ {
1805
+ id: "datasheet",
1806
+ defaultOpen: false,
1807
+ title: value ? `All datasheet fields (${count})` : "All datasheet fields",
1808
+ action: value ? popOut?.("datasheet") : null,
1809
+ folds,
1810
+ children: waiting ?? /* @__PURE__ */ jsx21(DatasheetFields, { record: value, look })
1811
+ }
1812
+ ),
1813
+ /* @__PURE__ */ jsx21(
1814
+ FoldSection,
1815
+ {
1816
+ id: "raw",
1817
+ defaultOpen: false,
1818
+ title: "Raw API record",
1819
+ action: value ? /* @__PURE__ */ jsxs19(Fragment8, { children: [
1820
+ /* @__PURE__ */ jsx21(CopyButton, { text: json }),
1821
+ popOut?.("raw")
1822
+ ] }) : null,
1823
+ folds,
1824
+ children: waiting ?? /* @__PURE__ */ jsx21(RawRecord, { json })
1825
+ }
1826
+ )
1827
+ ] });
1828
+ };
1829
+ var DatasheetFields = ({ record, look, wide = false }) => {
1830
+ const { fields } = useMemo5(() => datasheetFields(record?.datasheet), [record]);
1831
+ return record?.datasheet ? /* @__PURE__ */ jsx21(
1832
+ "dl",
1833
+ {
1834
+ className: cn11(
1835
+ "grid grid-cols-[minmax(0,1fr)_auto] gap-y-0.5 text-xs",
1836
+ wide ? "gap-x-4 p-4" : "gap-x-3"
1837
+ ),
1838
+ children: fields.map((field) => /* @__PURE__ */ jsxs19("div", { className: "contents", children: [
1839
+ /* @__PURE__ */ jsx21("dt", { className: cn11("truncate font-mono", secondaryText(look)), title: field.path, children: field.path }),
1840
+ /* @__PURE__ */ jsx21(
1841
+ "dd",
1842
+ {
1843
+ className: cn11(
1844
+ "truncate text-right font-mono text-gray-600 tabular-nums select-text dark:text-zinc-100",
1845
+ wide ? "max-w-60" : "max-w-40"
1846
+ ),
1847
+ title: field.value,
1848
+ children: field.value
1849
+ }
1850
+ )
1851
+ ] }, field.path))
1852
+ }
1853
+ ) : /* @__PURE__ */ jsx21("p", { className: cn11("text-sm", secondaryText(look), wide && "p-4"), children: "The API sent no datasheet for this feature." });
1854
+ };
1855
+ var RawRecord = ({
1856
+ json,
1857
+ wide = false
1858
+ }) => /* @__PURE__ */ jsx21(
1859
+ "pre",
1860
+ {
1861
+ className: cn11(
1862
+ "font-mono text-2xs leading-relaxed text-gray-600 select-text dark:text-zinc-200",
1863
+ wide ? "p-4" : "max-h-80 overflow-auto rounded bg-gray-50 p-2 dark:bg-zinc-800/60"
1864
+ ),
1865
+ children: json
1866
+ }
1867
+ );
1868
+ var CopyButton = ({ text }) => {
1869
+ const [copied, setCopied] = useTimedValue(COPIED_MS2);
1870
+ return /* @__PURE__ */ jsx21(
1871
+ IconButton2,
1872
+ {
1873
+ "aria-label": copied ? "Copied" : "Copy raw record",
1874
+ title: "Copy",
1875
+ variant: "muted",
1876
+ size: "sm",
1877
+ onClick: () => {
1878
+ void navigator.clipboard?.writeText(text).then(() => setCopied(true)).catch(() => void 0);
1879
+ },
1880
+ children: copied ? /* @__PURE__ */ jsx21(CheckIcon2, { weight: "bold" }) : /* @__PURE__ */ jsx21(CopyIcon2, { weight: "bold" })
1881
+ }
1882
+ );
1883
+ };
1884
+
1885
+ // src/feature-panel/pop-outs.tsx
1886
+ import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
1887
+ var TITLES = {
1888
+ reach: "Reach",
1889
+ datasheet: "All datasheet fields",
1890
+ raw: "Raw API record"
1891
+ };
1892
+ var LABELS = {
1893
+ reach: "Pop out the reach drawing",
1894
+ datasheet: "Pop out the datasheet fields",
1895
+ raw: "Pop out the raw record"
1896
+ };
1897
+ var PopOutButton = ({
1898
+ id,
1899
+ onOpen
1900
+ }) => /* @__PURE__ */ jsx22(
1901
+ IconButton3,
1902
+ {
1903
+ "aria-label": LABELS[id],
1904
+ title: "Pop out, larger",
1905
+ variant: "muted",
1906
+ size: "sm",
1907
+ onClick: () => onOpen(id),
1908
+ children: /* @__PURE__ */ jsx22(ArrowSquareOutIcon, { weight: "bold" })
1909
+ }
1910
+ );
1911
+ var PopOuts = ({
1912
+ open,
1913
+ onClose,
1914
+ Window,
1915
+ subtitle,
1916
+ record,
1917
+ units,
1918
+ profile,
1919
+ look
1920
+ }) => {
1921
+ const curve = useMemo6(() => readReachCurve(record?.datasheet), [record]);
1922
+ const json = useMemo6(() => record ? JSON.stringify(record, null, 2) : "", [record]);
1923
+ if (!record) return null;
1924
+ return /* @__PURE__ */ jsxs20(Fragment9, { children: [
1925
+ open.has("reach") && curve ? /* @__PURE__ */ jsx22(
1926
+ Window,
1927
+ {
1928
+ id: "reach",
1929
+ title: TITLES.reach,
1930
+ subtitle,
1931
+ icon: /* @__PURE__ */ jsx22(ChartLineIcon, { weight: "bold" }),
1932
+ onClose: () => onClose("reach"),
1933
+ children: /* @__PURE__ */ jsx22(ReachDrawing, { curve, units, feature: profile })
1934
+ }
1935
+ ) : null,
1936
+ open.has("datasheet") ? /* @__PURE__ */ jsx22(
1937
+ Window,
1938
+ {
1939
+ id: "datasheet",
1940
+ title: TITLES.datasheet,
1941
+ subtitle,
1942
+ icon: /* @__PURE__ */ jsx22(TableIcon, { weight: "bold" }),
1943
+ onClose: () => onClose("datasheet"),
1944
+ children: /* @__PURE__ */ jsx22(DatasheetFields, { record, look, wide: true })
1945
+ }
1946
+ ) : null,
1947
+ open.has("raw") ? /* @__PURE__ */ jsx22(
1948
+ Window,
1949
+ {
1950
+ id: "raw",
1951
+ title: TITLES.raw,
1952
+ subtitle,
1953
+ icon: /* @__PURE__ */ jsx22(BracketsCurlyIcon, { weight: "bold" }),
1954
+ action: /* @__PURE__ */ jsx22(CopyButton, { text: json }),
1955
+ onClose: () => onClose("raw"),
1956
+ children: /* @__PURE__ */ jsx22(RawRecord, { json, wide: true })
1957
+ }
1958
+ ) : null
1959
+ ] });
1960
+ };
1961
+
1962
+ // src/feature-panel/feature-details.tsx
1963
+ import { Fragment as Fragment10, jsx as jsx23, jsxs as jsxs21 } from "react/jsx-runtime";
1964
+ var FeatureDetails = ({
1965
+ feature,
1966
+ units,
1967
+ directionColor,
1968
+ profile,
1969
+ label,
1970
+ required,
1971
+ onFrame,
1972
+ actions,
1973
+ status,
1974
+ children,
1975
+ rules,
1976
+ measurements,
1977
+ record,
1978
+ PopOut,
1979
+ look: lookOption,
1980
+ folds
1981
+ }) => {
1982
+ const look = resolveLook(lookOption);
1983
+ const name = /* @__PURE__ */ jsx23(FeatureName, { feature, label, look });
1984
+ const [out, setOut] = useState12(/* @__PURE__ */ new Set());
1985
+ const openOut = useCallback2((id) => setOut((was) => new Set(was).add(id)), []);
1986
+ const closeOut = useCallback2(
1987
+ (id) => setOut((was) => {
1988
+ const next = new Set(was);
1989
+ next.delete(id);
1990
+ return next;
1991
+ }),
1992
+ []
1993
+ );
1994
+ const popOut = PopOut ? (id) => /* @__PURE__ */ jsx23(PopOutButton, { id, onOpen: openOut }) : void 0;
1995
+ return /* @__PURE__ */ jsxs21("div", { className: "flex flex-col gap-4 p-4", children: [
1996
+ /* @__PURE__ */ jsxs21("div", { className: "flex flex-col gap-2", children: [
1997
+ /* @__PURE__ */ jsxs21("div", { className: "flex items-start gap-2", children: [
1998
+ /* @__PURE__ */ jsxs21("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
1999
+ /* @__PURE__ */ jsx23("h3", { className: "truncate text-sm font-semibold text-gray-600 dark:text-zinc-100", children: name }),
2000
+ /* @__PURE__ */ jsxs21("p", { className: cn12("flex flex-wrap items-center gap-1.5 text-xs", secondaryText(look)), children: [
2001
+ /* @__PURE__ */ jsx23(Swatch, { color: directionColor, className: "size-2.5" }),
2002
+ "Machined from ",
2003
+ directionLabel(feature.machiningDirection),
2004
+ required ? /* @__PURE__ */ jsx23(RequiredBadge, {}) : null,
2005
+ status
2006
+ ] })
2007
+ ] }),
2008
+ actions,
2009
+ onFrame ? /* @__PURE__ */ jsx23(Button3, { variant: "secondary", size: "sm", onClick: onFrame, className: "shrink-0", children: /* @__PURE__ */ jsxs21("span", { className: "flex items-center gap-1 whitespace-nowrap", children: [
2010
+ /* @__PURE__ */ jsx23(CrosshairIcon, { weight: "bold", className: "size-3.5" }),
2011
+ "Show this feature"
2012
+ ] }) }) : null
2013
+ ] }),
2014
+ children
2015
+ ] }),
2016
+ /* @__PURE__ */ jsx23(FoldSection, { id: "rules", title: "Rules broken", folds, children: /* @__PURE__ */ jsx23(BrokenRuleList, { rules, look }) }),
2017
+ /* @__PURE__ */ jsx23(MeasurementSections, { measurements, look, folds }),
2018
+ /* @__PURE__ */ jsx23(
2019
+ ReachSection2,
2020
+ {
2021
+ record,
2022
+ units,
2023
+ profile,
2024
+ folds,
2025
+ action: popOut?.("reach")
2026
+ }
2027
+ ),
2028
+ /* @__PURE__ */ jsx23(RecordSections, { record, look, folds, popOut }),
2029
+ PopOut ? /* @__PURE__ */ jsx23(
2030
+ PopOuts,
2031
+ {
2032
+ open: out,
2033
+ onClose: closeOut,
2034
+ Window: PopOut,
2035
+ subtitle: name,
2036
+ record: record.status === "ready" ? record.value : null,
2037
+ units,
2038
+ profile,
2039
+ look
2040
+ }
2041
+ ) : null
2042
+ ] });
2043
+ };
2044
+ var BrokenRuleList = ({
2045
+ rules,
2046
+ look
2047
+ }) => rules.length > 0 ? /* @__PURE__ */ jsx23("ul", { className: "flex flex-col gap-1", children: rules.map((rule) => /* @__PURE__ */ jsxs21(
2048
+ "li",
2049
+ {
2050
+ className: "flex items-start gap-2 text-sm text-gray-600 dark:text-zinc-100",
2051
+ children: [
2052
+ /* @__PURE__ */ jsx23(Swatch, { color: rule.color, className: "mt-0.5" }),
2053
+ /* @__PURE__ */ jsx23(Explained, { note: rule.note, children: /* @__PURE__ */ jsx23("span", { className: "min-w-0 flex-1", children: rule.text }) }),
2054
+ rule.figure ? /* @__PURE__ */ jsxs21(
2055
+ "span",
2056
+ {
2057
+ className: cn12(
2058
+ "shrink-0 tabular-nums",
2059
+ look.rules === "result" ? "font-semibold" : secondaryText(look)
2060
+ ),
2061
+ style: look.rules === "result" ? { color: rule.color } : void 0,
2062
+ children: [
2063
+ rule.figure,
2064
+ look.rules === "result" && rule.limit ? /* @__PURE__ */ jsx23("span", { className: "ml-1 font-normal opacity-60", children: rule.limit }) : null
2065
+ ]
2066
+ }
2067
+ ) : null
2068
+ ]
2069
+ },
2070
+ rule.key
2071
+ )) }) : /* @__PURE__ */ jsx23(None, { look });
2072
+ var Explained = ({ note, children }) => note ? /* @__PURE__ */ jsx23(Tooltip, { tip: note, side: "left", children: /* @__PURE__ */ jsx23(
2073
+ "span",
2074
+ {
2075
+ tabIndex: 0,
2076
+ className: "min-w-0 flex-1 rounded outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2077
+ children
2078
+ }
2079
+ ) }) : children;
2080
+ var None = ({ look }) => /* @__PURE__ */ jsx23("p", { className: cn12("text-sm", secondaryText(look)), children: "None" });
2081
+ var MeasurementSections = ({ measurements, look, folds }) => {
2082
+ if (measurements.status !== "ready") {
2083
+ return /* @__PURE__ */ jsx23(Fragment10, { children: /* @__PURE__ */ jsx23(FoldSection, { id: "measurements", title: "Measurements", folds, children: measurements.status === "loading" ? /* @__PURE__ */ jsx23(Reading, { children: "Reading the feature\u2019s datasheet\u2026" }) : /* @__PURE__ */ jsx23(ReadFailure, { failure: measurements }) }) });
2084
+ }
2085
+ const measured = measurements.value.filter((row) => !row.milling);
2086
+ const milling = measurements.value.filter((row) => row.milling);
2087
+ return /* @__PURE__ */ jsxs21(Fragment10, { children: [
2088
+ /* @__PURE__ */ jsx23(FoldSection, { id: "measurements", title: "Measurements", folds, children: /* @__PURE__ */ jsx23(MeasurementList, { rows: measured, look }) }),
2089
+ milling.length > 0 ? /* @__PURE__ */ jsx23(FoldSection, { id: "milling", title: "Milling considerations", folds, children: /* @__PURE__ */ jsx23(MeasurementList, { rows: milling, look }) }) : null
2090
+ ] });
2091
+ };
2092
+ var MeasurementList = ({ rows, look }) => rows.length > 0 ? /* @__PURE__ */ jsx23("dl", { className: "grid grid-cols-[1fr_auto] items-center gap-x-3 gap-y-1 text-sm", children: rows.map((row) => /* @__PURE__ */ jsxs21("div", { className: "contents", children: [
2093
+ /* @__PURE__ */ jsxs21("dt", { className: cn12("flex items-center gap-1", secondaryText(look)), children: [
2094
+ row.label,
2095
+ /* @__PURE__ */ jsx23(Derivation, { lines: row.derivation, look })
2096
+ ] }),
2097
+ /* @__PURE__ */ jsx23("dd", { className: "text-right text-gray-600 tabular-nums dark:text-zinc-100", title: row.alt, children: row.value })
2098
+ ] }, row.key)) }) : /* @__PURE__ */ jsx23(None, { look });
2099
+ var Derivation = ({
2100
+ lines,
2101
+ look
2102
+ }) => lines.length > 0 ? /* @__PURE__ */ jsx23(
2103
+ Tooltip,
2104
+ {
2105
+ side: "left",
2106
+ hoverable: true,
2107
+ tip: /* @__PURE__ */ jsx23("div", { className: "flex max-w-sm flex-col gap-0.5 font-mono text-2xs leading-snug whitespace-pre-wrap", children: lines.map((line, index) => /* @__PURE__ */ jsx23("span", { children: line }, index)) }),
2108
+ children: /* @__PURE__ */ jsx23(
2109
+ "button",
2110
+ {
2111
+ type: "button",
2112
+ "aria-label": "How this was measured",
2113
+ className: cn12(
2114
+ "shrink-0 rounded outline-none hover:text-gray-500 focus-visible:ring-2 focus-visible:ring-info/75 dark:hover:text-zinc-300",
2115
+ faintText(look)
2116
+ ),
2117
+ children: /* @__PURE__ */ jsx23(InfoIcon, { weight: "bold", className: "size-3" })
2118
+ }
2119
+ )
2120
+ }
2121
+ ) : null;
2122
+ var ReachSection2 = ({ record, units, profile, folds, action }) => {
2123
+ const curve = useMemo7(
2124
+ () => record.status === "ready" ? readReachCurve(record.value?.datasheet) : null,
2125
+ [record]
2126
+ );
2127
+ if (record.status === "ready" && !curve) return null;
2128
+ return /* @__PURE__ */ jsx23(FoldSection, { id: "reach", title: "Reach", folds, action: curve ? action : null, children: record.status === "loading" ? /* @__PURE__ */ jsx23(Reading, { children: "Reading the reach curve\u2026" }) : record.status === "error" ? /* @__PURE__ */ jsx23(ReadFailure, { failure: record }) : curve ? /* @__PURE__ */ jsx23(ReachChart, { curve, units, feature: profile }) : null });
2129
+ };
2130
+
2131
+ // src/feature-panel/candidate-list.tsx
2132
+ import { XIcon } from "@phosphor-icons/react";
2133
+ import { IconButton as IconButton4, cn as cn13 } from "@toolpath/ui";
2134
+ import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
2135
+ var candidateListKeys = listKeys("candidates");
2136
+ var faceLetter = (index) => String.fromCharCode(65 + index % 26);
2137
+ var FaceLetter = ({
2138
+ index,
2139
+ active
2140
+ }) => /* @__PURE__ */ jsx24(
2141
+ "span",
2142
+ {
2143
+ className: cn13(
2144
+ "inline-flex size-4 shrink-0 items-center justify-center rounded-sm text-2xs font-bold",
2145
+ active ? "bg-info text-white" : "bg-gray-100 text-gray-500 dark:bg-zinc-800 dark:text-zinc-300"
2146
+ ),
2147
+ children: faceLetter(index)
2148
+ }
2149
+ );
2150
+ var CandidateList = ({
2151
+ rows,
2152
+ selected,
2153
+ onSelect,
2154
+ onHover,
2155
+ face,
2156
+ look: lookOption,
2157
+ folds
2158
+ }) => {
2159
+ const look = resolveLook(lookOption);
2160
+ const count = `Candidate features (${rows.length})`;
2161
+ return /* @__PURE__ */ jsx24("div", { className: "border-b border-gray-100 p-2 dark:border-zinc-800", children: /* @__PURE__ */ jsx24(
2162
+ FoldSection,
2163
+ {
2164
+ id: face ? `candidates-${faceLetter(face.index)}` : "candidates",
2165
+ title: face ? /* @__PURE__ */ jsxs22("span", { className: "flex items-center gap-1.5", children: [
2166
+ /* @__PURE__ */ jsx24(FaceLetter, { index: face.index, active: face.active }),
2167
+ count
2168
+ ] }) : count,
2169
+ action: face?.onStop ? /* @__PURE__ */ jsx24(
2170
+ IconButton4,
2171
+ {
2172
+ "aria-label": `Stop comparing face ${faceLetter(face.index)}`,
2173
+ variant: "muted",
2174
+ size: "sm",
2175
+ onClick: face.onStop,
2176
+ children: /* @__PURE__ */ jsx24(XIcon, { weight: "bold" })
2177
+ }
2178
+ ) : null,
2179
+ folds,
2180
+ className: "px-2",
2181
+ children: /* @__PURE__ */ jsx24("ul", { className: "-mx-2 flex flex-col gap-0.5", children: rows.map((row) => /* @__PURE__ */ jsx24(
2182
+ Row,
2183
+ {
2184
+ row,
2185
+ chosen: row.feature.tag === selected,
2186
+ onSelect,
2187
+ onHover,
2188
+ look
2189
+ },
2190
+ row.feature.tag
2191
+ )) })
2192
+ }
2193
+ ) });
2194
+ };
2195
+ var Row = ({ row, chosen, onSelect, onHover, look }) => {
2196
+ const { feature } = row;
2197
+ const info = look.selected === "info";
2198
+ return /* @__PURE__ */ jsxs22(
2199
+ "li",
2200
+ {
2201
+ className: cn13(
2202
+ "flex items-center gap-1",
2203
+ info && "rounded border pr-1",
2204
+ info && (chosen ? "border-info/60 bg-info/15" : "border-transparent")
2205
+ ),
2206
+ children: [
2207
+ /* @__PURE__ */ jsxs22(
2208
+ "button",
2209
+ {
2210
+ type: "button",
2211
+ "data-list-item": "candidates",
2212
+ "data-follow": true,
2213
+ "aria-pressed": chosen,
2214
+ onClick: () => onSelect(feature.tag),
2215
+ onPointerEnter: onHover ? () => onHover(feature.tag) : void 0,
2216
+ onPointerLeave: onHover ? () => onHover(null) : void 0,
2217
+ className: cn13(
2218
+ "flex min-w-0 flex-1 cursor-pointer flex-col rounded px-2 py-1.5 text-left text-sm",
2219
+ "outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2220
+ "text-gray-600 dark:text-zinc-200",
2221
+ info ? "hover:bg-gray-50 dark:hover:bg-zinc-800/60" : "hover:bg-gray-100 dark:hover:bg-zinc-800",
2222
+ chosen && (info ? "font-medium" : "bg-gray-100 font-semibold dark:bg-zinc-800")
2223
+ ),
2224
+ children: [
2225
+ /* @__PURE__ */ jsxs22("span", { className: "flex w-full items-center gap-2", children: [
2226
+ row.color ? /* @__PURE__ */ jsx24(Swatch, { color: row.color }) : /* @__PURE__ */ jsx24("span", { "aria-hidden": true, className: "size-4 shrink-0" }),
2227
+ /* @__PURE__ */ jsx24("span", { className: "min-w-0 flex-1 truncate", children: /* @__PURE__ */ jsx24(FeatureName, { feature, label: row.label, look }) }),
2228
+ row.ruleCount ? /* @__PURE__ */ jsxs22("span", { className: cn13("text-2xs tabular-nums", secondaryText(look)), children: [
2229
+ row.ruleCount,
2230
+ " ",
2231
+ row.ruleCount === 1 ? "rule" : "rules"
2232
+ ] }) : null,
2233
+ row.required ? /* @__PURE__ */ jsx24(RequiredBadge, {}) : null,
2234
+ row.detail ? /* @__PURE__ */ jsx24("span", { className: cn13("max-w-32 shrink-0 truncate text-xs", secondaryText(look)), children: row.detail }) : null
2235
+ ] }),
2236
+ row.note ? /* @__PURE__ */ jsx24("span", { className: "pl-6 text-2xs text-warning-darken", children: row.note }) : null
2237
+ ]
2238
+ }
2239
+ ),
2240
+ row.action
2241
+ ]
2242
+ }
2243
+ );
2244
+ };
2245
+
2246
+ // src/feature-panel/comparison-table.tsx
2247
+ import { useMemo as useMemo8 } from "react";
2248
+ import { cn as cn14 } from "@toolpath/ui";
2249
+ import { jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
2250
+ var DASH = "\u2014";
2251
+ var ComparisonTable = ({
2252
+ faces,
2253
+ active,
2254
+ onSelect,
2255
+ onHover,
2256
+ look: lookOption,
2257
+ folds
2258
+ }) => {
2259
+ const look = resolveLook(lookOption);
2260
+ const rows = useMemo8(() => {
2261
+ const row = (key, label, read) => {
2262
+ const values = faces.map((face) => face.feature ? read(face, face.feature) : null);
2263
+ const texts = values.map((value) => value?.text ?? DASH);
2264
+ return {
2265
+ key,
2266
+ label,
2267
+ cells: values.map((value) => value?.node ?? value?.text ?? DASH),
2268
+ differs: new Set(texts).size > 1
2269
+ };
2270
+ };
2271
+ const fixed = [
2272
+ row("type", "Type", (_, feature) => ({ text: featureTypeLabel(feature.featureType) })),
2273
+ row("direction", "Machined from", (face, feature) => {
2274
+ const text = directionLabel(feature.machiningDirection);
2275
+ return {
2276
+ text,
2277
+ node: /* @__PURE__ */ jsxs23("span", { className: "inline-flex items-center gap-1", children: [
2278
+ /* @__PURE__ */ jsx25(Swatch, { color: face.directionColor, className: "size-2.5" }),
2279
+ text
2280
+ ] })
2281
+ };
2282
+ }),
2283
+ ...faces.some((face) => face.required !== void 0) ? [row("required", "Required", (face) => ({ text: face.required ? "Yes" : "No" }))] : [],
2284
+ row("rules", "Rules broken", (face) => ({
2285
+ text: face.rules.map((rule) => rule.key).join(",") || "None",
2286
+ node: face.rules.length > 0 ? /* @__PURE__ */ jsxs23(
2287
+ "span",
2288
+ {
2289
+ className: "inline-flex items-center gap-0.5",
2290
+ title: face.rules.map((rule) => rule.text).join("\n"),
2291
+ children: [
2292
+ face.rules.map((rule) => /* @__PURE__ */ jsx25(Swatch, { color: rule.color, className: "size-2.5" }, rule.key)),
2293
+ /* @__PURE__ */ jsx25("span", { className: "ml-0.5", children: face.rules.length })
2294
+ ]
2295
+ }
2296
+ ) : "None"
2297
+ }))
2298
+ ];
2299
+ const labels = /* @__PURE__ */ new Map();
2300
+ for (const face of faces)
2301
+ for (const each of face.measurements) labels.set(each.key, labels.get(each.key) ?? each.label);
2302
+ const measured = [...labels].map(([key, label]) => {
2303
+ const values = faces.map((face) => face.measurements.find((each) => each.key === key));
2304
+ return {
2305
+ key,
2306
+ label,
2307
+ cells: values.map((value) => value ? /* @__PURE__ */ jsx25("span", { title: value.alt, children: value.value }) : DASH),
2308
+ differs: new Set(values.map((value) => value?.value ?? DASH)).size > 1
2309
+ };
2310
+ });
2311
+ return [...fixed, ...measured];
2312
+ }, [faces]);
2313
+ return /* @__PURE__ */ jsx25("div", { className: "border-b border-gray-100 p-4 dark:border-zinc-800", children: /* @__PURE__ */ jsx25(FoldSection, { id: "comparison", title: "Comparison", folds, children: /* @__PURE__ */ jsx25("div", { className: "-mx-1 overflow-x-auto", children: /* @__PURE__ */ jsxs23("table", { className: "w-full border-separate border-spacing-x-1 border-spacing-y-0.5 text-sm", children: [
2314
+ /* @__PURE__ */ jsx25("thead", { children: /* @__PURE__ */ jsxs23("tr", { children: [
2315
+ /* @__PURE__ */ jsx25("th", { className: "w-0" }),
2316
+ faces.map(({ feature }, index) => /* @__PURE__ */ jsx25("th", { className: "text-left align-bottom font-normal", children: /* @__PURE__ */ jsxs23(
2317
+ "button",
2318
+ {
2319
+ type: "button",
2320
+ disabled: !feature,
2321
+ onClick: () => feature && onSelect(feature.tag, index),
2322
+ onPointerEnter: onHover ? () => onHover(feature?.tag ?? null) : void 0,
2323
+ onPointerLeave: onHover ? () => onHover(null) : void 0,
2324
+ className: cn14(
2325
+ "flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded px-1 py-1 text-left text-xs",
2326
+ "outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2327
+ "text-gray-600 hover:bg-gray-100 dark:text-zinc-200 dark:hover:bg-zinc-800",
2328
+ index === active && "bg-gray-100 font-semibold dark:bg-zinc-800"
2329
+ ),
2330
+ children: [
2331
+ /* @__PURE__ */ jsx25(FaceLetter, { index, active: index === active }),
2332
+ /* @__PURE__ */ jsx25("span", { className: "min-w-0 truncate", children: feature ? /* @__PURE__ */ jsx25(FeatureName, { feature, look }) : "No feature" })
2333
+ ]
2334
+ }
2335
+ ) }, index))
2336
+ ] }) }),
2337
+ /* @__PURE__ */ jsx25("tbody", { children: rows.map((row) => /* @__PURE__ */ jsxs23("tr", { "data-differs": row.differs || void 0, children: [
2338
+ /* @__PURE__ */ jsx25(
2339
+ "th",
2340
+ {
2341
+ scope: "row",
2342
+ className: cn14(
2343
+ "pr-2 text-left align-top font-normal whitespace-nowrap",
2344
+ secondaryText(look)
2345
+ ),
2346
+ children: row.label
2347
+ }
2348
+ ),
2349
+ row.cells.map((cell, index) => /* @__PURE__ */ jsx25(
2350
+ "td",
2351
+ {
2352
+ className: cn14(
2353
+ "px-1 align-top tabular-nums whitespace-nowrap",
2354
+ row.differs ? "text-gray-600 dark:text-zinc-100" : "text-gray-400 dark:text-zinc-500"
2355
+ ),
2356
+ children: cell
2357
+ },
2358
+ index
2359
+ ))
2360
+ ] }, row.key)) })
2361
+ ] }) }) }) });
2362
+ };
2363
+
2364
+ // src/feature-panel/feature-panel.tsx
2365
+ import { CubeIcon, XIcon as XIcon2 } from "@phosphor-icons/react";
2366
+ import { IconButton as IconButton5, ScrollArea as ScrollArea2, cn as cn15 } from "@toolpath/ui";
2367
+ import { Fragment as Fragment11, jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
2368
+ var title = (faces, active, group) => {
2369
+ if (faces.length > 1) return `Comparing ${faces.length} faces`;
2370
+ if (faces[active]?.region !== null && faces[active]?.region !== void 0)
2371
+ return "Features on this face";
2372
+ if (group !== void 0) return `${group} identical holes`;
2373
+ return "Feature";
2374
+ };
2375
+ var FeaturePanel = ({
2376
+ faces,
2377
+ active,
2378
+ group,
2379
+ comparison,
2380
+ onSelect,
2381
+ onDropFace,
2382
+ onHover,
2383
+ onClose,
2384
+ headerProps,
2385
+ className,
2386
+ look: lookOption,
2387
+ folds,
2388
+ children
2389
+ }) => {
2390
+ const look = resolveLook(lookOption);
2391
+ const comparing = faces.length > 1;
2392
+ const face = faces[active];
2393
+ const heading = title(faces, active, group);
2394
+ return /* @__PURE__ */ jsxs24(
2395
+ "section",
2396
+ {
2397
+ "aria-label": heading,
2398
+ "data-list-root": true,
2399
+ onKeyDown: (event) => {
2400
+ if (event.defaultPrevented || !event.currentTarget.contains(event.target)) return;
2401
+ if (event.key === "Escape") onClose();
2402
+ else candidateListKeys(event);
2403
+ },
2404
+ className: cn15(
2405
+ "flex h-full min-h-0 flex-col overflow-hidden rounded bg-white/90 shadow-lg ring-1 ring-gray-200/70 backdrop-blur-md",
2406
+ "dark:bg-zinc-900/90 dark:ring-zinc-800",
2407
+ className
2408
+ ),
2409
+ children: [
2410
+ /* @__PURE__ */ jsxs24(
2411
+ "header",
2412
+ {
2413
+ ...headerProps,
2414
+ className: cn15(
2415
+ "flex items-center gap-2 border-b border-gray-100 py-2 pr-2 pl-4 select-none dark:border-zinc-800",
2416
+ headerProps?.className
2417
+ ),
2418
+ children: [
2419
+ /* @__PURE__ */ jsx26(CubeIcon, { weight: "bold", className: cn15("size-5 shrink-0", secondaryText(look)) }),
2420
+ /* @__PURE__ */ jsx26("h2", { className: "flex-1 text-sm font-semibold text-gray-600 dark:text-zinc-100", children: heading }),
2421
+ /* @__PURE__ */ jsx26(IconButton5, { "aria-label": "Close", variant: "muted", onClick: onClose, children: /* @__PURE__ */ jsx26(XIcon2, { weight: "bold" }) })
2422
+ ]
2423
+ }
2424
+ ),
2425
+ /* @__PURE__ */ jsxs24(ScrollArea2, { className: "min-h-0 flex-1", children: [
2426
+ comparing ? /* @__PURE__ */ jsxs24(Fragment11, { children: [
2427
+ faces.map((each, index) => /* @__PURE__ */ jsx26(
2428
+ CandidateList,
2429
+ {
2430
+ rows: each.rows,
2431
+ selected: each.selected,
2432
+ onSelect: (tag) => onSelect(tag, index),
2433
+ onHover,
2434
+ face: {
2435
+ index,
2436
+ active: index === active,
2437
+ ...onDropFace ? { onStop: () => onDropFace(index) } : {}
2438
+ },
2439
+ look: lookOption,
2440
+ folds
2441
+ },
2442
+ `${each.region ?? "list"}-${index}`
2443
+ )),
2444
+ comparison ? /* @__PURE__ */ jsx26(
2445
+ ComparisonTable,
2446
+ {
2447
+ faces: comparison,
2448
+ active,
2449
+ onSelect,
2450
+ onHover,
2451
+ look: lookOption,
2452
+ folds
2453
+ }
2454
+ ) : null
2455
+ ] }) : face && face.region !== null ? /* @__PURE__ */ jsx26(
2456
+ CandidateList,
2457
+ {
2458
+ rows: face.rows,
2459
+ selected: face.selected,
2460
+ onSelect: (tag) => onSelect(tag, active),
2461
+ onHover,
2462
+ look: lookOption,
2463
+ folds
2464
+ }
2465
+ ) : null,
2466
+ face?.selected ? children : /* @__PURE__ */ jsx26("p", { className: cn15("px-4 py-3 text-sm", secondaryText(look)), children: face && face.rows.length > 0 ? "Choose a feature to read its details." : "No feature owns this face." })
2467
+ ] })
2468
+ ]
2469
+ }
2470
+ );
2471
+ };
1620
2472
  export {
2473
+ CandidateList,
2474
+ ComparisonTable,
2475
+ FeatureDetails,
2476
+ FeaturePanel,
1621
2477
  ReachChart,
1622
2478
  ReachDrawing,
1623
2479
  RuleList,
1624
2480
  Swatch,
2481
+ candidateListKeys,
1625
2482
  isField,
1626
2483
  listKeys,
1627
- ruleListKeys
2484
+ ruleListKeys,
2485
+ storageFolds
1628
2486
  };