@liveroom-tech/react-immersive 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -10,11 +10,166 @@ import {
10
10
  Suspense as Suspense2
11
11
  } from "react";
12
12
  import { createPortal } from "react-dom";
13
+
14
+ // src/utils/bvh.ts
15
+ import {
16
+ Box3,
17
+ BufferAttribute,
18
+ Mesh
19
+ } from "three";
20
+ import {
21
+ acceleratedRaycast,
22
+ computeBoundsTree,
23
+ disposeBoundsTree,
24
+ MeshBVH
25
+ } from "three-mesh-bvh";
26
+ var raycastPatched = false;
27
+ function enableAcceleratedRaycast() {
28
+ if (raycastPatched) return;
29
+ Mesh.prototype.raycast = acceleratedRaycast;
30
+ raycastPatched = true;
31
+ }
32
+ function collectGeometries(root) {
33
+ const seen = /* @__PURE__ */ new Set();
34
+ root.traverse((node) => {
35
+ const geometry = node.geometry;
36
+ if (!geometry || !geometry.attributes?.position) return;
37
+ if (geometry.boundsTree) return;
38
+ seen.add(geometry);
39
+ });
40
+ return [...seen];
41
+ }
42
+ function countTriangles(geometries) {
43
+ let total = 0;
44
+ for (const geometry of geometries) {
45
+ const count = geometry.index ? geometry.index.count : geometry.attributes.position.count;
46
+ total += count / 3;
47
+ }
48
+ return total;
49
+ }
50
+ var MAX_BVH_TRIANGLES = 12e6;
51
+ function exceedsBvhBudget(root) {
52
+ return countSceneTriangles(root) > MAX_BVH_TRIANGLES;
53
+ }
54
+ function countSceneTriangles(root) {
55
+ const seen = /* @__PURE__ */ new Set();
56
+ root.traverse((node) => {
57
+ const geometry = node.geometry;
58
+ if (geometry?.attributes?.position) seen.add(geometry);
59
+ });
60
+ return countTriangles([...seen]);
61
+ }
62
+ function buildOneInWorker(worker, geometry) {
63
+ return new Promise((resolve, reject) => {
64
+ const handleMessage = (event) => {
65
+ const data = event.data;
66
+ if (!data || !data.error && !data.serialized) return;
67
+ worker.removeEventListener("message", handleMessage);
68
+ worker.removeEventListener("error", handleError);
69
+ if (data.error) {
70
+ reject(new Error(String(data.error)));
71
+ return;
72
+ }
73
+ const { serialized } = data;
74
+ const bvh = MeshBVH.deserialize(serialized, geometry, { setIndex: false });
75
+ if (serialized.index) {
76
+ if (geometry.index) {
77
+ geometry.index.array = serialized.index;
78
+ } else {
79
+ geometry.setIndex(new BufferAttribute(serialized.index, 1, false));
80
+ }
81
+ }
82
+ geometry.boundingBox = bvh.getBoundingBox(new Box3());
83
+ resolve(bvh);
84
+ };
85
+ const handleError = (event) => {
86
+ worker.removeEventListener("message", handleMessage);
87
+ worker.removeEventListener("error", handleError);
88
+ reject(new Error(event.message || "BVH worker error"));
89
+ };
90
+ worker.addEventListener("message", handleMessage);
91
+ worker.addEventListener("error", handleError);
92
+ const index = geometry.index ? geometry.index.array.slice() : null;
93
+ const position = geometry.attributes.position.array.slice();
94
+ const transfer = [position, ...index ? [index] : []].map(
95
+ (arr) => arr.buffer
96
+ );
97
+ worker.postMessage(
98
+ {
99
+ index,
100
+ position,
101
+ options: { groups: [...geometry.groups] }
102
+ },
103
+ transfer
104
+ );
105
+ });
106
+ }
107
+ async function createBvhWorker() {
108
+ if (typeof window === "undefined" || typeof Worker === "undefined") {
109
+ return null;
110
+ }
111
+ try {
112
+ const { bvhWorkerCode } = await import("./bvhWorkerCode-JRKQF4QH.mjs");
113
+ const blob = new Blob([bvhWorkerCode], { type: "text/javascript" });
114
+ const url = URL.createObjectURL(blob);
115
+ const worker = new Worker(url);
116
+ return {
117
+ worker,
118
+ dispose: () => {
119
+ worker.terminate();
120
+ URL.revokeObjectURL(url);
121
+ }
122
+ };
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+ async function buildSceneBVH(root, signal) {
128
+ const geometries = collectGeometries(root);
129
+ if (geometries.length === 0) return;
130
+ if (countTriangles(geometries) > MAX_BVH_TRIANGLES) return;
131
+ enableAcceleratedRaycast();
132
+ const workerHandle = await createBvhWorker();
133
+ if (signal?.aborted) {
134
+ workerHandle?.dispose();
135
+ return;
136
+ }
137
+ try {
138
+ for (const geometry of geometries) {
139
+ if (signal?.aborted) return;
140
+ let tree;
141
+ if (workerHandle) {
142
+ try {
143
+ tree = await buildOneInWorker(workerHandle.worker, geometry);
144
+ } catch {
145
+ }
146
+ }
147
+ if (tree === void 0) {
148
+ tree = computeBoundsTree.call(geometry);
149
+ await new Promise((resolve) => setTimeout(resolve, 0));
150
+ }
151
+ if (signal?.aborted) return;
152
+ geometry.boundsTree = tree;
153
+ }
154
+ } finally {
155
+ workerHandle?.dispose();
156
+ }
157
+ }
158
+ function disposeSceneBVH(root) {
159
+ root.traverse((node) => {
160
+ const geometry = node.geometry;
161
+ if (!geometry?.boundsTree) return;
162
+ disposeBoundsTree.call(geometry);
163
+ geometry.boundsTree = void 0;
164
+ });
165
+ }
166
+
167
+ // src/components/ModelViewer/ModelViewer.tsx
13
168
  import { useFrame as useFrame8, useThree as useThree10 } from "@react-three/fiber";
14
169
  import { createXRStore } from "@react-three/xr";
15
170
  import { QRCodeSVG } from "qrcode.react";
16
171
  import {
17
- Box3 as Box33,
172
+ Box3 as Box34,
18
173
  PerspectiveCamera as PerspectiveCamera3,
19
174
  Raycaster,
20
175
  Sphere as Sphere2,
@@ -45,7 +200,7 @@ function styleInject(css, { insertAt } = {}) {
45
200
  }
46
201
 
47
202
  // src/styles/ModelViewer.css
48
- styleInject('.ri-viewer--license-checking,\n.ri-viewer--license-invalid {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background-color: #f9f9f9;\n}\n.ri-viewer {\n --ri-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --ri-viewer-bg: #1c1c1d;\n --ri-viewer-text: #111827;\n --ri-panel-bg: #ffffff;\n --ri-panel-border: #e5e7eb;\n --ri-panel-title: #111827;\n --ri-panel-muted-text: #4b5563;\n --ri-panel-subtle-text: #9ca3af;\n --ri-panel-surface: #f9fafb;\n --ri-panel-surface-hover: #f3f4f6;\n --ri-panel-surface-border: #e5e7eb;\n --ri-panel-button-text: #1f2937;\n --ri-panel-accent: #2563eb;\n --ri-panel-accent-contrast: #ffffff;\n --ri-license-overlay-bg: rgba(0, 0, 0, 0.04);\n --ri-license-overlay-border: rgba(0, 0, 0, 0.08);\n --ri-license-overlay-text: #888;\n --ri-license-overlay-error-bg: rgba(192, 57, 43, 0.06);\n --ri-license-overlay-error-border: rgba(192, 57, 43, 0.2);\n --ri-license-overlay-error-text: #c0392b;\n --ri-sidepanel-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --ri-sidepanel-bg: var(--ri-databinding-bg, #111318);\n --ri-sidepanel-open-btn-bg: rgba(20, 20, 20, 0.82);\n --ri-sidepanel-open-btn-bg-hover: #27282b7e;\n --ri-sidepanel-border: rgba(255, 255, 255, 0.06);\n --ri-sidepanel-divider: rgba(255, 255, 255, 0.04);\n --ri-sidepanel-border-strong: rgba(255, 255, 255, 0.08);\n --ri-sidepanel-border-active: rgba(255, 255, 255, 0.18);\n --ri-sidepanel-shadow-left: 4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-sidepanel-shadow-right: -4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-sidepanel-text: var(--ri-databinding-text, #f1f5f9);\n --ri-sidepanel-text-emphasis: var(--ri-databinding-text-hover, #cbd5e1);\n --ri-sidepanel-text-muted: var(--ri-databinding-text-muted, #94a3b8);\n --ri-sidepanel-text-subtle: var(--ri-databinding-text-subtle, #64748b);\n --ri-sidepanel-placeholder: var(--ri-databinding-text-faint, #475569);\n --ri-sidepanel-surface: rgba(255, 255, 255, 0.05);\n --ri-sidepanel-surface-hover: rgba(255, 255, 255, 0.1);\n --ri-sidepanel-accent: #3b82f6;\n --ri-sidepanel-accent-soft: rgba(59, 130, 246, 0.12);\n --ri-databinding-bg: #111318;\n --ri-databinding-border: rgba(255, 255, 255, 0.06);\n --ri-databinding-border-strong: rgba(255, 255, 255, 0.08);\n --ri-databinding-border-hover: rgba(255, 255, 255, 0.15);\n --ri-databinding-shadow: 4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-databinding-font-family:\n "Inter",\n "Segoe UI",\n sans-serif;\n --ri-databinding-mono-font-family:\n "JetBrains Mono",\n "Fira Code",\n "Menlo",\n monospace;\n --ri-databinding-text: #f1f5f9;\n --ri-databinding-text-muted: #94a3b8;\n --ri-databinding-text-subtle: #64748b;\n --ri-databinding-text-faint: #475569;\n --ri-databinding-text-hover: #cbd5e1;\n --ri-databinding-surface: rgba(255, 255, 255, 0.03);\n --ri-databinding-surface-hover: rgba(255, 255, 255, 0.06);\n --ri-databinding-accent: #93c5fd;\n --ri-databinding-accent-glow: rgba(147, 197, 253, 0.8);\n --ri-databinding-accent-border: rgba(96, 165, 250, 0.55);\n --ri-databinding-accent-gradient:\n linear-gradient(\n \n 135deg,\n #1e3a8a 0%,\n #1d4ed8 60%,\n #2563eb 100% );\n --ri-databinding-accent-gradient-active:\n linear-gradient(\n \n 135deg,\n #1e3a8a 0%,\n #1e40af 100% );\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n background: var(--ri-viewer-bg);\n position: relative;\n color: var(--ri-viewer-text);\n font-family: var(--ri-font-family);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-synthesis: none;\n}\n.ri-viewer * {\n box-sizing: border-box;\n}\n.ri-viewerLeft {\n display: flex;\n flex: 1;\n min-height: 0;\n min-width: 0;\n position: relative;\n}\n.ri-viewerRight {\n display: flex;\n flex-shrink: 0;\n min-width: 0;\n}\n.ri-panel {\n width: 280px;\n min-height: 0;\n background: var(--ri-panel-bg);\n border-right: 1px solid var(--ri-panel-border);\n display: flex;\n flex-direction: column;\n padding: 20px;\n overflow-y: auto;\n flex-shrink: 0;\n transition: margin-left 0.3s ease, transform 0.3s ease;\n}\n.ri-panel--open {\n transform: translateX(0);\n margin-left: 0;\n}\n.ri-panel--closed {\n transform: translateX(-100%);\n margin-left: -280px;\n}\n.ri-panelHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 12px;\n}\n.ri-panelTitle {\n margin: 0;\n font-size: 15px;\n font-weight: 700;\n color: var(--ri-panel-title);\n line-height: 1.4;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ri-closeButton {\n border: 0;\n background: transparent;\n color: var(--ri-panel-subtle-text);\n font-size: 22px;\n line-height: 1;\n padding: 0;\n cursor: pointer;\n flex-shrink: 0;\n transition: color 0.2s ease;\n}\n.ri-closeButton:hover {\n color: var(--ri-panel-muted-text);\n}\n.ri-json {\n margin: 0 0 16px;\n padding: 12px;\n background: var(--ri-panel-surface);\n border: 1px solid var(--ri-panel-surface-border);\n border-radius: 12px;\n font-size: 12px;\n line-height: 1.55;\n color: var(--ri-panel-button-text);\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ri-divider {\n border-top: 1px solid var(--ri-panel-border);\n margin-bottom: 12px;\n}\n.ri-sectionLabel {\n margin-bottom: 8px;\n font-size: 11px;\n font-weight: 700;\n color: var(--ri-panel-subtle-text);\n text-transform: uppercase;\n letter-spacing: 0.16em;\n}\n.ri-actionList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.ri-actionButton {\n width: 100%;\n border-radius: 8px;\n border: 1px solid var(--ri-panel-surface-border);\n background: var(--ri-panel-surface);\n color: var(--ri-panel-button-text);\n padding: 10px 12px;\n text-align: left;\n font-size: 14px;\n line-height: 1.4;\n cursor: pointer;\n transition:\n background-color 0.2s ease,\n border-color 0.2s ease,\n color 0.2s ease;\n}\n.ri-actionButton:hover {\n background: var(--ri-panel-surface-hover);\n border-color: var(--ri-panel-border);\n}\n.ri-actionButton--active {\n background: var(--ri-panel-accent);\n border-color: var(--ri-panel-accent);\n color: var(--ri-panel-accent-contrast);\n}\n.ri-actionButton--active:hover {\n background: var(--ri-panel-accent);\n border-color: var(--ri-panel-accent);\n}\n.ri-canvasArea {\n flex: 1 1 auto;\n position: relative;\n min-width: 0;\n}\n.ri-canvasArea--measuring {\n cursor: crosshair;\n}\n.ri-canvasArea canvas {\n opacity: 1;\n transition: opacity 0.18s ease;\n}\n.ri-canvasArea--positioning canvas {\n opacity: 0;\n transition: none;\n}\n.ri-bindingsMismatchBanner {\n position: absolute;\n top: 12px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 12px 7px 10px;\n border-radius: 8px;\n background: rgba(30, 20, 0, 0.88);\n border: 1px solid rgba(251, 191, 36, 0.35);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n color: rgb(251, 191, 36);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n white-space: nowrap;\n pointer-events: auto;\n max-width: calc(100% - 32px);\n white-space: normal;\n text-align: center;\n}\n.ri-bindingsMismatchBanner__icon {\n flex-shrink: 0;\n}\n.ri-bindingsMismatchBanner__dismiss {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 4px;\n padding: 2px;\n background: none;\n border: none;\n border-radius: 4px;\n color: rgba(251, 191, 36, 0.7);\n cursor: pointer;\n transition: color 0.15s ease, background 0.15s ease;\n}\n.ri-bindingsMismatchBanner__dismiss:hover {\n color: rgb(251, 191, 36);\n background: rgba(251, 191, 36, 0.12);\n}\n.ri-measureTools {\n position: absolute;\n top: 16px;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-measureButton {\n width: 34px;\n height: 34px;\n padding: 0;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: rgba(255, 255, 255, 0.75);\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease;\n}\n.ri-measureButton:hover {\n background: rgba(40, 40, 40, 0.92);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.25);\n}\n.ri-measureButton:active {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-measureButton--active {\n background: #22d3ee;\n border-color: #22d3ee;\n color: #06343c;\n}\n.ri-measureButton--active:hover {\n background: #38dbf0;\n color: #06343c;\n}\n.ri-measureHint {\n padding: 6px 10px;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n white-space: nowrap;\n}\n.ri-explodeTools {\n position: absolute;\n top: 16px;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-explodeSlider {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 34px;\n padding: 0 12px;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n}\n.ri-explodeSlider input[type=range] {\n width: 120px;\n accent-color: #22d3ee;\n cursor: pointer;\n}\n.ri-cinematicTools {\n position: absolute;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-measureDot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #22d3ee;\n border: 2px solid #fff;\n box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);\n pointer-events: none;\n}\n.ri-measureLabel {\n padding: 3px 7px;\n border-radius: 6px;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n color: #06343c;\n background: #22d3ee;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n user-select: none;\n}\n.ri-xrTools {\n position: absolute;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-xrButton {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 34px;\n padding: 0 10px;\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 8px;\n cursor: pointer;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n transition: background 0.15s ease, border-color 0.15s ease;\n}\n.ri-xrButton:hover {\n background: rgba(40, 40, 40, 0.92);\n border-color: rgba(255, 255, 255, 0.3);\n}\n.ri-mobileHandoffBackdrop {\n position: absolute;\n z-index: 30;\n inset: 0;\n display: grid;\n place-items: center;\n padding: 20px;\n background: rgba(2, 6, 23, 0.68);\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n}\n.ri-mobileHandoffDialog {\n position: relative;\n display: flex;\n width: min(100%, 300px);\n flex-direction: column;\n align-items: center;\n padding: 16px;\n text-align: center;\n color: #e2e8f0;\n background: #0f172a;\n border: 1px solid rgba(148, 163, 184, 0.3);\n border-radius: 16px;\n box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);\n}\n.ri-mobileHandoffDialog > svg {\n color: #67e8f9;\n}\n.ri-mobileHandoffDialog h2 {\n margin: 12px 0 0;\n font-size: 18px;\n line-height: 1.25;\n color: #fff;\n}\n.ri-mobileHandoffDialog p {\n margin: 8px 0 16px;\n font-size: 13px;\n line-height: 1.5;\n color: #cbd5e1;\n}\n.ri-mobileHandoffQr {\n display: grid;\n place-items: center;\n padding: 10px;\n background: #fff;\n border-radius: 10px;\n}\n.ri-mobileHandoffDialog a {\n margin-top: 16px;\n font-size: 13px;\n font-weight: 600;\n color: #a5f3fc;\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n.ri-mobileHandoffClose {\n position: absolute;\n top: 10px;\n right: 10px;\n display: grid;\n width: 30px;\n height: 30px;\n place-items: center;\n padding: 0;\n color: #cbd5e1;\n background: transparent;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.ri-mobileHandoffClose:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n}\n.ri-arHintOverlay {\n position: absolute;\n top: 18px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n width: min(calc(100% - 32px), 360px);\n pointer-events: none;\n}\n.ri-arHint {\n margin: 0 auto;\n width: fit-content;\n max-width: 100%;\n padding: 6px 12px;\n font-size: 11px;\n font-weight: 500;\n line-height: 1.2;\n text-align: center;\n color: rgba(255, 255, 255, 0.92);\n background: rgba(20, 20, 20, 0.58);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 999px;\n user-select: none;\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n.ri-arOverlay {\n position: absolute;\n bottom: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n width: min(calc(100% - 32px), 420px);\n pointer-events: none;\n}\n.ri-arControls {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n}\n.ri-arStatus,\n.ri-arActionButton {\n max-width: 100%;\n padding: 8px 16px;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n text-align: center;\n white-space: nowrap;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 19px;\n user-select: none;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n}\n.ri-arStatus {\n pointer-events: none;\n}\n.ri-arActionButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n cursor: pointer;\n pointer-events: auto;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n transition:\n background 0.15s ease,\n border-color 0.15s ease,\n color 0.15s ease,\n transform 0.15s ease;\n}\n.ri-arActionButton svg {\n flex-shrink: 0;\n}\n.ri-arActionButton:hover {\n background: rgba(40, 40, 40, 0.92);\n border-color: rgba(255, 255, 255, 0.3);\n}\n.ri-arActionButton:active {\n transform: scale(0.98);\n}\n.ri-arActionButton--primary {\n color: #06343c;\n background: #22d3ee;\n border-color: transparent;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n}\n.ri-arActionButton--primary:hover {\n background: #67e8f9;\n border-color: transparent;\n}\n.ri-arActionButton.ri-arActionButton--active,\n.ri-arActionButton[aria-pressed=true],\n.ri-arActionButton.ri-arActionButton--active:hover,\n.ri-arActionButton[aria-pressed=true]:hover,\n.ri-arActionButton.ri-arActionButton--active:active,\n.ri-arActionButton[aria-pressed=true]:active {\n color: #06343c;\n background: #22d3ee;\n border-color: transparent;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n}\n.ri-hiddenInput {\n display: none;\n}\n.ri-popover {\n position: absolute;\n z-index: 50;\n background: #ffffff;\n border: 1px solid #e5e7eb;\n border-radius: 16px;\n box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);\n color: #111827;\n}\n.ri-popover--inline {\n position: static;\n z-index: auto;\n width: 100%;\n border-radius: 10px;\n box-shadow: none;\n}\n.ri-popover--inline .react-colorful {\n width: 100%;\n}\n.ri-popover--inline {\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text, #f1f5f9);\n}\n.ri-popover--inline .ri-popoverHeader {\n border-bottom-color: var( --ri-databinding-border-strong, rgba(255, 255, 255, 0.08) );\n}\n.ri-popover--inline .ri-popoverLabel {\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-colorCode {\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-secondaryButton {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text, #f1f5f9);\n}\n.ri-popover--inline .ri-secondaryButton:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n.ri-popover--inline .ri-previewFrame {\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n}\n.ri-popover--inline .ri-emptyState {\n border-color: var(--ri-databinding-border-hover, rgba(255, 255, 255, 0.15));\n color: var(--ri-databinding-text-subtle, #64748b);\n}\n.ri-popover--inline .ri-uploadButton {\n background: var(--ri-databinding-accent-gradient, #2563eb);\n color: #ffffff;\n}\n.ri-popover--inline .ri-uploadButton:hover {\n background: var(--ri-databinding-accent-gradient-active, #1d4ed8);\n}\n.ri-popover--inline .ri-removeButton {\n background: transparent;\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-removeButton:hover {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n}\n.ri-popover--inline .ri-formatHint {\n color: var(--ri-databinding-text-subtle, #64748b);\n}\n.ri-popoverHeader {\n padding: 12px 16px;\n}\n.ri-popoverLabel {\n margin: 0 0 8px;\n font-size: 11px;\n font-weight: 700;\n color: #6b7280;\n text-transform: uppercase;\n letter-spacing: 0.16em;\n}\n.ri-inlineRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n margin-top: 8px;\n}\n.ri-colorCode {\n font-size: 12px;\n color: #9ca3af;\n font-family:\n ui-monospace,\n SFMono-Regular,\n Menlo,\n Monaco,\n Consolas,\n monospace;\n text-transform: uppercase;\n}\n.ri-secondaryButton {\n border: 1px solid #e5e7eb;\n background: #f3f4f6;\n color: #374151;\n border-radius: 8px;\n padding: 6px 10px;\n font-size: 12px;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-secondaryButton:hover {\n background: #e5e7eb;\n}\n.ri-materialBody {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding-left: 12px;\n padding-right: 12px;\n padding-bottom: 12px;\n}\n.ri-previewFrame {\n position: relative;\n height: 112px;\n border-radius: 12px;\n overflow: hidden;\n border: 1px solid #e5e7eb;\n}\n.ri-previewImage {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n.ri-previewCaption {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n padding: 4px 8px;\n color: #ffffff;\n font-size: 10px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ri-emptyState {\n height: 112px;\n border-radius: 12px;\n border: 2px dashed #e5e7eb;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n color: #d1d5db;\n text-align: center;\n}\n.ri-uploadButton {\n width: 100%;\n border: 0;\n border-radius: 10px;\n background: #111827;\n color: #ffffff;\n padding: 10px 12px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-uploadButton:hover {\n background: #374151;\n}\n.ri-removeButton {\n width: 100%;\n border-radius: 10px;\n border: 1px solid #e5e7eb;\n background: #ffffff;\n color: #6b7280;\n padding: 8px 12px;\n font-size: 12px;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-removeButton:hover {\n background: #f9fafb;\n}\n.ri-formatHint {\n margin: 0;\n text-align: center;\n font-size: 10px;\n color: #d1d5db;\n}\n.ri-buttonContainer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n position: absolute;\n bottom: 12px;\n left: 12px;\n right: 12px;\n z-index: 10;\n}\n.ri-tourControls {\n display: flex;\n gap: 6px;\n align-items: center;\n flex-shrink: 0;\n}\n.ri-rightControls {\n display: flex;\n gap: 6px;\n align-items: center;\n flex-shrink: 0;\n margin-left: auto;\n}\n.ri-rightControls:only-child {\n margin-left: auto;\n}\n.ri-navButtonSingle,\n.ri-tourButton,\n.ri-resetButton,\n.ri-downloadButton {\n height: 34px;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: rgba(255, 255, 255, 0.75);\n border-radius: 8px;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n cursor: pointer;\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease,\n transform 0.15s ease;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n.ri-navButtonSingle:hover:not(:disabled),\n.ri-tourButton:hover:not(:disabled),\n.ri-resetButton:hover:not(:disabled) {\n background: rgba(40, 40, 40, 0.92);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.25);\n}\n.ri-navButtonSingle:active:not(:disabled),\n.ri-tourButton:active:not(:disabled),\n.ri-resetButton:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-navButtonSingle:disabled,\n.ri-tourButton:disabled,\n.ri-resetButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ri-navButtonSingle {\n padding: 0 16px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 8px;\n white-space: nowrap;\n}\n.ri-tourButton {\n padding: 0 12px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 4px;\n white-space: nowrap;\n}\n.ri-tourButton svg {\n width: 15px;\n height: 15px;\n flex-shrink: 0;\n}\n.ri-tourButton--stop {\n border-color: rgba(255, 68, 68, 0.4);\n color: #ff6b6b;\n}\n.ri-tourButton--stop:hover:not(:disabled) {\n background: rgba(255, 68, 68, 0.15);\n border-color: rgba(255, 68, 68, 0.6);\n color: #ff6b6b;\n}\n.ri-tourButton--stop:active:not(:disabled) {\n background: #ff4444;\n color: #fff;\n border-color: #ff4444;\n}\n.ri-tourButton--next {\n border-color: rgba(56, 219, 240, 0.3);\n color: #38dbf0;\n}\n.ri-tourButton--next:hover:not(:disabled) {\n background: rgba(56, 219, 240, 0.12);\n border-color: rgba(56, 219, 240, 0.5);\n color: #38dbf0;\n}\n.ri-tourButton--next:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-resetButton {\n width: 34px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.ri-resetButton svg {\n width: 15px;\n height: 15px;\n pointer-events: none;\n flex-shrink: 0;\n}\n.ri-resetButton:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.5);\n outline-offset: 2px;\n}\n.ri-downloadWrapper {\n position: relative;\n display: inline-flex;\n flex-direction: column;\n align-items: stretch;\n}\n.ri-downloadButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 0 16px;\n font-size: 13px;\n font-weight: 500;\n white-space: nowrap;\n}\n.ri-downloadButton:hover:not(:disabled) {\n background: rgba(0, 0, 0, 0.72);\n border-color: rgba(255, 255, 255, 0.3);\n color: #fff;\n}\n.ri-downloadButton:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-downloadButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ri-downloadButton__spinner {\n display: inline-block;\n width: 13px;\n height: 13px;\n border: 2px solid rgba(255, 255, 255, 0.3);\n border-top-color: #fff;\n border-radius: 50%;\n animation: ri-spin 0.7s linear infinite;\n flex-shrink: 0;\n}\n.ri-downloadMenu {\n position: absolute;\n bottom: calc(100% + 6px);\n right: 0;\n min-width: 168px;\n background: rgba(20, 20, 20, 0.92);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 10px;\n overflow: hidden;\n z-index: 100;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n animation: ri-menuReveal 0.12s ease;\n}\n@keyframes ri-menuReveal {\n from {\n opacity: 0;\n transform: translateY(4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ri-downloadMenuItem {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 10px 14px;\n font-size: 13px;\n font-weight: 400;\n color: #fff;\n background: transparent;\n border: none;\n cursor: pointer;\n text-align: left;\n transition: background 0.1s ease;\n}\n.ri-downloadMenuItem:hover:not(:disabled) {\n background: rgba(255, 255, 255, 0.1);\n}\n.ri-downloadMenuItem:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.ri-downloadMenuDivider {\n height: 1px;\n background: rgba(255, 255, 255, 0.1);\n margin: 0 10px;\n}\n@keyframes ri-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.ri-annotationMarker {\n position: relative;\n width: 36px;\n height: 36px;\n border-radius: 50%;\n background: rgba(20, 20, 20, 0.82);\n border: 2px solid rgba(255, 255, 255, 0.9);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n font-weight: 700;\n cursor: pointer;\n z-index: 20;\n user-select: none;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);\n pointer-events: auto;\n transition: opacity 0.3s ease, transform 0.3s ease;\n}\n.ri-annotationMarker:hover {\n transform: scale(1.08);\n box-shadow: 0 12px 28px rgba(15, 23, 42, 0.18);\n background: rgba(40, 40, 40, 0.92);\n}\n.ri-annotationPopup {\n position: absolute;\n width: min(270px, calc(100vw - 32px));\n background: rgba(20, 20, 20, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 16px;\n padding: 16px;\n z-index: 30;\n display: flex;\n flex-direction: column;\n gap: 12px;\n box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n}\n.ri-annotationInput,\n.ri-annotationTextarea {\n width: 100%;\n border: 1px solid rgba(255, 255, 255, 0.12);\n background: rgba(255, 255, 255, 0.05);\n color: #e5e7eb;\n border-radius: 10px;\n padding: 10px 12px;\n outline: none;\n font-family: inherit;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n}\n.ri-annotationInput {\n font-size: 14px;\n font-weight: 500;\n}\n.ri-annotationTextarea {\n font-size: 13px;\n resize: vertical;\n min-height: 110px;\n}\n.ri-annotationInput:focus,\n.ri-annotationTextarea:focus {\n border-color: #38dbf0;\n box-shadow: 0 0 0 3px rgba(56, 219, 240, 0.12);\n}\n.ri-annotationActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.ri-annotationOkBtn {\n border: 0;\n border-radius: 8px;\n background: rgba(56, 219, 240, 0.8);\n color: #06343c;\n padding: 8px 18px;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.2s ease, opacity 0.2s ease;\n}\n.ri-annotationOkBtn:hover:not(:disabled) {\n background: #38dbf0;\n}\n.ri-annotationOkBtn:disabled,\n.ri-annotationOkBtn.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.ri-annotationCancelBtn {\n border: 1px solid rgba(255, 255, 255, 0.12);\n background: transparent;\n color: #9ca3af;\n border-radius: 8px;\n padding: 8px 12px;\n font-size: 13px;\n cursor: pointer;\n transition: background-color 0.2s ease, border-color 0.2s ease;\n}\n.ri-annotationCancelBtn:hover {\n background: rgba(255, 255, 255, 0.05);\n border-color: rgba(255, 255, 255, 0.2);\n}\n.ri-annotationIcons {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 12px;\n color: #9ca3af;\n}\n.ri-annotationIcons svg {\n flex-shrink: 0;\n}\n.ri-viewAnnotationPopup {\n position: relative;\n background: rgba(20, 20, 20, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n padding: 12px 16px;\n min-width: 200px;\n max-width: min(280px, calc(100vw - 48px));\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n animation: ri-viewAnnotationFadeIn 0.15s ease-out;\n margin-left: 24px;\n margin-top: -20px;\n isolation: isolate;\n z-index: 1000;\n}\n.ri-viewAnnotationHeader {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n.ri-viewAnnotationTitle {\n margin: 0;\n color: #e2e8f0;\n font-size: 14px;\n font-weight: 600;\n}\n.ri-viewAnnotationClose {\n background: transparent;\n border: none;\n color: #b7bdc6;\n cursor: pointer;\n font-size: 18px;\n padding: 0 4px;\n transition: color 0.2s;\n line-height: 1;\n}\n.ri-viewAnnotationClose:hover {\n color: #e2e8f0;\n}\n.ri-viewAnnotationDescription {\n margin: 0;\n color: #cbd5e1;\n font-size: 13px;\n line-height: 1.4;\n word-wrap: break-word;\n}\n@keyframes ri-viewAnnotationFadeIn {\n from {\n opacity: 0;\n transform: translateY(-6px) scale(0.96);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n.ri-annotationNavSingle {\n position: absolute;\n bottom: 120px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n pointer-events: none;\n}\n.ri-annotationNavSingle > * {\n pointer-events: auto;\n}\n.ri-navBadge {\n background: rgba(255, 255, 255, 0.1);\n padding: 2px 10px;\n border-radius: 4px;\n font-size: 12px;\n font-weight: 400;\n color: rgba(255, 255, 255, 0.7);\n}\n.ri-annotationTitleHint {\n background: rgba(0, 0, 0, 0.7);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n color: white;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n border: 1px solid rgba(255, 255, 255, 0.08);\n text-align: center;\n max-width: 350px;\n animation: slideUp 0.3s ease;\n}\n.ri-annotationHintTitle {\n font-weight: 600;\n color: #e5e7eb;\n}\n.ri-annotationHintDesc {\n font-size: 12px;\n opacity: 0.6;\n margin-top: 2px;\n color: #d1d5db;\n}\n@keyframes slideUp {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n@media (max-width: 768px) {\n .ri-xrTools {\n gap: 6px;\n }\n .ri-xrButton {\n height: 34px;\n gap: 6px;\n padding: 0 8px;\n font-size: 11px;\n }\n .ri-xrButton svg {\n width: 14px;\n height: 14px;\n }\n .ri-xrButtonPrefix {\n display: none;\n }\n .ri-annotationNavSingle {\n bottom: 100px;\n }\n .ri-navButtonSingle {\n padding: 8px 20px;\n font-size: 13px;\n min-width: 110px;\n }\n .ri-annotationTitleHint {\n padding: 6px 14px;\n font-size: 12px;\n max-width: 250px;\n }\n .ri-viewAnnotationPopup {\n min-width: 160px;\n max-width: 220px;\n padding: 10px 12px;\n }\n .ri-viewAnnotationTitle {\n font-size: 13px;\n }\n .ri-viewAnnotationDescription {\n font-size: 12px;\n }\n .ri-buttonContainer {\n flex-wrap: wrap;\n gap: 6px;\n }\n .ri-tourControls {\n flex-wrap: wrap;\n }\n .ri-rightControls {\n flex-wrap: wrap;\n }\n}\n@media (max-width: 480px) {\n .ri-xrTools {\n gap: 5px;\n }\n .ri-xrButton {\n height: 34px;\n gap: 5px;\n font-size: 10px;\n }\n .ri-xrButton svg {\n width: 13px;\n height: 13px;\n }\n .ri-annotationNavSingle {\n bottom: 80px;\n }\n .ri-navButtonSingle {\n padding: 6px 16px;\n font-size: 12px;\n min-width: 90px;\n gap: 6px;\n }\n .ri-annotationTitleHint {\n padding: 4px 10px;\n font-size: 11px;\n max-width: 200px;\n }\n .ri-navBadge {\n font-size: 10px;\n padding: 1px 6px;\n }\n .ri-tourButton {\n padding: 0 8px;\n font-size: 12px;\n }\n .ri-tourButton span {\n display: none;\n }\n .ri-downloadButton {\n padding: 0 10px;\n font-size: 12px;\n }\n .ri-downloadButton span {\n display: none;\n }\n}\n.ri-canvasArea .bb-loadingOverlay {\n position: absolute;\n inset: 0;\n z-index: 12;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 24px;\n text-align: center;\n border-radius: 14px;\n background: rgba(15, 23, 42, 0.46);\n backdrop-filter: blur(1px);\n}\n.ri-canvasArea .bb-loadingSpinner {\n width: 40px;\n height: 40px;\n border-radius: 999px;\n border: 3px solid rgba(226, 232, 240, 0.24);\n border-top-color: #93c5fd;\n animation: bb-spin 0.8s linear infinite;\n}\n.ri-canvasArea .bb-loadingMessage {\n max-width: 320px;\n color: #f7f8f9;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.5;\n}\n@keyframes bb-spin {\n to {\n transform: rotate(360deg);\n }\n}\n');
203
+ styleInject('.ri-viewer--license-checking,\n.ri-viewer--license-invalid {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background-color: #f9f9f9;\n}\n.ri-viewer {\n --ri-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --ri-viewer-bg: #1c1c1d;\n --ri-viewer-text: #111827;\n --ri-panel-bg: #ffffff;\n --ri-panel-border: #e5e7eb;\n --ri-panel-title: #111827;\n --ri-panel-muted-text: #4b5563;\n --ri-panel-subtle-text: #9ca3af;\n --ri-panel-surface: #f9fafb;\n --ri-panel-surface-hover: #f3f4f6;\n --ri-panel-surface-border: #e5e7eb;\n --ri-panel-button-text: #1f2937;\n --ri-panel-accent: #2563eb;\n --ri-panel-accent-contrast: #ffffff;\n --ri-license-overlay-bg: rgba(0, 0, 0, 0.04);\n --ri-license-overlay-border: rgba(0, 0, 0, 0.08);\n --ri-license-overlay-text: #888;\n --ri-license-overlay-error-bg: rgba(192, 57, 43, 0.06);\n --ri-license-overlay-error-border: rgba(192, 57, 43, 0.2);\n --ri-license-overlay-error-text: #c0392b;\n --ri-sidepanel-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --ri-sidepanel-bg: var(--ri-databinding-bg, #111318);\n --ri-sidepanel-open-btn-bg: rgba(20, 20, 20, 0.82);\n --ri-sidepanel-open-btn-bg-hover: #27282b7e;\n --ri-sidepanel-border: rgba(255, 255, 255, 0.06);\n --ri-sidepanel-divider: rgba(255, 255, 255, 0.04);\n --ri-sidepanel-border-strong: rgba(255, 255, 255, 0.08);\n --ri-sidepanel-border-active: rgba(255, 255, 255, 0.18);\n --ri-sidepanel-shadow-left: 4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-sidepanel-shadow-right: -4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-sidepanel-text: var(--ri-databinding-text, #f1f5f9);\n --ri-sidepanel-text-emphasis: var(--ri-databinding-text-hover, #cbd5e1);\n --ri-sidepanel-text-muted: var(--ri-databinding-text-muted, #94a3b8);\n --ri-sidepanel-text-subtle: var(--ri-databinding-text-subtle, #64748b);\n --ri-sidepanel-placeholder: var(--ri-databinding-text-faint, #475569);\n --ri-sidepanel-surface: rgba(255, 255, 255, 0.05);\n --ri-sidepanel-surface-hover: rgba(255, 255, 255, 0.1);\n --ri-sidepanel-accent: #3b82f6;\n --ri-sidepanel-accent-soft: rgba(59, 130, 246, 0.12);\n --ri-databinding-bg: #111318;\n --ri-databinding-border: rgba(255, 255, 255, 0.06);\n --ri-databinding-border-strong: rgba(255, 255, 255, 0.08);\n --ri-databinding-border-hover: rgba(255, 255, 255, 0.15);\n --ri-databinding-shadow: 4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-databinding-font-family:\n "Inter",\n "Segoe UI",\n sans-serif;\n --ri-databinding-mono-font-family:\n "JetBrains Mono",\n "Fira Code",\n "Menlo",\n monospace;\n --ri-databinding-text: #f1f5f9;\n --ri-databinding-text-muted: #94a3b8;\n --ri-databinding-text-subtle: #64748b;\n --ri-databinding-text-faint: #475569;\n --ri-databinding-text-hover: #cbd5e1;\n --ri-databinding-surface: rgba(255, 255, 255, 0.03);\n --ri-databinding-surface-hover: rgba(255, 255, 255, 0.06);\n --ri-databinding-accent: #93c5fd;\n --ri-databinding-accent-glow: rgba(147, 197, 253, 0.8);\n --ri-databinding-accent-border: rgba(96, 165, 250, 0.55);\n --ri-databinding-accent-gradient:\n linear-gradient(\n \n 135deg,\n #1e3a8a 0%,\n #1d4ed8 60%,\n #2563eb 100% );\n --ri-databinding-accent-gradient-active:\n linear-gradient(\n \n 135deg,\n #1e3a8a 0%,\n #1e40af 100% );\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n background: var(--ri-viewer-bg);\n position: relative;\n color: var(--ri-viewer-text);\n font-family: var(--ri-font-family);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-synthesis: none;\n}\n.ri-viewer * {\n box-sizing: border-box;\n}\n.ri-viewerLeft {\n display: flex;\n flex: 1;\n min-height: 0;\n min-width: 0;\n position: relative;\n}\n.ri-viewerRight {\n display: flex;\n flex-shrink: 0;\n min-width: 0;\n}\n.ri-panel {\n width: 280px;\n min-height: 0;\n background: var(--ri-panel-bg);\n border-right: 1px solid var(--ri-panel-border);\n display: flex;\n flex-direction: column;\n padding: 20px;\n overflow-y: auto;\n flex-shrink: 0;\n transition: margin-left 0.3s ease, transform 0.3s ease;\n}\n.ri-panel--open {\n transform: translateX(0);\n margin-left: 0;\n}\n.ri-panel--closed {\n transform: translateX(-100%);\n margin-left: -280px;\n}\n.ri-panelHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 12px;\n}\n.ri-panelTitle {\n margin: 0;\n font-size: 15px;\n font-weight: 700;\n color: var(--ri-panel-title);\n line-height: 1.4;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ri-closeButton {\n border: 0;\n background: transparent;\n color: var(--ri-panel-subtle-text);\n font-size: 22px;\n line-height: 1;\n padding: 0;\n cursor: pointer;\n flex-shrink: 0;\n transition: color 0.2s ease;\n}\n.ri-closeButton:hover {\n color: var(--ri-panel-muted-text);\n}\n.ri-json {\n margin: 0 0 16px;\n padding: 12px;\n background: var(--ri-panel-surface);\n border: 1px solid var(--ri-panel-surface-border);\n border-radius: 12px;\n font-size: 12px;\n line-height: 1.55;\n color: var(--ri-panel-button-text);\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ri-divider {\n border-top: 1px solid var(--ri-panel-border);\n margin-bottom: 12px;\n}\n.ri-sectionLabel {\n margin-bottom: 8px;\n font-size: 11px;\n font-weight: 700;\n color: var(--ri-panel-subtle-text);\n text-transform: uppercase;\n letter-spacing: 0.16em;\n}\n.ri-actionList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.ri-actionButton {\n width: 100%;\n border-radius: 8px;\n border: 1px solid var(--ri-panel-surface-border);\n background: var(--ri-panel-surface);\n color: var(--ri-panel-button-text);\n padding: 10px 12px;\n text-align: left;\n font-size: 14px;\n line-height: 1.4;\n cursor: pointer;\n transition:\n background-color 0.2s ease,\n border-color 0.2s ease,\n color 0.2s ease;\n}\n.ri-actionButton:hover {\n background: var(--ri-panel-surface-hover);\n border-color: var(--ri-panel-border);\n}\n.ri-actionButton--active {\n background: var(--ri-panel-accent);\n border-color: var(--ri-panel-accent);\n color: var(--ri-panel-accent-contrast);\n}\n.ri-actionButton--active:hover {\n background: var(--ri-panel-accent);\n border-color: var(--ri-panel-accent);\n}\n.ri-canvasArea {\n flex: 1 1 auto;\n position: relative;\n min-width: 0;\n}\n.ri-canvasArea--measuring {\n cursor: crosshair;\n}\n.ri-canvasArea canvas {\n opacity: 1;\n transition: opacity 0.18s ease;\n}\n.ri-canvasArea--positioning canvas {\n opacity: 0;\n transition: none;\n}\n.ri-bindingsMismatchBanner {\n position: absolute;\n top: 12px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 12px 7px 10px;\n border-radius: 8px;\n background: rgba(30, 20, 0, 0.88);\n border: 1px solid rgba(251, 191, 36, 0.35);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n color: rgb(251, 191, 36);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n white-space: nowrap;\n pointer-events: auto;\n max-width: calc(100% - 32px);\n white-space: normal;\n text-align: center;\n}\n.ri-bindingsMismatchBanner__icon {\n flex-shrink: 0;\n}\n.ri-bindingsMismatchBanner__dismiss {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 4px;\n padding: 2px;\n background: none;\n border: none;\n border-radius: 4px;\n color: rgba(251, 191, 36, 0.7);\n cursor: pointer;\n transition: color 0.15s ease, background 0.15s ease;\n}\n.ri-bindingsMismatchBanner__dismiss:hover {\n color: rgb(251, 191, 36);\n background: rgba(251, 191, 36, 0.12);\n}\n.ri-measureTools {\n position: absolute;\n top: 16px;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-measureButton {\n width: 34px;\n height: 34px;\n padding: 0;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: rgba(255, 255, 255, 0.75);\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease;\n}\n.ri-measureButton:hover {\n background: rgba(40, 40, 40, 0.92);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.25);\n}\n.ri-measureButton:active {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-measureButton--active {\n background: #22d3ee;\n border-color: #22d3ee;\n color: #06343c;\n}\n.ri-measureButton--active:hover {\n background: #38dbf0;\n color: #06343c;\n}\n.ri-measureHint {\n padding: 6px 10px;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n white-space: nowrap;\n}\n.ri-explodeTools {\n position: absolute;\n top: 16px;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-explodeSlider {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 34px;\n padding: 0 12px;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n}\n.ri-explodeSlider input[type=range] {\n width: 120px;\n accent-color: #22d3ee;\n cursor: pointer;\n}\n.ri-cinematicTools {\n position: absolute;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-measureDot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #22d3ee;\n border: 2px solid #fff;\n box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);\n pointer-events: none;\n}\n.ri-measureLabel {\n padding: 3px 7px;\n border-radius: 6px;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n color: #06343c;\n background: #22d3ee;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n user-select: none;\n}\n.ri-xrTools {\n position: absolute;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-xrButton {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 34px;\n padding: 0 10px;\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 8px;\n cursor: pointer;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n transition: background 0.15s ease, border-color 0.15s ease;\n}\n.ri-xrButton:hover {\n background: rgba(40, 40, 40, 0.92);\n border-color: rgba(255, 255, 255, 0.3);\n}\n.ri-mobileHandoffBackdrop {\n position: absolute;\n z-index: 30;\n inset: 0;\n display: grid;\n place-items: center;\n padding: 20px;\n background: rgba(2, 6, 23, 0.68);\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n}\n.ri-mobileHandoffDialog {\n position: relative;\n display: flex;\n width: min(100%, 300px);\n flex-direction: column;\n align-items: center;\n padding: 16px;\n text-align: center;\n color: #e2e8f0;\n background: #0f172a;\n border: 1px solid rgba(148, 163, 184, 0.3);\n border-radius: 16px;\n box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);\n}\n.ri-mobileHandoffDialog > svg {\n color: #67e8f9;\n}\n.ri-mobileHandoffDialog h2 {\n margin: 12px 0 0;\n font-size: 18px;\n line-height: 1.25;\n color: #fff;\n}\n.ri-mobileHandoffDialog p {\n margin: 8px 0 16px;\n font-size: 13px;\n line-height: 1.5;\n color: #cbd5e1;\n}\n.ri-mobileHandoffQr {\n display: grid;\n place-items: center;\n padding: 10px;\n background: #fff;\n border-radius: 10px;\n}\n.ri-mobileHandoffDialog a {\n margin-top: 16px;\n font-size: 13px;\n font-weight: 600;\n color: #a5f3fc;\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n.ri-mobileHandoffClose {\n position: absolute;\n top: 10px;\n right: 10px;\n display: grid;\n width: 30px;\n height: 30px;\n place-items: center;\n padding: 0;\n color: #cbd5e1;\n background: transparent;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.ri-mobileHandoffClose:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n}\n.ri-arHintOverlay {\n position: absolute;\n top: 18px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n width: min(calc(100% - 32px), 360px);\n pointer-events: none;\n}\n.ri-arHint {\n margin: 0 auto;\n width: fit-content;\n max-width: 100%;\n padding: 6px 12px;\n font-size: 11px;\n font-weight: 500;\n line-height: 1.2;\n text-align: center;\n color: rgba(255, 255, 255, 0.92);\n background: rgba(20, 20, 20, 0.58);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 999px;\n user-select: none;\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n.ri-arOverlay {\n position: absolute;\n bottom: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n width: min(calc(100% - 32px), 420px);\n pointer-events: none;\n}\n.ri-arControls {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n}\n.ri-arStatus,\n.ri-arActionButton {\n max-width: 100%;\n padding: 8px 16px;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n text-align: center;\n white-space: nowrap;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 19px;\n user-select: none;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n}\n.ri-arStatus {\n pointer-events: none;\n}\n.ri-arActionButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n cursor: pointer;\n pointer-events: auto;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n transition:\n background 0.15s ease,\n border-color 0.15s ease,\n color 0.15s ease,\n transform 0.15s ease;\n}\n.ri-arActionButton svg {\n flex-shrink: 0;\n}\n.ri-arActionButton:hover {\n background: rgba(40, 40, 40, 0.92);\n border-color: rgba(255, 255, 255, 0.3);\n}\n.ri-arActionButton:active {\n transform: scale(0.98);\n}\n.ri-arActionButton--primary {\n color: #06343c;\n background: #22d3ee;\n border-color: transparent;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n}\n.ri-arActionButton--primary:hover {\n background: #67e8f9;\n border-color: transparent;\n}\n.ri-arActionButton.ri-arActionButton--active,\n.ri-arActionButton[aria-pressed=true],\n.ri-arActionButton.ri-arActionButton--active:hover,\n.ri-arActionButton[aria-pressed=true]:hover,\n.ri-arActionButton.ri-arActionButton--active:active,\n.ri-arActionButton[aria-pressed=true]:active {\n color: #06343c;\n background: #22d3ee;\n border-color: transparent;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n}\n.ri-hiddenInput {\n display: none;\n}\n.ri-popover {\n position: absolute;\n z-index: 50;\n background: #ffffff;\n border: 1px solid #e5e7eb;\n border-radius: 16px;\n box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);\n color: #111827;\n}\n.ri-popover--inline {\n position: static;\n z-index: auto;\n width: 100%;\n border-radius: 10px;\n box-shadow: none;\n}\n.ri-popover--inline .react-colorful {\n width: 100%;\n}\n.ri-popover--inline {\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text, #f1f5f9);\n}\n.ri-popover--inline .ri-popoverHeader {\n border-bottom-color: var( --ri-databinding-border-strong, rgba(255, 255, 255, 0.08) );\n}\n.ri-popover--inline .ri-popoverLabel {\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-colorCode {\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-secondaryButton {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text, #f1f5f9);\n}\n.ri-popover--inline .ri-secondaryButton:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n.ri-popover--inline .ri-previewFrame {\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n}\n.ri-popover--inline .ri-emptyState {\n border-color: var(--ri-databinding-border-hover, rgba(255, 255, 255, 0.15));\n color: var(--ri-databinding-text-subtle, #64748b);\n}\n.ri-popover--inline .ri-uploadButton {\n background: var(--ri-databinding-accent-gradient, #2563eb);\n color: #ffffff;\n}\n.ri-popover--inline .ri-uploadButton:hover {\n background: var(--ri-databinding-accent-gradient-active, #1d4ed8);\n}\n.ri-popover--inline .ri-removeButton {\n background: transparent;\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-removeButton:hover {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n}\n.ri-popover--inline .ri-formatHint {\n color: var(--ri-databinding-text-subtle, #64748b);\n}\n.ri-popoverHeader {\n padding: 12px 16px;\n}\n.ri-popoverLabel {\n margin: 0 0 8px;\n font-size: 11px;\n font-weight: 700;\n color: #6b7280;\n text-transform: uppercase;\n letter-spacing: 0.16em;\n}\n.ri-inlineRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n margin-top: 8px;\n}\n.ri-colorCode {\n font-size: 12px;\n color: #9ca3af;\n font-family:\n ui-monospace,\n SFMono-Regular,\n Menlo,\n Monaco,\n Consolas,\n monospace;\n text-transform: uppercase;\n}\n.ri-secondaryButton {\n border: 1px solid #e5e7eb;\n background: #f3f4f6;\n color: #374151;\n border-radius: 8px;\n padding: 6px 10px;\n font-size: 12px;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-secondaryButton:hover {\n background: #e5e7eb;\n}\n.ri-materialBody {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding-left: 12px;\n padding-right: 12px;\n padding-bottom: 12px;\n}\n.ri-previewFrame {\n position: relative;\n height: 112px;\n border-radius: 12px;\n overflow: hidden;\n border: 1px solid #e5e7eb;\n}\n.ri-previewImage {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n.ri-previewCaption {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n padding: 4px 8px;\n color: #ffffff;\n font-size: 10px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ri-emptyState {\n height: 112px;\n border-radius: 12px;\n border: 2px dashed #e5e7eb;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n color: #d1d5db;\n text-align: center;\n}\n.ri-uploadButton {\n width: 100%;\n border: 0;\n border-radius: 10px;\n background: #111827;\n color: #ffffff;\n padding: 10px 12px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-uploadButton:hover {\n background: #374151;\n}\n.ri-removeButton {\n width: 100%;\n border-radius: 10px;\n border: 1px solid #e5e7eb;\n background: #ffffff;\n color: #6b7280;\n padding: 8px 12px;\n font-size: 12px;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-removeButton:hover {\n background: #f9fafb;\n}\n.ri-formatHint {\n margin: 0;\n text-align: center;\n font-size: 10px;\n color: #d1d5db;\n}\n.ri-buttonContainer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n position: absolute;\n bottom: 12px;\n left: 12px;\n right: 12px;\n z-index: 10;\n}\n.ri-tourControls {\n display: flex;\n gap: 6px;\n align-items: center;\n flex-shrink: 0;\n}\n.ri-rightControls {\n display: flex;\n gap: 6px;\n align-items: center;\n flex-shrink: 0;\n margin-left: auto;\n}\n.ri-rightControls:only-child {\n margin-left: auto;\n}\n.ri-navButtonSingle,\n.ri-tourButton,\n.ri-resetButton,\n.ri-downloadButton {\n height: 34px;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: rgba(255, 255, 255, 0.75);\n border-radius: 8px;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n cursor: pointer;\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease,\n transform 0.15s ease;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n.ri-navButtonSingle:hover:not(:disabled),\n.ri-tourButton:hover:not(:disabled),\n.ri-resetButton:hover:not(:disabled) {\n background: rgba(40, 40, 40, 0.92);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.25);\n}\n.ri-navButtonSingle:active:not(:disabled),\n.ri-tourButton:active:not(:disabled),\n.ri-resetButton:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-navButtonSingle:disabled,\n.ri-tourButton:disabled,\n.ri-resetButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ri-navButtonSingle {\n padding: 0 16px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 8px;\n white-space: nowrap;\n}\n.ri-tourButton {\n padding: 0 12px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 4px;\n white-space: nowrap;\n}\n.ri-tourButton svg {\n width: 15px;\n height: 15px;\n flex-shrink: 0;\n}\n.ri-tourButton--stop {\n border-color: rgba(255, 68, 68, 0.4);\n color: #ff6b6b;\n}\n.ri-tourButton--stop:hover:not(:disabled) {\n background: rgba(255, 68, 68, 0.15);\n border-color: rgba(255, 68, 68, 0.6);\n color: #ff6b6b;\n}\n.ri-tourButton--stop:active:not(:disabled) {\n background: #ff4444;\n color: #fff;\n border-color: #ff4444;\n}\n.ri-tourButton--next {\n border-color: rgba(56, 219, 240, 0.3);\n color: #38dbf0;\n}\n.ri-tourButton--next:hover:not(:disabled) {\n background: rgba(56, 219, 240, 0.12);\n border-color: rgba(56, 219, 240, 0.5);\n color: #38dbf0;\n}\n.ri-tourButton--next:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-resetButton {\n width: 34px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.ri-resetButton svg {\n width: 15px;\n height: 15px;\n pointer-events: none;\n flex-shrink: 0;\n}\n.ri-resetButton:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.5);\n outline-offset: 2px;\n}\n.ri-downloadWrapper {\n position: relative;\n display: inline-flex;\n flex-direction: column;\n align-items: stretch;\n}\n.ri-downloadButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 0 16px;\n font-size: 13px;\n font-weight: 500;\n white-space: nowrap;\n}\n.ri-downloadButton:hover:not(:disabled) {\n background: rgba(0, 0, 0, 0.72);\n border-color: rgba(255, 255, 255, 0.3);\n color: #fff;\n}\n.ri-downloadButton:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-downloadButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ri-downloadButton__spinner {\n display: inline-block;\n width: 13px;\n height: 13px;\n border: 2px solid rgba(255, 255, 255, 0.3);\n border-top-color: #fff;\n border-radius: 50%;\n animation: ri-spin 0.7s linear infinite;\n flex-shrink: 0;\n}\n.ri-downloadMenu {\n position: absolute;\n bottom: calc(100% + 6px);\n right: 0;\n min-width: 168px;\n background: rgba(20, 20, 20, 0.92);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 10px;\n overflow: hidden;\n z-index: 100;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n animation: ri-menuReveal 0.12s ease;\n}\n@keyframes ri-menuReveal {\n from {\n opacity: 0;\n transform: translateY(4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ri-downloadMenuItem {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 10px 14px;\n font-size: 13px;\n font-weight: 400;\n color: #fff;\n background: transparent;\n border: none;\n cursor: pointer;\n text-align: left;\n transition: background 0.1s ease;\n}\n.ri-downloadMenuItem:hover:not(:disabled) {\n background: rgba(255, 255, 255, 0.1);\n}\n.ri-downloadMenuItem:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.ri-downloadMenuDivider {\n height: 1px;\n background: rgba(255, 255, 255, 0.1);\n margin: 0 10px;\n}\n@keyframes ri-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.ri-annotationMarker {\n position: relative;\n width: 36px;\n height: 36px;\n border-radius: 50%;\n background: rgba(20, 20, 20, 0.82);\n border: 2px solid rgba(255, 255, 255, 0.9);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n font-weight: 700;\n cursor: pointer;\n z-index: 20;\n user-select: none;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);\n pointer-events: auto;\n transition: opacity 0.3s ease, transform 0.3s ease;\n}\n.ri-annotationMarker:hover {\n transform: scale(1.08);\n box-shadow: 0 12px 28px rgba(15, 23, 42, 0.18);\n background: rgba(40, 40, 40, 0.92);\n}\n.ri-annotationPopup {\n position: absolute;\n width: min(270px, calc(100vw - 32px));\n background: rgba(20, 20, 20, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 16px;\n padding: 16px;\n z-index: 30;\n display: flex;\n flex-direction: column;\n gap: 12px;\n box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n}\n.ri-annotationInput,\n.ri-annotationTextarea {\n width: 100%;\n border: 1px solid rgba(255, 255, 255, 0.12);\n background: rgba(255, 255, 255, 0.05);\n color: #e5e7eb;\n border-radius: 10px;\n padding: 10px 12px;\n outline: none;\n font-family: inherit;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n}\n.ri-annotationInput {\n font-size: 14px;\n font-weight: 500;\n}\n.ri-annotationTextarea {\n font-size: 13px;\n resize: vertical;\n min-height: 110px;\n}\n.ri-annotationInput:focus,\n.ri-annotationTextarea:focus {\n border-color: #38dbf0;\n box-shadow: 0 0 0 3px rgba(56, 219, 240, 0.12);\n}\n.ri-annotationActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.ri-annotationOkBtn {\n border: 0;\n border-radius: 8px;\n background: rgba(56, 219, 240, 0.8);\n color: #06343c;\n padding: 8px 18px;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.2s ease, opacity 0.2s ease;\n}\n.ri-annotationOkBtn:hover:not(:disabled) {\n background: #38dbf0;\n}\n.ri-annotationOkBtn:disabled,\n.ri-annotationOkBtn.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.ri-annotationCancelBtn {\n border: 1px solid rgba(255, 255, 255, 0.12);\n background: transparent;\n color: #9ca3af;\n border-radius: 8px;\n padding: 8px 12px;\n font-size: 13px;\n cursor: pointer;\n transition: background-color 0.2s ease, border-color 0.2s ease;\n}\n.ri-annotationCancelBtn:hover {\n background: rgba(255, 255, 255, 0.05);\n border-color: rgba(255, 255, 255, 0.2);\n}\n.ri-annotationIcons {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 12px;\n color: #9ca3af;\n}\n.ri-annotationIcons svg {\n flex-shrink: 0;\n}\n.ri-viewAnnotationPopup {\n position: relative;\n background: rgba(20, 20, 20, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n padding: 12px 16px;\n min-width: 200px;\n max-width: min(280px, calc(100vw - 48px));\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n animation: ri-viewAnnotationFadeIn 0.15s ease-out;\n margin-left: 24px;\n margin-top: -20px;\n isolation: isolate;\n z-index: 1000;\n}\n.ri-viewAnnotationHeader {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n.ri-viewAnnotationTitle {\n margin: 0;\n color: #e2e8f0;\n font-size: 14px;\n font-weight: 600;\n}\n.ri-viewAnnotationClose {\n background: transparent;\n border: none;\n color: #b7bdc6;\n cursor: pointer;\n font-size: 18px;\n padding: 0 4px;\n transition: color 0.2s;\n line-height: 1;\n}\n.ri-viewAnnotationClose:hover {\n color: #e2e8f0;\n}\n.ri-viewAnnotationDescription {\n margin: 0;\n color: #cbd5e1;\n font-size: 13px;\n line-height: 1.4;\n word-wrap: break-word;\n}\n@keyframes ri-viewAnnotationFadeIn {\n from {\n opacity: 0;\n transform: translateY(-6px) scale(0.96);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n.ri-annotationNavSingle {\n position: absolute;\n bottom: 120px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n pointer-events: none;\n}\n.ri-annotationNavSingle > * {\n pointer-events: auto;\n}\n.ri-navBadge {\n background: rgba(255, 255, 255, 0.1);\n padding: 2px 10px;\n border-radius: 4px;\n font-size: 12px;\n font-weight: 400;\n color: rgba(255, 255, 255, 0.7);\n}\n.ri-annotationTitleHint {\n background: rgba(0, 0, 0, 0.7);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n color: white;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n border: 1px solid rgba(255, 255, 255, 0.08);\n text-align: center;\n max-width: 350px;\n animation: slideUp 0.3s ease;\n}\n.ri-annotationHintTitle {\n font-weight: 600;\n color: #e5e7eb;\n}\n.ri-annotationHintDesc {\n font-size: 12px;\n opacity: 0.6;\n margin-top: 2px;\n color: #d1d5db;\n}\n@keyframes slideUp {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n@media (max-width: 768px) {\n .ri-xrTools {\n gap: 6px;\n }\n .ri-xrButton {\n height: 34px;\n gap: 6px;\n padding: 0 8px;\n font-size: 11px;\n }\n .ri-xrButton svg {\n width: 14px;\n height: 14px;\n }\n .ri-xrButtonPrefix {\n display: none;\n }\n .ri-annotationNavSingle {\n bottom: 100px;\n }\n .ri-navButtonSingle {\n padding: 8px 20px;\n font-size: 13px;\n min-width: 110px;\n }\n .ri-annotationTitleHint {\n padding: 6px 14px;\n font-size: 12px;\n max-width: 250px;\n }\n .ri-viewAnnotationPopup {\n min-width: 160px;\n max-width: 220px;\n padding: 10px 12px;\n }\n .ri-viewAnnotationTitle {\n font-size: 13px;\n }\n .ri-viewAnnotationDescription {\n font-size: 12px;\n }\n .ri-buttonContainer {\n flex-wrap: wrap;\n gap: 6px;\n }\n .ri-tourControls {\n flex-wrap: wrap;\n }\n .ri-rightControls {\n flex-wrap: wrap;\n }\n}\n@media (max-width: 480px) {\n .ri-xrTools {\n gap: 5px;\n }\n .ri-xrButton {\n height: 34px;\n gap: 5px;\n font-size: 10px;\n }\n .ri-xrButton svg {\n width: 13px;\n height: 13px;\n }\n .ri-annotationNavSingle {\n bottom: 80px;\n }\n .ri-navButtonSingle {\n padding: 6px 16px;\n font-size: 12px;\n min-width: 90px;\n gap: 6px;\n }\n .ri-annotationTitleHint {\n padding: 4px 10px;\n font-size: 11px;\n max-width: 200px;\n }\n .ri-navBadge {\n font-size: 10px;\n padding: 1px 6px;\n }\n .ri-tourButton {\n padding: 0 8px;\n font-size: 12px;\n }\n .ri-tourButton span {\n display: none;\n }\n .ri-downloadButton {\n padding: 0 10px;\n font-size: 12px;\n }\n .ri-downloadButton span {\n display: none;\n }\n}\n.ri-canvasArea .bb-loadingOverlay {\n position: absolute;\n inset: 0;\n z-index: 12;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 24px;\n text-align: center;\n border-radius: 14px;\n background: rgba(15, 23, 42, 0.46);\n backdrop-filter: blur(1px);\n}\n.ri-canvasArea .bb-loadingSpinner {\n width: 40px;\n height: 40px;\n border-radius: 999px;\n border: 3px solid rgba(226, 232, 240, 0.24);\n border-top-color: #93c5fd;\n animation: bb-spin 0.8s linear infinite;\n}\n.ri-canvasArea .bb-loadingMessage {\n max-width: 320px;\n color: #f7f8f9;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.5;\n}\n@keyframes bb-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.ri-largeModelBanner {\n position: absolute;\n top: 12px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 12px 7px 10px;\n border-radius: 8px;\n background: rgba(30, 20, 0, 0.88);\n border: 1px solid rgba(251, 191, 36, 0.35);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n color: rgb(251, 191, 36);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n pointer-events: auto;\n max-width: calc(100% - 32px);\n white-space: normal;\n text-align: center;\n}\n.ri-largeModelBanner--offset {\n top: 64px;\n}\n.ri-largeModelBanner__icon {\n flex-shrink: 0;\n}\n.ri-largeModelBanner__dismiss {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 4px;\n padding: 2px;\n background: none;\n border: none;\n border-radius: 4px;\n color: rgba(251, 191, 36, 0.7);\n cursor: pointer;\n transition: color 0.15s ease, background 0.15s ease;\n}\n.ri-largeModelBanner__dismiss:hover {\n color: rgb(251, 191, 36);\n background: rgba(251, 191, 36, 0.12);\n}\n');
49
204
 
50
205
  // src/components/LicenseOverlay.tsx
51
206
  import { AlertTriangle } from "lucide-react";
@@ -862,7 +1017,7 @@ var Exposure = forwardRef(
862
1017
  import { useFrame as useFrame2, useThree as useThree2 } from "@react-three/fiber";
863
1018
  import * as PostProcessing from "@react-three/postprocessing";
864
1019
  import { BlendFunction as BlendFunction2 } from "postprocessing";
865
- import { Color as Color3, MathUtils, Mesh, Vector2 as Vector22, Vector3 } from "three";
1020
+ import { Color as Color3, MathUtils, Mesh as Mesh2, Vector2 as Vector22, Vector3 } from "three";
866
1021
  import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
867
1022
  import { jsx as jsx3 } from "react/jsx-runtime";
868
1023
  var LensFlareEffect2 = PostProcessing.LensFlareEffect;
@@ -958,7 +1113,7 @@ function SafeLensFlare({
958
1113
  if (object) {
959
1114
  if (object.userData?.lensflare === "no-occlusion") {
960
1115
  target = 0;
961
- } else if (object instanceof Mesh) {
1116
+ } else if (object instanceof Mesh2) {
962
1117
  if (object.material.uniforms?._transmission?.value > 0.2) {
963
1118
  target = 0.2;
964
1119
  } else if (object.material._transmission && object.material._transmission > 0.2) {
@@ -1897,7 +2052,7 @@ import {
1897
2052
  AnimationMixer,
1898
2053
  LoopOnce,
1899
2054
  LoopRepeat,
1900
- Box3,
2055
+ Box3 as Box32,
1901
2056
  Vector3 as Vector32,
1902
2057
  Matrix4,
1903
2058
  MathUtils as MathUtils2
@@ -2512,60 +2667,6 @@ function refreshSkinnedBounds(root) {
2512
2667
  });
2513
2668
  }
2514
2669
 
2515
- // src/utils/bvh.ts
2516
- import { Mesh as Mesh4 } from "three";
2517
- import {
2518
- acceleratedRaycast,
2519
- computeBoundsTree,
2520
- disposeBoundsTree
2521
- } from "three-mesh-bvh";
2522
- var raycastPatched = false;
2523
- function enableAcceleratedRaycast() {
2524
- if (raycastPatched) return;
2525
- Mesh4.prototype.raycast = acceleratedRaycast;
2526
- raycastPatched = true;
2527
- }
2528
- function collectGeometries(root) {
2529
- const seen = /* @__PURE__ */ new Set();
2530
- root.traverse((node) => {
2531
- const geometry = node.geometry;
2532
- if (!geometry || !geometry.attributes?.position) return;
2533
- if (geometry.boundsTree) return;
2534
- seen.add(geometry);
2535
- });
2536
- return [...seen];
2537
- }
2538
- function countTriangles(geometries) {
2539
- let total = 0;
2540
- for (const geometry of geometries) {
2541
- const count = geometry.index ? geometry.index.count : geometry.attributes.position.count;
2542
- total += count / 3;
2543
- }
2544
- return total;
2545
- }
2546
- var MAX_BVH_TRIANGLES = 12e6;
2547
- async function buildSceneBVH(root, signal) {
2548
- const geometries = collectGeometries(root);
2549
- if (geometries.length === 0) return;
2550
- if (countTriangles(geometries) > MAX_BVH_TRIANGLES) return;
2551
- enableAcceleratedRaycast();
2552
- for (const geometry of geometries) {
2553
- if (signal?.aborted) return;
2554
- const tree = computeBoundsTree.call(geometry);
2555
- await new Promise((resolve) => setTimeout(resolve, 0));
2556
- if (signal?.aborted) return;
2557
- geometry.boundsTree = tree;
2558
- }
2559
- }
2560
- function disposeSceneBVH(root) {
2561
- root.traverse((node) => {
2562
- const geometry = node.geometry;
2563
- if (!geometry?.boundsTree) return;
2564
- disposeBoundsTree.call(geometry);
2565
- geometry.boundsTree = void 0;
2566
- });
2567
- }
2568
-
2569
2670
  // src/components/Model.tsx
2570
2671
  import { jsx as jsx8 } from "react/jsx-runtime";
2571
2672
  function firstMaterial(node) {
@@ -2623,11 +2724,11 @@ function Model({
2623
2724
  }, [scene]);
2624
2725
  const shellNodeNames = useMemo7(() => {
2625
2726
  const shell = /* @__PURE__ */ new Set();
2626
- const sceneBox = new Box3();
2727
+ const sceneBox = new Box32();
2627
2728
  const meshNodes = [];
2628
2729
  for (const [name, node] of Object.entries(nodeMap)) {
2629
2730
  if (!isMeshNode(node)) continue;
2630
- const box = new Box3().setFromObject(node);
2731
+ const box = new Box32().setFromObject(node);
2631
2732
  if (box.isEmpty()) continue;
2632
2733
  meshNodes.push({ name, box });
2633
2734
  sceneBox.union(box);
@@ -2758,9 +2859,11 @@ function Model({
2758
2859
  if (rendererMode === "matcap") return perNodeMatcapMaterials[nodeName];
2759
2860
  return perNodeMaterials[nodeName];
2760
2861
  };
2862
+ const onModelLoadedRef = useRef5(onModelLoaded);
2863
+ onModelLoadedRef.current = onModelLoaded;
2761
2864
  useEffect7(() => {
2762
- onModelLoaded?.(scene);
2763
- }, [scene, onModelLoaded]);
2865
+ onModelLoadedRef.current?.(scene);
2866
+ }, [scene]);
2764
2867
  useEffect7(() => {
2765
2868
  if (!nodeRefsOut) return;
2766
2869
  for (const [nodeName, binding] of Object.entries(objectBindings)) {
@@ -3146,7 +3249,7 @@ function Model({
3146
3249
  }
3147
3250
  currentExplodeRef.current = 0;
3148
3251
  scene.updateWorldMatrix(true, true);
3149
- const sceneBox = new Box3().setFromObject(scene);
3252
+ const sceneBox = new Box32().setFromObject(scene);
3150
3253
  const modelCenter = sceneBox.getCenter(new Vector32());
3151
3254
  const radius = sceneBox.getSize(new Vector32()).length() * 0.5;
3152
3255
  explodeStrengthRef.current = Number.isFinite(radius) ? radius : 0;
@@ -3172,7 +3275,7 @@ function Model({
3172
3275
  ancestor = ancestor.parent;
3173
3276
  }
3174
3277
  if (nested) continue;
3175
- const box = new Box3().setFromObject(node);
3278
+ const box = new Box32().setFromObject(node);
3176
3279
  if (box.isEmpty()) continue;
3177
3280
  box.getCenter(partCenter);
3178
3281
  const worldDir = partCenter.clone().sub(modelCenter);
@@ -3251,13 +3354,21 @@ function Model({
3251
3354
  setHoveredItem(null);
3252
3355
  onItemHover?.(null);
3253
3356
  };
3357
+ const pointerRaycastEnabled = useMemo7(
3358
+ () => !exceedsBvhBudget(scene),
3359
+ [scene]
3360
+ );
3254
3361
  return /* @__PURE__ */ jsx8("group", { ref: groupRef, children: /* @__PURE__ */ jsx8(
3255
3362
  "primitive",
3256
3363
  {
3257
3364
  object: scene,
3258
- onClick: handleClick,
3259
- onPointerOver: handlePointerOver,
3260
- onPointerOut: handlePointerOut
3365
+ ...pointerRaycastEnabled ? {
3366
+ onClick: handleClick,
3367
+ ...highlightOnHover ? {
3368
+ onPointerOver: handlePointerOver,
3369
+ onPointerOut: handlePointerOut
3370
+ } : {}
3371
+ } : {}
3261
3372
  }
3262
3373
  ) });
3263
3374
  }
@@ -3998,7 +4109,7 @@ var XRModelPresenter_default = XRModelPresenter;
3998
4109
  import { useEffect as useEffect11, useMemo as useMemo11, useRef as useRef8 } from "react";
3999
4110
  import { useFrame as useFrame7, useThree as useThree9 } from "@react-three/fiber";
4000
4111
  import {
4001
- Box3 as Box32,
4112
+ Box3 as Box33,
4002
4113
  CatmullRomCurve3,
4003
4114
  Sphere,
4004
4115
  Vector3 as Vector36
@@ -4061,7 +4172,7 @@ function CinematicCamera({
4061
4172
  if (!hasPath) {
4062
4173
  const scene = sceneRef.current;
4063
4174
  if (scene) {
4064
- const sphere = new Box32().setFromObject(scene).getBoundingSphere(new Sphere());
4175
+ const sphere = new Box33().setFromObject(scene).getBoundingSphere(new Sphere());
4065
4176
  orbitRef.current = {
4066
4177
  center: sphere.center.clone(),
4067
4178
  radius: sphere.radius > 0 ? sphere.radius : 1
@@ -5301,7 +5412,7 @@ var DEFAULT_AR_OVERLAY_STATE = {
5301
5412
  isModelPlaced: false
5302
5413
  };
5303
5414
  function computeMeasureBox(scene) {
5304
- const box = new Box33().setFromObject(scene);
5415
+ const box = new Box34().setFromObject(scene);
5305
5416
  const size = new Vector37();
5306
5417
  box.getSize(size);
5307
5418
  return {
@@ -5311,7 +5422,7 @@ function computeMeasureBox(scene) {
5311
5422
  };
5312
5423
  }
5313
5424
  function computeModelBounds(scene) {
5314
- const box = new Box33().setFromObject(scene);
5425
+ const box = new Box34().setFromObject(scene);
5315
5426
  if (box.isEmpty()) return null;
5316
5427
  const center = box.getCenter(new Vector37());
5317
5428
  const radius = box.getSize(new Vector37()).length() / 2;
@@ -5321,7 +5432,7 @@ function computeModelBounds(scene) {
5321
5432
  function fitClippingPlanesToScene(scene, controls) {
5322
5433
  const camera = controls?.camera;
5323
5434
  if (!(camera instanceof PerspectiveCamera3)) return;
5324
- const box = new Box33().setFromObject(scene);
5435
+ const box = new Box34().setFromObject(scene);
5325
5436
  const size = box.getSize(new Vector37()).length();
5326
5437
  if (!Number.isFinite(size) || size <= 0) return;
5327
5438
  camera.near = size / 100;
@@ -5469,6 +5580,11 @@ function ModelViewer({
5469
5580
  const [modelLoadFailed, setModelLoadFailed] = useState10(false);
5470
5581
  const [bindingsMismatch, setBindingsMismatch] = useState10(false);
5471
5582
  const [mismatchDismissed, setMismatchDismissed] = useState10(false);
5583
+ const [largeModelTriangles, setLargeModelTriangles] = useState10(
5584
+ null
5585
+ );
5586
+ const [largeModelNoticeDismissed, setLargeModelNoticeDismissed] = useState10(false);
5587
+ const [loaderMessage, setLoaderMessage] = useState10("Loading model...");
5472
5588
  const currentObjectBindingsRef = useRef11({});
5473
5589
  const captureInitialCameraState = useCallback4(() => {
5474
5590
  const controls = controlsRef.current;
@@ -5749,7 +5865,7 @@ function ModelViewer({
5749
5865
  controls.smoothTime = 0.6;
5750
5866
  const storedNormal = annotation.worldNormal;
5751
5867
  if (scene) {
5752
- const box = new Box33().setFromObject(scene);
5868
+ const box = new Box34().setFromObject(scene);
5753
5869
  const modelCenter = new Vector37();
5754
5870
  box.getCenter(modelCenter);
5755
5871
  const modelSize = new Vector37();
@@ -6056,7 +6172,7 @@ function ModelViewer({
6056
6172
  if (canvasContainer && cameraControls && object) {
6057
6173
  const canvasRect = canvasContainer.getBoundingClientRect();
6058
6174
  const objectCenter = new Vector37();
6059
- new Box33().setFromObject(object).getCenter(objectCenter);
6175
+ new Box34().setFromObject(object).getCenter(objectCenter);
6060
6176
  objectCenter.project(cameraControls);
6061
6177
  const projectedX = (objectCenter.x + 1) / 2 * canvasRect.width + (canvasRect.left - rect.left);
6062
6178
  const projectedY = (1 - objectCenter.y) / 2 * canvasRect.height + (canvasRect.top - rect.top);
@@ -6287,7 +6403,7 @@ function ModelViewer({
6287
6403
  direction.set(0, 0, 1);
6288
6404
  }
6289
6405
  direction.normalize();
6290
- const sphere = new Box33().setFromObject(scene).getBoundingSphere(new Sphere2());
6406
+ const sphere = new Box34().setFromObject(scene).getBoundingSphere(new Sphere2());
6291
6407
  const farDistance = Math.max(sphere.radius * 10, 20);
6292
6408
  const farPosition = target.clone().add(direction.multiplyScalar(farDistance));
6293
6409
  await controls.setLookAt(
@@ -6310,6 +6426,21 @@ function ModelViewer({
6310
6426
  paddingRight: 0.5
6311
6427
  })
6312
6428
  );
6429
+ await new Promise((resolveSettled) => {
6430
+ const startedAt = performance.now();
6431
+ const settle = () => {
6432
+ const active = controls.active;
6433
+ if (active === false || performance.now() - startedAt > 8e3) {
6434
+ controls.dispatchEvent({ type: "update" });
6435
+ rendererCaptureRef.current?.invalidate();
6436
+ resolveSettled();
6437
+ } else {
6438
+ rendererCaptureRef.current?.invalidate();
6439
+ setTimeout(settle, 100);
6440
+ }
6441
+ };
6442
+ setTimeout(settle, 100);
6443
+ });
6313
6444
  fitAttemptedRef.current = true;
6314
6445
  captureInitialCameraState();
6315
6446
  notifyViewerReady();
@@ -6330,6 +6461,7 @@ function ModelViewer({
6330
6461
  setModelLoaded(false);
6331
6462
  setModelLoadFailed(false);
6332
6463
  setBindingsMismatch(false);
6464
+ setLoaderMessage("Loading model...");
6333
6465
  if (!modelUrl) {
6334
6466
  setIsAutoFraming(false);
6335
6467
  setCameraPositioned(true);
@@ -6357,6 +6489,7 @@ function ModelViewer({
6357
6489
  notifyViewerReady();
6358
6490
  setIsAutoFraming(false);
6359
6491
  setCameraPositioned(true);
6492
+ rendererCaptureRef.current?.invalidate();
6360
6493
  return;
6361
6494
  }
6362
6495
  fitInFlightRef.current = true;
@@ -6517,6 +6650,14 @@ function ModelViewer({
6517
6650
  sceneRef.current = scene;
6518
6651
  setMeasureBox(computeMeasureBox(scene));
6519
6652
  setModelBounds(computeModelBounds(scene));
6653
+ const triangles = countSceneTriangles(scene);
6654
+ setLargeModelTriangles(triangles > MAX_BVH_TRIANGLES ? triangles : null);
6655
+ setLargeModelNoticeDismissed(false);
6656
+ if (triangles > MAX_BVH_TRIANGLES) {
6657
+ setLoaderMessage(
6658
+ `Preparing large model (${Math.round(triangles / 1e6)}M triangles) \u2014 the first display can take a while on this device...`
6659
+ );
6660
+ }
6520
6661
  setModelLoaded(true);
6521
6662
  setModelLoadFailed(false);
6522
6663
  setMismatchDismissed(false);
@@ -6839,6 +6980,33 @@ function ModelViewer({
6839
6980
  }
6840
6981
  )
6841
6982
  ] }),
6983
+ largeModelTriangles !== null && !largeModelNoticeDismissed && /* @__PURE__ */ jsxs15(
6984
+ "div",
6985
+ {
6986
+ className: `ri-largeModelBanner ${bindingsMismatch && !mismatchDismissed ? "ri-largeModelBanner--offset" : ""}`,
6987
+ children: [
6988
+ /* @__PURE__ */ jsx19(AlertTriangle2, { size: 14, className: "ri-largeModelBanner__icon" }),
6989
+ /* @__PURE__ */ jsxs15("span", { children: [
6990
+ "Very large model (",
6991
+ Math.round(largeModelTriangles / 1e6),
6992
+ "M triangles) \u2014 it may render and move slowly on this device, and click/hover selection on the model is disabled. Use the objects panel to select parts."
6993
+ ] }),
6994
+ /* @__PURE__ */ jsx19(
6995
+ "button",
6996
+ {
6997
+ type: "button",
6998
+ className: "ri-largeModelBanner__dismiss",
6999
+ onClick: (e) => {
7000
+ e.stopPropagation();
7001
+ setLargeModelNoticeDismissed(true);
7002
+ },
7003
+ "aria-label": "Dismiss",
7004
+ children: /* @__PURE__ */ jsx19(X, { size: 12 })
7005
+ }
7006
+ )
7007
+ ]
7008
+ }
7009
+ ),
6842
7010
  /* @__PURE__ */ jsx19(
6843
7011
  "input",
6844
7012
  {
@@ -6972,13 +7140,7 @@ function ModelViewer({
6972
7140
  ]
6973
7141
  }
6974
7142
  ),
6975
- /* @__PURE__ */ jsx19(
6976
- LoadingOverlay,
6977
- {
6978
- visible: showModelLoader,
6979
- message: "Loading model..."
6980
- }
6981
- ),
7143
+ /* @__PURE__ */ jsx19(LoadingOverlay, { visible: showModelLoader, message: loaderMessage }),
6982
7144
  xrToolsVisible && /* @__PURE__ */ jsxs15(
6983
7145
  "div",
6984
7146
  {
@@ -14512,11 +14674,12 @@ function BindingBuilder({ licenseKey }) {
14512
14674
  },
14513
14675
  [bindingsHistory, setSceneConfig]
14514
14676
  );
14515
- const generateBindingsForModel = (nextModelUrl, nextModelName) => {
14677
+ const generateBindingsForModel = (nextModelUrl, nextModelName, fileSizeBytes) => {
14516
14678
  const requestId = ++loadRequestRef.current;
14517
14679
  setLoading(true);
14680
+ const sizeMB = fileSizeBytes ? fileSizeBytes / 1048576 : 0;
14518
14681
  setStatusMessage(
14519
- `Reading ${nextModelName} and generating starter bindings...`
14682
+ sizeMB >= 100 ? `Reading ${nextModelName} (${Math.round(sizeMB)}MB \u2014 large file, this can take a while)...` : `Reading ${nextModelName} and generating starter bindings...`
14520
14683
  );
14521
14684
  setStatusTone("default");
14522
14685
  setModelName(nextModelName);
@@ -14586,7 +14749,11 @@ function BindingBuilder({ licenseKey }) {
14586
14749
  }
14587
14750
  uploadedModelRef.current?.cleanup();
14588
14751
  uploadedModelRef.current = resolvedModel;
14589
- await generateBindingsForModel(resolvedModel.url, resolvedModel.name);
14752
+ await generateBindingsForModel(
14753
+ resolvedModel.url,
14754
+ resolvedModel.name,
14755
+ file.size
14756
+ );
14590
14757
  };
14591
14758
  const handleUploadModel = async (event) => {
14592
14759
  const file = event.target.files?.[0];
@@ -15402,11 +15569,12 @@ function BindingBuilder({ licenseKey }) {
15402
15569
  onLoadError: handleViewerLoadError,
15403
15570
  licenseKey,
15404
15571
  sceneConfig,
15572
+ renderMode: "always",
15405
15573
  disableZoom: false,
15406
15574
  zoomOnSelected,
15407
15575
  highlightOnHover,
15408
15576
  panelHoveredObject: highlightOnHover ? panelHoveredBinding : null,
15409
- showExplodeControls: true
15577
+ showLoadingOverlay: false
15410
15578
  }
15411
15579
  ) : /* @__PURE__ */ jsx45(
15412
15580
  "div",
@@ -15491,14 +15659,14 @@ import { CameraControls as CameraControls2, Environment as Environment2 } from "
15491
15659
  import { useFrame as useFrame9, useThree as useThree11 } from "@react-three/fiber";
15492
15660
  import { Canvas as Canvas3 } from "@react-three/fiber";
15493
15661
  import {
15494
- Box3 as Box34,
15662
+ Box3 as Box35,
15495
15663
  PerspectiveCamera as PerspectiveCamera4,
15496
15664
  Vector3 as Vector39
15497
15665
  } from "three";
15498
15666
  import { SkeletonUtils as SkeletonUtils2 } from "three-stdlib";
15499
15667
 
15500
15668
  // src/styles/SimpleModelViewer.css
15501
- styleInject('.smv-root {\n --smv-panel-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --smv-panel-bg: #141824;\n --smv-panel-bg-strong: #1a1f2e;\n --smv-panel-bg-strong-hover: #252b3b;\n --smv-panel-border: rgba(255, 255, 255, 0.06);\n --smv-panel-border-strong: rgba(255, 255, 255, 0.08);\n --smv-panel-border-active: rgba(255, 255, 255, 0.18);\n --smv-panel-shadow-left: 4px 0 24px rgba(0, 0, 0, 0.35);\n --smv-panel-shadow-right: -4px 0 24px rgba(0, 0, 0, 0.35);\n --smv-panel-text: #f1f5f9;\n --smv-panel-text-emphasis: #ffffff;\n --smv-panel-text-muted: #94a3b8;\n --smv-panel-text-subtle: #64748b;\n --smv-panel-surface: rgba(255, 255, 255, 0.05);\n --smv-panel-surface-hover: rgba(255, 255, 255, 0.1);\n --smv-panel-surface-soft: rgba(255, 255, 255, 0.04);\n --smv-panel-accent: #3b82f6;\n --smv-panel-accent-contrast: #ffffff;\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n display: flex;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-synthesis: none;\n}\n.smv-canvasArea {\n flex: 1;\n position: relative;\n min-width: 0;\n}\n.smv-upload-input {\n display: none;\n}\n.smv-upload-toolbar {\n position: absolute;\n top: 12px;\n left: 50%;\n z-index: 120;\n display: flex;\n align-items: center;\n gap: 10px;\n max-width: calc(100% - 32px);\n padding: 5px 6px;\n border: 1px solid var(--smv-panel-border-strong);\n border-radius: 10px;\n background: rgba(20, 24, 36, 0.88);\n color: var(--smv-panel-text);\n box-shadow: 0 16px 30px rgba(0, 0, 0, 0.28);\n backdrop-filter: blur(14px);\n transform: translateX(-50%);\n}\n.smv-upload-toolbar__name {\n max-width: 260px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--smv-panel-text-muted);\n font-size: 12px;\n font-weight: 500;\n}\n.smv-upload-toolbar__button {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n min-height: 34px;\n padding: 0 14px;\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 10px;\n background: rgba(59, 130, 246, 0.18);\n color: var(--smv-panel-text-emphasis);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n.smv-upload-toolbar__button--ghost {\n background: var(--smv-panel-surface);\n color: var(--smv-panel-text-muted);\n}\n.smv-upload-empty-state {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n padding: 24px;\n}\n.smv-upload-empty-state__card {\n width: min(440px, 100%);\n padding: 28px;\n border: 1px dashed rgba(255, 255, 255, 0.2);\n border-radius: 8px;\n color: var(--smv-panel-text);\n text-align: center;\n box-shadow: 0 24px 40px rgba(0, 0, 0, 0.28);\n backdrop-filter: blur(14px);\n}\n.smv-upload-empty-state__icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n margin-bottom: 18px;\n border-radius: 10px;\n background: rgba(59, 130, 246, 0.18);\n color: #bfdbfe;\n}\n.smv-upload-empty-state__title {\n margin: 0 0 10px;\n color: var(--smv-panel-text-emphasis);\n font-size: 24px;\n line-height: 1.15;\n}\n.smv-upload-empty-state__body {\n margin: 0 0 18px;\n color: var(--smv-panel-text-muted);\n font-size: 14px;\n font-weight: 700;\n line-height: 1.6;\n}\n.smv-upload-empty-state__body code {\n color: var(--smv-panel-text-emphasis);\n font-family: var(--smv-panel-font-family);\n}\n.smv-upload-empty-state__button {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n min-height: 40px;\n padding: 0 18px;\n border: none;\n border-radius: 8px;\n background: #3b82f6;\n color: #eff6ff;\n font-size: 13px;\n font-weight: 700;\n cursor: pointer;\n}\n.smv-upload-drop-overlay {\n position: absolute;\n inset: 0;\n z-index: 130;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: rgba(8, 12, 20, 0.48);\n backdrop-filter: blur(4px);\n}\n.smv-upload-drop-overlay__card {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 16px 20px;\n border: 1px dashed rgba(191, 219, 254, 0.75);\n border-radius: 10px;\n background: rgba(30, 41, 59, 0.92);\n color: #dbeafe;\n font-size: 14px;\n font-weight: 700;\n box-shadow: 0 18px 36px rgba(0, 0, 0, 0.28);\n}\n.smv-canvasArea .bb-loadingOverlay {\n position: absolute;\n inset: 0;\n z-index: 110;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 24px;\n text-align: center;\n border-radius: 14px;\n background: rgba(15, 23, 42, 0.46);\n backdrop-filter: blur(1px);\n}\n.smv-canvasArea .bb-loadingSpinner {\n width: 40px;\n height: 40px;\n border-radius: 999px;\n border: 3px solid rgba(226, 232, 240, 0.24);\n border-top-color: #93c5fd;\n animation: bb-spin 0.8s linear infinite;\n}\n.smv-canvasArea .bb-loadingMessage {\n max-width: 320px;\n color: #f7f8fa;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.5;\n}\n@keyframes bb-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.right-panel-wrapper {\n height: 100%;\n transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n overflow: visible;\n}\n.right-panel-wrapper--open {\n width: 300px;\n flex-shrink: 0;\n}\n.right-panel-wrapper--closed {\n width: 0;\n flex-shrink: 0;\n}\n.left-panel-wrapper {\n height: 100%;\n transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n overflow: visible;\n order: -1;\n}\n.left-panel-wrapper--open {\n width: 280px;\n flex-shrink: 0;\n}\n.left-panel-wrapper--closed {\n width: 0;\n flex-shrink: 0;\n}\n.side-panel__open-btn.side-panel__open-btn--left {\n left: 12px;\n right: auto;\n}\n.side-panel.side-panel--left {\n left: 0;\n right: auto;\n width: 280px;\n transform: translateX(-100%);\n border-left: none;\n border-right: 1px solid var(--smv-panel-border);\n box-shadow: var(--smv-panel-shadow-left);\n}\n.side-panel.side-panel--open-left {\n transform: translateX(0);\n}\n.side-panel.side-panel--closed-left {\n transform: translateX(-100%);\n}\n.env-controls {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n flex: 1;\n}\n.env-section {\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.env-section__header {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 9px 14px;\n border: none;\n background: var(--smv-panel-surface-soft);\n color: var(--smv-panel-text-muted);\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n cursor: pointer;\n}\n.env-section__chevron {\n transition: transform 0.15s;\n}\n.env-section__chevron--collapsed {\n transform: rotate(-90deg);\n}\n.env-section__body {\n display: flex;\n flex-direction: column;\n}\n.env-row {\n display: flex;\n gap: 6px;\n align-items: center;\n min-height: 32px;\n border-bottom: 1px solid var(--smv-panel-surface);\n}\n.env-row__label {\n flex: 0 0 42%;\n padding: 0 0 0 14px;\n font-size: 11.5px;\n font-weight: 500;\n color: var(--smv-panel-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.env-row__value {\n flex: 1;\n display: flex;\n align-items: center;\n padding: 5px 10px 5px 8px;\n min-width: 0;\n}\n.env-row__value--slider {\n gap: 6px;\n}\n.env-checkbox-btn {\n display: flex;\n align-items: center;\n cursor: pointer;\n background: none;\n border: none;\n padding: 0;\n}\n.env-checkbox {\n width: 15px;\n height: 15px;\n border-radius: 3px;\n border: 1.5px solid var(--smv-panel-border-active);\n background: var(--smv-panel-surface-soft);\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s, border-color 0.15s;\n}\n.env-checkbox--checked {\n background: var(--smv-panel-accent);\n border-color: var(--smv-panel-accent);\n}\n.env-select {\n width: 100%;\n padding: 5px 8px;\n border-radius: 4px;\n border: none;\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-text);\n font-size: 11.5px;\n outline: none;\n box-sizing: border-box;\n text-transform: capitalize;\n}\n.env-color-swatch {\n position: relative;\n width: 100%;\n height: 22px;\n border-radius: 4px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n cursor: pointer;\n}\n.env-color-swatch__hex {\n font-size: 10.5px;\n font-weight: 700;\n color: #0f172a;\n text-shadow: 0 1px 1px rgba(255, 255, 255, 0.35);\n pointer-events: none;\n text-transform: uppercase;\n}\n.env-color-swatch__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n border: none;\n padding: 0;\n opacity: 0;\n cursor: pointer;\n}\n.env-slider {\n flex: 1;\n accent-color: var(--smv-panel-accent);\n cursor: pointer;\n min-width: 0;\n}\n.env-slider__value {\n flex-shrink: 0;\n min-width: 32px;\n padding: 2px 5px;\n border-radius: 4px;\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-accent);\n font-size: 10.5px;\n font-weight: 600;\n text-align: center;\n}\n.side-panel__open-btn {\n position: absolute;\n top: 12px;\n right: 12px;\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 7px 13px;\n border-radius: 7px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-bg-strong);\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: var(--smv-panel-text);\n z-index: 190;\n transition: background 0.15s;\n}\n.side-panel__open-btn:hover {\n background: var(--smv-panel-bg-strong-hover);\n}\n.side-panel {\n position: absolute;\n top: 0;\n right: 0;\n width: 300px;\n height: 100%;\n background: var(--smv-panel-bg);\n display: flex;\n flex-direction: column;\n font-family: var(--smv-panel-font-family);\n transform: translateX(100%);\n transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n z-index: 200;\n border-left: 1px solid var(--smv-panel-border);\n box-shadow: var(--smv-panel-shadow-right);\n}\n.side-panel--open {\n transform: translateX(0);\n}\n.side-panel--closed {\n transform: translateX(100%);\n}\n.side-panel__header {\n padding: 24px 20px 16px 20px;\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.side-panel__header-top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 4px;\n}\n.side-panel__eyebrow {\n font-size: 15px;\n font-weight: 700;\n letter-spacing: 0.12em;\n color: var(--smv-panel-text-subtle);\n text-transform: uppercase;\n}\n.side-panel__title {\n font-size: 22px;\n font-weight: 700;\n color: var(--smv-panel-text);\n margin: 0;\n line-height: 1.2;\n}\n.side-panel__close-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n border-radius: 8px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-surface);\n cursor: pointer;\n color: var(--smv-panel-text-muted);\n transition: background 0.15s, color 0.15s;\n flex-shrink: 0;\n}\n.side-panel__close-btn:hover {\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-text);\n}\n.side-panel__search-wrap {\n padding: 14px 16px;\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.side-panel__search {\n width: 100%;\n padding: 10px 14px;\n border-radius: 10px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-surface);\n color: var(--smv-panel-text);\n font-size: 13px;\n outline: none;\n box-sizing: border-box;\n transition: border-color 0.15s, background 0.15s;\n}\n.side-panel__search::placeholder {\n color: var(--smv-panel-text-subtle);\n}\n.side-panel__search:focus {\n border-color: var(--smv-panel-border-active);\n background: var(--smv-panel-surface-hover);\n}\n.side-panel__list {\n display: flex;\n flex-direction: column;\n padding: 10px 0;\n overflow-y: auto;\n flex: 1;\n}\n.side-panel__list::-webkit-scrollbar {\n width: 4px;\n}\n.side-panel__list::-webkit-scrollbar-track {\n background: transparent;\n}\n.side-panel__list::-webkit-scrollbar-thumb {\n background: var(--smv-panel-surface-hover);\n border-radius: 4px;\n}\n.side-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 20px;\n cursor: pointer;\n transition: background 0.12s;\n border-bottom: 1px solid var(--smv-panel-surface-soft);\n}\n.side-panel__row:hover,\n.side-panel__row--hovered {\n background: var(--smv-panel-surface);\n}\n.side-panel__label {\n font-size: 14px;\n font-weight: 600;\n color: var(--smv-panel-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1;\n}\n.side-panel__label--hovered {\n color: var(--smv-panel-text-emphasis);\n}\n.side-panel__vis-btn {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n cursor: pointer;\n background: none;\n border: none;\n padding: 0;\n margin-left: 10px;\n}\n.side-panel__checkbox {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1.5px solid var(--smv-panel-border-active);\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s, border-color 0.15s;\n}\n.side-panel__checkbox--checked {\n background: var(--smv-panel-accent);\n border-color: var(--smv-panel-accent);\n}\n@media (max-width: 900px) {\n .smv-upload-toolbar {\n top: 56px;\n gap: 8px;\n max-width: calc(100% - 24px);\n padding: 8px 10px;\n }\n .smv-upload-toolbar__name {\n max-width: 140px;\n }\n .right-panel-wrapper--open,\n .left-panel-wrapper--open {\n width: 0;\n }\n .side-panel,\n .side-panel.side-panel--left {\n width: min(300px, 85vw);\n }\n}\n');
15669
+ styleInject('.smv-root {\n --smv-panel-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --smv-panel-bg: #141824;\n --smv-panel-bg-strong: #1a1f2e;\n --smv-panel-bg-strong-hover: #252b3b;\n --smv-panel-border: rgba(255, 255, 255, 0.06);\n --smv-panel-border-strong: rgba(255, 255, 255, 0.08);\n --smv-panel-border-active: rgba(255, 255, 255, 0.18);\n --smv-panel-shadow-left: 4px 0 24px rgba(0, 0, 0, 0.35);\n --smv-panel-shadow-right: -4px 0 24px rgba(0, 0, 0, 0.35);\n --smv-panel-text: #f1f5f9;\n --smv-panel-text-emphasis: #ffffff;\n --smv-panel-text-muted: #94a3b8;\n --smv-panel-text-subtle: #64748b;\n --smv-panel-surface: rgba(255, 255, 255, 0.05);\n --smv-panel-surface-hover: rgba(255, 255, 255, 0.1);\n --smv-panel-surface-soft: rgba(255, 255, 255, 0.04);\n --smv-panel-accent: #3b82f6;\n --smv-panel-accent-contrast: #ffffff;\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n display: flex;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-synthesis: none;\n}\n.smv-canvasArea {\n flex: 1;\n position: relative;\n min-width: 0;\n}\n.smv-upload-input {\n display: none;\n}\n.smv-upload-toolbar {\n position: absolute;\n top: 12px;\n left: 50%;\n z-index: 120;\n display: flex;\n align-items: center;\n gap: 10px;\n max-width: calc(100% - 32px);\n padding: 5px 6px;\n border: 1px solid var(--smv-panel-border-strong);\n border-radius: 10px;\n background: rgba(20, 24, 36, 0.88);\n color: var(--smv-panel-text);\n box-shadow: 0 16px 30px rgba(0, 0, 0, 0.28);\n backdrop-filter: blur(14px);\n transform: translateX(-50%);\n}\n.smv-upload-toolbar__name {\n max-width: 260px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--smv-panel-text-muted);\n font-size: 12px;\n font-weight: 500;\n}\n.smv-upload-toolbar__button {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n min-height: 34px;\n padding: 0 14px;\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 10px;\n background: rgba(59, 130, 246, 0.18);\n color: var(--smv-panel-text-emphasis);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n.smv-upload-toolbar__button--ghost {\n background: var(--smv-panel-surface);\n color: var(--smv-panel-text-muted);\n}\n.smv-upload-empty-state {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n padding: 24px;\n}\n.smv-upload-empty-state__card {\n width: min(440px, 100%);\n padding: 28px;\n border: 1px dashed rgba(255, 255, 255, 0.2);\n border-radius: 8px;\n color: var(--smv-panel-text);\n text-align: center;\n box-shadow: 0 24px 40px rgba(0, 0, 0, 0.28);\n backdrop-filter: blur(14px);\n}\n.smv-upload-empty-state__icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n margin-bottom: 18px;\n border-radius: 10px;\n background: rgba(59, 130, 246, 0.18);\n color: #bfdbfe;\n}\n.smv-upload-empty-state__title {\n margin: 0 0 10px;\n color: var(--smv-panel-text-emphasis);\n font-size: 24px;\n line-height: 1.15;\n}\n.smv-upload-empty-state__body {\n margin: 0 0 18px;\n color: var(--smv-panel-text-muted);\n font-size: 14px;\n font-weight: 700;\n line-height: 1.6;\n}\n.smv-upload-empty-state__body code {\n color: var(--smv-panel-text-emphasis);\n font-family: var(--smv-panel-font-family);\n}\n.smv-upload-empty-state__button {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n min-height: 40px;\n padding: 0 18px;\n border: none;\n border-radius: 8px;\n background: #3b82f6;\n color: #eff6ff;\n font-size: 13px;\n font-weight: 700;\n cursor: pointer;\n}\n.smv-upload-drop-overlay {\n position: absolute;\n inset: 0;\n z-index: 130;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: rgba(8, 12, 20, 0.48);\n backdrop-filter: blur(4px);\n}\n.smv-upload-drop-overlay__card {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 16px 20px;\n border: 1px dashed rgba(191, 219, 254, 0.75);\n border-radius: 10px;\n background: rgba(30, 41, 59, 0.92);\n color: #dbeafe;\n font-size: 14px;\n font-weight: 700;\n box-shadow: 0 18px 36px rgba(0, 0, 0, 0.28);\n}\n.smv-canvasArea .bb-loadingOverlay {\n position: absolute;\n inset: 0;\n z-index: 110;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 24px;\n text-align: center;\n border-radius: 14px;\n background: rgba(15, 23, 42, 0.46);\n backdrop-filter: blur(1px);\n}\n.smv-canvasArea .bb-loadingSpinner {\n width: 40px;\n height: 40px;\n border-radius: 999px;\n border: 3px solid rgba(226, 232, 240, 0.24);\n border-top-color: #93c5fd;\n animation: bb-spin 0.8s linear infinite;\n}\n.smv-canvasArea .bb-loadingMessage {\n max-width: 320px;\n color: #f7f8fa;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.5;\n}\n@keyframes bb-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.right-panel-wrapper {\n height: 100%;\n transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n overflow: visible;\n}\n.right-panel-wrapper--open {\n width: 300px;\n flex-shrink: 0;\n}\n.right-panel-wrapper--closed {\n width: 0;\n flex-shrink: 0;\n}\n.left-panel-wrapper {\n height: 100%;\n transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n overflow: visible;\n order: -1;\n}\n.left-panel-wrapper--open {\n width: 280px;\n flex-shrink: 0;\n}\n.left-panel-wrapper--closed {\n width: 0;\n flex-shrink: 0;\n}\n.side-panel__open-btn.side-panel__open-btn--left {\n left: 12px;\n right: auto;\n}\n.side-panel.side-panel--left {\n left: 0;\n right: auto;\n width: 280px;\n transform: translateX(-100%);\n border-left: none;\n border-right: 1px solid var(--smv-panel-border);\n box-shadow: var(--smv-panel-shadow-left);\n}\n.side-panel.side-panel--open-left {\n transform: translateX(0);\n}\n.side-panel.side-panel--closed-left {\n transform: translateX(-100%);\n}\n.env-controls {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n flex: 1;\n}\n.env-section {\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.env-section__header {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 9px 14px;\n border: none;\n background: var(--smv-panel-surface-soft);\n color: var(--smv-panel-text-muted);\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n cursor: pointer;\n}\n.env-section__chevron {\n transition: transform 0.15s;\n}\n.env-section__chevron--collapsed {\n transform: rotate(-90deg);\n}\n.env-section__body {\n display: flex;\n flex-direction: column;\n}\n.env-row {\n display: flex;\n gap: 6px;\n align-items: center;\n min-height: 32px;\n border-bottom: 1px solid var(--smv-panel-surface);\n}\n.env-row__label {\n flex: 0 0 42%;\n padding: 0 0 0 14px;\n font-size: 11.5px;\n font-weight: 500;\n color: var(--smv-panel-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.env-row__value {\n flex: 1;\n display: flex;\n align-items: center;\n padding: 5px 10px 5px 8px;\n min-width: 0;\n}\n.env-row__value--slider {\n gap: 6px;\n}\n.env-checkbox-btn {\n display: flex;\n align-items: center;\n cursor: pointer;\n background: none;\n border: none;\n padding: 0;\n}\n.env-checkbox {\n width: 15px;\n height: 15px;\n border-radius: 3px;\n border: 1.5px solid var(--smv-panel-border-active);\n background: var(--smv-panel-surface-soft);\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s, border-color 0.15s;\n}\n.env-checkbox--checked {\n background: var(--smv-panel-accent);\n border-color: var(--smv-panel-accent);\n}\n.env-select {\n width: 100%;\n padding: 5px 8px;\n border-radius: 4px;\n border: none;\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-text);\n font-size: 11.5px;\n outline: none;\n box-sizing: border-box;\n text-transform: capitalize;\n}\n.env-color-swatch {\n position: relative;\n width: 100%;\n height: 22px;\n border-radius: 4px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n cursor: pointer;\n}\n.env-color-swatch__hex {\n font-size: 10.5px;\n font-weight: 700;\n color: #0f172a;\n text-shadow: 0 1px 1px rgba(255, 255, 255, 0.35);\n pointer-events: none;\n text-transform: uppercase;\n}\n.env-color-swatch__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n border: none;\n padding: 0;\n opacity: 0;\n cursor: pointer;\n}\n.env-slider {\n flex: 1;\n accent-color: var(--smv-panel-accent);\n cursor: pointer;\n min-width: 0;\n}\n.env-slider__value {\n flex-shrink: 0;\n min-width: 32px;\n padding: 2px 5px;\n border-radius: 4px;\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-accent);\n font-size: 10.5px;\n font-weight: 600;\n text-align: center;\n}\n.side-panel__open-btn {\n position: absolute;\n top: 12px;\n right: 12px;\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 7px 13px;\n border-radius: 7px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-bg-strong);\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: var(--smv-panel-text);\n z-index: 190;\n transition: background 0.15s;\n}\n.side-panel__open-btn:hover {\n background: var(--smv-panel-bg-strong-hover);\n}\n.side-panel {\n position: absolute;\n top: 0;\n right: 0;\n width: 300px;\n height: 100%;\n background: var(--smv-panel-bg);\n display: flex;\n flex-direction: column;\n font-family: var(--smv-panel-font-family);\n transform: translateX(100%);\n transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);\n z-index: 200;\n border-left: 1px solid var(--smv-panel-border);\n box-shadow: var(--smv-panel-shadow-right);\n}\n.side-panel--open {\n transform: translateX(0);\n}\n.side-panel--closed {\n transform: translateX(100%);\n}\n.side-panel__header {\n padding: 24px 20px 16px 20px;\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.side-panel__header-top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 4px;\n}\n.side-panel__eyebrow {\n font-size: 15px;\n font-weight: 700;\n letter-spacing: 0.12em;\n color: var(--smv-panel-text-subtle);\n text-transform: uppercase;\n}\n.side-panel__title {\n font-size: 22px;\n font-weight: 700;\n color: var(--smv-panel-text);\n margin: 0;\n line-height: 1.2;\n}\n.side-panel__close-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n border-radius: 8px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-surface);\n cursor: pointer;\n color: var(--smv-panel-text-muted);\n transition: background 0.15s, color 0.15s;\n flex-shrink: 0;\n}\n.side-panel__close-btn:hover {\n background: var(--smv-panel-surface-hover);\n color: var(--smv-panel-text);\n}\n.side-panel__search-wrap {\n padding: 14px 16px;\n border-bottom: 1px solid var(--smv-panel-border-strong);\n}\n.side-panel__search {\n width: 100%;\n padding: 10px 14px;\n border-radius: 10px;\n border: 1px solid var(--smv-panel-border-strong);\n background: var(--smv-panel-surface);\n color: var(--smv-panel-text);\n font-size: 13px;\n outline: none;\n box-sizing: border-box;\n transition: border-color 0.15s, background 0.15s;\n}\n.side-panel__search::placeholder {\n color: var(--smv-panel-text-subtle);\n}\n.side-panel__search:focus {\n border-color: var(--smv-panel-border-active);\n background: var(--smv-panel-surface-hover);\n}\n.side-panel__list {\n display: flex;\n flex-direction: column;\n padding: 10px 0;\n overflow-y: auto;\n flex: 1;\n}\n.side-panel__list::-webkit-scrollbar {\n width: 4px;\n}\n.side-panel__list::-webkit-scrollbar-track {\n background: transparent;\n}\n.side-panel__list::-webkit-scrollbar-thumb {\n background: var(--smv-panel-surface-hover);\n border-radius: 4px;\n}\n.side-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 20px;\n cursor: pointer;\n transition: background 0.12s;\n border-bottom: 1px solid var(--smv-panel-surface-soft);\n}\n.side-panel__row:hover,\n.side-panel__row--hovered {\n background: var(--smv-panel-surface);\n}\n.side-panel__label {\n font-size: 14px;\n font-weight: 600;\n color: var(--smv-panel-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1;\n}\n.side-panel__label--hovered {\n color: var(--smv-panel-text-emphasis);\n}\n.side-panel__vis-btn {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n cursor: pointer;\n background: none;\n border: none;\n padding: 0;\n margin-left: 10px;\n}\n.side-panel__checkbox {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1.5px solid var(--smv-panel-border-active);\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s, border-color 0.15s;\n}\n.side-panel__checkbox--checked {\n background: var(--smv-panel-accent);\n border-color: var(--smv-panel-accent);\n}\n@media (max-width: 900px) {\n .smv-upload-toolbar {\n top: 56px;\n gap: 8px;\n max-width: calc(100% - 24px);\n padding: 8px 10px;\n }\n .smv-upload-toolbar__name {\n max-width: 140px;\n }\n .right-panel-wrapper--open,\n .left-panel-wrapper--open {\n width: 0;\n }\n .side-panel,\n .side-panel.side-panel--left {\n width: min(300px, 85vw);\n }\n}\n.smv-largeModelBanner {\n position: absolute;\n bottom: 12px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 12px 7px 10px;\n border-radius: 8px;\n background: rgba(30, 20, 0, 0.88);\n border: 1px solid rgba(251, 191, 36, 0.35);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n color: rgb(251, 191, 36);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n pointer-events: auto;\n max-width: calc(100% - 32px);\n text-align: center;\n}\n.smv-largeModelBanner__icon {\n flex-shrink: 0;\n}\n.smv-largeModelBanner__dismiss {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 4px;\n padding: 2px;\n background: none;\n border: none;\n border-radius: 4px;\n color: rgba(251, 191, 36, 0.7);\n cursor: pointer;\n transition: color 0.15s ease, background 0.15s ease;\n}\n.smv-largeModelBanner__dismiss:hover {\n color: rgb(251, 191, 36);\n background: rgba(251, 191, 36, 0.12);\n}\n');
15502
15670
 
15503
15671
  // src/components/SimpleModelViewer/SimpleModelViewer.tsx
15504
15672
  import {
@@ -15508,7 +15676,8 @@ import {
15508
15676
  ChevronDown as ChevronDown6,
15509
15677
  Boxes as Boxes2,
15510
15678
  Trash2 as Trash26,
15511
- Upload as Upload2
15679
+ Upload as Upload2,
15680
+ AlertTriangle as AlertTriangle4
15512
15681
  } from "lucide-react";
15513
15682
  import { Fragment as Fragment14, jsx as jsx46, jsxs as jsxs41 } from "react/jsx-runtime";
15514
15683
  var HIGHLIGHT2 = { color: "#93c5fd", intensity: 0.05 };
@@ -15531,7 +15700,7 @@ function applyClippingPlanes(scene, controls) {
15531
15700
  if (!controls) return;
15532
15701
  const camera = controls.camera;
15533
15702
  if (!(camera instanceof PerspectiveCamera4)) return;
15534
- const size = new Box34().setFromObject(scene).getSize(new Vector39()).length();
15703
+ const size = new Box35().setFromObject(scene).getSize(new Vector39()).length();
15535
15704
  if (!Number.isFinite(size) || size <= 0) return;
15536
15705
  camera.near = size / 100;
15537
15706
  camera.far = size * 100;
@@ -15615,6 +15784,18 @@ function InnerModel({
15615
15784
  onMeshesReady(meshMap);
15616
15785
  onModelLoaded(scene);
15617
15786
  }, [url, scene, meshMap, onMeshesReady, onModelLoaded]);
15787
+ useEffect21(() => {
15788
+ const controller = new AbortController();
15789
+ void buildSceneBVH(scene, controller.signal);
15790
+ return () => {
15791
+ controller.abort();
15792
+ disposeSceneBVH(scene);
15793
+ };
15794
+ }, [scene]);
15795
+ const pointerRaycastEnabled = useMemo17(
15796
+ () => !exceedsBvhBudget(scene),
15797
+ [scene]
15798
+ );
15618
15799
  useEffect21(() => {
15619
15800
  for (const [key, { ref }] of Object.entries(meshMap)) {
15620
15801
  nodeRefsOut.current[key] = ref;
@@ -15670,18 +15851,15 @@ function InnerModel({
15670
15851
  }
15671
15852
  return null;
15672
15853
  };
15673
- return /* @__PURE__ */ jsx46(
15674
- "primitive",
15675
- {
15676
- object: scene,
15677
- onClick: (e) => {
15678
- const key = resolveKey(e.object);
15679
- if (!key) return;
15680
- e.stopPropagation();
15681
- onMeshClick(key);
15682
- },
15854
+ const pointerHandlers = pointerRaycastEnabled ? {
15855
+ onClick: (e) => {
15856
+ const key = resolveKey(e.object);
15857
+ if (!key) return;
15858
+ e.stopPropagation();
15859
+ onMeshClick(key);
15860
+ },
15861
+ ...highlightOnHover ? {
15683
15862
  onPointerOver: (e) => {
15684
- if (!highlightOnHover) return;
15685
15863
  const key = resolveKey(e.object);
15686
15864
  if (!key) return;
15687
15865
  e.stopPropagation();
@@ -15689,13 +15867,13 @@ function InnerModel({
15689
15867
  setHoveredKey(key);
15690
15868
  },
15691
15869
  onPointerOut: (e) => {
15692
- if (!highlightOnHover) return;
15693
15870
  e.stopPropagation();
15694
15871
  document.body.style.cursor = "default";
15695
15872
  setHoveredKey(null);
15696
15873
  }
15697
- }
15698
- );
15874
+ } : {}
15875
+ } : {};
15876
+ return /* @__PURE__ */ jsx46("primitive", { object: scene, ...pointerHandlers });
15699
15877
  }
15700
15878
  function CameraFitter({
15701
15879
  scene,
@@ -16159,6 +16337,10 @@ function SimpleModelViewer({
16159
16337
  const uploadedModelRef = useRef19(null);
16160
16338
  const dragDepthRef = useRef19(0);
16161
16339
  const [loadedScene, setLoadedScene] = useState25(null);
16340
+ const [largeModelTriangles, setLargeModelTriangles] = useState25(
16341
+ null
16342
+ );
16343
+ const [largeModelNoticeDismissed, setLargeModelNoticeDismissed] = useState25(false);
16162
16344
  const [meshKeys, setMeshKeys] = useState25({});
16163
16345
  const [hiddenKeys, setHiddenKeys] = useState25(/* @__PURE__ */ new Set());
16164
16346
  const [selectedKey, setSelectedKey] = useState25(null);
@@ -16191,6 +16373,8 @@ function SimpleModelViewer({
16191
16373
  const activeModelUrl = enableModelUpload ? uploadedModelUrl : modelUrl;
16192
16374
  const resetModelState = useCallback9(() => {
16193
16375
  setLoadedScene(null);
16376
+ setLargeModelTriangles(null);
16377
+ setLargeModelNoticeDismissed(false);
16194
16378
  setMeshKeys({});
16195
16379
  setHiddenKeys(/* @__PURE__ */ new Set());
16196
16380
  setSelectedKey(null);
@@ -16284,6 +16468,9 @@ function SimpleModelViewer({
16284
16468
  (scene) => {
16285
16469
  setLoadedScene(scene);
16286
16470
  setModelLoadFailed(false);
16471
+ const triangles = countSceneTriangles(scene);
16472
+ setLargeModelTriangles(triangles > MAX_BVH_TRIANGLES ? triangles : null);
16473
+ setLargeModelNoticeDismissed(false);
16287
16474
  onModelLoaded?.(scene);
16288
16475
  },
16289
16476
  [onModelLoaded]
@@ -16392,6 +16579,24 @@ function SimpleModelViewer({
16392
16579
  }
16393
16580
  ),
16394
16581
  /* @__PURE__ */ jsxs41("div", { className: "smv-canvasArea", children: [
16582
+ largeModelTriangles !== null && !largeModelNoticeDismissed && /* @__PURE__ */ jsxs41("div", { className: "smv-largeModelBanner", children: [
16583
+ /* @__PURE__ */ jsx46(AlertTriangle4, { size: 14, className: "smv-largeModelBanner__icon" }),
16584
+ /* @__PURE__ */ jsxs41("span", { children: [
16585
+ "Very large model detected (",
16586
+ Math.round(largeModelTriangles / 1e6),
16587
+ "M triangles) \u2014 it may render and move slowly on this device, and click/hover selection on the model is disabled. Use the scene objects panel to select parts."
16588
+ ] }),
16589
+ /* @__PURE__ */ jsx46(
16590
+ "button",
16591
+ {
16592
+ type: "button",
16593
+ className: "smv-largeModelBanner__dismiss",
16594
+ onClick: () => setLargeModelNoticeDismissed(true),
16595
+ "aria-label": "Dismiss",
16596
+ children: /* @__PURE__ */ jsx46(X3, { size: 12 })
16597
+ }
16598
+ )
16599
+ ] }),
16395
16600
  enableModelUpload ? /* @__PURE__ */ jsxs41(Fragment14, { children: [
16396
16601
  /* @__PURE__ */ jsx46(
16397
16602
  "input",
@@ -16426,7 +16631,9 @@ function SimpleModelViewer({
16426
16631
  Canvas3,
16427
16632
  {
16428
16633
  dpr: [1, 2],
16634
+ frameloop: "demand",
16429
16635
  resize: { debounce: 0, scroll: false },
16636
+ raycaster: { firstHitOnly: true },
16430
16637
  style: { background: envSettings.backgroundColor },
16431
16638
  camera: { position: [0, 0, 5], fov: 45, near: 0.1, far: 1e3 },
16432
16639
  children: /* @__PURE__ */ jsxs41(Suspense3, { fallback: null, children: [
@@ -16717,7 +16924,7 @@ function useViewerHover() {
16717
16924
 
16718
16925
  // src/hooks/useViewerModel.ts
16719
16926
  import { useCallback as useCallback13, useState as useState28 } from "react";
16720
- import { Box3 as Box35, Vector3 as Vector310 } from "three";
16927
+ import { Box3 as Box36, Vector3 as Vector310 } from "three";
16721
16928
  function toTuple2(vector) {
16722
16929
  return [vector.x, vector.y, vector.z];
16723
16930
  }
@@ -16728,7 +16935,7 @@ function useViewerModel() {
16728
16935
  const [bounds, setBounds] = useState28(null);
16729
16936
  const [objectCount, setObjectCount] = useState28(0);
16730
16937
  const handleModelLoaded = useCallback13((scene) => {
16731
- const box = new Box35().setFromObject(scene);
16938
+ const box = new Box36().setFromObject(scene);
16732
16939
  const min = box.min.clone();
16733
16940
  const max = box.max.clone();
16734
16941
  const center = new Vector310();