efficient-shell 0.0.0-stage → 4.126.2

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.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +753 -2
  3. package/dist/Browser-DBRSIQ4J.js +478 -0
  4. package/dist/Browser-DBRSIQ4J.js.map +1 -0
  5. package/dist/Calculator-HCKFTSJR.js +188 -0
  6. package/dist/Calculator-HCKFTSJR.js.map +1 -0
  7. package/dist/ConfirmDialog-LJXMB45A.js +3 -0
  8. package/dist/ConfirmDialog-LJXMB45A.js.map +1 -0
  9. package/dist/CurrencyConverter-BKKORN6K.js +232 -0
  10. package/dist/CurrencyConverter-BKKORN6K.js.map +1 -0
  11. package/dist/Documents-5MD5SKPB.js +437 -0
  12. package/dist/Documents-5MD5SKPB.js.map +1 -0
  13. package/dist/Files-KY35DPWB.js +801 -0
  14. package/dist/Files-KY35DPWB.js.map +1 -0
  15. package/dist/Notepad-2BO5VEUG.js +406 -0
  16. package/dist/Notepad-2BO5VEUG.js.map +1 -0
  17. package/dist/PdfViewer-LSP3YNFC.js +4 -0
  18. package/dist/PdfViewer-LSP3YNFC.js.map +1 -0
  19. package/dist/PomodoroTimer-SVJ62RXM.js +629 -0
  20. package/dist/PomodoroTimer-SVJ62RXM.js.map +1 -0
  21. package/dist/Preview-N4UYYXG6.js +2955 -0
  22. package/dist/Preview-N4UYYXG6.js.map +1 -0
  23. package/dist/Sidebar-J76PV4QF.js +290 -0
  24. package/dist/Sidebar-J76PV4QF.js.map +1 -0
  25. package/dist/Spreadsheet-5EWZGMXN.js +515 -0
  26. package/dist/Spreadsheet-5EWZGMXN.js.map +1 -0
  27. package/dist/Stock-CDUS4VCZ.js +154 -0
  28. package/dist/Stock-CDUS4VCZ.js.map +1 -0
  29. package/dist/Weather-EEDBWBPQ.js +427 -0
  30. package/dist/Weather-EEDBWBPQ.js.map +1 -0
  31. package/dist/WorldClock-FFFULVTJ.js +198 -0
  32. package/dist/WorldClock-FFFULVTJ.js.map +1 -0
  33. package/dist/apps/index.d.ts +267 -0
  34. package/dist/apps/index.js +147 -0
  35. package/dist/apps/index.js.map +1 -0
  36. package/dist/brand.css +2 -0
  37. package/dist/chunk-2VM2FRZJ.js +215 -0
  38. package/dist/chunk-2VM2FRZJ.js.map +1 -0
  39. package/dist/chunk-32W6VY2J.js +549 -0
  40. package/dist/chunk-32W6VY2J.js.map +1 -0
  41. package/dist/chunk-3CUSBZWG.js +59 -0
  42. package/dist/chunk-3CUSBZWG.js.map +1 -0
  43. package/dist/chunk-3L463T5H.js +26 -0
  44. package/dist/chunk-3L463T5H.js.map +1 -0
  45. package/dist/chunk-4TVV2BIU.js +13334 -0
  46. package/dist/chunk-4TVV2BIU.js.map +1 -0
  47. package/dist/chunk-6XQHH7D2.js +105 -0
  48. package/dist/chunk-6XQHH7D2.js.map +1 -0
  49. package/dist/chunk-7CCHEEYC.js +94 -0
  50. package/dist/chunk-7CCHEEYC.js.map +1 -0
  51. package/dist/chunk-7KIOEKHS.js +617 -0
  52. package/dist/chunk-7KIOEKHS.js.map +1 -0
  53. package/dist/chunk-AXWC3332.js +1103 -0
  54. package/dist/chunk-AXWC3332.js.map +1 -0
  55. package/dist/chunk-BFBWLCO6.js +16 -0
  56. package/dist/chunk-BFBWLCO6.js.map +1 -0
  57. package/dist/chunk-C3NELUTX.js +79 -0
  58. package/dist/chunk-C3NELUTX.js.map +1 -0
  59. package/dist/chunk-CT3HVZRZ.js +26 -0
  60. package/dist/chunk-CT3HVZRZ.js.map +1 -0
  61. package/dist/chunk-D7PYW2QS.js +265 -0
  62. package/dist/chunk-D7PYW2QS.js.map +1 -0
  63. package/dist/chunk-DMCFOWOJ.js +153 -0
  64. package/dist/chunk-DMCFOWOJ.js.map +1 -0
  65. package/dist/chunk-FQB24B42.js +416 -0
  66. package/dist/chunk-FQB24B42.js.map +1 -0
  67. package/dist/chunk-GA35NEND.js +56 -0
  68. package/dist/chunk-GA35NEND.js.map +1 -0
  69. package/dist/chunk-HNH7FOYB.js +69 -0
  70. package/dist/chunk-HNH7FOYB.js.map +1 -0
  71. package/dist/chunk-HYZ5QMVO.js +462 -0
  72. package/dist/chunk-HYZ5QMVO.js.map +1 -0
  73. package/dist/chunk-I464GYJJ.js +17 -0
  74. package/dist/chunk-I464GYJJ.js.map +1 -0
  75. package/dist/chunk-JFQGPY7W.js +819 -0
  76. package/dist/chunk-JFQGPY7W.js.map +1 -0
  77. package/dist/chunk-MGGY74RY.js +14 -0
  78. package/dist/chunk-MGGY74RY.js.map +1 -0
  79. package/dist/chunk-N3CG5HG5.js +144 -0
  80. package/dist/chunk-N3CG5HG5.js.map +1 -0
  81. package/dist/chunk-W6XTG23K.js +3708 -0
  82. package/dist/chunk-W6XTG23K.js.map +1 -0
  83. package/dist/chunk-YG45DAUT.js +96 -0
  84. package/dist/chunk-YG45DAUT.js.map +1 -0
  85. package/dist/chunk-YMGXZABY.js +7 -0
  86. package/dist/chunk-YMGXZABY.js.map +1 -0
  87. package/dist/chunk-YNGPTOL6.js +46 -0
  88. package/dist/chunk-YNGPTOL6.js.map +1 -0
  89. package/dist/chunk-YNHLQ24V.js +400 -0
  90. package/dist/chunk-YNHLQ24V.js.map +1 -0
  91. package/dist/chunk-YNKK7NE3.js +171 -0
  92. package/dist/chunk-YNKK7NE3.js.map +1 -0
  93. package/dist/chunk-YPO6RDMX.js +20 -0
  94. package/dist/chunk-YPO6RDMX.js.map +1 -0
  95. package/dist/file-intake/index.d.ts +87 -0
  96. package/dist/file-intake/index.js +3 -0
  97. package/dist/file-intake/index.js.map +1 -0
  98. package/dist/index-dQia6NpB.d.ts +5904 -0
  99. package/dist/index.d.ts +2495 -0
  100. package/dist/index.js +7460 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/markdown/index.d.ts +118 -0
  103. package/dist/markdown/index.js +184 -0
  104. package/dist/markdown/index.js.map +1 -0
  105. package/dist/markup/index.d.ts +235 -0
  106. package/dist/markup/index.js +135 -0
  107. package/dist/markup/index.js.map +1 -0
  108. package/dist/page-tabs/index.d.ts +154 -0
  109. package/dist/page-tabs/index.js +706 -0
  110. package/dist/page-tabs/index.js.map +1 -0
  111. package/dist/shell.css +105 -0
  112. package/dist/sounds-NT4DEZGD.js +3 -0
  113. package/dist/sounds-NT4DEZGD.js.map +1 -0
  114. package/dist/styles.css +22 -0
  115. package/dist/themes.css +249 -0
  116. package/dist/types-BOinJXUf.d.ts +118 -0
  117. package/dist/ui/index.d.ts +4 -0
  118. package/dist/ui/index.js +15 -0
  119. package/dist/ui/index.js.map +1 -0
  120. package/dist/ui.css +1518 -0
  121. package/package.json +178 -4
