react-os-shell 0.1.27 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Preview-DO7KHVKG.js +6 -0
- package/dist/{Preview-RQPIGKTJ.js.map → Preview-DO7KHVKG.js.map} +1 -1
- package/dist/apps/index.d.ts +4 -2
- package/dist/apps/index.js +2 -2
- package/dist/chunk-RGYSM6P5.js +1264 -0
- package/dist/chunk-RGYSM6P5.js.map +1 -0
- package/dist/index.js +2 -2
- package/package.json +6 -1
- package/dist/Preview-RQPIGKTJ.js +0 -6
- package/dist/chunk-4XBIXMZC.js +0 -483
- package/dist/chunk-4XBIXMZC.js.map +0 -1
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
import { toast_default } from './chunk-WIJ45SYD.js';
|
|
2
|
+
import { WindowTitle } from './chunk-AKZTZLKP.js';
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import * as pdfjsLib from 'pdfjs-dist';
|
|
5
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
var TITLE_DISPLAY_MAX = 24;
|
|
8
|
+
function truncateForTitle(s) {
|
|
9
|
+
return s.length > TITLE_DISPLAY_MAX ? `${s.slice(0, TITLE_DISPLAY_MAX - 1)}\u2026` : s;
|
|
10
|
+
}
|
|
11
|
+
if (typeof window !== "undefined" && !pdfjsLib.GlobalWorkerOptions.workerSrc) {
|
|
12
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
|
|
13
|
+
}
|
|
14
|
+
var EVENT_NAME = "react-os-shell:pdf-preview";
|
|
15
|
+
var pendingData = null;
|
|
16
|
+
function setPdfPreview(data) {
|
|
17
|
+
pendingData = data;
|
|
18
|
+
if (typeof window !== "undefined") {
|
|
19
|
+
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: data }));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function Preview() {
|
|
23
|
+
const [data, setData] = useState(() => {
|
|
24
|
+
const d = pendingData;
|
|
25
|
+
pendingData = null;
|
|
26
|
+
return d;
|
|
27
|
+
});
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const handler = (e) => {
|
|
30
|
+
const next = e.detail;
|
|
31
|
+
setData((prev) => {
|
|
32
|
+
if (prev?.url && prev.url !== next.url && prev.url.startsWith("blob:")) {
|
|
33
|
+
URL.revokeObjectURL(prev.url);
|
|
34
|
+
}
|
|
35
|
+
return next;
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
window.addEventListener(EVENT_NAME, handler);
|
|
39
|
+
return () => window.removeEventListener(EVENT_NAME, handler);
|
|
40
|
+
}, []);
|
|
41
|
+
useEffect(() => () => {
|
|
42
|
+
if (data?.url?.startsWith("blob:")) URL.revokeObjectURL(data.url);
|
|
43
|
+
}, []);
|
|
44
|
+
const titleName = data?.filename ? truncateForTitle(data.filename) : "Untitled";
|
|
45
|
+
const fileRef = useRef(null);
|
|
46
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
47
|
+
const handlePick = () => fileRef.current?.click();
|
|
48
|
+
const ingestFile = (file) => {
|
|
49
|
+
const url = URL.createObjectURL(file);
|
|
50
|
+
const ext = (file.name.split(".").pop() || "").toLowerCase();
|
|
51
|
+
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;
|
|
52
|
+
if (!kind) {
|
|
53
|
+
URL.revokeObjectURL(url);
|
|
54
|
+
if (ext === "dwg") toast_default.error("DWG files need server-side conversion. Convert to PDF or DXF first.");
|
|
55
|
+
else toast_default.error(`Unsupported file type: .${ext || "unknown"}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setPdfPreview({ url, filename: file.name, kind });
|
|
59
|
+
};
|
|
60
|
+
const handleFile = (e) => {
|
|
61
|
+
const file = e.target.files?.[0];
|
|
62
|
+
if (file) ingestFile(file);
|
|
63
|
+
if (fileRef.current) fileRef.current.value = "";
|
|
64
|
+
};
|
|
65
|
+
const handleDrop = (e) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
setIsDragging(false);
|
|
68
|
+
const file = e.dataTransfer.files?.[0];
|
|
69
|
+
if (file) ingestFile(file);
|
|
70
|
+
};
|
|
71
|
+
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: [
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
"input",
|
|
74
|
+
{
|
|
75
|
+
ref: fileRef,
|
|
76
|
+
type: "file",
|
|
77
|
+
accept: ".pdf,.dxf,.jpg,.jpeg,.png,.gif,.webp,.svg,.avif,.bmp,.stp,.step,.stl,.obj,.gltf,.glb,.3mf,.iges,.igs,.ply,.fbx",
|
|
78
|
+
onChange: handleFile,
|
|
79
|
+
className: "hidden"
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
/* @__PURE__ */ jsxs(
|
|
83
|
+
"button",
|
|
84
|
+
{
|
|
85
|
+
onClick: handlePick,
|
|
86
|
+
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",
|
|
87
|
+
children: [
|
|
88
|
+
/* @__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" }) }),
|
|
89
|
+
"Open"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-400 ml-1", children: "PDF \xB7 DXF \xB7 3D \xB7 Images" }),
|
|
94
|
+
data?.filename && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
95
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300 mx-1" }),
|
|
96
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-700 truncate max-w-[200px]", title: data.filename, children: data.filename })
|
|
97
|
+
] })
|
|
98
|
+
] });
|
|
99
|
+
let body;
|
|
100
|
+
if (!data) {
|
|
101
|
+
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: [
|
|
102
|
+
/* @__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" }) }),
|
|
103
|
+
/* @__PURE__ */ jsxs("p", { className: "font-medium text-gray-700", children: [
|
|
104
|
+
"Drop a file here, or click ",
|
|
105
|
+
/* @__PURE__ */ jsx("button", { onClick: handlePick, className: "text-blue-600 hover:underline", children: "Open" }),
|
|
106
|
+
"."
|
|
107
|
+
] }),
|
|
108
|
+
/* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-500 max-w-sm", children: [
|
|
109
|
+
/* @__PURE__ */ jsx("p", { className: "font-semibold uppercase tracking-wide text-[10px] text-gray-400 mb-1", children: "Supported formats" }),
|
|
110
|
+
/* @__PURE__ */ jsxs("ul", { className: "space-y-0.5", children: [
|
|
111
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
112
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-gray-700", children: ".pdf" }),
|
|
113
|
+
" \u2014 multi-page document viewer"
|
|
114
|
+
] }),
|
|
115
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
116
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-gray-700", children: ".dxf" }),
|
|
117
|
+
" \u2014 vector CAD drawings (optional ",
|
|
118
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono", children: "dxf-viewer" }),
|
|
119
|
+
" peer dep)"
|
|
120
|
+
] }),
|
|
121
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
122
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-gray-700", children: ".stp .step .stl .obj .gltf .glb .3mf .iges" }),
|
|
123
|
+
" \u2014 3D models (optional ",
|
|
124
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono", children: "online-3d-viewer" }),
|
|
125
|
+
" peer dep)"
|
|
126
|
+
] }),
|
|
127
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
128
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-gray-700", children: ".jpg .jpeg .png .gif .webp .svg .avif .bmp" }),
|
|
129
|
+
" \u2014 raster images"
|
|
130
|
+
] })
|
|
131
|
+
] }),
|
|
132
|
+
/* @__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)." })
|
|
133
|
+
] })
|
|
134
|
+
] });
|
|
135
|
+
} else if (data.converting || !data.url) {
|
|
136
|
+
body = /* @__PURE__ */ jsx(ConvertingPanel, { filename: data.filename, message: data.convertingMessage });
|
|
137
|
+
} else if (data.kind === "dxf") {
|
|
138
|
+
body = /* @__PURE__ */ jsx(DxfPanel, { url: data.url, filename: data.filename, onDownload: data.onDownload, onEmail: data.onEmail }, data.url);
|
|
139
|
+
} else if (data.kind === "3d") {
|
|
140
|
+
body = /* @__PURE__ */ jsx(StepPanel, { url: data.url, filename: data.filename, onDownload: data.onDownload, onEmail: data.onEmail }, data.url);
|
|
141
|
+
} else if (data.kind === "image") {
|
|
142
|
+
body = /* @__PURE__ */ jsx(ImagePanel, { url: data.url, filename: data.filename, onDownload: data.onDownload, onEmail: data.onEmail }, data.url);
|
|
143
|
+
} else {
|
|
144
|
+
body = /* @__PURE__ */ jsx(PdfPanel, { url: data.url, filename: data.filename, onDownload: data.onDownload, onEmail: data.onEmail }, data.url);
|
|
145
|
+
}
|
|
146
|
+
return /* @__PURE__ */ jsxs(
|
|
147
|
+
"div",
|
|
148
|
+
{
|
|
149
|
+
className: "relative flex flex-col h-full",
|
|
150
|
+
onDragOver: (e) => {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
if (!isDragging) setIsDragging(true);
|
|
153
|
+
},
|
|
154
|
+
onDragLeave: (e) => {
|
|
155
|
+
if (e.currentTarget === e.target) setIsDragging(false);
|
|
156
|
+
},
|
|
157
|
+
onDrop: handleDrop,
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsx(WindowTitle, { title: `${titleName} - Preview` }),
|
|
160
|
+
Toolbar,
|
|
161
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: body }),
|
|
162
|
+
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" }) })
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
function ConvertingPanel({ filename, message }) {
|
|
168
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center h-full bg-gray-100 gap-4 px-8", children: [
|
|
169
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
|
|
170
|
+
/* @__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: [
|
|
171
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.2" }),
|
|
172
|
+
/* @__PURE__ */ jsx("path", { d: "M22 12a10 10 0 0 1-10 10", strokeLinecap: "round" })
|
|
173
|
+
] }),
|
|
174
|
+
/* @__PURE__ */ jsx("div", { className: "text-base font-semibold tracking-wide text-gray-700 uppercase", children: message || "Converting file" }),
|
|
175
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-gray-400 truncate max-w-md", children: filename })
|
|
176
|
+
] }),
|
|
177
|
+
/* @__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" } }) }),
|
|
178
|
+
/* @__PURE__ */ jsx("style", { children: `@keyframes preview-bar { 0% { transform: translateX(-110%); } 100% { transform: translateX(310%); } }` })
|
|
179
|
+
] });
|
|
180
|
+
}
|
|
181
|
+
function PdfPanel({ url, filename, onDownload, onEmail }) {
|
|
182
|
+
const canvasRef = useRef(null);
|
|
183
|
+
const containerRef = useRef(null);
|
|
184
|
+
const [pdf, setPdf] = useState(null);
|
|
185
|
+
const [page, setPage] = useState(1);
|
|
186
|
+
const [totalPages, setTotalPages] = useState(0);
|
|
187
|
+
const [scale, setScale] = useState(1.5);
|
|
188
|
+
const [loading, setLoading] = useState(true);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
let cancelled = false;
|
|
191
|
+
setLoading(true);
|
|
192
|
+
pdfjsLib.getDocument(url).promise.then((doc) => {
|
|
193
|
+
if (cancelled) return;
|
|
194
|
+
setPdf(doc);
|
|
195
|
+
setTotalPages(doc.numPages);
|
|
196
|
+
setLoading(false);
|
|
197
|
+
}).catch(() => {
|
|
198
|
+
if (!cancelled) {
|
|
199
|
+
toast_default.error("Failed to load PDF");
|
|
200
|
+
setLoading(false);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
return () => {
|
|
204
|
+
cancelled = true;
|
|
205
|
+
};
|
|
206
|
+
}, [url]);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (!pdf || !containerRef.current) return;
|
|
209
|
+
pdf.getPage(1).then((p) => {
|
|
210
|
+
const containerW = containerRef.current?.clientWidth || 800;
|
|
211
|
+
const viewport = p.getViewport({ scale: 1 });
|
|
212
|
+
const fitScale = (containerW - 40) / viewport.width;
|
|
213
|
+
setScale(Math.min(Math.max(fitScale, 0.5), 3));
|
|
214
|
+
});
|
|
215
|
+
}, [pdf]);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!pdf || !canvasRef.current) return;
|
|
218
|
+
let cancelled = false;
|
|
219
|
+
pdf.getPage(page).then((p) => {
|
|
220
|
+
if (cancelled || !canvasRef.current) return;
|
|
221
|
+
const viewport = p.getViewport({ scale });
|
|
222
|
+
const canvas = canvasRef.current;
|
|
223
|
+
canvas.width = viewport.width;
|
|
224
|
+
canvas.height = viewport.height;
|
|
225
|
+
const ctx = canvas.getContext("2d");
|
|
226
|
+
p.render({ canvas, canvasContext: ctx, viewport }).promise.catch(() => {
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
return () => {
|
|
230
|
+
cancelled = true;
|
|
231
|
+
};
|
|
232
|
+
}, [pdf, page, scale]);
|
|
233
|
+
const handlePrint = () => {
|
|
234
|
+
if (!pdf) return;
|
|
235
|
+
const win = window.open("", "_blank");
|
|
236
|
+
if (!win) {
|
|
237
|
+
toast_default.error("Allow popups to print");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const promises = [];
|
|
241
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
242
|
+
promises.push(pdf.getPage(i).then((p) => {
|
|
243
|
+
const vp = p.getViewport({ scale: 2 });
|
|
244
|
+
const c = document.createElement("canvas");
|
|
245
|
+
c.width = vp.width;
|
|
246
|
+
c.height = vp.height;
|
|
247
|
+
return p.render({ canvas: c, canvasContext: c.getContext("2d"), viewport: vp }).promise.then(() => c.toDataURL());
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
Promise.all(promises).then((images) => {
|
|
251
|
+
win.document.write(`<html><head><title>${filename}</title><style>@media print{body{margin:0}img{width:100%;page-break-after:always}}</style></head><body>`);
|
|
252
|
+
win.document.write(images.map((src) => `<img src="${src}"/>`).join(""));
|
|
253
|
+
win.document.write("</body></html>");
|
|
254
|
+
win.document.close();
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
win.print();
|
|
257
|
+
win.close();
|
|
258
|
+
}, 300);
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
const handleDefaultDownload = () => {
|
|
262
|
+
const a = document.createElement("a");
|
|
263
|
+
a.href = url;
|
|
264
|
+
a.download = filename;
|
|
265
|
+
a.click();
|
|
266
|
+
};
|
|
267
|
+
const fitWidth = () => {
|
|
268
|
+
if (!pdf || !containerRef.current) return;
|
|
269
|
+
pdf.getPage(page).then((p) => {
|
|
270
|
+
const containerW = containerRef.current?.clientWidth || 800;
|
|
271
|
+
const viewport = p.getViewport({ scale: 1 });
|
|
272
|
+
setScale(Math.min(Math.max((containerW - 40) / viewport.width, 0.5), 3));
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
|
|
276
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
277
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-gray-50 shrink-0 text-xs", children: [
|
|
278
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
279
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setPage((p) => Math.max(1, p - 1)), disabled: page <= 1, className: "px-2 py-1 rounded hover:bg-gray-200 disabled:opacity-30", children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 19.5L8.25 12l7.5-7.5" }) }) }),
|
|
280
|
+
/* @__PURE__ */ jsxs("span", { className: "text-gray-600 font-medium tabular-nums", children: [
|
|
281
|
+
page,
|
|
282
|
+
" / ",
|
|
283
|
+
totalPages
|
|
284
|
+
] }),
|
|
285
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setPage((p) => Math.min(totalPages, p + 1)), disabled: page >= totalPages, className: "px-2 py-1 rounded hover:bg-gray-200 disabled:opacity-30", children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8.25 4.5l7.5 7.5-7.5 7.5" }) }) })
|
|
286
|
+
] }),
|
|
287
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
288
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setScale((s) => Math.max(0.3, Math.round((s - 0.25) * 100) / 100)), className: btn, children: "\u2212" }),
|
|
289
|
+
/* @__PURE__ */ jsxs("span", { className: "text-gray-500 w-12 text-center tabular-nums", children: [
|
|
290
|
+
Math.round(scale * 100),
|
|
291
|
+
"%"
|
|
292
|
+
] }),
|
|
293
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setScale((s) => Math.min(4, Math.round((s + 0.25) * 100) / 100)), className: btn, children: "+" }),
|
|
294
|
+
/* @__PURE__ */ jsx("button", { onClick: fitWidth, className: btn, children: "Fit" })
|
|
295
|
+
] }),
|
|
296
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
297
|
+
/* @__PURE__ */ jsxs("button", { onClick: handlePrint, className: btn, children: [
|
|
298
|
+
/* @__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.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" }) }),
|
|
299
|
+
"Print"
|
|
300
|
+
] }),
|
|
301
|
+
/* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
|
|
302
|
+
/* @__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" }) }),
|
|
303
|
+
"Download"
|
|
304
|
+
] }),
|
|
305
|
+
onEmail && /* @__PURE__ */ jsxs("button", { onClick: onEmail, className: btn, children: [
|
|
306
|
+
/* @__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" }) }),
|
|
307
|
+
"Email"
|
|
308
|
+
] })
|
|
309
|
+
] })
|
|
310
|
+
] }),
|
|
311
|
+
/* @__PURE__ */ jsx("div", { ref: containerRef, className: "flex-1 overflow-auto bg-gray-100 flex justify-center p-4", children: loading ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-20 text-gray-400 text-sm", children: "Loading PDF..." }) : /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "shadow-lg rounded" }) })
|
|
312
|
+
] });
|
|
313
|
+
}
|
|
314
|
+
var DEFAULT_DXF_FONTS = [
|
|
315
|
+
"https://cdn.jsdelivr.net/gh/vagran/dxf-viewer-example-src@master/src/assets/fonts/Roboto-LightItalic.ttf",
|
|
316
|
+
"https://cdn.jsdelivr.net/gh/vagran/dxf-viewer-example-src@master/src/assets/fonts/NotoSansDisplay-SemiCondensedLightItalic.ttf",
|
|
317
|
+
"https://cdn.jsdelivr.net/gh/vagran/dxf-viewer-example-src@master/src/assets/fonts/NanumGothic-Regular.ttf"
|
|
318
|
+
];
|
|
319
|
+
function DxfPanel({ url, filename, onDownload, onEmail }) {
|
|
320
|
+
const containerRef = useRef(null);
|
|
321
|
+
const viewerRef = useRef(null);
|
|
322
|
+
const [loading, setLoading] = useState(true);
|
|
323
|
+
const [error, setError] = useState(null);
|
|
324
|
+
const [layers, setLayers] = useState([]);
|
|
325
|
+
const [showLayers, setShowLayers] = useState(false);
|
|
326
|
+
const [showHint, setShowHint] = useState(true);
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
let cancelled = false;
|
|
329
|
+
let viewer = null;
|
|
330
|
+
setLoading(true);
|
|
331
|
+
setError(null);
|
|
332
|
+
setLayers([]);
|
|
333
|
+
(async () => {
|
|
334
|
+
let DxfViewer;
|
|
335
|
+
try {
|
|
336
|
+
const mod = await import('dxf-viewer');
|
|
337
|
+
DxfViewer = mod.DxfViewer;
|
|
338
|
+
} catch (e) {
|
|
339
|
+
if (!cancelled) {
|
|
340
|
+
setError("dxf-viewer is not installed in this app.");
|
|
341
|
+
setLoading(false);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (cancelled || !containerRef.current) return;
|
|
346
|
+
try {
|
|
347
|
+
await new Promise((resolve) => {
|
|
348
|
+
const tryStart = () => {
|
|
349
|
+
const r = containerRef.current?.getBoundingClientRect();
|
|
350
|
+
if (r && r.width > 4 && r.height > 4) resolve();
|
|
351
|
+
else requestAnimationFrame(tryStart);
|
|
352
|
+
};
|
|
353
|
+
tryStart();
|
|
354
|
+
});
|
|
355
|
+
if (cancelled || !containerRef.current) return;
|
|
356
|
+
let three = null;
|
|
357
|
+
try {
|
|
358
|
+
three = await import(
|
|
359
|
+
/* @vite-ignore */
|
|
360
|
+
'three'
|
|
361
|
+
);
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
const ClearColor = three?.Color ?? null;
|
|
365
|
+
const viewerOpts = { autoResize: true };
|
|
366
|
+
if (ClearColor) viewerOpts.clearColor = new ClearColor(16777215);
|
|
367
|
+
viewer = new DxfViewer(containerRef.current, viewerOpts);
|
|
368
|
+
viewerRef.current = viewer;
|
|
369
|
+
const fontUrls = typeof window !== "undefined" && window.__REACT_OS_SHELL_DXF_FONTS__ || DEFAULT_DXF_FONTS;
|
|
370
|
+
await viewer.Load({ url, fonts: fontUrls, workerFactory: null });
|
|
371
|
+
if (cancelled) return;
|
|
372
|
+
try {
|
|
373
|
+
const list = viewer.GetLayers?.() ?? [];
|
|
374
|
+
if (Array.isArray(list)) {
|
|
375
|
+
setLayers(list.map((l) => ({
|
|
376
|
+
name: l.name,
|
|
377
|
+
displayName: l.displayName ?? l.name,
|
|
378
|
+
color: typeof l.color === "number" ? l.color : void 0,
|
|
379
|
+
visible: true
|
|
380
|
+
})));
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
const refit = () => {
|
|
385
|
+
try {
|
|
386
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
387
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
388
|
+
viewer.SetSize?.(Math.round(rect.width), Math.round(rect.height));
|
|
389
|
+
}
|
|
390
|
+
const bounds = viewer.GetBounds?.();
|
|
391
|
+
const origin = viewer.GetOrigin?.();
|
|
392
|
+
if (bounds && origin) {
|
|
393
|
+
viewer.FitView(
|
|
394
|
+
bounds.minX - origin.x,
|
|
395
|
+
bounds.maxX - origin.x,
|
|
396
|
+
bounds.minY - origin.y,
|
|
397
|
+
bounds.maxY - origin.y
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
viewer.Render?.();
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.warn("[Preview] DXF refit failed", err);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
refit();
|
|
406
|
+
requestAnimationFrame(refit);
|
|
407
|
+
setLoading(false);
|
|
408
|
+
} catch (e) {
|
|
409
|
+
if (!cancelled) {
|
|
410
|
+
setError(e?.message || "Failed to render DXF.");
|
|
411
|
+
setLoading(false);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
})();
|
|
415
|
+
return () => {
|
|
416
|
+
cancelled = true;
|
|
417
|
+
try {
|
|
418
|
+
viewer?.Destroy?.();
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
viewerRef.current = null;
|
|
422
|
+
};
|
|
423
|
+
}, [url]);
|
|
424
|
+
useEffect(() => {
|
|
425
|
+
if (!showHint || loading) return;
|
|
426
|
+
const t = setTimeout(() => setShowHint(false), 5e3);
|
|
427
|
+
return () => clearTimeout(t);
|
|
428
|
+
}, [showHint, loading]);
|
|
429
|
+
const toggleLayer = (name) => {
|
|
430
|
+
setLayers((prev) => prev.map((l) => {
|
|
431
|
+
if (l.name !== name) return l;
|
|
432
|
+
const next = !l.visible;
|
|
433
|
+
try {
|
|
434
|
+
viewerRef.current?.ShowLayer?.(name, next);
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
return { ...l, visible: next };
|
|
438
|
+
}));
|
|
439
|
+
};
|
|
440
|
+
const setAllLayers = (visible) => {
|
|
441
|
+
setLayers((prev) => prev.map((l) => {
|
|
442
|
+
if (l.visible === visible) return l;
|
|
443
|
+
try {
|
|
444
|
+
viewerRef.current?.ShowLayer?.(l.name, visible);
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
return { ...l, visible };
|
|
448
|
+
}));
|
|
449
|
+
};
|
|
450
|
+
const handleDefaultDownload = () => {
|
|
451
|
+
const a = document.createElement("a");
|
|
452
|
+
a.href = url;
|
|
453
|
+
a.download = filename;
|
|
454
|
+
a.click();
|
|
455
|
+
};
|
|
456
|
+
const handleResetView = () => {
|
|
457
|
+
try {
|
|
458
|
+
const v = viewerRef.current;
|
|
459
|
+
const bounds = v?.GetBounds?.();
|
|
460
|
+
const origin = v?.GetOrigin?.();
|
|
461
|
+
if (bounds && origin) {
|
|
462
|
+
v.FitView(
|
|
463
|
+
bounds.minX - origin.x,
|
|
464
|
+
bounds.maxX - origin.x,
|
|
465
|
+
bounds.minY - origin.y,
|
|
466
|
+
bounds.maxY - origin.y
|
|
467
|
+
);
|
|
468
|
+
} else {
|
|
469
|
+
v?.FitView?.();
|
|
470
|
+
}
|
|
471
|
+
v?.Render?.();
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
|
|
476
|
+
const colorHex = (n) => {
|
|
477
|
+
if (typeof n !== "number") return "#999";
|
|
478
|
+
return "#" + n.toString(16).padStart(6, "0");
|
|
479
|
+
};
|
|
480
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
481
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-gray-50 shrink-0 text-xs", children: [
|
|
482
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
483
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-gray-600", children: "DXF" }),
|
|
484
|
+
/* @__PURE__ */ jsx("span", { className: "text-gray-400 truncate max-w-xs", children: filename })
|
|
485
|
+
] }),
|
|
486
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 relative", children: [
|
|
487
|
+
/* @__PURE__ */ jsxs(
|
|
488
|
+
"button",
|
|
489
|
+
{
|
|
490
|
+
onClick: () => setShowLayers((s) => !s),
|
|
491
|
+
className: btn + (showLayers ? " bg-gray-200" : ""),
|
|
492
|
+
title: "Toggle layer visibility",
|
|
493
|
+
disabled: layers.length === 0,
|
|
494
|
+
children: [
|
|
495
|
+
/* @__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" }) }),
|
|
496
|
+
"Layers ",
|
|
497
|
+
layers.length > 0 && /* @__PURE__ */ jsxs("span", { className: "text-gray-400", children: [
|
|
498
|
+
"(",
|
|
499
|
+
layers.filter((l) => l.visible).length,
|
|
500
|
+
"/",
|
|
501
|
+
layers.length,
|
|
502
|
+
")"
|
|
503
|
+
] })
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
),
|
|
507
|
+
/* @__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" }) }) }),
|
|
508
|
+
/* @__PURE__ */ jsx("button", { onClick: handleResetView, className: btn, title: "Fit drawing to view", children: "Fit" }),
|
|
509
|
+
/* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
|
|
510
|
+
/* @__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" }) }),
|
|
511
|
+
"Download"
|
|
512
|
+
] }),
|
|
513
|
+
onEmail && /* @__PURE__ */ jsxs("button", { onClick: onEmail, className: btn, children: [
|
|
514
|
+
/* @__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" }) }),
|
|
515
|
+
"Email"
|
|
516
|
+
] })
|
|
517
|
+
] })
|
|
518
|
+
] }),
|
|
519
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex-1 bg-white min-h-0", children: [
|
|
520
|
+
/* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%", height: "100%" } }),
|
|
521
|
+
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: [
|
|
522
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 border-b border-gray-200 bg-gray-50", children: [
|
|
523
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "Layers" }),
|
|
524
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
525
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setAllLayers(true), className: "px-1.5 py-0.5 rounded hover:bg-gray-200 text-gray-600", children: "All" }),
|
|
526
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setAllLayers(false), className: "px-1.5 py-0.5 rounded hover:bg-gray-200 text-gray-600", children: "None" }),
|
|
527
|
+
/* @__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" })
|
|
528
|
+
] })
|
|
529
|
+
] }),
|
|
530
|
+
/* @__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: [
|
|
531
|
+
/* @__PURE__ */ jsx(
|
|
532
|
+
"input",
|
|
533
|
+
{
|
|
534
|
+
type: "checkbox",
|
|
535
|
+
checked: l.visible,
|
|
536
|
+
onChange: () => toggleLayer(l.name),
|
|
537
|
+
className: "h-3.5 w-3.5"
|
|
538
|
+
}
|
|
539
|
+
),
|
|
540
|
+
/* @__PURE__ */ jsx(
|
|
541
|
+
"span",
|
|
542
|
+
{
|
|
543
|
+
className: "inline-block h-3 w-3 rounded-sm border border-gray-300 shrink-0",
|
|
544
|
+
style: { background: colorHex(l.color) }
|
|
545
|
+
}
|
|
546
|
+
),
|
|
547
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-gray-700", title: l.displayName, children: l.displayName || l.name })
|
|
548
|
+
] }, l.name)) })
|
|
549
|
+
] }),
|
|
550
|
+
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: [
|
|
551
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
552
|
+
/* @__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" }) }),
|
|
553
|
+
"Drag to pan"
|
|
554
|
+
] }),
|
|
555
|
+
/* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
|
|
556
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
557
|
+
/* @__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" }) }),
|
|
558
|
+
"Scroll to zoom"
|
|
559
|
+
] }),
|
|
560
|
+
/* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
|
|
561
|
+
/* @__PURE__ */ jsx("span", { children: "Fit to reset" })
|
|
562
|
+
] }),
|
|
563
|
+
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" }),
|
|
564
|
+
error && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-sm text-red-600 px-6 text-center", children: error })
|
|
565
|
+
] })
|
|
566
|
+
] });
|
|
567
|
+
}
|
|
568
|
+
var DEFAULT_O3DV_LIBS = "https://cdn.jsdelivr.net/npm/online-3d-viewer@0.18.0/libs/";
|
|
569
|
+
function buildTree(node, depth = 0) {
|
|
570
|
+
return {
|
|
571
|
+
id: node.GetId?.() ?? 0,
|
|
572
|
+
name: node.GetName?.() || (depth === 0 ? "Root" : "Node"),
|
|
573
|
+
isMeshNode: !!node.IsMeshNode?.(),
|
|
574
|
+
meshIndices: node.GetMeshIndices?.() ?? [],
|
|
575
|
+
children: (node.GetChildNodes?.() ?? []).map((c) => buildTree(c, depth + 1))
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function collectNodeIds(node, out) {
|
|
579
|
+
out.add(node.id);
|
|
580
|
+
for (const c of node.children) collectNodeIds(c, out);
|
|
581
|
+
}
|
|
582
|
+
function hexToRgb(OV, hex) {
|
|
583
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex);
|
|
584
|
+
const n = m ? parseInt(m[1], 16) : 0;
|
|
585
|
+
return new OV.RGBColor(n >> 16 & 255, n >> 8 & 255, n & 255);
|
|
586
|
+
}
|
|
587
|
+
function StepPanel({ url, filename, onDownload, onEmail }) {
|
|
588
|
+
const containerRef = useRef(null);
|
|
589
|
+
const viewerRef = useRef(null);
|
|
590
|
+
const ovRef = useRef(null);
|
|
591
|
+
const [loading, setLoading] = useState(true);
|
|
592
|
+
const [error, setError] = useState(null);
|
|
593
|
+
const [showHint, setShowHint] = useState(true);
|
|
594
|
+
const [tree, setTree] = useState(null);
|
|
595
|
+
const [expanded, setExpanded] = useState({});
|
|
596
|
+
const [hidden, setHidden] = useState(/* @__PURE__ */ new Set());
|
|
597
|
+
const [bgColor, setBgColor] = useState("#f5f6f8");
|
|
598
|
+
const [showEdges, setShowEdges] = useState(true);
|
|
599
|
+
const [edgeColor, setEdgeColor] = useState("#000000");
|
|
600
|
+
const [edgeThreshold, setEdgeThreshold] = useState(1);
|
|
601
|
+
const [showMeshes, setShowMeshes] = useState(true);
|
|
602
|
+
const [showSettings, setShowSettings] = useState(true);
|
|
603
|
+
const [sectionEnabled, setSectionEnabled] = useState(false);
|
|
604
|
+
const [sectionAxis, setSectionAxis] = useState("z");
|
|
605
|
+
const [sectionFlip, setSectionFlip] = useState(false);
|
|
606
|
+
const [sectionPosition, setSectionPosition] = useState(0.5);
|
|
607
|
+
const [sectionCapColor, setSectionCapColor] = useState("#9aa6b3");
|
|
608
|
+
const sectionRef = useRef(null);
|
|
609
|
+
useEffect(() => {
|
|
610
|
+
let cancelled = false;
|
|
611
|
+
let viewer = null;
|
|
612
|
+
setLoading(true);
|
|
613
|
+
setError(null);
|
|
614
|
+
setTree(null);
|
|
615
|
+
setExpanded({});
|
|
616
|
+
setHidden(/* @__PURE__ */ new Set());
|
|
617
|
+
(async () => {
|
|
618
|
+
let OV;
|
|
619
|
+
try {
|
|
620
|
+
OV = await import('online-3d-viewer');
|
|
621
|
+
} catch {
|
|
622
|
+
if (!cancelled) {
|
|
623
|
+
setError("online-3d-viewer is not installed in this app. Add it to enable 3D file viewing.");
|
|
624
|
+
setLoading(false);
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
ovRef.current = OV;
|
|
629
|
+
if (cancelled || !containerRef.current) return;
|
|
630
|
+
await new Promise((resolve) => {
|
|
631
|
+
const tryStart = () => {
|
|
632
|
+
const r = containerRef.current?.getBoundingClientRect();
|
|
633
|
+
if (r && r.width > 4 && r.height > 4) resolve();
|
|
634
|
+
else requestAnimationFrame(tryStart);
|
|
635
|
+
};
|
|
636
|
+
tryStart();
|
|
637
|
+
});
|
|
638
|
+
if (cancelled || !containerRef.current) return;
|
|
639
|
+
try {
|
|
640
|
+
const libsBase = typeof window !== "undefined" && window.__REACT_OS_SHELL_O3DV_LIBS__ || DEFAULT_O3DV_LIBS;
|
|
641
|
+
OV.SetExternalLibLocation?.(libsBase);
|
|
642
|
+
viewer = new OV.EmbeddedViewer(containerRef.current, {
|
|
643
|
+
backgroundColor: new OV.RGBAColor(245, 246, 248, 255),
|
|
644
|
+
defaultColor: new OV.RGBColor(180, 188, 200),
|
|
645
|
+
edgeSettings: new OV.EdgeSettings(true, new OV.RGBColor(0, 0, 0), 1),
|
|
646
|
+
onModelLoaded: () => {
|
|
647
|
+
if (cancelled) return;
|
|
648
|
+
try {
|
|
649
|
+
const model = viewer.GetModel?.();
|
|
650
|
+
const root = model?.GetRootNode?.();
|
|
651
|
+
if (root) {
|
|
652
|
+
const t = buildTree(root);
|
|
653
|
+
setTree(t);
|
|
654
|
+
const expandIds = { [t.id]: true };
|
|
655
|
+
for (const c of t.children) expandIds[c.id] = true;
|
|
656
|
+
setExpanded(expandIds);
|
|
657
|
+
}
|
|
658
|
+
} catch (err) {
|
|
659
|
+
console.warn("[Preview] mesh tree extraction failed", err);
|
|
660
|
+
}
|
|
661
|
+
setLoading(false);
|
|
662
|
+
},
|
|
663
|
+
onModelLoadFailed: () => {
|
|
664
|
+
if (!cancelled) {
|
|
665
|
+
setError("Failed to load 3D model.");
|
|
666
|
+
setLoading(false);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
viewerRef.current = viewer;
|
|
671
|
+
const inputFile = new OV.InputFile(filename, OV.FileSource.Url, url);
|
|
672
|
+
viewer.LoadModelFromInputFiles([inputFile]);
|
|
673
|
+
} catch (e) {
|
|
674
|
+
if (!cancelled) {
|
|
675
|
+
setError(e?.message || "Failed to load 3D model.");
|
|
676
|
+
setLoading(false);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
})();
|
|
680
|
+
return () => {
|
|
681
|
+
cancelled = true;
|
|
682
|
+
try {
|
|
683
|
+
viewer?.Destroy?.();
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
viewerRef.current = null;
|
|
687
|
+
};
|
|
688
|
+
}, [url, filename]);
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
const v = viewerRef.current;
|
|
691
|
+
if (!v?.viewer) return;
|
|
692
|
+
try {
|
|
693
|
+
const visit = (mesh) => {
|
|
694
|
+
const ud = mesh.userData?.originalMeshInstance ?? mesh.userData;
|
|
695
|
+
const nodeId = ud?.id?.nodeId ?? ud?.nodeId;
|
|
696
|
+
if (typeof nodeId === "number") {
|
|
697
|
+
mesh.visible = !hidden.has(nodeId);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
v.viewer.mainModel?.EnumerateMeshesAndLines?.(visit);
|
|
701
|
+
v.viewer.mainModel?.EnumerateEdges?.(visit);
|
|
702
|
+
v.viewer.Render?.();
|
|
703
|
+
} catch (err) {
|
|
704
|
+
console.warn("[Preview] visibility update failed", err);
|
|
705
|
+
}
|
|
706
|
+
}, [hidden, tree]);
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
const OV = ovRef.current;
|
|
709
|
+
const v = viewerRef.current;
|
|
710
|
+
if (!OV || !v?.viewer) return;
|
|
711
|
+
try {
|
|
712
|
+
v.viewer.SetEdgeSettings(new OV.EdgeSettings(showEdges, hexToRgb(OV, edgeColor), edgeThreshold));
|
|
713
|
+
v.viewer.Render?.();
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
}, [showEdges, edgeColor, edgeThreshold]);
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
const OV = ovRef.current;
|
|
719
|
+
const v = viewerRef.current;
|
|
720
|
+
if (!OV || !v?.viewer) return;
|
|
721
|
+
try {
|
|
722
|
+
const c = hexToRgb(OV, bgColor);
|
|
723
|
+
v.viewer.SetBackgroundColor(new OV.RGBAColor(c.r, c.g, c.b, 255));
|
|
724
|
+
v.viewer.Render?.();
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
}, [bgColor]);
|
|
728
|
+
useEffect(() => {
|
|
729
|
+
const v = viewerRef.current;
|
|
730
|
+
if (!v?.viewer || loading) return;
|
|
731
|
+
let cancelled = false;
|
|
732
|
+
let teardown = null;
|
|
733
|
+
(async () => {
|
|
734
|
+
const THREE = await import('three');
|
|
735
|
+
if (cancelled) return;
|
|
736
|
+
const renderer = v.viewer.renderer;
|
|
737
|
+
const scene = v.viewer.scene;
|
|
738
|
+
if (!renderer || !scene) return;
|
|
739
|
+
if (sectionRef.current) {
|
|
740
|
+
const s = sectionRef.current;
|
|
741
|
+
for (const [mat, prev] of s.materialState.entries()) {
|
|
742
|
+
mat.clippingPlanes = prev.clippingPlanes;
|
|
743
|
+
mat.clipShadows = prev.clipShadows;
|
|
744
|
+
mat.needsUpdate = true;
|
|
745
|
+
}
|
|
746
|
+
for (const helper of s.helpers) {
|
|
747
|
+
helper.parent?.remove(helper);
|
|
748
|
+
helper.geometry?.dispose?.();
|
|
749
|
+
helper.material?.dispose?.();
|
|
750
|
+
}
|
|
751
|
+
if (s.capMesh) {
|
|
752
|
+
scene.remove(s.capMesh);
|
|
753
|
+
s.capMesh.geometry?.dispose?.();
|
|
754
|
+
s.capMesh.material?.dispose?.();
|
|
755
|
+
}
|
|
756
|
+
sectionRef.current = null;
|
|
757
|
+
}
|
|
758
|
+
if (!sectionEnabled) {
|
|
759
|
+
renderer.localClippingEnabled = false;
|
|
760
|
+
v.viewer.Render?.();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const bbox = v.viewer.GetBoundingBox?.(() => true);
|
|
764
|
+
if (!bbox) return;
|
|
765
|
+
const plane = new THREE.Plane(new THREE.Vector3(0, 0, -1), 0);
|
|
766
|
+
const helpers = [];
|
|
767
|
+
const materialState = /* @__PURE__ */ new Map();
|
|
768
|
+
const applyToMaterial = (mat) => {
|
|
769
|
+
if (!mat || materialState.has(mat)) return;
|
|
770
|
+
materialState.set(mat, {
|
|
771
|
+
clippingPlanes: mat.clippingPlanes,
|
|
772
|
+
clipShadows: mat.clipShadows
|
|
773
|
+
});
|
|
774
|
+
mat.clippingPlanes = [plane];
|
|
775
|
+
mat.clipShadows = true;
|
|
776
|
+
mat.needsUpdate = true;
|
|
777
|
+
};
|
|
778
|
+
v.viewer.mainModel?.EnumerateMeshes?.((mesh) => {
|
|
779
|
+
const mat = mesh.material;
|
|
780
|
+
if (Array.isArray(mat)) for (const m of mat) applyToMaterial(m);
|
|
781
|
+
else applyToMaterial(mat);
|
|
782
|
+
const makeStencil = (side, op) => {
|
|
783
|
+
const m = new THREE.MeshBasicMaterial({
|
|
784
|
+
depthWrite: false,
|
|
785
|
+
depthTest: false,
|
|
786
|
+
colorWrite: false,
|
|
787
|
+
stencilWrite: true,
|
|
788
|
+
stencilFunc: THREE.AlwaysStencilFunc,
|
|
789
|
+
stencilFail: op,
|
|
790
|
+
stencilZFail: op,
|
|
791
|
+
stencilZPass: op,
|
|
792
|
+
side,
|
|
793
|
+
clippingPlanes: [plane]
|
|
794
|
+
});
|
|
795
|
+
const helper = new THREE.Mesh(mesh.geometry, m);
|
|
796
|
+
helper.matrixAutoUpdate = false;
|
|
797
|
+
helper.renderOrder = 1;
|
|
798
|
+
helper.userData.__sectionHelper = true;
|
|
799
|
+
mesh.add(helper);
|
|
800
|
+
helpers.push(helper);
|
|
801
|
+
};
|
|
802
|
+
makeStencil(THREE.BackSide, THREE.IncrementWrapStencilOp);
|
|
803
|
+
makeStencil(THREE.FrontSide, THREE.DecrementWrapStencilOp);
|
|
804
|
+
});
|
|
805
|
+
const dx = bbox.max.x - bbox.min.x;
|
|
806
|
+
const dy = bbox.max.y - bbox.min.y;
|
|
807
|
+
const dz = bbox.max.z - bbox.min.z;
|
|
808
|
+
const capSize = Math.max(dx, dy, dz) * 2 || 1;
|
|
809
|
+
const capGeom = new THREE.PlaneGeometry(capSize, capSize);
|
|
810
|
+
const capMat = new THREE.MeshPhongMaterial({
|
|
811
|
+
color: 10135219,
|
|
812
|
+
side: THREE.DoubleSide,
|
|
813
|
+
stencilWrite: true,
|
|
814
|
+
stencilRef: 0,
|
|
815
|
+
stencilFunc: THREE.NotEqualStencilFunc,
|
|
816
|
+
stencilFail: THREE.ReplaceStencilOp,
|
|
817
|
+
stencilZFail: THREE.ReplaceStencilOp,
|
|
818
|
+
stencilZPass: THREE.ReplaceStencilOp
|
|
819
|
+
});
|
|
820
|
+
const capMesh = new THREE.Mesh(capGeom, capMat);
|
|
821
|
+
capMesh.renderOrder = 2;
|
|
822
|
+
scene.add(capMesh);
|
|
823
|
+
renderer.localClippingEnabled = true;
|
|
824
|
+
sectionRef.current = { plane, capMesh, helpers, materialState, bbox };
|
|
825
|
+
v.viewer.Render?.();
|
|
826
|
+
teardown = () => {
|
|
827
|
+
};
|
|
828
|
+
})();
|
|
829
|
+
return () => {
|
|
830
|
+
cancelled = true;
|
|
831
|
+
if (teardown) teardown();
|
|
832
|
+
};
|
|
833
|
+
}, [sectionEnabled, loading, tree]);
|
|
834
|
+
useEffect(() => {
|
|
835
|
+
const v = viewerRef.current;
|
|
836
|
+
const s = sectionRef.current;
|
|
837
|
+
if (!v?.viewer || !s || !sectionEnabled) return;
|
|
838
|
+
try {
|
|
839
|
+
const bbox = s.bbox;
|
|
840
|
+
const axisIdx = sectionAxis === "x" ? 0 : sectionAxis === "y" ? 1 : 2;
|
|
841
|
+
const min = [bbox.min.x, bbox.min.y, bbox.min.z][axisIdx];
|
|
842
|
+
const max = [bbox.max.x, bbox.max.y, bbox.max.z][axisIdx];
|
|
843
|
+
const value = min + (max - min) * sectionPosition;
|
|
844
|
+
const dir = sectionFlip ? 1 : -1;
|
|
845
|
+
const nx = sectionAxis === "x" ? dir : 0;
|
|
846
|
+
const ny = sectionAxis === "y" ? dir : 0;
|
|
847
|
+
const nz = sectionAxis === "z" ? dir : 0;
|
|
848
|
+
s.plane.normal.set(nx, ny, nz);
|
|
849
|
+
s.plane.constant = -dir * value;
|
|
850
|
+
const cx = (bbox.min.x + bbox.max.x) / 2;
|
|
851
|
+
const cy = (bbox.min.y + bbox.max.y) / 2;
|
|
852
|
+
const cz = (bbox.min.z + bbox.max.z) / 2;
|
|
853
|
+
const center = { x: cx, y: cy, z: cz };
|
|
854
|
+
const dist = nx * center.x + ny * center.y + nz * center.z + s.plane.constant;
|
|
855
|
+
const px = center.x - nx * dist;
|
|
856
|
+
const py = center.y - ny * dist;
|
|
857
|
+
const pz = center.z - nz * dist;
|
|
858
|
+
s.capMesh.position.set(px, py, pz);
|
|
859
|
+
s.capMesh.lookAt(px + nx, py + ny, pz + nz);
|
|
860
|
+
try {
|
|
861
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(sectionCapColor);
|
|
862
|
+
const n = m ? parseInt(m[1], 16) : 10135219;
|
|
863
|
+
s.capMesh.material.color.setHex(n);
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
v.viewer.Render?.();
|
|
867
|
+
} catch (err) {
|
|
868
|
+
console.warn("[Preview] section update failed", err);
|
|
869
|
+
}
|
|
870
|
+
}, [sectionEnabled, sectionAxis, sectionFlip, sectionPosition, sectionCapColor]);
|
|
871
|
+
useEffect(() => {
|
|
872
|
+
if (!showHint || loading) return;
|
|
873
|
+
const t = setTimeout(() => setShowHint(false), 5e3);
|
|
874
|
+
return () => clearTimeout(t);
|
|
875
|
+
}, [showHint, loading]);
|
|
876
|
+
const toggleNodeVisible = (node) => {
|
|
877
|
+
const ids = /* @__PURE__ */ new Set();
|
|
878
|
+
collectNodeIds(node, ids);
|
|
879
|
+
setHidden((prev) => {
|
|
880
|
+
const next = new Set(prev);
|
|
881
|
+
const anyVisible = [...ids].some((id) => !next.has(id));
|
|
882
|
+
for (const id of ids) {
|
|
883
|
+
if (anyVisible) next.add(id);
|
|
884
|
+
else next.delete(id);
|
|
885
|
+
}
|
|
886
|
+
return next;
|
|
887
|
+
});
|
|
888
|
+
};
|
|
889
|
+
const toggleExpanded = (id) => {
|
|
890
|
+
setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
891
|
+
};
|
|
892
|
+
const fitNode = (node) => {
|
|
893
|
+
handleFit();
|
|
894
|
+
};
|
|
895
|
+
const handleFit = () => {
|
|
896
|
+
try {
|
|
897
|
+
const v = viewerRef.current;
|
|
898
|
+
const sphere = v?.GetBoundingSphere?.(() => true);
|
|
899
|
+
if (sphere) v.viewer.FitSphereToWindow(sphere, true);
|
|
900
|
+
v?.viewer?.Render?.();
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
const setCameraPreset = (preset) => {
|
|
905
|
+
const OV = ovRef.current;
|
|
906
|
+
const v = viewerRef.current;
|
|
907
|
+
if (!OV || !v?.viewer) return;
|
|
908
|
+
try {
|
|
909
|
+
const sphere = v.GetBoundingSphere?.(() => true);
|
|
910
|
+
if (!sphere) return;
|
|
911
|
+
const c = sphere.center;
|
|
912
|
+
const r = sphere.radius || 1;
|
|
913
|
+
const dist = r * 3;
|
|
914
|
+
const cx = c.x ?? 0, cy = c.y ?? 0, cz = c.z ?? 0;
|
|
915
|
+
let eye, up;
|
|
916
|
+
switch (preset) {
|
|
917
|
+
case "top":
|
|
918
|
+
eye = new OV.Coord3D(cx, cy, cz + dist);
|
|
919
|
+
up = new OV.Coord3D(0, 1, 0);
|
|
920
|
+
break;
|
|
921
|
+
case "front":
|
|
922
|
+
eye = new OV.Coord3D(cx, cy - dist, cz);
|
|
923
|
+
up = new OV.Coord3D(0, 0, 1);
|
|
924
|
+
break;
|
|
925
|
+
case "side":
|
|
926
|
+
eye = new OV.Coord3D(cx + dist, cy, cz);
|
|
927
|
+
up = new OV.Coord3D(0, 0, 1);
|
|
928
|
+
break;
|
|
929
|
+
case "iso":
|
|
930
|
+
default: {
|
|
931
|
+
const k = dist / Math.sqrt(3);
|
|
932
|
+
eye = new OV.Coord3D(cx + k, cy - k, cz + k);
|
|
933
|
+
up = new OV.Coord3D(0, 0, 1);
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
const center = new OV.Coord3D(cx, cy, cz);
|
|
938
|
+
const cam = new OV.Camera(eye, center, up, 45);
|
|
939
|
+
v.viewer.SetCamera(cam);
|
|
940
|
+
v.viewer.AdjustClippingPlanesToSphere?.(sphere);
|
|
941
|
+
v.viewer.Render?.();
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
const handleSnapshot = () => {
|
|
946
|
+
try {
|
|
947
|
+
const v = viewerRef.current;
|
|
948
|
+
const size = v?.viewer?.GetCanvasSize?.() ?? { width: 1280, height: 720 };
|
|
949
|
+
const dataUrl = v?.viewer?.GetImageAsDataUrl?.(size.width, size.height, false);
|
|
950
|
+
if (!dataUrl) return;
|
|
951
|
+
const a = document.createElement("a");
|
|
952
|
+
a.href = dataUrl;
|
|
953
|
+
a.download = `${filename.replace(/\.[^.]+$/, "")}-snapshot.png`;
|
|
954
|
+
a.click();
|
|
955
|
+
} catch {
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
const handleResetDisplay = () => {
|
|
959
|
+
setBgColor("#f5f6f8");
|
|
960
|
+
setShowEdges(true);
|
|
961
|
+
setEdgeColor("#000000");
|
|
962
|
+
setEdgeThreshold(1);
|
|
963
|
+
setSectionEnabled(false);
|
|
964
|
+
setSectionAxis("z");
|
|
965
|
+
setSectionFlip(false);
|
|
966
|
+
setSectionPosition(0.5);
|
|
967
|
+
setSectionCapColor("#9aa6b3");
|
|
968
|
+
};
|
|
969
|
+
const handleDefaultDownload = () => {
|
|
970
|
+
const a = document.createElement("a");
|
|
971
|
+
a.href = url;
|
|
972
|
+
a.download = filename;
|
|
973
|
+
a.click();
|
|
974
|
+
};
|
|
975
|
+
const ext = (filename.split(".").pop() || "").toUpperCase();
|
|
976
|
+
const renderTreeNode = (node, depth = 0) => {
|
|
977
|
+
const hasChildren = node.children.length > 0;
|
|
978
|
+
const isExpanded = expanded[node.id] !== false;
|
|
979
|
+
const isVisible = !hidden.has(node.id);
|
|
980
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
981
|
+
/* @__PURE__ */ jsxs(
|
|
982
|
+
"div",
|
|
983
|
+
{
|
|
984
|
+
className: "group flex items-center gap-1 px-1.5 py-1 hover:bg-slate-700/50 cursor-default text-[12px] text-slate-200",
|
|
985
|
+
style: { paddingLeft: `${depth * 12 + 6}px` },
|
|
986
|
+
children: [
|
|
987
|
+
hasChildren ? /* @__PURE__ */ jsx(
|
|
988
|
+
"button",
|
|
989
|
+
{
|
|
990
|
+
onClick: () => toggleExpanded(node.id),
|
|
991
|
+
className: "h-4 w-4 shrink-0 flex items-center justify-center text-slate-400 hover:text-slate-100",
|
|
992
|
+
title: isExpanded ? "Collapse" : "Expand",
|
|
993
|
+
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" }) })
|
|
994
|
+
}
|
|
995
|
+
) : /* @__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-slate-500" }) }),
|
|
996
|
+
/* @__PURE__ */ jsx("span", { className: `flex-1 truncate ${isVisible ? "" : "opacity-40"}`, title: node.name, children: node.name }),
|
|
997
|
+
/* @__PURE__ */ jsx(
|
|
998
|
+
"button",
|
|
999
|
+
{
|
|
1000
|
+
onClick: () => fitNode(),
|
|
1001
|
+
className: "h-4 w-4 shrink-0 text-slate-500 hover:text-slate-100 opacity-0 group-hover:opacity-100 transition-opacity",
|
|
1002
|
+
title: "Fit to view",
|
|
1003
|
+
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" }) })
|
|
1004
|
+
}
|
|
1005
|
+
),
|
|
1006
|
+
/* @__PURE__ */ jsx(
|
|
1007
|
+
"button",
|
|
1008
|
+
{
|
|
1009
|
+
onClick: () => toggleNodeVisible(node),
|
|
1010
|
+
className: "h-4 w-4 shrink-0 text-slate-400 hover:text-slate-100",
|
|
1011
|
+
title: isVisible ? "Hide" : "Show",
|
|
1012
|
+
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: [
|
|
1013
|
+
/* @__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" }),
|
|
1014
|
+
/* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" })
|
|
1015
|
+
] }) : /* @__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" }) })
|
|
1016
|
+
}
|
|
1017
|
+
)
|
|
1018
|
+
]
|
|
1019
|
+
}
|
|
1020
|
+
),
|
|
1021
|
+
isExpanded && hasChildren && /* @__PURE__ */ jsx("div", { children: node.children.map((c) => renderTreeNode(c, depth + 1)) })
|
|
1022
|
+
] }, node.id);
|
|
1023
|
+
};
|
|
1024
|
+
const tBtn = "h-8 w-8 shrink-0 flex items-center justify-center rounded text-slate-300 hover:bg-slate-700 hover:text-white transition-colors";
|
|
1025
|
+
const tBtnActive = "h-8 w-8 shrink-0 flex items-center justify-center rounded bg-slate-700 text-white";
|
|
1026
|
+
const tBtnSep = "h-5 w-px bg-slate-700 mx-1";
|
|
1027
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-slate-900", children: [
|
|
1028
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5 bg-slate-800 border-b border-slate-700 shrink-0", children: [
|
|
1029
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold tracking-wide text-slate-300 px-2 truncate max-w-xs", title: filename, children: filename }),
|
|
1030
|
+
/* @__PURE__ */ jsx("div", { className: tBtnSep }),
|
|
1031
|
+
/* @__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" }) }) }),
|
|
1032
|
+
/* @__PURE__ */ jsx("div", { className: tBtnSep }),
|
|
1033
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setCameraPreset("iso"), className: tBtn, title: "Isometric view", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold", children: "ISO" }) }),
|
|
1034
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setCameraPreset("top"), className: tBtn, title: "Top view", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold", children: "TOP" }) }),
|
|
1035
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setCameraPreset("front"), className: tBtn, title: "Front view", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold", children: "FRT" }) }),
|
|
1036
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setCameraPreset("side"), className: tBtn, title: "Side view", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold", children: "SDE" }) }),
|
|
1037
|
+
/* @__PURE__ */ jsx("div", { className: tBtnSep }),
|
|
1038
|
+
/* @__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: [
|
|
1039
|
+
/* @__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" }),
|
|
1040
|
+
/* @__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" })
|
|
1041
|
+
] }) }),
|
|
1042
|
+
/* @__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" }) }) }),
|
|
1043
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
1044
|
+
/* @__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" }) }) }),
|
|
1045
|
+
/* @__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: [
|
|
1046
|
+
/* @__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" }),
|
|
1047
|
+
/* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" })
|
|
1048
|
+
] }) }),
|
|
1049
|
+
/* @__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" }) }) }),
|
|
1050
|
+
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" }) }) })
|
|
1051
|
+
] }),
|
|
1052
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex min-h-0", children: [
|
|
1053
|
+
showMeshes && /* @__PURE__ */ jsxs("div", { className: "w-60 shrink-0 bg-slate-800 border-r border-slate-700 flex flex-col", children: [
|
|
1054
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-slate-400 border-b border-slate-700", children: "Meshes" }),
|
|
1055
|
+
/* @__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-slate-500 italic", children: loading ? "Reading model\u2026" : "No structure available" }) }),
|
|
1056
|
+
tree && /* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 text-[10px] text-slate-500 border-t border-slate-700", children: hidden.size === 0 ? "All visible" : `${hidden.size} hidden` })
|
|
1057
|
+
] }),
|
|
1058
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", style: { background: bgColor }, children: [
|
|
1059
|
+
/* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%", height: "100%" } }),
|
|
1060
|
+
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: [
|
|
1061
|
+
/* @__PURE__ */ jsx("span", { children: "Drag to rotate" }),
|
|
1062
|
+
/* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
|
|
1063
|
+
/* @__PURE__ */ jsx("span", { children: "Right-click drag to pan" }),
|
|
1064
|
+
/* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
|
|
1065
|
+
/* @__PURE__ */ jsx("span", { children: "Scroll to zoom" })
|
|
1066
|
+
] }),
|
|
1067
|
+
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: [
|
|
1068
|
+
/* @__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: [
|
|
1069
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.2" }),
|
|
1070
|
+
/* @__PURE__ */ jsx("path", { d: "M22 12a10 10 0 0 1-10 10", strokeLinecap: "round" })
|
|
1071
|
+
] }),
|
|
1072
|
+
/* @__PURE__ */ jsx("span", { children: "Loading 3D model\u2026" }),
|
|
1073
|
+
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
|
|
1074
|
+
] }),
|
|
1075
|
+
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 })
|
|
1076
|
+
] }),
|
|
1077
|
+
showSettings && /* @__PURE__ */ jsxs("div", { className: "w-60 shrink-0 bg-slate-800 border-l border-slate-700 flex flex-col", children: [
|
|
1078
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-slate-400 border-b border-slate-700", children: "Model Display" }),
|
|
1079
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-3 py-3 space-y-3 text-[12px] text-slate-200", children: [
|
|
1080
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
|
|
1081
|
+
/* @__PURE__ */ jsx("span", { children: "Background Color" }),
|
|
1082
|
+
/* @__PURE__ */ jsx("input", { type: "color", value: bgColor, onChange: (e) => setBgColor(e.target.value), className: "h-6 w-10 rounded border border-slate-600 bg-transparent" })
|
|
1083
|
+
] }),
|
|
1084
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
|
|
1085
|
+
/* @__PURE__ */ jsx("span", { children: "Show Edges" }),
|
|
1086
|
+
/* @__PURE__ */ jsx(
|
|
1087
|
+
"button",
|
|
1088
|
+
{
|
|
1089
|
+
onClick: () => setShowEdges((s) => !s),
|
|
1090
|
+
className: `relative h-5 w-9 rounded-full transition-colors ${showEdges ? "bg-blue-500" : "bg-slate-600"}`,
|
|
1091
|
+
children: /* @__PURE__ */ jsx("span", { className: `absolute top-0.5 h-4 w-4 rounded-full bg-white transition-transform ${showEdges ? "translate-x-4" : "translate-x-0.5"}` })
|
|
1092
|
+
}
|
|
1093
|
+
)
|
|
1094
|
+
] }),
|
|
1095
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
|
|
1096
|
+
/* @__PURE__ */ jsx("span", { className: showEdges ? "" : "opacity-40", children: "Edge Color" }),
|
|
1097
|
+
/* @__PURE__ */ jsx(
|
|
1098
|
+
"input",
|
|
1099
|
+
{
|
|
1100
|
+
type: "color",
|
|
1101
|
+
value: edgeColor,
|
|
1102
|
+
onChange: (e) => setEdgeColor(e.target.value),
|
|
1103
|
+
disabled: !showEdges,
|
|
1104
|
+
className: "h-6 w-10 rounded border border-slate-600 bg-transparent disabled:opacity-40"
|
|
1105
|
+
}
|
|
1106
|
+
)
|
|
1107
|
+
] }),
|
|
1108
|
+
/* @__PURE__ */ jsxs("div", { className: showEdges ? "" : "opacity-40 pointer-events-none", children: [
|
|
1109
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
|
|
1110
|
+
/* @__PURE__ */ jsx("span", { children: "Edge Threshold" }),
|
|
1111
|
+
/* @__PURE__ */ jsxs("span", { className: "text-slate-400 tabular-nums", children: [
|
|
1112
|
+
edgeThreshold,
|
|
1113
|
+
"\xB0"
|
|
1114
|
+
] })
|
|
1115
|
+
] }),
|
|
1116
|
+
/* @__PURE__ */ jsx(
|
|
1117
|
+
"input",
|
|
1118
|
+
{
|
|
1119
|
+
type: "range",
|
|
1120
|
+
min: 0,
|
|
1121
|
+
max: 45,
|
|
1122
|
+
step: 1,
|
|
1123
|
+
value: edgeThreshold,
|
|
1124
|
+
onChange: (e) => setEdgeThreshold(Number(e.target.value)),
|
|
1125
|
+
className: "w-full accent-blue-500"
|
|
1126
|
+
}
|
|
1127
|
+
)
|
|
1128
|
+
] }),
|
|
1129
|
+
/* @__PURE__ */ jsxs("div", { className: "border-t border-slate-700 -mx-3 px-3 pt-3 mt-1", children: [
|
|
1130
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
|
|
1131
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: "Section View" }),
|
|
1132
|
+
/* @__PURE__ */ jsx(
|
|
1133
|
+
"button",
|
|
1134
|
+
{
|
|
1135
|
+
onClick: () => setSectionEnabled((s) => !s),
|
|
1136
|
+
className: `relative h-5 w-9 rounded-full transition-colors ${sectionEnabled ? "bg-blue-500" : "bg-slate-600"}`,
|
|
1137
|
+
children: /* @__PURE__ */ jsx("span", { className: `absolute top-0.5 h-4 w-4 rounded-full bg-white transition-transform ${sectionEnabled ? "translate-x-4" : "translate-x-0.5"}` })
|
|
1138
|
+
}
|
|
1139
|
+
)
|
|
1140
|
+
] }),
|
|
1141
|
+
/* @__PURE__ */ jsxs("div", { className: sectionEnabled ? "mt-2 space-y-2" : "mt-2 space-y-2 opacity-40 pointer-events-none", children: [
|
|
1142
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1143
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
|
|
1144
|
+
/* @__PURE__ */ jsx("span", { children: "Axis" }),
|
|
1145
|
+
/* @__PURE__ */ jsx(
|
|
1146
|
+
"button",
|
|
1147
|
+
{
|
|
1148
|
+
onClick: () => setSectionFlip((f) => !f),
|
|
1149
|
+
className: "text-[10px] text-slate-300 hover:text-white px-1.5 py-0.5 rounded bg-slate-700 hover:bg-slate-600",
|
|
1150
|
+
title: "Flip section direction",
|
|
1151
|
+
children: sectionFlip ? "\u2190 flipped" : "flip \u2192"
|
|
1152
|
+
}
|
|
1153
|
+
)
|
|
1154
|
+
] }),
|
|
1155
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-1", children: ["x", "y", "z"].map((ax) => /* @__PURE__ */ jsx(
|
|
1156
|
+
"button",
|
|
1157
|
+
{
|
|
1158
|
+
onClick: () => setSectionAxis(ax),
|
|
1159
|
+
className: `py-1 rounded text-[11px] font-semibold ${sectionAxis === ax ? "bg-blue-500 text-white" : "bg-slate-700 text-slate-300 hover:bg-slate-600"}`,
|
|
1160
|
+
children: ax.toUpperCase()
|
|
1161
|
+
},
|
|
1162
|
+
ax
|
|
1163
|
+
)) })
|
|
1164
|
+
] }),
|
|
1165
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1166
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
|
|
1167
|
+
/* @__PURE__ */ jsx("span", { children: "Position" }),
|
|
1168
|
+
/* @__PURE__ */ jsxs("span", { className: "text-slate-400 tabular-nums", children: [
|
|
1169
|
+
Math.round(sectionPosition * 100),
|
|
1170
|
+
"%"
|
|
1171
|
+
] })
|
|
1172
|
+
] }),
|
|
1173
|
+
/* @__PURE__ */ jsx(
|
|
1174
|
+
"input",
|
|
1175
|
+
{
|
|
1176
|
+
type: "range",
|
|
1177
|
+
min: 0,
|
|
1178
|
+
max: 1,
|
|
1179
|
+
step: 0.01,
|
|
1180
|
+
value: sectionPosition,
|
|
1181
|
+
onChange: (e) => setSectionPosition(Number(e.target.value)),
|
|
1182
|
+
className: "w-full accent-blue-500"
|
|
1183
|
+
}
|
|
1184
|
+
)
|
|
1185
|
+
] }),
|
|
1186
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
|
|
1187
|
+
/* @__PURE__ */ jsx("span", { children: "Cap Color" }),
|
|
1188
|
+
/* @__PURE__ */ jsx(
|
|
1189
|
+
"input",
|
|
1190
|
+
{
|
|
1191
|
+
type: "color",
|
|
1192
|
+
value: sectionCapColor,
|
|
1193
|
+
onChange: (e) => setSectionCapColor(e.target.value),
|
|
1194
|
+
className: "h-6 w-10 rounded border border-slate-600 bg-transparent"
|
|
1195
|
+
}
|
|
1196
|
+
)
|
|
1197
|
+
] })
|
|
1198
|
+
] })
|
|
1199
|
+
] })
|
|
1200
|
+
] }),
|
|
1201
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-2 border-t border-slate-700", children: /* @__PURE__ */ jsx(
|
|
1202
|
+
"button",
|
|
1203
|
+
{
|
|
1204
|
+
onClick: handleResetDisplay,
|
|
1205
|
+
className: "w-full text-[11px] text-slate-300 bg-slate-700 hover:bg-slate-600 rounded py-1.5 transition-colors",
|
|
1206
|
+
children: "Reset to Default"
|
|
1207
|
+
}
|
|
1208
|
+
) })
|
|
1209
|
+
] })
|
|
1210
|
+
] })
|
|
1211
|
+
] });
|
|
1212
|
+
}
|
|
1213
|
+
function ImagePanel({ url, filename, onDownload, onEmail }) {
|
|
1214
|
+
const [zoom, setZoom] = useState(1);
|
|
1215
|
+
const [error, setError] = useState(false);
|
|
1216
|
+
const handleDefaultDownload = () => {
|
|
1217
|
+
const a = document.createElement("a");
|
|
1218
|
+
a.href = url;
|
|
1219
|
+
a.download = filename;
|
|
1220
|
+
a.click();
|
|
1221
|
+
};
|
|
1222
|
+
const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
|
|
1223
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
1224
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-gray-50 shrink-0 text-xs", children: [
|
|
1225
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1226
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-gray-600", children: "Image" }),
|
|
1227
|
+
/* @__PURE__ */ jsx("span", { className: "text-gray-400 truncate max-w-xs", children: filename })
|
|
1228
|
+
] }),
|
|
1229
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1230
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, Math.round((z - 0.25) * 100) / 100)), className: btn, children: "\u2212" }),
|
|
1231
|
+
/* @__PURE__ */ jsxs("span", { className: "text-gray-500 w-12 text-center tabular-nums", children: [
|
|
1232
|
+
Math.round(zoom * 100),
|
|
1233
|
+
"%"
|
|
1234
|
+
] }),
|
|
1235
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(8, Math.round((z + 0.25) * 100) / 100)), className: btn, children: "+" }),
|
|
1236
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: btn, children: "1:1" })
|
|
1237
|
+
] }),
|
|
1238
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1239
|
+
/* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
|
|
1240
|
+
/* @__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" }) }),
|
|
1241
|
+
"Download"
|
|
1242
|
+
] }),
|
|
1243
|
+
onEmail && /* @__PURE__ */ jsxs("button", { onClick: onEmail, className: btn, children: [
|
|
1244
|
+
/* @__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" }) }),
|
|
1245
|
+
"Email"
|
|
1246
|
+
] })
|
|
1247
|
+
] })
|
|
1248
|
+
] }),
|
|
1249
|
+
/* @__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(
|
|
1250
|
+
"img",
|
|
1251
|
+
{
|
|
1252
|
+
src: url,
|
|
1253
|
+
alt: filename,
|
|
1254
|
+
onError: () => setError(true),
|
|
1255
|
+
style: { transform: `scale(${zoom})`, transformOrigin: "center center", transition: "transform 120ms ease" },
|
|
1256
|
+
className: "max-w-full max-h-full shadow-lg rounded bg-white"
|
|
1257
|
+
}
|
|
1258
|
+
) })
|
|
1259
|
+
] });
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
export { Preview, setPdfPreview };
|
|
1263
|
+
//# sourceMappingURL=chunk-RGYSM6P5.js.map
|
|
1264
|
+
//# sourceMappingURL=chunk-RGYSM6P5.js.map
|