@@ -0,0 +1,2955 @@
1
+ import { PdfViewer, ToolbarSlotContext, PanelActions } from './chunk-FQB24B42.js';
2
+ import { ImageAnnotator_default } from './chunk-AXWC3332.js';
3
+ import { AboutApp } from './chunk-C3NELUTX.js';
4
+ import { peekPdfPreviewStage, claimPdfPreviewStage, PDF_PREVIEW_UPDATE_EVENT } from './chunk-3L463T5H.js';
5
+ import { WindowTitle, getActiveModalId } from './chunk-W6XTG23K.js';
6
+ import './chunk-GA35NEND.js';
7
+ import './chunk-YMGXZABY.js';
8
+ import { toast_default } from './chunk-2VM2FRZJ.js';
9
+ import './chunk-YNHLQ24V.js';
10
+ import './chunk-7KIOEKHS.js';
11
+ import { registerModalEscapeInterceptor } from './chunk-HYZ5QMVO.js';
12
+ import './chunk-3CUSBZWG.js';
13
+ import { useRef, useState, useEffect } from 'react';
14
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
15
+
16
+ var _stencilContextPatched = false;
17
+ function ensureStencilContextAttribute() {
18
+ if (_stencilContextPatched) return;
19
+ if (typeof HTMLCanvasElement === "undefined") return;
20
+ _stencilContextPatched = true;
21
+ const orig = HTMLCanvasElement.prototype.getContext;
22
+ HTMLCanvasElement.prototype.getContext = function patchedGetContext(type, attrs) {
23
+ if (type === "webgl" || type === "webgl2" || type === "experimental-webgl") {
24
+ attrs = { ...attrs || {}, stencil: true };
25
+ }
26
+ return orig.call(this, type, attrs);
27
+ };
28
+ console.info("[Preview] section: canvas getContext patched (force stencil=true)");
29
+ }
30
+ var TITLE_DISPLAY_MAX = 24;
31
+ function truncateForTitle(s) {
32
+ return s.length > TITLE_DISPLAY_MAX ? `${s.slice(0, TITLE_DISPLAY_MAX - 1)}\u2026` : s;
33
+ }
34
+ function swapPreviewData(prev, next) {
35
+ if (prev?.url && prev.url !== next.url && prev.url.startsWith("blob:")) {
36
+ URL.revokeObjectURL(prev.url);
37
+ }
38
+ return next;
39
+ }
40
+ function Preview() {
41
+ const consumedRef = useRef(void 0);
42
+ if (consumedRef.current === void 0) {
43
+ consumedRef.current = peekPdfPreviewStage();
44
+ }
45
+ const [data, setData] = useState(consumedRef.current?.data ?? null);
46
+ useEffect(() => {
47
+ const stage = consumedRef.current;
48
+ if (stage == null) return;
49
+ claimPdfPreviewStage(stage);
50
+ setData((prev) => prev === stage.data ? prev : swapPreviewData(prev, stage.data));
51
+ }, []);
52
+ useEffect(() => {
53
+ const myToken = consumedRef.current?.token;
54
+ if (myToken == null) return;
55
+ const handler = (e) => {
56
+ const detail = e.detail;
57
+ if (!detail || detail.token !== myToken) return;
58
+ setData((prev) => swapPreviewData(prev, detail.data));
59
+ };
60
+ window.addEventListener(PDF_PREVIEW_UPDATE_EVENT, handler);
61
+ return () => window.removeEventListener(PDF_PREVIEW_UPDATE_EVENT, handler);
62
+ }, []);
63
+ const shownUrlRef = useRef(void 0);
64
+ useEffect(() => {
65
+ shownUrlRef.current = data?.url;
66
+ }, [data?.url]);
67
+ const pendingRevokeRef = useRef(null);
68
+ useEffect(() => {
69
+ if (pendingRevokeRef.current !== null) {
70
+ clearTimeout(pendingRevokeRef.current);
71
+ pendingRevokeRef.current = null;
72
+ }
73
+ return () => {
74
+ const url = shownUrlRef.current;
75
+ if (url?.startsWith("blob:")) pendingRevokeRef.current = setTimeout(() => URL.revokeObjectURL(url), 0);
76
+ };
77
+ }, []);
78
+ const titleName = data?.filename ? truncateForTitle(data.filename) : "Untitled";
79
+ const fileRef = useRef(null);
80
+ const rootRef = useRef(null);
81
+ const [toolbarSlotEl, setToolbarSlotEl] = useState(null);
82
+ const [isDragging, setIsDragging] = useState(false);
83
+ const dragDepthRef = useRef(0);
84
+ const handlePick = () => fileRef.current?.click();
85
+ const resetDrag = () => {
86
+ dragDepthRef.current = 0;
87
+ setIsDragging(false);
88
+ };
89
+ const isActiveWindow = (el) => {
90
+ const myModal = el.closest("[data-modal-id]");
91
+ if (!myModal) return true;
92
+ return getActiveModalId() === myModal.dataset.modalId;
93
+ };
94
+ useEffect(() => {
95
+ const onWindowDragEnd = () => resetDrag();
96
+ const onWindowDrop = () => resetDrag();
97
+ const onKey = (e) => {
98
+ if (e.key === "Escape") resetDrag();
99
+ };
100
+ window.addEventListener("dragend", onWindowDragEnd);
101
+ window.addEventListener("drop", onWindowDrop);
102
+ window.addEventListener("keydown", onKey);
103
+ return () => {
104
+ window.removeEventListener("dragend", onWindowDragEnd);
105
+ window.removeEventListener("drop", onWindowDrop);
106
+ window.removeEventListener("keydown", onKey);
107
+ };
108
+ }, []);
109
+ const ingestFile = (file) => {
110
+ const ext = (file.name.split(".").pop() || "").toLowerCase();
111
+ const kind = ext === "pdf" ? "pdf" : ext === "dxf" ? "dxf" : ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif", "bmp"].includes(ext) ? "image" : ["stp", "step", "stl", "obj", "gltf", "glb", "3mf", "iges", "igs", "ply", "fbx"].includes(ext) ? "3d" : void 0;
112
+ if (!kind) {
113
+ if (ext === "dwg") toast_default.error("DWG files need server-side conversion. Convert to PDF or DXF first.");
114
+ else toast_default.error(`Unsupported file type: .${ext || "unknown"}`);
115
+ return;
116
+ }
117
+ const url = URL.createObjectURL(file);
118
+ setData((prev) => {
119
+ if (prev?.url?.startsWith("blob:") && prev.url !== url) {
120
+ URL.revokeObjectURL(prev.url);
121
+ }
122
+ return { url, filename: file.name, kind };
123
+ });
124
+ };
125
+ const handleFile = (e) => {
126
+ const file = e.target.files?.[0];
127
+ if (file) ingestFile(file);
128
+ if (fileRef.current) fileRef.current.value = "";
129
+ };
130
+ const handleDrop = (e) => {
131
+ e.preventDefault();
132
+ resetDrag();
133
+ const file = e.dataTransfer.files?.[0];
134
+ if (file) ingestFile(file);
135
+ };
136
+ const Toolbar = /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-gray-200 bg-gray-50 shrink-0", children: [
137
+ /* @__PURE__ */ jsx(
138
+ "input",
139
+ {
140
+ ref: fileRef,
141
+ type: "file",
142
+ accept: ".pdf,.dxf,.jpg,.jpeg,.png,.gif,.webp,.svg,.avif,.bmp,.stp,.step,.stl,.obj,.gltf,.glb,.3mf,.iges,.igs,.ply,.fbx",
143
+ onChange: handleFile,
144
+ className: "hidden"
145
+ }
146
+ ),
147
+ /* @__PURE__ */ jsxs(
148
+ "button",
149
+ {
150
+ onClick: handlePick,
151
+ className: "text-xs text-gray-700 hover:text-gray-900 px-2 py-1 rounded hover:bg-gray-200 transition-colors flex items-center gap-1",
152
+ children: [
153
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776" }) }),
154
+ "Open"
155
+ ]
156
+ }
157
+ ),
158
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-400 ml-1", children: "PDF \xB7 DXF \xB7 3D \xB7 Images" }),
159
+ data?.filename && /* @__PURE__ */ jsxs(Fragment, { children: [
160
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300 mx-1" }),
161
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-700 truncate max-w-[200px]", title: data.filename, children: data.filename })
162
+ ] }),
163
+ /* @__PURE__ */ jsx("div", { ref: setToolbarSlotEl, className: "ml-auto flex items-center gap-1 text-xs" })
164
+ ] });
165
+ let body;
166
+ if (!data) {
167
+ body = /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center text-gray-500 text-sm gap-3 p-8 text-center", children: [
168
+ /* @__PURE__ */ jsx("svg", { className: "h-12 w-12 text-gray-300", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" }) }),
169
+ /* @__PURE__ */ jsxs("p", { className: "font-medium text-gray-700", children: [
170
+ "Drop a file here, or click ",
171
+ /* @__PURE__ */ jsx("button", { onClick: handlePick, className: "text-blue-600 hover:underline", children: "Open" }),
172
+ "."
173
+ ] }),
174
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-500 max-w-sm", children: [
175
+ /* @__PURE__ */ jsx("p", { className: "font-semibold uppercase tracking-wide text-[10px] text-gray-400 mb-1", children: "Supported formats" }),
176
+ /* @__PURE__ */ jsxs("ul", { className: "space-y-0.5", children: [
177
+ /* @__PURE__ */ jsxs("li", { children: [
178
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-700", children: ".pdf" }),
179
+ " \u2014 multi-page document viewer"
180
+ ] }),
181
+ /* @__PURE__ */ jsxs("li", { children: [
182
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-700", children: ".dxf" }),
183
+ " \u2014 vector CAD drawings (optional ",
184
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: "dxf-viewer" }),
185
+ " peer dep)"
186
+ ] }),
187
+ /* @__PURE__ */ jsxs("li", { children: [
188
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-700", children: ".stp .step .stl .obj .gltf .glb .3mf .iges" }),
189
+ " \u2014 3D models (optional ",
190
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: "online-3d-viewer" }),
191
+ " peer dep)"
192
+ ] }),
193
+ /* @__PURE__ */ jsxs("li", { children: [
194
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-gray-700", children: ".jpg .jpeg .png .gif .webp .svg .avif .bmp" }),
195
+ " \u2014 raster images"
196
+ ] })
197
+ ] }),
198
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-[11px] text-gray-400 italic", children: "DWG files need to be converted to PDF or DXF first (server-side)." })
199
+ ] })
200
+ ] });
201
+ } else if (data.converting || !data.url) {
202
+ body = /* @__PURE__ */ jsx(ConvertingPanel, { filename: data.filename, message: data.convertingMessage });
203
+ } else if (data.kind === "dxf") {
204
+ body = /* @__PURE__ */ jsx(DxfPanel, { url: data.url, filename: data.filename, onDownload: data.onDownload, onEmail: data.onEmail }, data.url);
205
+ } else if (data.kind === "3d") {
206
+ body = /* @__PURE__ */ jsx(StepPanel, { url: data.url, filename: data.filename, onDownload: data.onDownload, onEmail: data.onEmail }, data.url);
207
+ } else if (data.kind === "image") {
208
+ body = /* @__PURE__ */ jsx(ImagePanel, { url: data.url, filename: data.filename, onDownload: data.onDownload, onEmail: data.onEmail }, data.url);
209
+ } else {
210
+ body = /* @__PURE__ */ jsx(PdfViewer, { url: data.url, filename: data.filename, onDownload: data.onDownload, onEmail: data.onEmail }, data.url);
211
+ }
212
+ return /* @__PURE__ */ jsxs(
213
+ "div",
214
+ {
215
+ className: "relative flex flex-col h-full",
216
+ ref: rootRef,
217
+ onDragEnter: (e) => {
218
+ if (!e.dataTransfer?.types?.includes?.("Files")) return;
219
+ if (!isActiveWindow(e.currentTarget)) return;
220
+ e.preventDefault();
221
+ dragDepthRef.current++;
222
+ if (!isDragging) setIsDragging(true);
223
+ },
224
+ onDragOver: (e) => {
225
+ if (!e.dataTransfer?.types?.includes?.("Files")) return;
226
+ if (!isActiveWindow(e.currentTarget)) return;
227
+ e.preventDefault();
228
+ },
229
+ onDragLeave: () => {
230
+ if (dragDepthRef.current > 0) dragDepthRef.current--;
231
+ if (dragDepthRef.current === 0) setIsDragging(false);
232
+ },
233
+ onDrop: (e) => {
234
+ if (!isActiveWindow(e.currentTarget)) {
235
+ resetDrag();
236
+ return;
237
+ }
238
+ handleDrop(e);
239
+ },
240
+ children: [
241
+ /* @__PURE__ */ jsx(WindowTitle, { title: `${titleName} - Preview` }),
242
+ /* @__PURE__ */ jsx(AboutApp, { app: "preview" }),
243
+ Toolbar,
244
+ /* @__PURE__ */ jsx(ToolbarSlotContext.Provider, { value: toolbarSlotEl, children: /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: body }) }),
245
+ isDragging && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-blue-500/15 border-4 border-dashed border-blue-500 pointer-events-none flex items-center justify-center z-20", children: /* @__PURE__ */ jsx("div", { className: "px-4 py-2 rounded-md bg-blue-600 text-white text-sm font-medium shadow-lg", children: "Drop to open" }) })
246
+ ]
247
+ }
248
+ );
249
+ }
250
+ function ConvertingPanel({ filename, message }) {
251
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center h-full bg-gray-100 gap-4 px-8", children: [
252
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
253
+ /* @__PURE__ */ jsxs("svg", { className: "h-12 w-12 text-blue-500 animate-spin", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: [
254
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.2" }),
255
+ /* @__PURE__ */ jsx("path", { d: "M22 12a10 10 0 0 1-10 10", strokeLinecap: "round" })
256
+ ] }),
257
+ /* @__PURE__ */ jsx("div", { className: "text-base font-semibold tracking-wide text-gray-700 uppercase", children: message || "Converting file" }),
258
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-400 truncate max-w-md", children: filename })
259
+ ] }),
260
+ /* @__PURE__ */ jsx("div", { className: "w-72 h-1.5 rounded-full bg-gray-200 overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "h-full w-1/3 bg-blue-500 rounded-full animate-pulse", style: { animation: "preview-bar 1.4s ease-in-out infinite" } }) }),
261
+ /* @__PURE__ */ jsx("style", { children: `@keyframes preview-bar { 0% { transform: translateX(-110%); } 100% { transform: translateX(310%); } }` })
262
+ ] });
263
+ }
264
+ var DEFAULT_DXF_FONTS = [
265
+ "https://cdn.jsdelivr.net/gh/vagran/dxf-viewer-example-src@master/src/assets/fonts/Roboto-LightItalic.ttf",
266
+ "https://cdn.jsdelivr.net/gh/vagran/dxf-viewer-example-src@master/src/assets/fonts/NotoSansDisplay-SemiCondensedLightItalic.ttf",
267
+ "https://cdn.jsdelivr.net/gh/vagran/dxf-viewer-example-src@master/src/assets/fonts/NanumGothic-Regular.ttf"
268
+ ];
269
+ var DXF_EDIT_COMMANDS = {
270
+ l: "LINE",
271
+ line: "LINE",
272
+ pl: "PLINE",
273
+ pline: "PLINE",
274
+ ex: "EXTEND",
275
+ extend: "EXTEND",
276
+ tr: "TRIM",
277
+ trim: "TRIM",
278
+ e: "ERASE",
279
+ erase: "ERASE",
280
+ m: "MOVE",
281
+ move: "MOVE",
282
+ co: "COPY",
283
+ cp: "COPY",
284
+ copy: "COPY",
285
+ o: "OFFSET",
286
+ offset: "OFFSET",
287
+ f: "FILLET",
288
+ fillet: "FILLET",
289
+ cha: "CHAMFER",
290
+ chamfer: "CHAMFER",
291
+ x: "EXPLODE",
292
+ explode: "EXPLODE",
293
+ mi: "MIRROR",
294
+ mirror: "MIRROR",
295
+ ro: "ROTATE",
296
+ rotate: "ROTATE",
297
+ sc: "SCALE",
298
+ scale: "SCALE",
299
+ s: "STRETCH",
300
+ stretch: "STRETCH",
301
+ ar: "ARRAY",
302
+ array: "ARRAY",
303
+ rec: "RECTANG",
304
+ rectang: "RECTANG",
305
+ rectangle: "RECTANG",
306
+ c: "CIRCLE",
307
+ circle: "CIRCLE",
308
+ a: "ARC",
309
+ arc: "ARC",
310
+ el: "ELLIPSE",
311
+ ellipse: "ELLIPSE",
312
+ t: "MTEXT",
313
+ mt: "MTEXT",
314
+ mtext: "MTEXT",
315
+ text: "TEXT",
316
+ b: "BLOCK",
317
+ block: "BLOCK",
318
+ i: "INSERT",
319
+ insert: "INSERT",
320
+ ha: "HATCH",
321
+ hatch: "HATCH"
322
+ };
323
+ var DXF_CMD_HELP = "Commands: DI distance \xB7 DIM/DLI linear dim \xB7 H / V force axis \xB7 <number> lock \u0394 \xB7 U undo pick \xB7 Z fit \xB7 LA layers \xB7 Esc exit";
324
+ function DxfPanel({ url, filename, onDownload, onEmail }) {
325
+ const containerRef = useRef(null);
326
+ const viewerRef = useRef(null);
327
+ const [loading, setLoading] = useState(true);
328
+ const [error, setError] = useState(null);
329
+ const [layers, setLayers] = useState([]);
330
+ const [showLayers, setShowLayers] = useState(false);
331
+ const [showHint, setShowHint] = useState(true);
332
+ const [measureEnabled, setMeasureEnabled] = useState(false);
333
+ const [measureMode, setMeasureMode] = useState("auto");
334
+ const [measureDistance, setMeasureDistance] = useState(null);
335
+ const [measureResolved, setMeasureResolved] = useState(null);
336
+ const [measureFixedDist, setMeasureFixedDist] = useState(null);
337
+ const [measureFixedInput, setMeasureFixedInput] = useState("");
338
+ const measureModeRef = useRef(measureMode);
339
+ const measureFixedDistRef = useRef(measureFixedDist);
340
+ useEffect(() => {
341
+ measureModeRef.current = measureMode;
342
+ }, [measureMode]);
343
+ useEffect(() => {
344
+ measureFixedDistRef.current = measureFixedDist;
345
+ }, [measureFixedDist]);
346
+ const measureRedrawRef = useRef(null);
347
+ const measureResetRef = useRef(null);
348
+ const measureUndoRef = useRef(null);
349
+ const [cmdValue, setCmdValue] = useState("");
350
+ const [cmdEcho, setCmdEcho] = useState(null);
351
+ const lastCmdRef = useRef("");
352
+ const cmdInputRef = useRef(null);
353
+ const rootRef = useRef(null);
354
+ const measureRef = useRef(null);
355
+ useEffect(() => {
356
+ let cancelled = false;
357
+ let viewer = null;
358
+ setLoading(true);
359
+ setError(null);
360
+ setLayers([]);
361
+ (async () => {
362
+ let DxfViewer;
363
+ try {
364
+ const mod = await import('dxf-viewer');
365
+ DxfViewer = mod.DxfViewer;
366
+ } catch (e) {
367
+ if (!cancelled) {
368
+ setError("dxf-viewer is not installed in this app.");
369
+ setLoading(false);
370
+ }
371
+ return;
372
+ }
373
+ if (cancelled || !containerRef.current) return;
374
+ try {
375
+ await new Promise((resolve) => {
376
+ const tryStart = () => {
377
+ const r = containerRef.current?.getBoundingClientRect();
378
+ if (r && r.width > 4 && r.height > 4) resolve();
379
+ else requestAnimationFrame(tryStart);
380
+ };
381
+ tryStart();
382
+ });
383
+ if (cancelled || !containerRef.current) return;
384
+ const viewerOpts = { autoResize: true };
385
+ const defaultClear = DxfViewer.DefaultOptions?.clearColor;
386
+ if (typeof defaultClear?.clone === "function") {
387
+ viewerOpts.clearColor = defaultClear.clone().setHex(16777215);
388
+ }
389
+ viewer = new DxfViewer(containerRef.current, viewerOpts);
390
+ viewerRef.current = viewer;
391
+ const fontUrls = typeof window !== "undefined" && window.__REACT_OS_SHELL_DXF_FONTS__ || DEFAULT_DXF_FONTS;
392
+ await viewer.Load({ url, fonts: fontUrls, workerFactory: null });
393
+ if (cancelled) return;
394
+ try {
395
+ const list = viewer.GetLayers?.() ?? [];
396
+ if (Array.isArray(list)) {
397
+ setLayers(list.map((l) => ({
398
+ name: l.name,
399
+ displayName: l.displayName ?? l.name,
400
+ color: typeof l.color === "number" ? l.color : void 0,
401
+ visible: true
402
+ })));
403
+ }
404
+ } catch {
405
+ }
406
+ const refit = () => {
407
+ try {
408
+ const rect = containerRef.current?.getBoundingClientRect();
409
+ if (rect && rect.width > 0 && rect.height > 0) {
410
+ viewer.SetSize?.(Math.round(rect.width), Math.round(rect.height));
411
+ }
412
+ const bounds = viewer.GetBounds?.();
413
+ const origin = viewer.GetOrigin?.();
414
+ if (bounds && origin) {
415
+ viewer.FitView(
416
+ bounds.minX - origin.x,
417
+ bounds.maxX - origin.x,
418
+ bounds.minY - origin.y,
419
+ bounds.maxY - origin.y
420
+ );
421
+ }
422
+ viewer.Render?.();
423
+ } catch (err) {
424
+ console.warn("[Preview] DXF refit failed", err);
425
+ }
426
+ };
427
+ refit();
428
+ requestAnimationFrame(refit);
429
+ setLoading(false);
430
+ } catch (e) {
431
+ if (!cancelled) {
432
+ setError(e?.message || "Failed to render DXF.");
433
+ setLoading(false);
434
+ }
435
+ }
436
+ })();
437
+ return () => {
438
+ cancelled = true;
439
+ try {
440
+ viewer?.Destroy?.();
441
+ } catch {
442
+ }
443
+ viewerRef.current = null;
444
+ };
445
+ }, [url]);
446
+ useEffect(() => {
447
+ if (!showHint || loading) return;
448
+ const t = setTimeout(() => setShowHint(false), 5e3);
449
+ return () => clearTimeout(t);
450
+ }, [showHint, loading]);
451
+ const layersKey = layers.map((l) => l.visible ? "1" : "0").join("");
452
+ useEffect(() => {
453
+ const v = viewerRef.current;
454
+ if (loading || error || !v) return;
455
+ const scene = v.GetScene?.();
456
+ const camera = v.GetCamera?.();
457
+ const canvas = v.GetCanvas?.();
458
+ if (!scene || !camera || !canvas || !containerRef.current) return;
459
+ const teardown = () => {
460
+ const s = measureRef.current;
461
+ if (!s) return;
462
+ if (s.overlay && s.overlay.parentElement) s.overlay.parentElement.removeChild(s.overlay);
463
+ measureRef.current = null;
464
+ };
465
+ if (!measureEnabled) {
466
+ teardown();
467
+ setMeasureDistance(null);
468
+ setMeasureResolved(null);
469
+ return;
470
+ }
471
+ const overlay = document.createElement("div");
472
+ overlay.style.cssText = "position:absolute;inset:0;pointer-events:none;z-index:5;";
473
+ containerRef.current.appendChild(overlay);
474
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
475
+ svg.setAttribute("style", "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;");
476
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
477
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
478
+ marker.setAttribute("id", "dxf-measure-arrow");
479
+ marker.setAttribute("viewBox", "0 0 10 10");
480
+ marker.setAttribute("refX", "9");
481
+ marker.setAttribute("refY", "5");
482
+ marker.setAttribute("markerWidth", "6");
483
+ marker.setAttribute("markerHeight", "6");
484
+ marker.setAttribute("orient", "auto-start-reverse");
485
+ const arrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
486
+ arrowPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
487
+ arrowPath.setAttribute("fill", "#ff8800");
488
+ marker.appendChild(arrowPath);
489
+ defs.appendChild(marker);
490
+ svg.appendChild(defs);
491
+ overlay.appendChild(svg);
492
+ const mkRefLine = () => {
493
+ const el = document.createElementNS("http://www.w3.org/2000/svg", "line");
494
+ el.setAttribute("stroke", "#ff8800");
495
+ el.setAttribute("stroke-width", "1");
496
+ el.setAttribute("stroke-dasharray", "6,4");
497
+ el.setAttribute("opacity", "0.55");
498
+ el.style.display = "none";
499
+ svg.appendChild(el);
500
+ return el;
501
+ };
502
+ const refLineEl = mkRefLine();
503
+ const refLineVEl = mkRefLine();
504
+ const extLineA = document.createElementNS("http://www.w3.org/2000/svg", "line");
505
+ extLineA.setAttribute("stroke", "#ff8800");
506
+ extLineA.setAttribute("stroke-width", "1");
507
+ extLineA.setAttribute("opacity", "0.85");
508
+ extLineA.style.display = "none";
509
+ svg.appendChild(extLineA);
510
+ const extLineB = document.createElementNS("http://www.w3.org/2000/svg", "line");
511
+ extLineB.setAttribute("stroke", "#ff8800");
512
+ extLineB.setAttribute("stroke-width", "1");
513
+ extLineB.setAttribute("opacity", "0.85");
514
+ extLineB.style.display = "none";
515
+ svg.appendChild(extLineB);
516
+ const lineEl = document.createElementNS("http://www.w3.org/2000/svg", "line");
517
+ lineEl.setAttribute("stroke", "#ff8800");
518
+ lineEl.setAttribute("stroke-width", "1.5");
519
+ lineEl.setAttribute("stroke-linecap", "round");
520
+ lineEl.style.display = "none";
521
+ svg.appendChild(lineEl);
522
+ const fixedLineEl = document.createElementNS("http://www.w3.org/2000/svg", "line");
523
+ fixedLineEl.setAttribute("stroke", "#ff8800");
524
+ fixedLineEl.setAttribute("stroke-width", "1.5");
525
+ fixedLineEl.setAttribute("stroke-linecap", "round");
526
+ fixedLineEl.style.display = "none";
527
+ svg.appendChild(fixedLineEl);
528
+ const snapEl = document.createElement("div");
529
+ snapEl.style.cssText = `position:absolute;transform:translate(-50%,-50%);pointer-events:none;display:none;`;
530
+ const SVGNS = "http://www.w3.org/2000/svg";
531
+ const snapSvg = document.createElementNS(SVGNS, "svg");
532
+ snapSvg.setAttribute("width", "22");
533
+ snapSvg.setAttribute("height", "22");
534
+ snapSvg.setAttribute("viewBox", "0 0 22 22");
535
+ snapSvg.setAttribute("style", "overflow:visible;display:block");
536
+ snapEl.appendChild(snapSvg);
537
+ const mkSnapGlyph = (build) => {
538
+ const g = document.createElementNS(SVGNS, "g");
539
+ build(g);
540
+ g.style.display = "none";
541
+ snapSvg.appendChild(g);
542
+ return g;
543
+ };
544
+ const snapGlyphEndpoint = mkSnapGlyph((g) => {
545
+ const r = document.createElementNS(SVGNS, "rect");
546
+ r.setAttribute("x", "4");
547
+ r.setAttribute("y", "4");
548
+ r.setAttribute("width", "14");
549
+ r.setAttribute("height", "14");
550
+ r.setAttribute("fill", "rgba(255,255,255,0.9)");
551
+ r.setAttribute("stroke", "#ff8800");
552
+ r.setAttribute("stroke-width", "1.8");
553
+ g.appendChild(r);
554
+ });
555
+ const snapGlyphIntersection = mkSnapGlyph((g) => {
556
+ const mkLn = (x1, y1, x2, y2) => {
557
+ const ln = document.createElementNS(SVGNS, "line");
558
+ ln.setAttribute("x1", String(x1));
559
+ ln.setAttribute("y1", String(y1));
560
+ ln.setAttribute("x2", String(x2));
561
+ ln.setAttribute("y2", String(y2));
562
+ ln.setAttribute("stroke", "#ff8800");
563
+ ln.setAttribute("stroke-width", "2.2");
564
+ ln.setAttribute("stroke-linecap", "round");
565
+ return ln;
566
+ };
567
+ g.appendChild(mkLn(4, 4, 18, 18));
568
+ g.appendChild(mkLn(18, 4, 4, 18));
569
+ });
570
+ const snapGlyphLine = mkSnapGlyph((g) => {
571
+ const poly = document.createElementNS(SVGNS, "polygon");
572
+ poly.setAttribute("points", "4,4 18,4 4,18 18,18");
573
+ poly.setAttribute("fill", "rgba(255,255,255,0.9)");
574
+ poly.setAttribute("stroke", "#ff8800");
575
+ poly.setAttribute("stroke-width", "1.8");
576
+ poly.setAttribute("stroke-linejoin", "round");
577
+ g.appendChild(poly);
578
+ });
579
+ const snapGlyphMidpoint = mkSnapGlyph((g) => {
580
+ const poly = document.createElementNS(SVGNS, "polygon");
581
+ poly.setAttribute("points", "11,3 19,18 3,18");
582
+ poly.setAttribute("fill", "rgba(255,255,255,0.9)");
583
+ poly.setAttribute("stroke", "#ff8800");
584
+ poly.setAttribute("stroke-width", "1.8");
585
+ poly.setAttribute("stroke-linejoin", "round");
586
+ g.appendChild(poly);
587
+ });
588
+ const snapGlyphNode = mkSnapGlyph((g) => {
589
+ const c = document.createElementNS(SVGNS, "circle");
590
+ c.setAttribute("cx", "11");
591
+ c.setAttribute("cy", "11");
592
+ c.setAttribute("r", "7");
593
+ c.setAttribute("fill", "rgba(255,255,255,0.9)");
594
+ c.setAttribute("stroke", "#ff8800");
595
+ c.setAttribute("stroke-width", "1.8");
596
+ g.appendChild(c);
597
+ const mkLn = (x1, y1, x2, y2) => {
598
+ const ln = document.createElementNS(SVGNS, "line");
599
+ ln.setAttribute("x1", String(x1));
600
+ ln.setAttribute("y1", String(y1));
601
+ ln.setAttribute("x2", String(x2));
602
+ ln.setAttribute("y2", String(y2));
603
+ ln.setAttribute("stroke", "#ff8800");
604
+ ln.setAttribute("stroke-width", "1.6");
605
+ ln.setAttribute("stroke-linecap", "round");
606
+ return ln;
607
+ };
608
+ g.appendChild(mkLn(6.5, 6.5, 15.5, 15.5));
609
+ g.appendChild(mkLn(15.5, 6.5, 6.5, 15.5));
610
+ });
611
+ const setSnapGlyph = (type) => {
612
+ snapGlyphEndpoint.style.display = type === "endpoint" ? "" : "none";
613
+ snapGlyphIntersection.style.display = type === "intersection" ? "" : "none";
614
+ snapGlyphLine.style.display = type === "line" ? "" : "none";
615
+ snapGlyphMidpoint.style.display = type === "midpoint" ? "" : "none";
616
+ snapGlyphNode.style.display = type === "node" ? "" : "none";
617
+ };
618
+ overlay.appendChild(snapEl);
619
+ measureRef.current = {
620
+ picks: [],
621
+ rawSecondClick: null,
622
+ overlay,
623
+ svg,
624
+ line: lineEl,
625
+ fixedLine: fixedLineEl,
626
+ fixedLabel: null,
627
+ refLine: refLineEl,
628
+ refLineV: refLineVEl,
629
+ extLineA,
630
+ extLineB,
631
+ markers: [],
632
+ label: null,
633
+ snap: snapEl,
634
+ segs: null,
635
+ nodes: null
636
+ };
637
+ const Vector3Ctor = camera?.position?.constructor ?? scene?.position?.constructor ?? null;
638
+ let ready = false;
639
+ const pxFromScene = (sx, sy) => {
640
+ if (!Vector3Ctor) return { x: 0, y: 0 };
641
+ const v3 = new Vector3Ctor(sx, sy, 0).project(camera);
642
+ const w = canvas.clientWidth, h = canvas.clientHeight;
643
+ return { x: (v3.x + 1) / 2 * w, y: (-v3.y + 1) / 2 * h };
644
+ };
645
+ const MAX_SNAP_SEGS = 4e5;
646
+ (() => {
647
+ try {
648
+ const st = measureRef.current;
649
+ if (!st) return;
650
+ const segXY = [];
651
+ const nodeXY = [];
652
+ let truncated = false;
653
+ const instanceXforms = (g) => {
654
+ const t0 = g?.attributes?.instanceTransform0;
655
+ const t1 = g?.attributes?.instanceTransform1;
656
+ if (t0 && t1) {
657
+ const out = [];
658
+ const n = Math.min(t0.count, t1.count);
659
+ for (let k = 0; k < n; k++) {
660
+ out.push([t0.getX(k), t0.getY(k), t0.getZ(k), t1.getX(k), t1.getY(k), t1.getZ(k)]);
661
+ }
662
+ return out;
663
+ }
664
+ const tp = g?.attributes?.instanceTransform;
665
+ if (tp) {
666
+ const out = [];
667
+ for (let k = 0; k < tp.count; k++) out.push([1, 0, tp.getX(k), 0, 1, tp.getY(k)]);
668
+ return out;
669
+ }
670
+ return [[1, 0, 0, 0, 1, 0]];
671
+ };
672
+ const visit = (obj) => {
673
+ const isSeg = !!obj?.isLineSegments;
674
+ const isPts = !!obj?.isPoints;
675
+ if (!isSeg && !isPts) return;
676
+ const g = obj.geometry;
677
+ const pos = g?.attributes?.position;
678
+ if (!pos) return;
679
+ const xfs = instanceXforms(g);
680
+ if (isSeg) {
681
+ const idx = g.index;
682
+ const pairCount = Math.floor((idx ? idx.count : pos.count) / 2);
683
+ for (let p = 0; p < pairCount; p++) {
684
+ const ia = idx ? idx.getX(2 * p) : 2 * p;
685
+ const ib = idx ? idx.getX(2 * p + 1) : 2 * p + 1;
686
+ const ax = pos.getX(ia), ay = pos.getY(ia);
687
+ const bx = pos.getX(ib), by = pos.getY(ib);
688
+ if (!Number.isFinite(ax + ay + bx + by)) continue;
689
+ for (const m of xfs) {
690
+ if (segXY.length >= MAX_SNAP_SEGS * 4) {
691
+ truncated = true;
692
+ return;
693
+ }
694
+ const tax = m[0] * ax + m[1] * ay + m[2], tay = m[3] * ax + m[4] * ay + m[5];
695
+ const tbx = m[0] * bx + m[1] * by + m[2], tby = m[3] * bx + m[4] * by + m[5];
696
+ if (tax === tbx && tay === tby) continue;
697
+ segXY.push(tax, tay, tbx, tby);
698
+ }
699
+ }
700
+ } else {
701
+ for (let i = 0; i < pos.count; i++) {
702
+ const x = pos.getX(i), y = pos.getY(i);
703
+ if (!Number.isFinite(x + y)) continue;
704
+ for (const m of xfs) {
705
+ nodeXY.push(m[0] * x + m[1] * y + m[2], m[3] * x + m[4] * y + m[5]);
706
+ }
707
+ }
708
+ }
709
+ };
710
+ if (typeof scene.traverseVisible === "function") scene.traverseVisible(visit);
711
+ else scene.traverse(visit);
712
+ st.segs = new Float64Array(segXY);
713
+ st.nodes = new Float64Array(nodeXY);
714
+ if (truncated) {
715
+ console.warn(`[Preview] DXF snap cache truncated at ${MAX_SNAP_SEGS} segments \u2014 snapping may miss some geometry.`);
716
+ }
717
+ ready = true;
718
+ } catch {
719
+ }
720
+ })();
721
+ const SNAP_PX = 18;
722
+ const segSegIntersectPx = (apx, apy, bpx, bpy, cpx, cpy, dpx, dpy) => {
723
+ const d = (bpx - apx) * (dpy - cpy) - (bpy - apy) * (dpx - cpx);
724
+ if (Math.abs(d) < 1e-9) return null;
725
+ const t = ((cpx - apx) * (dpy - cpy) - (cpy - apy) * (dpx - cpx)) / d;
726
+ const u = ((cpx - apx) * (bpy - apy) - (cpy - apy) * (bpx - apx)) / d;
727
+ const eps = 1e-4;
728
+ if (t < -eps || t > 1 + eps || u < -eps || u > 1 + eps) return null;
729
+ return { px: apx + t * (bpx - apx), py: apy + t * (bpy - apy), t };
730
+ };
731
+ const findSnap = (cx, cy) => {
732
+ const s = measureRef.current;
733
+ if (!s || !ready || !s.segs) return null;
734
+ const o = pxFromScene(0, 0);
735
+ const ex = pxFromScene(1, 0);
736
+ const ey = pxFromScene(0, 1);
737
+ const mxx = ex.x - o.x, mxy = ex.y - o.y;
738
+ const myx = ey.x - o.x, myy = ey.y - o.y;
739
+ const det = mxx * myy - myx * mxy;
740
+ if (!det) return null;
741
+ const csx = ((cx - o.x) * myy - (cy - o.y) * myx) / det;
742
+ const csy = ((cy - o.y) * mxx - (cx - o.x) * mxy) / det;
743
+ const rx = SNAP_PX / (Math.hypot(mxx, mxy) || 1);
744
+ const ry = SNAP_PX / (Math.hypot(myx, myy) || 1);
745
+ const minX = csx - rx, maxX = csx + rx, minY = csy - ry, maxY = csy + ry;
746
+ const T2 = SNAP_PX * SNAP_PX;
747
+ let bestPt = null, dPt = T2;
748
+ let bestMid = null, dMid = T2;
749
+ let bestLine = null, dLine = T2;
750
+ let bestX = null, dX = T2;
751
+ const cand = [];
752
+ const segs = s.segs;
753
+ for (let i = 0; i < segs.length; i += 4) {
754
+ const ax = segs[i], ay = segs[i + 1], bx = segs[i + 2], by = segs[i + 3];
755
+ if (ax < minX && bx < minX || ax > maxX && bx > maxX || ay < minY && by < minY || ay > maxY && by > maxY) continue;
756
+ const apx = o.x + mxx * ax + myx * ay, apy = o.y + mxy * ax + myy * ay;
757
+ const bpx = o.x + mxx * bx + myx * by, bpy = o.y + mxy * bx + myy * by;
758
+ let near = false;
759
+ let dx = cx - apx, dy = cy - apy;
760
+ let d2 = dx * dx + dy * dy;
761
+ if (d2 < T2) {
762
+ near = true;
763
+ if (d2 < dPt) {
764
+ dPt = d2;
765
+ bestPt = { sx: ax, sy: ay, type: "endpoint" };
766
+ }
767
+ }
768
+ dx = cx - bpx;
769
+ dy = cy - bpy;
770
+ d2 = dx * dx + dy * dy;
771
+ if (d2 < T2) {
772
+ near = true;
773
+ if (d2 < dPt) {
774
+ dPt = d2;
775
+ bestPt = { sx: bx, sy: by, type: "endpoint" };
776
+ }
777
+ }
778
+ dx = cx - (apx + bpx) / 2;
779
+ dy = cy - (apy + bpy) / 2;
780
+ d2 = dx * dx + dy * dy;
781
+ if (d2 < T2) {
782
+ near = true;
783
+ if (d2 < dMid) {
784
+ dMid = d2;
785
+ bestMid = { sx: (ax + bx) / 2, sy: (ay + by) / 2, type: "midpoint" };
786
+ }
787
+ }
788
+ const sdx = bpx - apx, sdy = bpy - apy;
789
+ const len2 = sdx * sdx + sdy * sdy;
790
+ if (len2 > 0) {
791
+ const t = ((cx - apx) * sdx + (cy - apy) * sdy) / len2;
792
+ if (t > 0 && t < 1) {
793
+ dx = cx - (apx + t * sdx);
794
+ dy = cy - (apy + t * sdy);
795
+ d2 = dx * dx + dy * dy;
796
+ if (d2 < T2) {
797
+ near = true;
798
+ if (d2 < dLine) {
799
+ dLine = d2;
800
+ bestLine = { sx: ax + t * (bx - ax), sy: ay + t * (by - ay), type: "line" };
801
+ }
802
+ }
803
+ }
804
+ }
805
+ if (near) cand.push({ ax, ay, bx, by, apx, apy, bpx, bpy });
806
+ }
807
+ const nodes = s.nodes;
808
+ if (nodes) {
809
+ for (let i = 0; i < nodes.length; i += 2) {
810
+ const x = nodes[i], y = nodes[i + 1];
811
+ if (x < minX || x > maxX || y < minY || y > maxY) continue;
812
+ const dx = cx - (o.x + mxx * x + myx * y);
813
+ const dy = cy - (o.y + mxy * x + myy * y);
814
+ const d2 = dx * dx + dy * dy;
815
+ if (d2 < dPt) {
816
+ dPt = d2;
817
+ bestPt = { sx: x, sy: y, type: "node" };
818
+ }
819
+ }
820
+ }
821
+ for (let i = 0; i < cand.length; i++) {
822
+ const A = cand[i];
823
+ for (let j = i + 1; j < cand.length; j++) {
824
+ const B = cand[j];
825
+ const ix = segSegIntersectPx(A.apx, A.apy, A.bpx, A.bpy, B.apx, B.apy, B.bpx, B.bpy);
826
+ if (!ix) continue;
827
+ const dx = cx - ix.px, dy = cy - ix.py;
828
+ const d2 = dx * dx + dy * dy;
829
+ if (d2 >= T2 || d2 >= dX) continue;
830
+ dX = d2;
831
+ bestX = { sx: A.ax + ix.t * (A.bx - A.ax), sy: A.ay + ix.t * (A.by - A.ay), type: "intersection" };
832
+ }
833
+ }
834
+ const top = bestX && bestPt ? dX < dPt - 1 ? bestX : bestPt : bestX ?? bestPt;
835
+ return top ?? bestMid ?? bestLine;
836
+ };
837
+ const makeMarker = () => {
838
+ const el = document.createElement("div");
839
+ el.style.cssText = `position:absolute;width:10px;height:10px;border-radius:50%;background:#ff8800;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,0.25);transform:translate(-50%,-50%);pointer-events:none;`;
840
+ overlay.appendChild(el);
841
+ return el;
842
+ };
843
+ const ensureLabel = () => {
844
+ const s = measureRef.current;
845
+ if (s.label) return s.label;
846
+ const el = document.createElement("div");
847
+ el.style.cssText = `position:absolute;left:-9999px;top:-9999px;transform:translate(-50%,-50%);padding:2px 6px;font-size:11px;font-weight:600;font-family:system-ui,-apple-system,sans-serif;background:rgba(255,136,0,0.95);color:#fff;border-radius:4px;white-space:nowrap;box-shadow:0 1px 4px rgba(0,0,0,0.25);pointer-events:none;`;
848
+ overlay.appendChild(el);
849
+ s.label = el;
850
+ return el;
851
+ };
852
+ const positionMarker = (el, x, y) => {
853
+ const p = pxFromScene(x, y);
854
+ el.style.left = `${p.x}px`;
855
+ el.style.top = `${p.y}px`;
856
+ };
857
+ const reconcileLockedPick = () => {
858
+ const s = measureRef.current;
859
+ if (!s || !s.rawSecondClick || s.picks.length < 2) return;
860
+ const a = s.picks[0];
861
+ const raw = s.rawSecondClick;
862
+ const fixed = measureFixedDistRef.current;
863
+ const mode = measureModeRef.current;
864
+ if (fixed !== null && Number.isFinite(fixed) && (mode === "horizontal" || mode === "vertical")) {
865
+ if (mode === "horizontal") {
866
+ const sign = raw.x >= a.x ? 1 : -1;
867
+ s.picks[1] = { x: a.x + sign * Math.abs(fixed), y: raw.y };
868
+ } else {
869
+ const sign = raw.y >= a.y ? 1 : -1;
870
+ s.picks[1] = { x: raw.x, y: a.y + sign * Math.abs(fixed) };
871
+ }
872
+ } else {
873
+ s.picks[1] = { x: raw.x, y: raw.y };
874
+ }
875
+ };
876
+ const resolveMode = (a, b) => {
877
+ const mode = measureModeRef.current;
878
+ if (mode !== "auto") return mode;
879
+ if (!a || !b) return "horizontal";
880
+ return Math.abs(b.x - a.x) >= Math.abs(b.y - a.y) ? "horizontal" : "vertical";
881
+ };
882
+ const computeMainDimEnds = () => {
883
+ const s = measureRef.current;
884
+ const a = s.picks[0];
885
+ const b = s.picks[1];
886
+ const mode = resolveMode(a, b);
887
+ const rawMode = measureModeRef.current;
888
+ const fixed = measureFixedDistRef.current;
889
+ if (fixed !== null && Number.isFinite(fixed) && rawMode === "horizontal") {
890
+ return { from: { x: b.x, y: a.y }, to: { x: b.x, y: b.y } };
891
+ }
892
+ if (fixed !== null && Number.isFinite(fixed) && rawMode === "vertical") {
893
+ return { from: { x: a.x, y: b.y }, to: { x: b.x, y: b.y } };
894
+ }
895
+ if (mode === "horizontal") return { from: a, to: { x: b.x, y: a.y } };
896
+ if (mode === "vertical") return { from: a, to: { x: a.x, y: b.y } };
897
+ return { from: a, to: b };
898
+ };
899
+ const updateOverlay = () => {
900
+ const s = measureRef.current;
901
+ if (!s) return;
902
+ reconcileLockedPick();
903
+ const rawMode = measureModeRef.current;
904
+ const fixed = measureFixedDistRef.current;
905
+ const fixedActive = fixed !== null && Number.isFinite(fixed) && (rawMode === "horizontal" || rawMode === "vertical");
906
+ if (s.markers[0]) positionMarker(s.markers[0], s.picks[0].x, s.picks[0].y);
907
+ if (s.markers[1]) positionMarker(s.markers[1], s.picks[1].x, s.picks[1].y);
908
+ const drawRefLine = (el, dir, visible) => {
909
+ if (!el) return;
910
+ if (!visible || s.picks.length < 1) {
911
+ el.style.display = "none";
912
+ return;
913
+ }
914
+ const a = s.picks[0];
915
+ const w = canvas.clientWidth, h = canvas.clientHeight;
916
+ const screenSpan = Math.hypot(w, h) * 4;
917
+ const ap = pxFromScene(a.x, a.y);
918
+ const probe = pxFromScene(a.x + dir.dx, a.y + dir.dy);
919
+ const pxLen = Math.hypot(probe.x - ap.x, probe.y - ap.y) || 1;
920
+ const sceneStep = screenSpan / pxLen;
921
+ const p0 = pxFromScene(a.x - dir.dx * sceneStep, a.y - dir.dy * sceneStep);
922
+ const p1 = pxFromScene(a.x + dir.dx * sceneStep, a.y + dir.dy * sceneStep);
923
+ el.setAttribute("x1", String(p0.x));
924
+ el.setAttribute("y1", String(p0.y));
925
+ el.setAttribute("x2", String(p1.x));
926
+ el.setAttribute("y2", String(p1.y));
927
+ el.style.display = "";
928
+ };
929
+ let showH = rawMode === "horizontal";
930
+ let showV = rawMode === "vertical";
931
+ if (rawMode === "auto") {
932
+ if (s.picks.length >= 2) {
933
+ const r = resolveMode(s.picks[0], s.picks[1]);
934
+ showH = r === "horizontal";
935
+ showV = r === "vertical";
936
+ } else {
937
+ showH = true;
938
+ showV = true;
939
+ }
940
+ }
941
+ drawRefLine(s.refLine, { dx: 1, dy: 0 }, showH);
942
+ drawRefLine(s.refLineV, { dx: 0, dy: 1 }, showV);
943
+ if (s.picks.length === 2) {
944
+ const a = s.picks[0], b = s.picks[1];
945
+ const mode = resolveMode(a, b);
946
+ const ends = computeMainDimEnds();
947
+ const fp = pxFromScene(ends.from.x, ends.from.y);
948
+ const tp = pxFromScene(ends.to.x, ends.to.y);
949
+ s.line.setAttribute("x1", String(fp.x));
950
+ s.line.setAttribute("y1", String(fp.y));
951
+ s.line.setAttribute("x2", String(tp.x));
952
+ s.line.setAttribute("y2", String(tp.y));
953
+ s.line.setAttribute("marker-start", "url(#dxf-measure-arrow)");
954
+ s.line.setAttribute("marker-end", "url(#dxf-measure-arrow)");
955
+ const mainLen = Math.hypot(tp.x - fp.x, tp.y - fp.y);
956
+ s.line.style.display = mainLen > 0.5 ? "" : "none";
957
+ if (fixedActive && s.fixedLine) {
958
+ const r = mode === "horizontal" ? { x: b.x, y: a.y } : { x: a.x, y: b.y };
959
+ const ap = pxFromScene(a.x, a.y);
960
+ const rp = pxFromScene(r.x, r.y);
961
+ s.fixedLine.setAttribute("x1", String(ap.x));
962
+ s.fixedLine.setAttribute("y1", String(ap.y));
963
+ s.fixedLine.setAttribute("x2", String(rp.x));
964
+ s.fixedLine.setAttribute("y2", String(rp.y));
965
+ s.fixedLine.setAttribute("marker-start", "url(#dxf-measure-arrow)");
966
+ s.fixedLine.setAttribute("marker-end", "url(#dxf-measure-arrow)");
967
+ s.fixedLine.style.display = "";
968
+ const fLabel = ensureFixedLabel();
969
+ fLabel.textContent = formatMeasureDistance(Math.abs(fixed));
970
+ const fcx = (ap.x + rp.x) / 2;
971
+ const fcy = (ap.y + rp.y) / 2;
972
+ fLabel.style.left = `${fcx}px`;
973
+ fLabel.style.top = `${fcy}px`;
974
+ const w = canvas.clientWidth, h = canvas.clientHeight;
975
+ fLabel.style.display = fcx < 0 || fcy < 0 || fcx > w || fcy > h ? "none" : "";
976
+ } else {
977
+ if (s.fixedLine) s.fixedLine.style.display = "none";
978
+ if (s.fixedLabel) s.fixedLabel.style.display = "none";
979
+ }
980
+ const extA = s.extLineA;
981
+ const extB = s.extLineB;
982
+ if (!fixedActive && mode === "horizontal" && Math.abs(b.y - a.y) > 1e-9) {
983
+ const p = pxFromScene(b.x, a.y);
984
+ const q = pxFromScene(b.x, b.y);
985
+ extB.setAttribute("x1", String(p.x));
986
+ extB.setAttribute("y1", String(p.y));
987
+ extB.setAttribute("x2", String(q.x));
988
+ extB.setAttribute("y2", String(q.y));
989
+ extB.style.display = "";
990
+ extA.style.display = "none";
991
+ } else if (!fixedActive && mode === "vertical" && Math.abs(b.x - a.x) > 1e-9) {
992
+ const p = pxFromScene(a.x, b.y);
993
+ const q = pxFromScene(b.x, b.y);
994
+ extB.setAttribute("x1", String(p.x));
995
+ extB.setAttribute("y1", String(p.y));
996
+ extB.setAttribute("x2", String(q.x));
997
+ extB.setAttribute("y2", String(q.y));
998
+ extB.style.display = "";
999
+ extA.style.display = "none";
1000
+ } else {
1001
+ extA.style.display = "none";
1002
+ extB.style.display = "none";
1003
+ }
1004
+ if (s.label) {
1005
+ const cx = (fp.x + tp.x) / 2;
1006
+ const cy = (fp.y + tp.y) / 2;
1007
+ s.label.style.left = `${cx}px`;
1008
+ s.label.style.top = `${cy}px`;
1009
+ const w = canvas.clientWidth, h = canvas.clientHeight;
1010
+ s.label.style.display = cx < 0 || cy < 0 || cx > w || cy > h ? "none" : "";
1011
+ if (mainLen <= 0.5) s.label.style.display = "none";
1012
+ }
1013
+ } else {
1014
+ s.line.style.display = "none";
1015
+ if (s.fixedLine) s.fixedLine.style.display = "none";
1016
+ if (s.fixedLabel) s.fixedLabel.style.display = "none";
1017
+ if (s.extLineA) s.extLineA.style.display = "none";
1018
+ if (s.extLineB) s.extLineB.style.display = "none";
1019
+ }
1020
+ };
1021
+ const ensureFixedLabel = () => {
1022
+ const s = measureRef.current;
1023
+ if (s.fixedLabel) return s.fixedLabel;
1024
+ const el = document.createElement("div");
1025
+ el.style.cssText = `position:absolute;left:-9999px;top:-9999px;transform:translate(-50%,-50%);padding:2px 6px;font-size:11px;font-weight:600;font-family:system-ui,-apple-system,sans-serif;background:rgba(255,136,0,0.75);color:#fff;border-radius:4px;white-space:nowrap;box-shadow:0 1px 4px rgba(0,0,0,0.25);pointer-events:none;`;
1026
+ overlay.appendChild(el);
1027
+ s.fixedLabel = el;
1028
+ return el;
1029
+ };
1030
+ const DRAG_TOL = 4;
1031
+ const DRAG_TIME = 350;
1032
+ let downX = 0, downY = 0, downTime = 0, downActive = false, dragging = false;
1033
+ let lastSnap = null;
1034
+ const handlePointerDown = (ev) => {
1035
+ const d = ev?.detail;
1036
+ if (!d || d.domEvent?.button !== 0) return;
1037
+ downX = d.canvasCoord.x;
1038
+ downY = d.canvasCoord.y;
1039
+ downTime = performance.now();
1040
+ dragging = false;
1041
+ downActive = true;
1042
+ };
1043
+ const handlePointerMove = (ev) => {
1044
+ const rect = canvas.getBoundingClientRect();
1045
+ const cx = ev.clientX - rect.left;
1046
+ const cy = ev.clientY - rect.top;
1047
+ if (downActive) {
1048
+ if ((cx - downX) ** 2 + (cy - downY) ** 2 > DRAG_TOL * DRAG_TOL) dragging = true;
1049
+ }
1050
+ const s = measureRef.current;
1051
+ if (!s || !s.snap) return;
1052
+ if (downActive && dragging) {
1053
+ s.snap.style.display = "none";
1054
+ return;
1055
+ }
1056
+ lastSnap = findSnap(cx, cy);
1057
+ if (lastSnap) {
1058
+ const p = pxFromScene(lastSnap.sx, lastSnap.sy);
1059
+ s.snap.style.left = `${p.x}px`;
1060
+ s.snap.style.top = `${p.y}px`;
1061
+ s.snap.style.display = "";
1062
+ setSnapGlyph(lastSnap.type);
1063
+ } else {
1064
+ s.snap.style.display = "none";
1065
+ }
1066
+ };
1067
+ const handlePointerUp = (ev) => {
1068
+ if (!downActive) return;
1069
+ downActive = false;
1070
+ const elapsed = performance.now() - downTime;
1071
+ if (dragging || elapsed > DRAG_TIME) return;
1072
+ const raw = ev?.detail?.position;
1073
+ if (!raw) return;
1074
+ const picked = lastSnap ? { x: lastSnap.sx, y: lastSnap.sy } : { x: raw.x, y: raw.y };
1075
+ doPick(picked);
1076
+ };
1077
+ const recomputeLabel = () => {
1078
+ const s = measureRef.current;
1079
+ if (!s || s.picks.length !== 2) return;
1080
+ reconcileLockedPick();
1081
+ const a = s.picks[0], b = s.picks[1];
1082
+ const rawMode = measureModeRef.current;
1083
+ const mode = resolveMode(a, b);
1084
+ const fixed = measureFixedDistRef.current;
1085
+ const fixedActive = fixed !== null && Number.isFinite(fixed) && (rawMode === "horizontal" || rawMode === "vertical");
1086
+ let dist;
1087
+ let suffix = "";
1088
+ let echo;
1089
+ let shown = mode;
1090
+ const adx = Math.abs(b.x - a.x), ady = Math.abs(b.y - a.y);
1091
+ if (fixedActive && rawMode === "horizontal") {
1092
+ dist = ady;
1093
+ suffix = " \u2195";
1094
+ shown = "vertical";
1095
+ echo = `\u0394Y = ${formatMeasureDistance(ady)} with \u0394X locked to ${formatMeasureDistance(Math.abs(fixed))}`;
1096
+ } else if (fixedActive && rawMode === "vertical") {
1097
+ dist = adx;
1098
+ suffix = " \u2194";
1099
+ shown = "horizontal";
1100
+ echo = `\u0394X = ${formatMeasureDistance(adx)} with \u0394Y locked to ${formatMeasureDistance(Math.abs(fixed))}`;
1101
+ } else if (mode === "horizontal") {
1102
+ dist = adx;
1103
+ suffix = " \u2194";
1104
+ echo = `Linear dimension = ${formatMeasureDistance(adx)} (\u0394X)`;
1105
+ } else if (mode === "vertical") {
1106
+ dist = ady;
1107
+ suffix = " \u2195";
1108
+ echo = `Linear dimension = ${formatMeasureDistance(ady)} (\u0394Y)`;
1109
+ } else {
1110
+ dist = Math.hypot(b.x - a.x, b.y - a.y);
1111
+ echo = `Distance = ${formatMeasureDistance(dist)} \u0394X = ${formatMeasureDistance(adx)} \u0394Y = ${formatMeasureDistance(ady)}`;
1112
+ }
1113
+ if (!Number.isFinite(dist)) {
1114
+ setMeasureDistance(null);
1115
+ setMeasureResolved(null);
1116
+ if (s.label) s.label.style.opacity = "0";
1117
+ return;
1118
+ }
1119
+ setMeasureDistance(dist);
1120
+ setMeasureResolved(shown);
1121
+ setCmdEcho(echo);
1122
+ const label = ensureLabel();
1123
+ label.style.opacity = "1";
1124
+ label.textContent = `${formatMeasureDistance(dist)}${suffix}`;
1125
+ };
1126
+ measureRedrawRef.current = () => {
1127
+ recomputeLabel();
1128
+ updateOverlay();
1129
+ };
1130
+ const hideDimVisuals = (s) => {
1131
+ s.line.style.display = "none";
1132
+ if (s.fixedLine) s.fixedLine.style.display = "none";
1133
+ if (s.fixedLabel) s.fixedLabel.style.display = "none";
1134
+ if (s.extLineA) s.extLineA.style.display = "none";
1135
+ if (s.extLineB) s.extLineB.style.display = "none";
1136
+ if (s.label) s.label.style.opacity = "0";
1137
+ setMeasureDistance(null);
1138
+ setMeasureResolved(null);
1139
+ };
1140
+ const resetPicks = () => {
1141
+ const s = measureRef.current;
1142
+ if (!s) return;
1143
+ for (const m of s.markers) m.parentElement?.removeChild(m);
1144
+ s.markers = [];
1145
+ s.picks = [];
1146
+ s.rawSecondClick = null;
1147
+ if (s.refLine) s.refLine.style.display = "none";
1148
+ if (s.refLineV) s.refLineV.style.display = "none";
1149
+ hideDimVisuals(s);
1150
+ };
1151
+ measureResetRef.current = resetPicks;
1152
+ measureUndoRef.current = () => {
1153
+ const s = measureRef.current;
1154
+ if (!s || s.picks.length === 0) return;
1155
+ s.picks.pop();
1156
+ const m = s.markers.pop();
1157
+ m?.parentElement?.removeChild(m);
1158
+ s.rawSecondClick = null;
1159
+ hideDimVisuals(s);
1160
+ updateOverlay();
1161
+ };
1162
+ const doPick = (p) => {
1163
+ const s = measureRef.current;
1164
+ if (!s) return;
1165
+ if (s.picks.length === 2) resetPicks();
1166
+ if (s.picks.length === 0) {
1167
+ s.picks.push({ x: p.x, y: p.y });
1168
+ s.markers.push(makeMarker());
1169
+ } else {
1170
+ s.rawSecondClick = { x: p.x, y: p.y };
1171
+ s.picks.push({ x: p.x, y: p.y });
1172
+ s.markers.push(makeMarker());
1173
+ recomputeLabel();
1174
+ }
1175
+ updateOverlay();
1176
+ };
1177
+ const onKeyDown = (ev) => {
1178
+ if (ev.key === "Escape") setMeasureEnabled(false);
1179
+ };
1180
+ canvas.style.cursor = "crosshair";
1181
+ try {
1182
+ v.Subscribe?.("pointerdown", handlePointerDown);
1183
+ } catch {
1184
+ }
1185
+ try {
1186
+ v.Subscribe?.("pointerup", handlePointerUp);
1187
+ } catch {
1188
+ }
1189
+ try {
1190
+ v.Subscribe?.("viewChanged", updateOverlay);
1191
+ } catch {
1192
+ }
1193
+ canvas.addEventListener("pointermove", handlePointerMove);
1194
+ window.addEventListener("keydown", onKeyDown);
1195
+ return () => {
1196
+ canvas.style.cursor = "";
1197
+ try {
1198
+ v.Unsubscribe?.("pointerdown", handlePointerDown);
1199
+ } catch {
1200
+ }
1201
+ try {
1202
+ v.Unsubscribe?.("pointerup", handlePointerUp);
1203
+ } catch {
1204
+ }
1205
+ try {
1206
+ v.Unsubscribe?.("viewChanged", updateOverlay);
1207
+ } catch {
1208
+ }
1209
+ canvas.removeEventListener("pointermove", handlePointerMove);
1210
+ window.removeEventListener("keydown", onKeyDown);
1211
+ measureRedrawRef.current = null;
1212
+ measureResetRef.current = null;
1213
+ measureUndoRef.current = null;
1214
+ teardown();
1215
+ setMeasureDistance(null);
1216
+ setMeasureResolved(null);
1217
+ };
1218
+ }, [measureEnabled, loading, error, layersKey]);
1219
+ useEffect(() => {
1220
+ measureRedrawRef.current?.();
1221
+ }, [measureMode, measureFixedDist]);
1222
+ const toggleLayer = (name) => {
1223
+ setLayers((prev) => prev.map((l) => {
1224
+ if (l.name !== name) return l;
1225
+ const next = !l.visible;
1226
+ try {
1227
+ viewerRef.current?.ShowLayer?.(name, next);
1228
+ } catch {
1229
+ }
1230
+ return { ...l, visible: next };
1231
+ }));
1232
+ };
1233
+ const setAllLayers = (visible) => {
1234
+ setLayers((prev) => prev.map((l) => {
1235
+ if (l.visible === visible) return l;
1236
+ try {
1237
+ viewerRef.current?.ShowLayer?.(l.name, visible);
1238
+ } catch {
1239
+ }
1240
+ return { ...l, visible };
1241
+ }));
1242
+ };
1243
+ const handleDefaultDownload = () => {
1244
+ const a = document.createElement("a");
1245
+ a.href = url;
1246
+ a.download = filename;
1247
+ a.click();
1248
+ };
1249
+ const handleResetView = () => {
1250
+ try {
1251
+ const v = viewerRef.current;
1252
+ const bounds = v?.GetBounds?.();
1253
+ const origin = v?.GetOrigin?.();
1254
+ if (bounds && origin) {
1255
+ v.FitView(
1256
+ bounds.minX - origin.x,
1257
+ bounds.maxX - origin.x,
1258
+ bounds.minY - origin.y,
1259
+ bounds.maxY - origin.y
1260
+ );
1261
+ } else {
1262
+ v?.FitView?.();
1263
+ }
1264
+ v?.Render?.();
1265
+ } catch {
1266
+ }
1267
+ };
1268
+ const runCommand = (raw) => {
1269
+ const exec = raw.trim() || lastCmdRef.current;
1270
+ setCmdValue("");
1271
+ if (!exec) return;
1272
+ const cmd = exec.toLowerCase().split(/\s+/)[0];
1273
+ const remember = () => {
1274
+ lastCmdRef.current = exec;
1275
+ };
1276
+ const startMeasure = (mode, echo) => {
1277
+ setMeasureEnabled(true);
1278
+ setMeasureMode(mode);
1279
+ measureResetRef.current?.();
1280
+ setCmdEcho(echo);
1281
+ remember();
1282
+ };
1283
+ if (/^-?(\d+\.?\d*|\.\d+)$/.test(cmd)) {
1284
+ if (measureEnabled && (measureMode === "horizontal" || measureMode === "vertical")) {
1285
+ const n = parseFloat(cmd);
1286
+ if (n) {
1287
+ setMeasureFixedInput(cmd);
1288
+ setMeasureFixedDist(n);
1289
+ setCmdEcho(`\u0394${measureMode === "horizontal" ? "X" : "Y"} locked to ${formatMeasureDistance(Math.abs(n))} \u2014 the label shows the perpendicular distance`);
1290
+ } else {
1291
+ setMeasureFixedInput("");
1292
+ setMeasureFixedDist(null);
1293
+ setCmdEcho("Fixed distance cleared.");
1294
+ }
1295
+ } else {
1296
+ setCmdEcho("Fixed distances apply in H or V mode \u2014 type H or V first.");
1297
+ }
1298
+ return;
1299
+ }
1300
+ switch (cmd) {
1301
+ case "di":
1302
+ case "dist":
1303
+ case "mea":
1304
+ case "measuregeom":
1305
+ startMeasure("point", "DIST \u2014 click two points; straight-line distance (Esc to exit)");
1306
+ break;
1307
+ case "dim":
1308
+ case "dli":
1309
+ case "dimlin":
1310
+ case "dimlinear":
1311
+ startMeasure("auto", "DIMLINEAR \u2014 click two points; measures \u0394X or \u0394Y, whichever is larger (H / V to force)");
1312
+ break;
1313
+ // H / V / AUTO switch the axis without dropping existing picks —
1314
+ // same as clicking the pill.
1315
+ case "h":
1316
+ case "hor":
1317
+ case "horizontal":
1318
+ setMeasureEnabled(true);
1319
+ setMeasureMode("horizontal");
1320
+ setCmdEcho("Horizontal \u2014 \u0394X between the two picks");
1321
+ remember();
1322
+ break;
1323
+ case "v":
1324
+ case "ver":
1325
+ case "vertical":
1326
+ setMeasureEnabled(true);
1327
+ setMeasureMode("vertical");
1328
+ setCmdEcho("Vertical \u2014 \u0394Y between the two picks");
1329
+ remember();
1330
+ break;
1331
+ case "au":
1332
+ case "auto":
1333
+ setMeasureEnabled(true);
1334
+ setMeasureMode("auto");
1335
+ setCmdEcho("Auto (DIMLINEAR) \u2014 measures along the dominant axis of the two picks");
1336
+ remember();
1337
+ break;
1338
+ case "measure":
1339
+ setMeasureEnabled(!measureEnabled);
1340
+ setCmdEcho(measureEnabled ? "Measure off." : "Measure on \u2014 click two points.");
1341
+ remember();
1342
+ break;
1343
+ case "u":
1344
+ case "undo":
1345
+ measureUndoRef.current?.();
1346
+ setCmdEcho("Last pick removed.");
1347
+ remember();
1348
+ break;
1349
+ case "z":
1350
+ case "ze":
1351
+ case "zoom":
1352
+ case "fit":
1353
+ case "zoomextents":
1354
+ handleResetView();
1355
+ setCmdEcho("Zoom extents.");
1356
+ remember();
1357
+ break;
1358
+ case "la":
1359
+ case "layer":
1360
+ case "layers":
1361
+ setShowLayers((s) => !s);
1362
+ setCmdEcho("Layer panel toggled.");
1363
+ remember();
1364
+ break;
1365
+ case "off":
1366
+ case "clear":
1367
+ setMeasureFixedDist(null);
1368
+ setMeasureFixedInput("");
1369
+ measureResetRef.current?.();
1370
+ setCmdEcho("Measurement cleared.");
1371
+ remember();
1372
+ break;
1373
+ case "?":
1374
+ case "help":
1375
+ setCmdEcho(DXF_CMD_HELP);
1376
+ break;
1377
+ default: {
1378
+ const editCmd = DXF_EDIT_COMMANDS[cmd];
1379
+ if (editCmd) setCmdEcho(`${editCmd} is a drawing command \u2014 Preview is a read-only viewer. Measuring: DI, DIM, H, V (? for help)`);
1380
+ else setCmdEcho(`Unknown command "${cmd.toUpperCase()}" \u2014 try DI, DIM, H, V, Z, LA (? for help)`);
1381
+ }
1382
+ }
1383
+ };
1384
+ useEffect(() => {
1385
+ const onKey = (e) => {
1386
+ if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
1387
+ if (e.key.length !== 1 || e.key === " ") return;
1388
+ const t = e.target;
1389
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
1390
+ const root = rootRef.current;
1391
+ if (!root) return;
1392
+ const myModal = root.closest("[data-modal-id]");
1393
+ if (myModal && getActiveModalId() !== myModal.dataset.modalId) return;
1394
+ cmdInputRef.current?.focus();
1395
+ };
1396
+ window.addEventListener("keydown", onKey);
1397
+ return () => window.removeEventListener("keydown", onKey);
1398
+ }, []);
1399
+ const cmdValueRef = useRef(cmdValue);
1400
+ const measureEnabledRef = useRef(measureEnabled);
1401
+ useEffect(() => {
1402
+ cmdValueRef.current = cmdValue;
1403
+ }, [cmdValue]);
1404
+ useEffect(() => {
1405
+ measureEnabledRef.current = measureEnabled;
1406
+ }, [measureEnabled]);
1407
+ useEffect(() => {
1408
+ return registerModalEscapeInterceptor(() => {
1409
+ const root = rootRef.current;
1410
+ if (!root) return false;
1411
+ const myModal = root.closest("[data-modal-id]");
1412
+ if (!myModal || getActiveModalId() !== myModal.dataset.modalId) return false;
1413
+ if (cmdValueRef.current) {
1414
+ setCmdValue("");
1415
+ setCmdEcho("*Cancel*");
1416
+ return true;
1417
+ }
1418
+ if (measureEnabledRef.current) {
1419
+ setMeasureEnabled(false);
1420
+ setCmdEcho("Measure off.");
1421
+ return true;
1422
+ }
1423
+ return false;
1424
+ });
1425
+ }, []);
1426
+ const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
1427
+ const colorHex = (n) => {
1428
+ if (typeof n !== "number") return "#999";
1429
+ return "#" + n.toString(16).padStart(6, "0");
1430
+ };
1431
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "flex flex-col h-full", children: [
1432
+ /* @__PURE__ */ jsxs(PanelActions, { children: [
1433
+ /* @__PURE__ */ jsxs(
1434
+ "button",
1435
+ {
1436
+ onClick: () => setShowLayers((s) => !s),
1437
+ className: btn + (showLayers ? " bg-gray-200" : ""),
1438
+ title: "Toggle layer visibility",
1439
+ disabled: layers.length === 0,
1440
+ children: [
1441
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6.429 9.75L2.25 12l4.179 2.25m0-4.5l5.571 3 5.571-3m-11.142 0L2.25 7.5 12 2.25l9.75 5.25-4.179 2.25m0 0L21.75 12l-4.179 2.25m0 0l4.179 2.25L12 21.75 2.25 16.5l4.179-2.25m11.142 0l-5.571 3-5.571-3" }) }),
1442
+ "Layers ",
1443
+ layers.length > 0 && /* @__PURE__ */ jsxs("span", { className: "text-gray-400", children: [
1444
+ "(",
1445
+ layers.filter((l) => l.visible).length,
1446
+ "/",
1447
+ layers.length,
1448
+ ")"
1449
+ ] })
1450
+ ]
1451
+ }
1452
+ ),
1453
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowHint((s) => !s), className: btn, title: "How to navigate", children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" }) }) }),
1454
+ /* @__PURE__ */ jsxs(
1455
+ "button",
1456
+ {
1457
+ onClick: () => setMeasureEnabled((m) => !m),
1458
+ className: btn + (measureEnabled ? " bg-gray-200" : ""),
1459
+ title: measureEnabled ? "Stop measuring (Esc)" : "Measure distance \u2014 click two points on the drawing, or type DI / DIM below",
1460
+ children: [
1461
+ /* @__PURE__ */ jsxs("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
1462
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 14.25l6-6 6 6 4.5-4.5M9.75 8.25v3M12.75 11.25v3M15.75 14.25v3" }),
1463
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.25 18.75h19.5" })
1464
+ ] }),
1465
+ "Measure"
1466
+ ]
1467
+ }
1468
+ ),
1469
+ measureEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
1470
+ /* @__PURE__ */ jsxs("div", { className: "flex items-stretch h-7 rounded border border-gray-200 overflow-hidden text-[11px] font-semibold", children: [
1471
+ /* @__PURE__ */ jsx(
1472
+ "button",
1473
+ {
1474
+ onClick: () => setMeasureMode("auto"),
1475
+ className: `px-2 transition-colors ${measureMode === "auto" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
1476
+ title: "Auto \u2014 AutoCAD DIMLINEAR: measures \u0394X or \u0394Y, whichever is larger between the two picks",
1477
+ children: "Auto"
1478
+ }
1479
+ ),
1480
+ /* @__PURE__ */ jsx(
1481
+ "button",
1482
+ {
1483
+ onClick: () => setMeasureMode("horizontal"),
1484
+ className: `px-2 transition-colors ${measureMode === "horizontal" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
1485
+ title: "Horizontal \u2014 distance along the X axis between two picks (AutoCAD DIMLINEAR horizontal)",
1486
+ children: "H"
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ jsx(
1490
+ "button",
1491
+ {
1492
+ onClick: () => setMeasureMode("vertical"),
1493
+ className: `px-2 transition-colors ${measureMode === "vertical" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
1494
+ title: "Vertical \u2014 distance along the Y axis between two picks (AutoCAD DIMLINEAR vertical)",
1495
+ children: "V"
1496
+ }
1497
+ ),
1498
+ /* @__PURE__ */ jsx(
1499
+ "button",
1500
+ {
1501
+ onClick: () => setMeasureMode("point"),
1502
+ className: `px-2 transition-colors ${measureMode === "point" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
1503
+ title: "Point \u2014 straight-line (Euclidean) distance between two picks (AutoCAD DIST)",
1504
+ children: "Point"
1505
+ }
1506
+ )
1507
+ ] }),
1508
+ (measureMode === "horizontal" || measureMode === "vertical") && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1509
+ /* @__PURE__ */ jsx(
1510
+ "input",
1511
+ {
1512
+ type: "number",
1513
+ inputMode: "decimal",
1514
+ step: "any",
1515
+ value: measureFixedInput,
1516
+ placeholder: measureMode === "horizontal" ? "fix \u0394x" : "fix \u0394y",
1517
+ onChange: (e) => {
1518
+ const raw = e.target.value;
1519
+ setMeasureFixedInput(raw);
1520
+ if (raw.trim() === "") setMeasureFixedDist(null);
1521
+ else {
1522
+ const n = parseFloat(raw);
1523
+ setMeasureFixedDist(Number.isFinite(n) && n !== 0 ? n : null);
1524
+ }
1525
+ },
1526
+ className: "h-7 w-20 px-1.5 text-[11px] font-mono rounded border border-gray-200 bg-white text-gray-700 focus:outline-none focus:border-orange-400",
1527
+ title: "Lock the second pick's axis coord to first pick + this value (mm). The reported measurement becomes the perpendicular distance."
1528
+ }
1529
+ ),
1530
+ measureFixedDist !== null && /* @__PURE__ */ jsx(
1531
+ "button",
1532
+ {
1533
+ onClick: () => {
1534
+ setMeasureFixedDist(null);
1535
+ setMeasureFixedInput("");
1536
+ },
1537
+ className: "text-gray-400 hover:text-gray-600 text-[11px] px-1",
1538
+ title: "Clear fixed distance",
1539
+ children: "\xD7"
1540
+ }
1541
+ )
1542
+ ] })
1543
+ ] }),
1544
+ measureEnabled && measureDistance !== null && /* @__PURE__ */ jsxs(
1545
+ "div",
1546
+ {
1547
+ className: "px-2 py-1 text-[11px] font-mono font-semibold text-orange-600 bg-orange-50 border border-orange-200 rounded whitespace-nowrap",
1548
+ title: measureResolved === "horizontal" ? `Horizontal distance (\u0394x) between the two picked points${measureMode === "auto" ? " \u2014 Auto resolved to the X axis" : ""}` : measureResolved === "vertical" ? `Vertical distance (\u0394y) between the two picked points${measureMode === "auto" ? " \u2014 Auto resolved to the Y axis" : ""}` : "Straight-line distance between the two picked points",
1549
+ children: [
1550
+ formatMeasureDistance(measureDistance),
1551
+ measureResolved === "horizontal" ? " \u2194" : measureResolved === "vertical" ? " \u2195" : ""
1552
+ ]
1553
+ }
1554
+ ),
1555
+ /* @__PURE__ */ jsx("button", { onClick: handleResetView, className: btn, title: "Fit drawing to view", children: "Fit" }),
1556
+ /* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
1557
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
1558
+ "Download"
1559
+ ] }),
1560
+ onEmail && /* @__PURE__ */ jsxs("button", { onClick: onEmail, className: btn, children: [
1561
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" }) }),
1562
+ "Email"
1563
+ ] })
1564
+ ] }),
1565
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 bg-white min-h-0", children: [
1566
+ /* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%", height: "100%" } }),
1567
+ showLayers && layers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "absolute top-2 right-2 w-64 max-h-[70%] flex flex-col bg-white/95 backdrop-blur border border-gray-200 rounded-md shadow-xl z-10 text-xs", children: [
1568
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 border-b border-gray-200 bg-gray-50", children: [
1569
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "Layers" }),
1570
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1571
+ /* @__PURE__ */ jsx("button", { onClick: () => setAllLayers(true), className: "px-1.5 py-0.5 rounded hover:bg-gray-200 text-gray-600", children: "All" }),
1572
+ /* @__PURE__ */ jsx("button", { onClick: () => setAllLayers(false), className: "px-1.5 py-0.5 rounded hover:bg-gray-200 text-gray-600", children: "None" }),
1573
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowLayers(false), className: "px-1.5 py-0.5 rounded hover:bg-gray-200 text-gray-600", title: "Close", children: "\xD7" })
1574
+ ] })
1575
+ ] }),
1576
+ /* @__PURE__ */ jsx("div", { className: "overflow-y-auto py-1", children: layers.map((l) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 px-2 py-1 hover:bg-gray-100 cursor-pointer", children: [
1577
+ /* @__PURE__ */ jsx(
1578
+ "input",
1579
+ {
1580
+ type: "checkbox",
1581
+ checked: l.visible,
1582
+ onChange: () => toggleLayer(l.name),
1583
+ className: "h-3.5 w-3.5"
1584
+ }
1585
+ ),
1586
+ /* @__PURE__ */ jsx(
1587
+ "span",
1588
+ {
1589
+ className: "inline-block h-3 w-3 rounded-sm border border-gray-300 shrink-0",
1590
+ style: { background: colorHex(l.color) }
1591
+ }
1592
+ ),
1593
+ /* @__PURE__ */ jsx("span", { className: "truncate text-gray-700", title: l.displayName, children: l.displayName || l.name })
1594
+ ] }, l.name)) })
1595
+ ] }),
1596
+ showHint && !loading && !error && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-3 left-1/2 -translate-x-1/2 bg-gray-900/85 text-white text-[11px] px-3 py-1.5 rounded-full shadow-lg flex items-center gap-3 z-10 pointer-events-none", children: [
1597
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
1598
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" }) }),
1599
+ "Drag to pan"
1600
+ ] }),
1601
+ /* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
1602
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
1603
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" }) }),
1604
+ "Scroll to zoom"
1605
+ ] }),
1606
+ /* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
1607
+ /* @__PURE__ */ jsx("span", { children: "Fit to reset" }),
1608
+ /* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
1609
+ /* @__PURE__ */ jsxs("span", { children: [
1610
+ "Type ",
1611
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold", children: "DI" }),
1612
+ " / ",
1613
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold", children: "DIM" }),
1614
+ " to measure"
1615
+ ] })
1616
+ ] }),
1617
+ loading && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-white/80 text-sm text-gray-500", children: "Loading drawing\u2026" }),
1618
+ error && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-sm text-red-600 px-6 text-center", children: error })
1619
+ ] }),
1620
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-t border-gray-200 bg-gray-50", children: [
1621
+ cmdEcho && /* @__PURE__ */ jsx("div", { className: "px-2.5 pt-1 text-[11px] font-mono text-gray-500 truncate", title: cmdEcho, children: cmdEcho }),
1622
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2.5 h-7", children: [
1623
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-mono font-semibold text-gray-400 select-none", children: ">" }),
1624
+ /* @__PURE__ */ jsx(
1625
+ "input",
1626
+ {
1627
+ ref: cmdInputRef,
1628
+ value: cmdValue,
1629
+ onChange: (e) => setCmdValue(e.target.value),
1630
+ onKeyDown: (e) => {
1631
+ if (e.key === "Enter" || e.key === " ") {
1632
+ e.preventDefault();
1633
+ runCommand(cmdValue);
1634
+ } else if (e.key === "Escape") {
1635
+ if (cmdValue) {
1636
+ e.stopPropagation();
1637
+ setCmdValue("");
1638
+ setCmdEcho("*Cancel*");
1639
+ } else {
1640
+ e.target.blur();
1641
+ }
1642
+ }
1643
+ },
1644
+ placeholder: "Type a command \u2014 DI, DIM, H, V, Z, LA (? for help)",
1645
+ spellCheck: false,
1646
+ autoComplete: "off",
1647
+ className: "flex-1 min-w-0 bg-transparent text-[11px] font-mono text-gray-700 focus:outline-none placeholder:text-gray-300"
1648
+ }
1649
+ )
1650
+ ] })
1651
+ ] })
1652
+ ] });
1653
+ }
1654
+ var DEFAULT_O3DV_LIBS = "https://cdn.jsdelivr.net/npm/online-3d-viewer@0.18.0/libs/";
1655
+ function buildTree(node, depth = 0) {
1656
+ return {
1657
+ id: node.GetId?.() ?? 0,
1658
+ name: node.GetName?.() || (depth === 0 ? "Root" : "Node"),
1659
+ isMeshNode: !!node.IsMeshNode?.(),
1660
+ meshIndices: node.GetMeshIndices?.() ?? [],
1661
+ children: (node.GetChildNodes?.() ?? []).map((c) => buildTree(c, depth + 1))
1662
+ };
1663
+ }
1664
+ function collectNodeIds(node, out) {
1665
+ out.add(node.id);
1666
+ for (const c of node.children) collectNodeIds(c, out);
1667
+ }
1668
+ function hexToRgb(OV, hex) {
1669
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex);
1670
+ const n = m ? parseInt(m[1], 16) : 0;
1671
+ return new OV.RGBColor(n >> 16 & 255, n >> 8 & 255, n & 255);
1672
+ }
1673
+ function formatMeasureDistance(mm) {
1674
+ if (mm >= 1e3) return `${(mm / 1e3).toFixed(3)} m`;
1675
+ return `${mm.toFixed(2)} mm`;
1676
+ }
1677
+ function StepPanel({ url, filename, onDownload, onEmail }) {
1678
+ const containerRef = useRef(null);
1679
+ const viewerRef = useRef(null);
1680
+ const ovRef = useRef(null);
1681
+ const [loading, setLoading] = useState(true);
1682
+ const [error, setError] = useState(null);
1683
+ const [showHint, setShowHint] = useState(true);
1684
+ const [tree, setTree] = useState(null);
1685
+ const [expanded, setExpanded] = useState({});
1686
+ const [hidden, setHidden] = useState(/* @__PURE__ */ new Set());
1687
+ const [bgColor, setBgColor] = useState("#f5f6f8");
1688
+ const [showEdges, setShowEdges] = useState(true);
1689
+ const [edgeColor, setEdgeColor] = useState("#000000");
1690
+ const [edgeThreshold, setEdgeThreshold] = useState(1);
1691
+ const [showMeshes, setShowMeshes] = useState(false);
1692
+ const [showSettings, setShowSettings] = useState(false);
1693
+ const [perspective, setPerspective] = useState(false);
1694
+ const [sectionEnabled, setSectionEnabled] = useState(false);
1695
+ const [sectionAxis, setSectionAxis] = useState("x");
1696
+ const [sectionFlip, setSectionFlip] = useState(false);
1697
+ const [sectionPosition, setSectionPosition] = useState(0.5);
1698
+ const [sectionAngle, setSectionAngle] = useState(0);
1699
+ const [measureEnabled, setMeasureEnabled] = useState(false);
1700
+ const [measureMode, setMeasureMode] = useState("point");
1701
+ const [measureDistance, setMeasureDistance] = useState(null);
1702
+ const measureRef = useRef(null);
1703
+ const sectionRef = useRef(null);
1704
+ useEffect(() => {
1705
+ let cancelled = false;
1706
+ let viewer = null;
1707
+ let resizeObserver = null;
1708
+ setLoading(true);
1709
+ setError(null);
1710
+ setTree(null);
1711
+ setExpanded({});
1712
+ setHidden(/* @__PURE__ */ new Set());
1713
+ (async () => {
1714
+ let OV;
1715
+ try {
1716
+ OV = await import('online-3d-viewer');
1717
+ } catch {
1718
+ if (!cancelled) {
1719
+ setError("online-3d-viewer is not installed in this app. Add it to enable 3D file viewing.");
1720
+ setLoading(false);
1721
+ }
1722
+ return;
1723
+ }
1724
+ ovRef.current = OV;
1725
+ if (cancelled || !containerRef.current) return;
1726
+ await new Promise((resolve) => {
1727
+ const tryStart = () => {
1728
+ const r = containerRef.current?.getBoundingClientRect();
1729
+ if (r && r.width > 4 && r.height > 4) resolve();
1730
+ else requestAnimationFrame(tryStart);
1731
+ };
1732
+ tryStart();
1733
+ });
1734
+ if (cancelled || !containerRef.current) return;
1735
+ try {
1736
+ const libsBase = typeof window !== "undefined" && window.__REACT_OS_SHELL_O3DV_LIBS__ || DEFAULT_O3DV_LIBS;
1737
+ OV.SetExternalLibLocation?.(libsBase);
1738
+ ensureStencilContextAttribute();
1739
+ viewer = new OV.EmbeddedViewer(containerRef.current, {
1740
+ backgroundColor: new OV.RGBAColor(245, 246, 248, 255),
1741
+ defaultColor: new OV.RGBColor(180, 188, 200),
1742
+ edgeSettings: new OV.EdgeSettings(true, new OV.RGBColor(0, 0, 0), 1),
1743
+ onModelLoaded: () => {
1744
+ if (cancelled) return;
1745
+ try {
1746
+ const model = viewer.GetModel?.();
1747
+ const root = model?.GetRootNode?.();
1748
+ if (root) {
1749
+ const t = buildTree(root);
1750
+ setTree(t);
1751
+ const expandIds = { [t.id]: true };
1752
+ for (const c of t.children) expandIds[c.id] = true;
1753
+ setExpanded(expandIds);
1754
+ }
1755
+ } catch (err) {
1756
+ console.warn("[Preview] mesh tree extraction failed", err);
1757
+ }
1758
+ setLoading(false);
1759
+ },
1760
+ onModelLoadFailed: () => {
1761
+ if (!cancelled) {
1762
+ setError("Failed to load 3D model.");
1763
+ setLoading(false);
1764
+ }
1765
+ }
1766
+ });
1767
+ viewerRef.current = viewer;
1768
+ const inputFile = new OV.InputFile(filename, OV.FileSource.Url, url);
1769
+ viewer.LoadModelFromInputFiles([inputFile]);
1770
+ if (containerRef.current && typeof ResizeObserver !== "undefined") {
1771
+ resizeObserver = new ResizeObserver(() => {
1772
+ try {
1773
+ const v = viewerRef.current;
1774
+ if (v?.Resize) v.Resize();
1775
+ else if (v?.viewer?.Resize) v.viewer.Resize();
1776
+ v?.viewer?.Render?.();
1777
+ } catch {
1778
+ }
1779
+ });
1780
+ resizeObserver.observe(containerRef.current);
1781
+ }
1782
+ } catch (e) {
1783
+ if (!cancelled) {
1784
+ setError(e?.message || "Failed to load 3D model.");
1785
+ setLoading(false);
1786
+ }
1787
+ }
1788
+ })();
1789
+ return () => {
1790
+ cancelled = true;
1791
+ try {
1792
+ resizeObserver?.disconnect();
1793
+ } catch {
1794
+ }
1795
+ try {
1796
+ viewer?.Destroy?.();
1797
+ } catch {
1798
+ }
1799
+ viewerRef.current = null;
1800
+ };
1801
+ }, [url, filename]);
1802
+ useEffect(() => {
1803
+ const v = viewerRef.current;
1804
+ if (!v?.viewer) return;
1805
+ try {
1806
+ const visit = (mesh) => {
1807
+ if (mesh.userData?.__sectionHelper) return;
1808
+ const ud = mesh.userData?.originalMeshInstance ?? mesh.userData;
1809
+ const nodeId = ud?.id?.nodeId ?? ud?.nodeId;
1810
+ if (typeof nodeId === "number") {
1811
+ mesh.visible = !hidden.has(nodeId);
1812
+ }
1813
+ };
1814
+ v.viewer.mainModel?.EnumerateMeshesAndLines?.(visit);
1815
+ v.viewer.mainModel?.EnumerateEdges?.(visit);
1816
+ v.viewer.Render?.();
1817
+ } catch (err) {
1818
+ console.warn("[Preview] visibility update failed", err);
1819
+ }
1820
+ }, [hidden, tree]);
1821
+ useEffect(() => {
1822
+ const OV = ovRef.current;
1823
+ const v = viewerRef.current;
1824
+ if (!OV || !v?.viewer) return;
1825
+ try {
1826
+ v.viewer.SetEdgeSettings(new OV.EdgeSettings(showEdges, hexToRgb(OV, edgeColor), edgeThreshold));
1827
+ const s = sectionRef.current;
1828
+ if (s) {
1829
+ const helperEdges = [];
1830
+ v.viewer.mainModel?.EnumerateEdges?.((edge) => {
1831
+ if (edge.userData?.__sectionHelper) helperEdges.push(edge);
1832
+ });
1833
+ for (const e of helperEdges) {
1834
+ e.parent?.remove(e);
1835
+ e.geometry?.dispose?.();
1836
+ e.material?.dispose?.();
1837
+ }
1838
+ const plane = s.plane;
1839
+ const applyMatClip = (mat) => {
1840
+ if (!mat || s.materialState.has(mat)) return;
1841
+ s.materialState.set(mat, { clippingPlanes: mat.clippingPlanes, clipShadows: mat.clipShadows });
1842
+ mat.clippingPlanes = [plane];
1843
+ mat.clipShadows = true;
1844
+ mat.needsUpdate = true;
1845
+ };
1846
+ v.viewer.mainModel?.EnumerateEdges?.((edge) => {
1847
+ const mat = edge.material;
1848
+ if (Array.isArray(mat)) for (const m of mat) applyMatClip(m);
1849
+ else applyMatClip(mat);
1850
+ });
1851
+ }
1852
+ v.viewer.Render?.();
1853
+ } catch {
1854
+ }
1855
+ }, [showEdges, edgeColor, edgeThreshold]);
1856
+ useEffect(() => {
1857
+ const OV = ovRef.current;
1858
+ const v = viewerRef.current;
1859
+ if (!OV || !v?.viewer) return;
1860
+ try {
1861
+ const c = hexToRgb(OV, bgColor);
1862
+ v.viewer.SetBackgroundColor(new OV.RGBAColor(c.r, c.g, c.b, 255));
1863
+ v.viewer.Render?.();
1864
+ } catch {
1865
+ }
1866
+ }, [bgColor]);
1867
+ useEffect(() => {
1868
+ const v = viewerRef.current;
1869
+ if (!v?.viewer || loading) return;
1870
+ const renderer = v.viewer.renderer;
1871
+ const scene = v.viewer.scene;
1872
+ if (!renderer || !scene) return;
1873
+ if (sectionRef.current) {
1874
+ const s = sectionRef.current;
1875
+ for (const [mat, prev] of s.materialState.entries()) {
1876
+ mat.clippingPlanes = prev.clippingPlanes;
1877
+ mat.clipShadows = prev.clipShadows;
1878
+ mat.needsUpdate = true;
1879
+ }
1880
+ for (const helper of s.helpers) {
1881
+ helper.parent?.remove(helper);
1882
+ helper.material?.dispose?.();
1883
+ }
1884
+ if (s.capMesh) {
1885
+ scene.remove(s.capMesh);
1886
+ s.capMesh.geometry?.dispose?.();
1887
+ s.capMesh.material?.dispose?.();
1888
+ }
1889
+ sectionRef.current = null;
1890
+ }
1891
+ if (!sectionEnabled) {
1892
+ renderer.localClippingEnabled = false;
1893
+ v.viewer.Render?.();
1894
+ return;
1895
+ }
1896
+ const bbox = v.viewer.GetBoundingBox?.(() => true);
1897
+ if (!bbox) return;
1898
+ const targets = [];
1899
+ v.viewer.mainModel?.EnumerateMeshes?.((mesh) => {
1900
+ if (!mesh.userData?.__sectionHelper) targets.push(mesh);
1901
+ });
1902
+ if (!targets.length) {
1903
+ renderer.localClippingEnabled = false;
1904
+ v.viewer.Render?.();
1905
+ return;
1906
+ }
1907
+ const sample = targets[0];
1908
+ const Mesh = sample.constructor;
1909
+ const Material = sample.material?.constructor;
1910
+ const Geometry = sample.geometry?.constructor;
1911
+ const BufferAttr = sample.geometry?.attributes?.position?.constructor;
1912
+ if (!Mesh || !Material || !Geometry || !BufferAttr) {
1913
+ console.warn("[Preview] section: missing THREE constructors, falling back to no cap");
1914
+ }
1915
+ const FrontSide = 0, BackSide = 1, DoubleSide = 2;
1916
+ const AlwaysStencilFunc = 519, NotEqualStencilFunc = 517;
1917
+ const IncrementWrapStencilOp = 7682, DecrementWrapStencilOp = 7683;
1918
+ const ReplaceStencilOp = 7681;
1919
+ let hasStencil = true;
1920
+ try {
1921
+ const gl = renderer.getContext?.();
1922
+ const attrs = gl?.getContextAttributes?.();
1923
+ hasStencil = attrs?.stencil !== false;
1924
+ renderer.autoClearStencil = true;
1925
+ console.info("[Preview] section: stencil buffer =", hasStencil, "targets =", targets.length);
1926
+ } catch {
1927
+ }
1928
+ renderer.localClippingEnabled = true;
1929
+ const plane = {
1930
+ normal: { x: 0, y: 0, z: -1 },
1931
+ constant: 0
1932
+ };
1933
+ plane.clone = function planeClone() {
1934
+ const c = {
1935
+ normal: { x: this.normal.x, y: this.normal.y, z: this.normal.z },
1936
+ constant: this.constant
1937
+ };
1938
+ c.clone = planeClone;
1939
+ return c;
1940
+ };
1941
+ const helpers = [];
1942
+ const materialState = /* @__PURE__ */ new Map();
1943
+ const applyToMaterial = (mat) => {
1944
+ if (!mat || materialState.has(mat)) return;
1945
+ materialState.set(mat, { clippingPlanes: mat.clippingPlanes, clipShadows: mat.clipShadows });
1946
+ mat.clippingPlanes = [plane];
1947
+ mat.clipShadows = true;
1948
+ mat.needsUpdate = true;
1949
+ };
1950
+ for (const mesh of targets) {
1951
+ const mat = mesh.material;
1952
+ if (Array.isArray(mat)) for (const m of mat) applyToMaterial(m);
1953
+ else applyToMaterial(mat);
1954
+ const sourceMat2 = Array.isArray(mat) ? mat[0] : mat;
1955
+ if (Mesh && hasStencil && sourceMat2?.clone) {
1956
+ const makeStencil = (side, op) => {
1957
+ const m = sourceMat2.clone();
1958
+ m.depthWrite = false;
1959
+ m.depthTest = false;
1960
+ m.colorWrite = false;
1961
+ m.stencilWrite = true;
1962
+ m.stencilFunc = AlwaysStencilFunc;
1963
+ m.stencilFail = op;
1964
+ m.stencilZFail = op;
1965
+ m.stencilZPass = op;
1966
+ m.side = side;
1967
+ m.transparent = false;
1968
+ m.clippingPlanes = [plane];
1969
+ m.clipShadows = true;
1970
+ m.needsUpdate = true;
1971
+ const helper = new Mesh(mesh.geometry, m);
1972
+ helper.matrixAutoUpdate = false;
1973
+ helper.renderOrder = 1;
1974
+ helper.userData.__sectionHelper = true;
1975
+ mesh.add(helper);
1976
+ helpers.push(helper);
1977
+ };
1978
+ makeStencil(BackSide, IncrementWrapStencilOp);
1979
+ makeStencil(FrontSide, DecrementWrapStencilOp);
1980
+ }
1981
+ }
1982
+ v.viewer.mainModel?.EnumerateEdges?.((edge) => {
1983
+ const mat = edge.material;
1984
+ if (Array.isArray(mat)) for (const m of mat) applyToMaterial(m);
1985
+ else applyToMaterial(mat);
1986
+ });
1987
+ let capMesh = null;
1988
+ const sourceMat = targets[0]?.material && (Array.isArray(targets[0].material) ? targets[0].material[0] : targets[0].material);
1989
+ if (hasStencil && Mesh && Geometry && BufferAttr && sourceMat?.clone) {
1990
+ const dx = bbox.max.x - bbox.min.x;
1991
+ const dy = bbox.max.y - bbox.min.y;
1992
+ const dz = bbox.max.z - bbox.min.z;
1993
+ const capSize = Math.max(dx, dy, dz, 1) * 2;
1994
+ const half = capSize / 2;
1995
+ const capGeom = new Geometry();
1996
+ const positions = new Float32Array([
1997
+ -half,
1998
+ -half,
1999
+ 0,
2000
+ half,
2001
+ -half,
2002
+ 0,
2003
+ half,
2004
+ half,
2005
+ 0,
2006
+ -half,
2007
+ half,
2008
+ 0
2009
+ ]);
2010
+ const normals = new Float32Array([
2011
+ 0,
2012
+ 0,
2013
+ 1,
2014
+ 0,
2015
+ 0,
2016
+ 1,
2017
+ 0,
2018
+ 0,
2019
+ 1,
2020
+ 0,
2021
+ 0,
2022
+ 1
2023
+ ]);
2024
+ capGeom.setAttribute("position", new BufferAttr(positions, 3));
2025
+ capGeom.setAttribute("normal", new BufferAttr(normals, 3));
2026
+ capGeom.setIndex([0, 1, 2, 0, 2, 3]);
2027
+ const capMat = sourceMat.clone();
2028
+ const sourceColorHex = sourceMat.color?.getHex?.() ?? 11844808;
2029
+ capMat.emissive?.setHex?.(sourceColorHex);
2030
+ if ("emissiveIntensity" in capMat) capMat.emissiveIntensity = 0.25;
2031
+ capMat.side = DoubleSide;
2032
+ capMat.transparent = false;
2033
+ capMat.opacity = 1;
2034
+ capMat.stencilWrite = true;
2035
+ capMat.stencilRef = 0;
2036
+ capMat.stencilFunc = NotEqualStencilFunc;
2037
+ capMat.stencilFail = ReplaceStencilOp;
2038
+ capMat.stencilZFail = ReplaceStencilOp;
2039
+ capMat.stencilZPass = ReplaceStencilOp;
2040
+ capMat.clippingPlanes = [];
2041
+ capMat.polygonOffset = true;
2042
+ capMat.polygonOffsetFactor = -1;
2043
+ capMat.polygonOffsetUnits = -1;
2044
+ capMat.needsUpdate = true;
2045
+ capMesh = new Mesh(capGeom, capMat);
2046
+ capMesh.renderOrder = 2;
2047
+ capMesh.userData.__sectionHelper = true;
2048
+ scene.add(capMesh);
2049
+ }
2050
+ sectionRef.current = { plane, capMesh, helpers, materialState, bbox };
2051
+ console.info("[Preview] section: helpers =", helpers.length, "cap =", !!capMesh);
2052
+ v.viewer.Render?.();
2053
+ }, [sectionEnabled, loading, tree]);
2054
+ useEffect(() => {
2055
+ const v = viewerRef.current;
2056
+ const s = sectionRef.current;
2057
+ if (!v?.viewer || !s || !sectionEnabled) return;
2058
+ try {
2059
+ const bbox = s.bbox;
2060
+ const axisIdx = sectionAxis === "x" ? 0 : sectionAxis === "y" ? 1 : 2;
2061
+ const min = [bbox.min.x, bbox.min.y, bbox.min.z][axisIdx];
2062
+ const max = [bbox.max.x, bbox.max.y, bbox.max.z][axisIdx];
2063
+ const t = min + (max - min) * sectionPosition;
2064
+ const \u03B8 = sectionAngle * Math.PI / 180;
2065
+ const cos\u03B8 = Math.cos(\u03B8), sin\u03B8 = Math.sin(\u03B8);
2066
+ const sign = sectionFlip ? -1 : 1;
2067
+ let nx = 0, ny = 0, nz = 0;
2068
+ if (sectionAxis === "x") {
2069
+ nx = sign * cos\u03B8;
2070
+ nz = sign * sin\u03B8;
2071
+ } else if (sectionAxis === "y") {
2072
+ ny = sign * cos\u03B8;
2073
+ nx = sign * sin\u03B8;
2074
+ } else {
2075
+ nz = sign * cos\u03B8;
2076
+ ny = sign * sin\u03B8;
2077
+ }
2078
+ const center = {
2079
+ x: (bbox.min.x + bbox.max.x) / 2,
2080
+ y: (bbox.min.y + bbox.max.y) / 2,
2081
+ z: (bbox.min.z + bbox.max.z) / 2
2082
+ };
2083
+ const Px = sectionAxis === "x" ? t : center.x;
2084
+ const Py = sectionAxis === "y" ? t : center.y;
2085
+ const Pz = sectionAxis === "z" ? t : center.z;
2086
+ s.plane.normal.x = nx;
2087
+ s.plane.normal.y = ny;
2088
+ s.plane.normal.z = nz;
2089
+ s.plane.constant = -(nx * Px + ny * Py + nz * Pz);
2090
+ if (s.capMesh) {
2091
+ const cx = center.x, cy = center.y, cz = center.z;
2092
+ const dist = nx * cx + ny * cy + nz * cz + s.plane.constant;
2093
+ const px = cx - nx * dist;
2094
+ const py = cy - ny * dist;
2095
+ const pz = cz - nz * dist;
2096
+ s.capMesh.position.set(px, py, pz);
2097
+ s.capMesh.lookAt?.(px + nx, py + ny, pz + nz);
2098
+ }
2099
+ v.viewer.Render?.();
2100
+ } catch (err) {
2101
+ console.warn("[Preview] section update failed", err);
2102
+ }
2103
+ }, [sectionEnabled, sectionAxis, sectionFlip, sectionPosition, sectionAngle]);
2104
+ useEffect(() => {
2105
+ const v = viewerRef.current;
2106
+ if (!v?.viewer || loading || !containerRef.current) return;
2107
+ const renderer = v.viewer.renderer;
2108
+ const scene = v.viewer.scene;
2109
+ const canvas = renderer?.domElement;
2110
+ if (!renderer || !scene || !canvas) return;
2111
+ let sampleMesh = null;
2112
+ v.viewer.mainModel?.EnumerateMeshes?.((m) => {
2113
+ if (!sampleMesh && !m.userData?.__sectionHelper && !m.userData?.__measureHelper) sampleMesh = m;
2114
+ });
2115
+ if (!sampleMesh) return;
2116
+ const Vector3Ctor = sampleMesh.position.constructor;
2117
+ const MeshCtor = sampleMesh.constructor;
2118
+ const MaterialCtor = (Array.isArray(sampleMesh.material) ? sampleMesh.material[0] : sampleMesh.material)?.constructor;
2119
+ const GeometryCtor = sampleMesh.geometry.constructor;
2120
+ const BufferAttrCtor = sampleMesh.geometry.attributes?.position?.constructor;
2121
+ if (!Vector3Ctor || !MeshCtor || !MaterialCtor || !GeometryCtor || !BufferAttrCtor) return;
2122
+ let LineSegmentsCtor = null;
2123
+ let LineBasicMaterialCtor = null;
2124
+ scene.traverse((obj) => {
2125
+ if (LineSegmentsCtor && LineBasicMaterialCtor) return;
2126
+ if (!obj?.isLineSegments) return;
2127
+ LineSegmentsCtor = obj.constructor;
2128
+ const mat = Array.isArray(obj.material) ? obj.material[0] : obj.material;
2129
+ if (mat?.constructor) LineBasicMaterialCtor = mat.constructor;
2130
+ });
2131
+ const teardown = () => {
2132
+ const s = measureRef.current;
2133
+ if (!s) return;
2134
+ if (s.rafId !== null) cancelAnimationFrame(s.rafId);
2135
+ for (const m of s.markers) {
2136
+ scene.remove(m);
2137
+ m.geometry?.dispose?.();
2138
+ m.material?.dispose?.();
2139
+ }
2140
+ if (s.line) {
2141
+ scene.remove(s.line);
2142
+ s.line.geometry?.dispose?.();
2143
+ s.line.material?.dispose?.();
2144
+ }
2145
+ if (s.label && s.label.parentElement) s.label.parentElement.removeChild(s.label);
2146
+ measureRef.current = null;
2147
+ v.viewer.Render?.();
2148
+ };
2149
+ if (!measureEnabled) {
2150
+ teardown();
2151
+ setMeasureDistance(null);
2152
+ return;
2153
+ }
2154
+ measureRef.current = { points: [], normals: [], markers: [], line: null, label: null, rafId: null };
2155
+ const OV = ovRef.current;
2156
+ const intersectionMode = OV?.IntersectionMode?.MeshOnly ?? 1;
2157
+ const bbox = v.viewer.GetBoundingBox?.(() => true);
2158
+ const diag = bbox ? Math.sqrt(
2159
+ (bbox.max.x - bbox.min.x) ** 2 + (bbox.max.y - bbox.min.y) ** 2 + (bbox.max.z - bbox.min.z) ** 2
2160
+ ) : 100;
2161
+ const markerRadius = Math.max(diag * 5e-3, 0.2);
2162
+ const makeMarker = (point) => {
2163
+ const widthSegs = 16, heightSegs = 12;
2164
+ const positions = [];
2165
+ const indices = [];
2166
+ for (let iy = 0; iy <= heightSegs; iy++) {
2167
+ const v2 = iy / heightSegs;
2168
+ const phi = v2 * Math.PI;
2169
+ for (let ix = 0; ix <= widthSegs; ix++) {
2170
+ const u = ix / widthSegs;
2171
+ const theta = u * Math.PI * 2;
2172
+ positions.push(
2173
+ markerRadius * Math.sin(phi) * Math.cos(theta),
2174
+ markerRadius * Math.cos(phi),
2175
+ markerRadius * Math.sin(phi) * Math.sin(theta)
2176
+ );
2177
+ }
2178
+ }
2179
+ for (let iy = 0; iy < heightSegs; iy++) {
2180
+ for (let ix = 0; ix < widthSegs; ix++) {
2181
+ const a = iy * (widthSegs + 1) + ix;
2182
+ const b = a + widthSegs + 1;
2183
+ indices.push(a, b, a + 1, b, b + 1, a + 1);
2184
+ }
2185
+ }
2186
+ const geom = new GeometryCtor();
2187
+ geom.setAttribute("position", new BufferAttrCtor(new Float32Array(positions), 3));
2188
+ geom.setIndex(indices);
2189
+ geom.computeVertexNormals?.();
2190
+ const mat = new MaterialCtor();
2191
+ mat.color?.setHex?.(16746496);
2192
+ mat.depthTest = false;
2193
+ mat.depthWrite = false;
2194
+ mat.transparent = true;
2195
+ mat.opacity = 0.95;
2196
+ const mesh = new MeshCtor(geom, mat);
2197
+ mesh.position.copy(point);
2198
+ mesh.renderOrder = 9999;
2199
+ mesh.userData.__measureHelper = true;
2200
+ scene.add(mesh);
2201
+ return mesh;
2202
+ };
2203
+ const buildFaceHighlight = (mesh, hit) => {
2204
+ const geom = mesh?.geometry;
2205
+ const posAttr = geom?.attributes?.position;
2206
+ if (!geom || !posAttr || !hit?.face?.normal) return null;
2207
+ const positions = posAttr.array;
2208
+ const indices = geom.index ? geom.index.array : null;
2209
+ const localPoint = new Vector3Ctor(hit.point.x, hit.point.y, hit.point.z);
2210
+ mesh.worldToLocal?.(localPoint);
2211
+ const ln = hit.face.normal;
2212
+ const planeC = -(ln.x * localPoint.x + ln.y * localPoint.y + ln.z * localPoint.z);
2213
+ geom.computeBoundingBox?.();
2214
+ const bb = geom.boundingBox;
2215
+ const diag2 = bb ? Math.sqrt((bb.max.x - bb.min.x) ** 2 + (bb.max.y - bb.min.y) ** 2 + (bb.max.z - bb.min.z) ** 2) : 100;
2216
+ const PLANE_TOL = Math.max(diag2 * 1e-3, 5e-3);
2217
+ const NORMAL_TOL = 0.9995;
2218
+ const triCount = indices ? indices.length / 3 : posAttr.count / 3;
2219
+ const matched = [];
2220
+ const tmpV = new Vector3Ctor();
2221
+ for (let t = 0; t < triCount; t++) {
2222
+ const ia = indices ? indices[t * 3] : t * 3;
2223
+ const ib = indices ? indices[t * 3 + 1] : t * 3 + 1;
2224
+ const ic = indices ? indices[t * 3 + 2] : t * 3 + 2;
2225
+ const ax = positions[ia * 3], ay = positions[ia * 3 + 1], az = positions[ia * 3 + 2];
2226
+ const bx = positions[ib * 3], by = positions[ib * 3 + 1], bz = positions[ib * 3 + 2];
2227
+ const cx = positions[ic * 3], cy = positions[ic * 3 + 1], cz = positions[ic * 3 + 2];
2228
+ const ux = bx - ax, uy = by - ay, uz = bz - az;
2229
+ const vx = cx - ax, vy = cy - ay, vz = cz - az;
2230
+ let nx = uy * vz - uz * vy;
2231
+ let ny = uz * vx - ux * vz;
2232
+ let nz = ux * vy - uy * vx;
2233
+ const nlen = Math.sqrt(nx * nx + ny * ny + nz * nz);
2234
+ if (nlen === 0) continue;
2235
+ nx /= nlen;
2236
+ ny /= nlen;
2237
+ nz /= nlen;
2238
+ const ndot = nx * ln.x + ny * ln.y + nz * ln.z;
2239
+ if (ndot < NORMAL_TOL) continue;
2240
+ const ccx = (ax + bx + cx) / 3, ccy = (ay + by + cy) / 3, ccz = (az + bz + cz) / 3;
2241
+ const dist = Math.abs(ln.x * ccx + ln.y * ccy + ln.z * ccz + planeC);
2242
+ if (dist > PLANE_TOL) continue;
2243
+ for (const [vx_, vy_, vz_] of [[ax, ay, az], [bx, by, bz], [cx, cy, cz]]) {
2244
+ tmpV.set?.(vx_, vy_, vz_);
2245
+ mesh.localToWorld?.(tmpV);
2246
+ matched.push(tmpV.x, tmpV.y, tmpV.z);
2247
+ }
2248
+ }
2249
+ if (matched.length === 0) return null;
2250
+ const hgeom = new GeometryCtor();
2251
+ hgeom.setAttribute("position", new BufferAttrCtor(new Float32Array(matched), 3));
2252
+ hgeom.computeVertexNormals?.();
2253
+ const hmat = new MaterialCtor();
2254
+ hmat.color?.setHex?.(16746496);
2255
+ hmat.transparent = true;
2256
+ hmat.opacity = 0.45;
2257
+ hmat.depthWrite = false;
2258
+ hmat.polygonOffset = true;
2259
+ hmat.polygonOffsetFactor = -2;
2260
+ hmat.polygonOffsetUnits = -2;
2261
+ const highlight = new MeshCtor(hgeom, hmat);
2262
+ highlight.renderOrder = 9998;
2263
+ highlight.userData.__measureHelper = true;
2264
+ scene.add(highlight);
2265
+ return highlight;
2266
+ };
2267
+ const drawLine = (a, b) => {
2268
+ const geom = new GeometryCtor();
2269
+ geom.setAttribute("position", new BufferAttrCtor(new Float32Array([a.x, a.y, a.z, b.x, b.y, b.z]), 3));
2270
+ const LineCtor = LineSegmentsCtor ?? MeshCtor;
2271
+ const LineMatCtor = LineBasicMaterialCtor ?? MaterialCtor;
2272
+ const mat = new LineMatCtor({ color: 16746496, depthTest: false, depthWrite: false, transparent: true, opacity: 0.95 });
2273
+ const line = new LineCtor(geom, mat);
2274
+ line.renderOrder = 9998;
2275
+ line.userData.__measureHelper = true;
2276
+ scene.add(line);
2277
+ return line;
2278
+ };
2279
+ const ensureLabel = () => {
2280
+ const s = measureRef.current;
2281
+ if (s.label) return s.label;
2282
+ const el = document.createElement("div");
2283
+ el.style.position = "absolute";
2284
+ el.style.left = "-9999px";
2285
+ el.style.top = "-9999px";
2286
+ el.style.transform = "translate(-50%, -50%)";
2287
+ el.style.padding = "2px 6px";
2288
+ el.style.fontSize = "11px";
2289
+ el.style.fontWeight = "600";
2290
+ el.style.fontFamily = "system-ui, -apple-system, sans-serif";
2291
+ el.style.background = "rgba(255, 136, 0, 0.95)";
2292
+ el.style.color = "#fff";
2293
+ el.style.borderRadius = "4px";
2294
+ el.style.pointerEvents = "none";
2295
+ el.style.whiteSpace = "nowrap";
2296
+ el.style.boxShadow = "0 1px 4px rgba(0,0,0,0.25)";
2297
+ el.style.zIndex = "5";
2298
+ containerRef.current.appendChild(el);
2299
+ s.label = el;
2300
+ return el;
2301
+ };
2302
+ const updateLabel = () => {
2303
+ const s = measureRef.current;
2304
+ if (!s || s.points.length < 2 || !s.label) return;
2305
+ const camera = v.viewer.camera;
2306
+ if (!camera) return;
2307
+ const a = s.points[0], b = s.points[1];
2308
+ const mid = new Vector3Ctor((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2);
2309
+ const projected = mid.clone();
2310
+ projected.project?.(camera);
2311
+ const rect = canvas.getBoundingClientRect();
2312
+ const x = (projected.x + 1) / 2 * rect.width;
2313
+ const y = (-projected.y + 1) / 2 * rect.height;
2314
+ s.label.style.left = `${x}px`;
2315
+ s.label.style.top = `${y}px`;
2316
+ const offScreen = x < 0 || y < 0 || x > rect.width || y > rect.height;
2317
+ s.label.style.opacity = projected.z > 1 || offScreen ? "0" : "1";
2318
+ };
2319
+ const tick = () => {
2320
+ const s = measureRef.current;
2321
+ if (!s) return;
2322
+ updateLabel();
2323
+ s.rafId = requestAnimationFrame(tick);
2324
+ };
2325
+ measureRef.current.rafId = requestAnimationFrame(tick);
2326
+ const DRAG_TOL = 4;
2327
+ const DRAG_TIME = 350;
2328
+ let downX = 0, downY = 0, downTime = 0, dragging = false, downActive = false;
2329
+ const worldNormalFromHit = (hit) => {
2330
+ const fn = hit?.face?.normal;
2331
+ const obj = hit?.object;
2332
+ if (!fn || !obj) return null;
2333
+ try {
2334
+ const local = new Vector3Ctor(fn.x, fn.y, fn.z);
2335
+ local.transformDirection?.(obj.matrixWorld);
2336
+ return { x: local.x, y: local.y, z: local.z };
2337
+ } catch {
2338
+ return { x: fn.x, y: fn.y, z: fn.z };
2339
+ }
2340
+ };
2341
+ const doMeasurePick = (ev) => {
2342
+ if (!v.viewer.camera) return;
2343
+ const rect = canvas.getBoundingClientRect();
2344
+ const mouseCoords = {
2345
+ x: ev.clientX - rect.left,
2346
+ y: ev.clientY - rect.top
2347
+ };
2348
+ const hit = v.viewer.GetMeshIntersectionUnderMouse?.(intersectionMode, mouseCoords);
2349
+ if (!hit) return;
2350
+ const s = measureRef.current;
2351
+ const point = new Vector3Ctor(hit.point.x, hit.point.y, hit.point.z);
2352
+ const normal = worldNormalFromHit(hit);
2353
+ if (s.points.length === 2) {
2354
+ for (const m of s.markers) {
2355
+ scene.remove(m);
2356
+ m.geometry?.dispose?.();
2357
+ m.material?.dispose?.();
2358
+ }
2359
+ if (s.line) {
2360
+ scene.remove(s.line);
2361
+ s.line.geometry?.dispose?.();
2362
+ s.line.material?.dispose?.();
2363
+ }
2364
+ if (s.label) s.label.style.opacity = "0";
2365
+ s.points = [];
2366
+ s.markers = [];
2367
+ s.line = null;
2368
+ s.normals = [];
2369
+ setMeasureDistance(null);
2370
+ }
2371
+ s.points.push(point);
2372
+ s.normals.push(normal);
2373
+ if (measureMode === "perp") {
2374
+ const highlight = buildFaceHighlight(hit.object, hit);
2375
+ s.markers.push(highlight ?? makeMarker(point));
2376
+ } else {
2377
+ s.markers.push(makeMarker(point));
2378
+ }
2379
+ if (s.points.length === 2) {
2380
+ const a = s.points[0];
2381
+ let b = s.points[1];
2382
+ let dist;
2383
+ let suffix = "";
2384
+ if (measureMode === "perp") {
2385
+ const refN = s.normals[0] ?? s.normals[1];
2386
+ if (refN) {
2387
+ const len = Math.sqrt(refN.x * refN.x + refN.y * refN.y + refN.z * refN.z) || 1;
2388
+ const nx = refN.x / len, ny = refN.y / len, nz = refN.z / len;
2389
+ const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
2390
+ const proj = dx * nx + dy * ny + dz * nz;
2391
+ const projectedB = new Vector3Ctor(a.x + nx * proj, a.y + ny * proj, a.z + nz * proj);
2392
+ s.points[1] = projectedB;
2393
+ b = projectedB;
2394
+ dist = Math.abs(proj);
2395
+ suffix = " \u22A5";
2396
+ } else {
2397
+ const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
2398
+ dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
2399
+ }
2400
+ } else {
2401
+ const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
2402
+ dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
2403
+ }
2404
+ s.line = drawLine(a, b);
2405
+ setMeasureDistance(dist);
2406
+ const label = ensureLabel();
2407
+ label.textContent = `${formatMeasureDistance(dist)}${suffix}`;
2408
+ }
2409
+ v.viewer.Render?.();
2410
+ };
2411
+ const onPointerDown = (ev) => {
2412
+ if (ev.button !== 0) return;
2413
+ downX = ev.clientX;
2414
+ downY = ev.clientY;
2415
+ downTime = performance.now();
2416
+ dragging = false;
2417
+ downActive = true;
2418
+ };
2419
+ const onPointerMove = (ev) => {
2420
+ if (!downActive) return;
2421
+ const dx = ev.clientX - downX, dy = ev.clientY - downY;
2422
+ if (dx * dx + dy * dy > DRAG_TOL * DRAG_TOL) dragging = true;
2423
+ };
2424
+ const onPointerUp = (ev) => {
2425
+ if (ev.button !== 0 || !downActive) return;
2426
+ const elapsed = performance.now() - downTime;
2427
+ const wasClick = !dragging && elapsed < DRAG_TIME;
2428
+ downActive = false;
2429
+ if (!wasClick) return;
2430
+ doMeasurePick(ev);
2431
+ };
2432
+ const onKeyDown = (ev) => {
2433
+ if (ev.key === "Escape") setMeasureEnabled(false);
2434
+ };
2435
+ canvas.style.cursor = "crosshair";
2436
+ canvas.addEventListener("pointerdown", onPointerDown);
2437
+ canvas.addEventListener("pointermove", onPointerMove);
2438
+ canvas.addEventListener("pointerup", onPointerUp);
2439
+ canvas.addEventListener("pointercancel", onPointerUp);
2440
+ window.addEventListener("keydown", onKeyDown);
2441
+ return () => {
2442
+ canvas.style.cursor = "";
2443
+ canvas.removeEventListener("pointerdown", onPointerDown);
2444
+ canvas.removeEventListener("pointermove", onPointerMove);
2445
+ canvas.removeEventListener("pointerup", onPointerUp);
2446
+ canvas.removeEventListener("pointercancel", onPointerUp);
2447
+ window.removeEventListener("keydown", onKeyDown);
2448
+ teardown();
2449
+ setMeasureDistance(null);
2450
+ };
2451
+ }, [measureEnabled, measureMode, loading, tree]);
2452
+ useEffect(() => {
2453
+ if (!showHint || loading) return;
2454
+ const t = setTimeout(() => setShowHint(false), 5e3);
2455
+ return () => clearTimeout(t);
2456
+ }, [showHint, loading]);
2457
+ const toggleNodeVisible = (node) => {
2458
+ const ids = /* @__PURE__ */ new Set();
2459
+ collectNodeIds(node, ids);
2460
+ setHidden((prev) => {
2461
+ const next = new Set(prev);
2462
+ const anyVisible = [...ids].some((id) => !next.has(id));
2463
+ for (const id of ids) {
2464
+ if (anyVisible) next.add(id);
2465
+ else next.delete(id);
2466
+ }
2467
+ return next;
2468
+ });
2469
+ };
2470
+ const toggleExpanded = (id) => {
2471
+ setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
2472
+ };
2473
+ const fitNode = (node) => {
2474
+ handleFit();
2475
+ };
2476
+ const handleFit = () => {
2477
+ try {
2478
+ const v = viewerRef.current;
2479
+ const sphere = v?.viewer?.GetBoundingSphere?.(() => true);
2480
+ if (sphere) v.viewer.FitSphereToWindow(sphere, true);
2481
+ v?.viewer?.Render?.();
2482
+ } catch {
2483
+ }
2484
+ };
2485
+ useEffect(() => {
2486
+ const OV = ovRef.current;
2487
+ const v = viewerRef.current;
2488
+ if (!OV || !v?.viewer || loading) return;
2489
+ try {
2490
+ v.viewer.SetProjectionMode(perspective ? OV.ProjectionMode.Perspective : OV.ProjectionMode.Orthographic);
2491
+ } catch {
2492
+ }
2493
+ }, [perspective, loading]);
2494
+ const setCameraPreset = (preset) => {
2495
+ const OV = ovRef.current;
2496
+ const v = viewerRef.current;
2497
+ if (!OV || !v?.viewer) return;
2498
+ try {
2499
+ const sphere = v.viewer.GetBoundingSphere?.(() => true);
2500
+ if (!sphere) return;
2501
+ const c = sphere.center;
2502
+ const r = sphere.radius || 1;
2503
+ const dist = r * 3;
2504
+ const cx = c.x ?? 0, cy = c.y ?? 0, cz = c.z ?? 0;
2505
+ let eye, up;
2506
+ switch (preset) {
2507
+ case "top":
2508
+ eye = new OV.Coord3D(cx, cy, cz + dist);
2509
+ up = new OV.Coord3D(0, 1, 0);
2510
+ break;
2511
+ case "front":
2512
+ eye = new OV.Coord3D(cx, cy - dist, cz);
2513
+ up = new OV.Coord3D(0, 0, 1);
2514
+ break;
2515
+ case "side":
2516
+ eye = new OV.Coord3D(cx + dist, cy, cz);
2517
+ up = new OV.Coord3D(0, 0, 1);
2518
+ break;
2519
+ case "iso":
2520
+ default: {
2521
+ const k = dist / Math.sqrt(3);
2522
+ eye = new OV.Coord3D(cx + k, cy - k, cz + k);
2523
+ up = new OV.Coord3D(0, 0, 1);
2524
+ break;
2525
+ }
2526
+ }
2527
+ const center = new OV.Coord3D(cx, cy, cz);
2528
+ const cam = new OV.Camera(eye, center, up, 45);
2529
+ v.viewer.SetCamera(cam);
2530
+ v.viewer.AdjustClippingPlanesToSphere?.(sphere);
2531
+ v.viewer.Render?.();
2532
+ } catch {
2533
+ }
2534
+ };
2535
+ const handleSnapshot = () => {
2536
+ try {
2537
+ const v = viewerRef.current;
2538
+ const size = v?.viewer?.GetCanvasSize?.() ?? { width: 1280, height: 720 };
2539
+ const dataUrl = v?.viewer?.GetImageAsDataUrl?.(size.width, size.height, false);
2540
+ if (!dataUrl) return;
2541
+ const a = document.createElement("a");
2542
+ a.href = dataUrl;
2543
+ a.download = `${filename.replace(/\.[^.]+$/, "")}-snapshot.png`;
2544
+ a.click();
2545
+ } catch {
2546
+ }
2547
+ };
2548
+ const handleResetDisplay = () => {
2549
+ setBgColor("#f5f6f8");
2550
+ setShowEdges(true);
2551
+ setEdgeColor("#000000");
2552
+ setEdgeThreshold(1);
2553
+ setSectionEnabled(false);
2554
+ setSectionAxis("x");
2555
+ setSectionFlip(false);
2556
+ setSectionPosition(0.5);
2557
+ setSectionAngle(0);
2558
+ };
2559
+ const handleDefaultDownload = () => {
2560
+ const a = document.createElement("a");
2561
+ a.href = url;
2562
+ a.download = filename;
2563
+ a.click();
2564
+ };
2565
+ const ext = (filename.split(".").pop() || "").toUpperCase();
2566
+ const renderTreeNode = (node, depth = 0) => {
2567
+ const hasChildren = node.children.length > 0;
2568
+ const isExpanded = expanded[node.id] !== false;
2569
+ const isVisible = !hidden.has(node.id);
2570
+ return /* @__PURE__ */ jsxs("div", { children: [
2571
+ /* @__PURE__ */ jsxs(
2572
+ "div",
2573
+ {
2574
+ className: "group flex items-center gap-1 px-1.5 py-1 hover:bg-gray-100 cursor-default text-[12px] text-gray-700",
2575
+ style: { paddingLeft: `${depth * 12 + 6}px` },
2576
+ children: [
2577
+ hasChildren ? /* @__PURE__ */ jsx(
2578
+ "button",
2579
+ {
2580
+ onClick: () => toggleExpanded(node.id),
2581
+ className: "h-4 w-4 shrink-0 flex items-center justify-center text-gray-500 hover:text-gray-900",
2582
+ title: isExpanded ? "Collapse" : "Expand",
2583
+ children: /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: isExpanded ? "M19.5 8.25l-7.5 7.5-7.5-7.5" : "M8.25 4.5l7.5 7.5-7.5 7.5" }) })
2584
+ }
2585
+ ) : /* @__PURE__ */ jsx("span", { className: "h-4 w-4 shrink-0 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "h-1 w-1 rounded-full bg-gray-400" }) }),
2586
+ /* @__PURE__ */ jsx("span", { className: `flex-1 truncate ${isVisible ? "" : "opacity-40"}`, title: node.name, children: node.name }),
2587
+ /* @__PURE__ */ jsx(
2588
+ "button",
2589
+ {
2590
+ onClick: () => fitNode(),
2591
+ className: "h-4 w-4 shrink-0 text-gray-400 hover:text-gray-900 opacity-0 group-hover:opacity-100 transition-opacity",
2592
+ title: "Fit to view",
2593
+ children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" }) })
2594
+ }
2595
+ ),
2596
+ /* @__PURE__ */ jsx(
2597
+ "button",
2598
+ {
2599
+ onClick: () => toggleNodeVisible(node),
2600
+ className: "h-4 w-4 shrink-0 text-gray-500 hover:text-gray-900",
2601
+ title: isVisible ? "Hide" : "Show",
2602
+ children: isVisible ? /* @__PURE__ */ jsxs("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
2603
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" }),
2604
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" })
2605
+ ] }) : /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" }) })
2606
+ }
2607
+ )
2608
+ ]
2609
+ }
2610
+ ),
2611
+ isExpanded && hasChildren && /* @__PURE__ */ jsx("div", { children: node.children.map((c) => renderTreeNode(c, depth + 1)) })
2612
+ ] }, node.id);
2613
+ };
2614
+ const tBtn = "h-8 w-8 shrink-0 flex items-center justify-center rounded text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors";
2615
+ const tBtnActive = "h-8 w-8 shrink-0 flex items-center justify-center rounded bg-gray-200 text-gray-900";
2616
+ const tBtnWide = "h-8 shrink-0 flex items-center justify-center px-2 rounded text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors";
2617
+ const tBtnWideActive = "h-8 shrink-0 flex items-center justify-center px-2 rounded bg-gray-200 text-gray-900";
2618
+ const tBtnSep = "h-5 w-px bg-gray-300 mx-1";
2619
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-white", children: [
2620
+ /* @__PURE__ */ jsxs(PanelActions, { children: [
2621
+ /* @__PURE__ */ jsx("button", { onClick: handleFit, className: tBtn, title: "Fit to view", children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" }) }) }),
2622
+ /* @__PURE__ */ jsx("div", { className: tBtnSep }),
2623
+ /* @__PURE__ */ jsx("button", { onClick: () => setCameraPreset("iso"), className: tBtn, title: "Isometric view", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold", children: "ISO" }) }),
2624
+ /* @__PURE__ */ jsx("button", { onClick: () => setCameraPreset("top"), className: tBtn, title: "Top view", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold", children: "TOP" }) }),
2625
+ /* @__PURE__ */ jsx("button", { onClick: () => setCameraPreset("front"), className: tBtn, title: "Front view", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold", children: "FRT" }) }),
2626
+ /* @__PURE__ */ jsx("button", { onClick: () => setCameraPreset("side"), className: tBtn, title: "Side view", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold", children: "SDE" }) }),
2627
+ /* @__PURE__ */ jsx(
2628
+ "button",
2629
+ {
2630
+ onClick: () => setPerspective((p) => !p),
2631
+ className: perspective ? tBtnWideActive : tBtnWide,
2632
+ title: perspective ? "Switch to orthographic view" : "Switch to perspective view",
2633
+ children: /* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold whitespace-nowrap", children: perspective ? "Perspective" : "Orthographic" })
2634
+ }
2635
+ ),
2636
+ /* @__PURE__ */ jsx("div", { className: tBtnSep }),
2637
+ /* @__PURE__ */ jsx("button", { onClick: handleSnapshot, className: tBtn, title: "Save snapshot as PNG", children: /* @__PURE__ */ jsxs("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
2638
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z" }),
2639
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z" })
2640
+ ] }) }),
2641
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowHint((s) => !s), className: tBtn, title: "How to navigate", children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" }) }) }),
2642
+ /* @__PURE__ */ jsx(
2643
+ "button",
2644
+ {
2645
+ onClick: () => setMeasureEnabled((m) => !m),
2646
+ className: measureEnabled ? tBtnActive : tBtn,
2647
+ title: measureEnabled ? "Stop measuring (Esc)" : "Measure distance \u2014 click two points on the model",
2648
+ children: /* @__PURE__ */ jsxs("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
2649
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 14.25l6-6 6 6 4.5-4.5M9.75 8.25v3M12.75 11.25v3M15.75 14.25v3" }),
2650
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.25 18.75h19.5" })
2651
+ ] })
2652
+ }
2653
+ ),
2654
+ measureEnabled && /* @__PURE__ */ jsxs("div", { className: "flex items-stretch h-8 rounded border border-gray-200 overflow-hidden", children: [
2655
+ /* @__PURE__ */ jsx(
2656
+ "button",
2657
+ {
2658
+ onClick: () => setMeasureMode("point"),
2659
+ className: `px-2 text-[11px] font-semibold transition-colors ${measureMode === "point" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
2660
+ title: "Point \u2014 measure straight-line distance between two picked points",
2661
+ children: "Point"
2662
+ }
2663
+ ),
2664
+ /* @__PURE__ */ jsx(
2665
+ "button",
2666
+ {
2667
+ onClick: () => setMeasureMode("perp"),
2668
+ className: `px-2 text-[12px] font-semibold transition-colors ${measureMode === "perp" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
2669
+ title: "Perpendicular \u2014 pick two surfaces and measure the perpendicular gap between them",
2670
+ children: "\u22A5"
2671
+ }
2672
+ )
2673
+ ] }),
2674
+ measureEnabled && measureDistance !== null && /* @__PURE__ */ jsxs("div", { className: "px-2 py-1 text-[11px] font-mono font-semibold text-orange-600 bg-orange-50 border border-orange-200 rounded whitespace-nowrap", title: measureMode === "perp" ? "Perpendicular distance between the two picked surfaces" : "Straight-line distance between the two picked points", children: [
2675
+ formatMeasureDistance(measureDistance),
2676
+ measureMode === "perp" ? " \u22A5" : ""
2677
+ ] }),
2678
+ /* @__PURE__ */ jsx("div", { className: tBtnSep }),
2679
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowMeshes((s) => !s), className: showMeshes ? tBtnActive : tBtn, title: "Toggle meshes panel", children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" }) }) }),
2680
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowSettings((s) => !s), className: showSettings ? tBtnActive : tBtn, title: "Toggle display panel", children: /* @__PURE__ */ jsxs("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
2681
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" }),
2682
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" })
2683
+ ] }) }),
2684
+ /* @__PURE__ */ jsx("button", { onClick: onDownload ?? handleDefaultDownload, className: tBtn, title: "Download original file", children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }) }),
2685
+ onEmail && /* @__PURE__ */ jsx("button", { onClick: onEmail, className: tBtn, title: "Email", children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" }) }) })
2686
+ ] }),
2687
+ /* @__PURE__ */ jsx("div", { className: "flex-1 flex min-h-0", children: /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", style: { background: bgColor }, children: [
2688
+ /* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%", height: "100%" } }),
2689
+ showMeshes && /* @__PURE__ */ jsxs("div", { className: "absolute top-2 left-2 w-64 max-h-[80%] flex flex-col bg-white/95 backdrop-blur border border-gray-200 rounded-md shadow-xl z-10 text-xs", children: [
2690
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 border-b border-gray-200 bg-gray-50", children: [
2691
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "Meshes" }),
2692
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowMeshes(false), className: "px-1.5 py-0.5 rounded hover:bg-gray-200 text-gray-600", title: "Close", children: "\xD7" })
2693
+ ] }),
2694
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto py-1", children: tree ? renderTreeNode(tree) : /* @__PURE__ */ jsx("div", { className: "px-3 py-3 text-[11px] text-gray-500 italic", children: loading ? "Reading model\u2026" : "No structure available" }) }),
2695
+ tree && /* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 text-[10px] text-gray-500 border-t border-gray-200", children: hidden.size === 0 ? "All visible" : `${hidden.size} hidden` })
2696
+ ] }),
2697
+ showHint && !loading && !error && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-3 left-1/2 -translate-x-1/2 bg-gray-900/85 text-white text-[11px] px-3 py-1.5 rounded-full shadow-lg flex items-center gap-3 z-10 pointer-events-none", children: [
2698
+ /* @__PURE__ */ jsx("span", { children: "Drag to rotate" }),
2699
+ /* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
2700
+ /* @__PURE__ */ jsx("span", { children: "Right-click drag to pan" }),
2701
+ /* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
2702
+ /* @__PURE__ */ jsx("span", { children: "Scroll to zoom" })
2703
+ ] }),
2704
+ loading && /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-white/85 text-sm text-gray-600 gap-2", children: [
2705
+ /* @__PURE__ */ jsxs("svg", { className: "h-6 w-6 text-blue-500 animate-spin", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: [
2706
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.2" }),
2707
+ /* @__PURE__ */ jsx("path", { d: "M22 12a10 10 0 0 1-10 10", strokeLinecap: "round" })
2708
+ ] }),
2709
+ /* @__PURE__ */ jsx("span", { children: "Loading 3D model\u2026" }),
2710
+ ext === "STP" || ext === "STEP" ? /* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-400", children: "STEP files load OpenCascade WASM (~5 MB) on first use." }) : null
2711
+ ] }),
2712
+ error && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-sm text-red-600 px-6 text-center bg-white/85", children: error }),
2713
+ showSettings && /* @__PURE__ */ jsxs("div", { className: "absolute top-2 right-2 w-64 max-h-[80%] flex flex-col bg-white/95 backdrop-blur border border-gray-200 rounded-md shadow-xl z-10 text-xs", children: [
2714
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 border-b border-gray-200 bg-gray-50", children: [
2715
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "Model Display" }),
2716
+ /* @__PURE__ */ jsx("button", { onClick: () => setShowSettings(false), className: "px-1.5 py-0.5 rounded hover:bg-gray-200 text-gray-600", title: "Close", children: "\xD7" })
2717
+ ] }),
2718
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-3 py-3 space-y-3 text-[12px] text-gray-700", children: [
2719
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
2720
+ /* @__PURE__ */ jsx("span", { children: "Background Color" }),
2721
+ /* @__PURE__ */ jsx("input", { type: "color", value: bgColor, onChange: (e) => setBgColor(e.target.value), className: "h-6 w-10 rounded border border-gray-300 bg-white" })
2722
+ ] }),
2723
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
2724
+ /* @__PURE__ */ jsx("span", { children: "Show Edges" }),
2725
+ /* @__PURE__ */ jsx(
2726
+ "button",
2727
+ {
2728
+ onClick: () => setShowEdges((s) => !s),
2729
+ role: "switch",
2730
+ "aria-checked": showEdges,
2731
+ className: `relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors ${showEdges ? "bg-blue-500" : "bg-gray-300"}`,
2732
+ children: /* @__PURE__ */ jsx("span", { className: `inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${showEdges ? "translate-x-[18px]" : "translate-x-0.5"}` })
2733
+ }
2734
+ )
2735
+ ] }),
2736
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
2737
+ /* @__PURE__ */ jsx("span", { className: showEdges ? "" : "opacity-40", children: "Edge Color" }),
2738
+ /* @__PURE__ */ jsx(
2739
+ "input",
2740
+ {
2741
+ type: "color",
2742
+ value: edgeColor,
2743
+ onChange: (e) => setEdgeColor(e.target.value),
2744
+ disabled: !showEdges,
2745
+ className: "h-6 w-10 rounded border border-gray-300 bg-white disabled:opacity-40"
2746
+ }
2747
+ )
2748
+ ] }),
2749
+ /* @__PURE__ */ jsxs("div", { className: showEdges ? "" : "opacity-40 pointer-events-none", children: [
2750
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
2751
+ /* @__PURE__ */ jsx("span", { children: "Edge Threshold" }),
2752
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-500 tabular-nums", children: [
2753
+ edgeThreshold,
2754
+ "\xB0"
2755
+ ] })
2756
+ ] }),
2757
+ /* @__PURE__ */ jsx(
2758
+ "input",
2759
+ {
2760
+ type: "range",
2761
+ min: 0,
2762
+ max: 45,
2763
+ step: 1,
2764
+ value: edgeThreshold,
2765
+ onChange: (e) => setEdgeThreshold(Number(e.target.value)),
2766
+ className: "w-full accent-blue-500"
2767
+ }
2768
+ )
2769
+ ] }),
2770
+ /* @__PURE__ */ jsxs("div", { className: "border-t border-gray-200 -mx-3 px-3 pt-3 mt-1", children: [
2771
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
2772
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Section View" }),
2773
+ /* @__PURE__ */ jsx(
2774
+ "button",
2775
+ {
2776
+ onClick: () => setSectionEnabled((s) => !s),
2777
+ role: "switch",
2778
+ "aria-checked": sectionEnabled,
2779
+ className: `relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors ${sectionEnabled ? "bg-blue-500" : "bg-gray-300"}`,
2780
+ children: /* @__PURE__ */ jsx("span", { className: `inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${sectionEnabled ? "translate-x-[18px]" : "translate-x-0.5"}` })
2781
+ }
2782
+ )
2783
+ ] }),
2784
+ /* @__PURE__ */ jsxs("div", { className: sectionEnabled ? "mt-2 space-y-2" : "mt-2 space-y-2 opacity-40 pointer-events-none", children: [
2785
+ /* @__PURE__ */ jsxs("div", { children: [
2786
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
2787
+ /* @__PURE__ */ jsx("span", { children: "Angle" }),
2788
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-500 tabular-nums", children: [
2789
+ sectionAngle,
2790
+ "\xB0"
2791
+ ] })
2792
+ ] }),
2793
+ /* @__PURE__ */ jsx(
2794
+ "input",
2795
+ {
2796
+ type: "range",
2797
+ min: 0,
2798
+ max: 180,
2799
+ step: 1,
2800
+ value: sectionAngle,
2801
+ onChange: (e) => setSectionAngle(Number(e.target.value)),
2802
+ className: "w-full accent-blue-500"
2803
+ }
2804
+ )
2805
+ ] }),
2806
+ /* @__PURE__ */ jsxs("div", { children: [
2807
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
2808
+ /* @__PURE__ */ jsx("span", { children: "Axis" }),
2809
+ /* @__PURE__ */ jsx(
2810
+ "button",
2811
+ {
2812
+ onClick: () => setSectionFlip((f) => !f),
2813
+ className: "text-[10px] text-gray-600 hover:text-gray-900 px-1.5 py-0.5 rounded bg-gray-100 hover:bg-gray-200",
2814
+ title: "Flip section direction",
2815
+ children: sectionFlip ? "\u2190 flipped" : "flip \u2192"
2816
+ }
2817
+ )
2818
+ ] }),
2819
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-1", children: ["x", "y", "z"].map((ax) => /* @__PURE__ */ jsx(
2820
+ "button",
2821
+ {
2822
+ onClick: () => setSectionAxis(ax),
2823
+ className: `py-1 rounded text-[11px] font-semibold ${sectionAxis === ax ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"}`,
2824
+ children: ax.toUpperCase()
2825
+ },
2826
+ ax
2827
+ )) })
2828
+ ] }),
2829
+ /* @__PURE__ */ jsxs("div", { children: [
2830
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
2831
+ /* @__PURE__ */ jsx("span", { children: "Position" }),
2832
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-500 tabular-nums", children: [
2833
+ Math.round(sectionPosition * 100),
2834
+ "%"
2835
+ ] })
2836
+ ] }),
2837
+ /* @__PURE__ */ jsx(
2838
+ "input",
2839
+ {
2840
+ type: "range",
2841
+ min: 0,
2842
+ max: 1,
2843
+ step: 0.01,
2844
+ value: sectionPosition,
2845
+ onChange: (e) => setSectionPosition(Number(e.target.value)),
2846
+ className: "w-full accent-blue-500"
2847
+ }
2848
+ )
2849
+ ] })
2850
+ ] })
2851
+ ] })
2852
+ ] }),
2853
+ /* @__PURE__ */ jsx("div", { className: "px-3 py-2 border-t border-gray-200", children: /* @__PURE__ */ jsx(
2854
+ "button",
2855
+ {
2856
+ onClick: handleResetDisplay,
2857
+ className: "w-full text-[11px] text-gray-700 bg-gray-100 hover:bg-gray-200 rounded py-1.5 transition-colors",
2858
+ children: "Reset to Default"
2859
+ }
2860
+ ) })
2861
+ ] })
2862
+ ] }) })
2863
+ ] });
2864
+ }
2865
+ function ImagePanel({ url, filename, onDownload, onEmail }) {
2866
+ const [zoom, setZoom] = useState(1);
2867
+ const [error, setError] = useState(false);
2868
+ const [annotating, setAnnotating] = useState(false);
2869
+ const annotatorRef = useRef(null);
2870
+ const handleDefaultDownload = async () => {
2871
+ if (url.startsWith("blob:") || url.startsWith("data:")) {
2872
+ const a = document.createElement("a");
2873
+ a.href = url;
2874
+ a.download = filename;
2875
+ a.click();
2876
+ return;
2877
+ }
2878
+ const tryFetch = async (init) => {
2879
+ const res = await fetch(url, init);
2880
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2881
+ return res.blob();
2882
+ };
2883
+ try {
2884
+ let blob;
2885
+ try {
2886
+ blob = await tryFetch({ credentials: "include" });
2887
+ } catch {
2888
+ blob = await tryFetch({ credentials: "omit" });
2889
+ }
2890
+ const blobUrl = URL.createObjectURL(blob);
2891
+ const a = document.createElement("a");
2892
+ a.href = blobUrl;
2893
+ a.download = filename;
2894
+ a.click();
2895
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 1e3);
2896
+ } catch {
2897
+ toast_default.error("Download failed \u2014 couldn't reach the file.");
2898
+ }
2899
+ };
2900
+ const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
2901
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
2902
+ /* @__PURE__ */ jsxs(PanelActions, { children: [
2903
+ !annotating && /* @__PURE__ */ jsxs(Fragment, { children: [
2904
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, Math.round((z - 0.25) * 100) / 100)), className: btn, children: "\u2212" }),
2905
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-500 w-12 text-center tabular-nums", children: [
2906
+ Math.round(zoom * 100),
2907
+ "%"
2908
+ ] }),
2909
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(8, Math.round((z + 0.25) * 100) / 100)), className: btn, children: "+" }),
2910
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: btn, children: "1:1" }),
2911
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300 mx-1" }),
2912
+ /* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(true), className: btn, title: "Annotate this image", children: [
2913
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" }) }),
2914
+ "Annotate"
2915
+ ] })
2916
+ ] }),
2917
+ annotating && /* @__PURE__ */ jsxs(Fragment, { children: [
2918
+ /* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(false), className: btn, title: "Back to viewer", children: [
2919
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 19.5L8.25 12l7.5-7.5" }) }),
2920
+ "View"
2921
+ ] }),
2922
+ /* @__PURE__ */ jsxs("button", { onClick: () => annotatorRef.current?.copy(), className: btn, children: [
2923
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" }) }),
2924
+ "Copy"
2925
+ ] }),
2926
+ /* @__PURE__ */ jsxs("button", { onClick: () => annotatorRef.current?.save(), className: btn, children: [
2927
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
2928
+ "Save"
2929
+ ] })
2930
+ ] }),
2931
+ /* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
2932
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
2933
+ "Download"
2934
+ ] }),
2935
+ onEmail && /* @__PURE__ */ jsxs("button", { onClick: onEmail, className: btn, children: [
2936
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" }) }),
2937
+ "Email"
2938
+ ] })
2939
+ ] }),
2940
+ annotating ? /* @__PURE__ */ jsx(ImageAnnotator_default, { ref: annotatorRef, src: url, filename }) : /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto bg-gray-100 flex items-center justify-center p-4", children: error ? /* @__PURE__ */ jsx("div", { className: "text-sm text-red-600", children: "Failed to load image." }) : /* @__PURE__ */ jsx(
2941
+ "img",
2942
+ {
2943
+ src: url,
2944
+ alt: filename,
2945
+ onError: () => setError(true),
2946
+ style: { transform: `scale(${zoom})`, transformOrigin: "center center", transition: "transform 120ms ease" },
2947
+ className: "max-w-full max-h-full shadow-lg rounded bg-white"
2948
+ }
2949
+ ) })
2950
+ ] });
2951
+ }
2952
+
2953
+ export { Preview as default };
2954
+ //# sourceMappingURL=Preview-N4UYYXG6.js.map
2955
+ //# sourceMappingURL=Preview-N4UYYXG6.js.map