@liveroom-tech/react-immersive 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -242,7 +242,7 @@ function styleInject(css, { insertAt } = {}) {
242
242
  }
243
243
 
244
244
  // src/styles/ModelViewer.css
245
- 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');
245
+ 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 text-wrap: nowrap;\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.ri-downloadMenuNotice {\n max-width: 220px;\n padding: 8px 14px 11px;\n color: #fca5a5;\n font-size: 11px;\n line-height: 1.4;\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.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@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');
246
246
 
247
247
  // src/components/LicenseOverlay.tsx
248
248
  var import_lucide_react = require("lucide-react");
@@ -367,6 +367,208 @@ function exportSceneMesh({
367
367
  });
368
368
  }
369
369
 
370
+ // src/components/ModelViewer/internal/exportUvLayout.ts
371
+ var import_fflate = require("fflate");
372
+ var SVG_SIZE = 2048;
373
+ var MESH_COLORS = [
374
+ "#00e5ff",
375
+ "#ff4fd8",
376
+ "#ffe45e",
377
+ "#7cff6b",
378
+ "#ff784f",
379
+ "#a78bfa",
380
+ "#38bdf8",
381
+ "#fb7185"
382
+ ];
383
+ function escapeXml(value) {
384
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
385
+ }
386
+ function slugify(value, fallback) {
387
+ const slug = value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
388
+ return slug || fallback;
389
+ }
390
+ function safeFilenameStem(value) {
391
+ return slugify(value, "model");
392
+ }
393
+ function getVertexIndex(geometry, elementIndex) {
394
+ return geometry.index ? Math.round(geometry.index.getX(elementIndex)) : elementIndex;
395
+ }
396
+ function readTriangle(geometry, uv, elementIndex) {
397
+ const a = getVertexIndex(geometry, elementIndex);
398
+ const b = getVertexIndex(geometry, elementIndex + 1);
399
+ const c = getVertexIndex(geometry, elementIndex + 2);
400
+ if (a >= uv.count || b >= uv.count || c >= uv.count) return null;
401
+ const values = [
402
+ { index: a, u: uv.getX(a), v: uv.getY(a) },
403
+ { index: b, u: uv.getX(b), v: uv.getY(b) },
404
+ { index: c, u: uv.getX(c), v: uv.getY(c) }
405
+ ];
406
+ return values.every(
407
+ (vertex) => Number.isFinite(vertex.u) && Number.isFinite(vertex.v)
408
+ ) ? values : null;
409
+ }
410
+ function uvPointKey(vertex) {
411
+ return `${vertex.u.toFixed(6)},${vertex.v.toFixed(6)}`;
412
+ }
413
+ function addEdge(edges, start, end, indexed) {
414
+ let startKey;
415
+ let endKey;
416
+ if (indexed) {
417
+ startKey = start.index;
418
+ endKey = end.index;
419
+ } else {
420
+ startKey = uvPointKey(start);
421
+ endKey = uvPointKey(end);
422
+ }
423
+ const key = startKey < endKey ? `${startKey}|${endKey}` : `${endKey}|${startKey}`;
424
+ const existing = edges.get(key);
425
+ if (existing) {
426
+ existing.count += 1;
427
+ } else {
428
+ edges.set(key, { count: 1, start, end });
429
+ }
430
+ }
431
+ function addTriangleEdges(edges, triangle, indexed) {
432
+ addEdge(edges, triangle[0], triangle[1], indexed);
433
+ addEdge(edges, triangle[1], triangle[2], indexed);
434
+ addEdge(edges, triangle[2], triangle[0], indexed);
435
+ }
436
+ function materialForIndex(material, materialIndex) {
437
+ if (!Array.isArray(material)) return material;
438
+ return material[materialIndex] ?? null;
439
+ }
440
+ function collectUvLayouts(scene) {
441
+ const layouts = /* @__PURE__ */ new Map();
442
+ let unnamedMeshIndex = 0;
443
+ let unnamedMaterialIndex = 0;
444
+ let meshColorIndex = 0;
445
+ scene.traverse((node) => {
446
+ const mesh = node;
447
+ if (!mesh.isMesh || !mesh.geometry || !mesh.material) return;
448
+ const geometry = mesh.geometry;
449
+ const uv = geometry.getAttribute("uv");
450
+ if (!uv || uv.itemSize < 2 || uv.count < 3) return;
451
+ unnamedMeshIndex += 1;
452
+ const meshName = mesh.name || `Mesh ${unnamedMeshIndex}`;
453
+ const elementCount = geometry.index?.count ?? uv.count;
454
+ const groups = geometry.groups.length > 0 ? geometry.groups : [{ start: 0, count: elementCount, materialIndex: 0 }];
455
+ for (const group of groups) {
456
+ const materialIndex = group.materialIndex ?? 0;
457
+ const material = materialForIndex(mesh.material, materialIndex);
458
+ if (!material) continue;
459
+ let layout = layouts.get(material.uuid);
460
+ if (!layout) {
461
+ unnamedMaterialIndex += 1;
462
+ layout = {
463
+ name: material.name || `Material ${unnamedMaterialIndex}`,
464
+ layers: /* @__PURE__ */ new Map()
465
+ };
466
+ layouts.set(material.uuid, layout);
467
+ }
468
+ let layer = layout.layers.get(mesh.uuid);
469
+ if (!layer) {
470
+ layer = {
471
+ name: meshName,
472
+ color: MESH_COLORS[meshColorIndex % MESH_COLORS.length],
473
+ edges: /* @__PURE__ */ new Map()
474
+ };
475
+ meshColorIndex += 1;
476
+ layout.layers.set(mesh.uuid, layer);
477
+ }
478
+ const start = Math.max(0, group.start);
479
+ const end = Math.min(elementCount, start + group.count);
480
+ for (let elementIndex = start; elementIndex + 2 < end; elementIndex += 3) {
481
+ const triangle = readTriangle(geometry, uv, elementIndex);
482
+ if (triangle) {
483
+ addTriangleEdges(layer.edges, triangle, Boolean(geometry.index));
484
+ }
485
+ }
486
+ }
487
+ });
488
+ return [...layouts.values()].filter(
489
+ (layout) => [...layout.layers.values()].some((layer) => layer.edges.size > 0)
490
+ );
491
+ }
492
+ function coordinate(value) {
493
+ return (value * SVG_SIZE).toFixed(2);
494
+ }
495
+ function edgePath(edge) {
496
+ return `M${coordinate(edge.start.u)} ${coordinate(1 - edge.start.v)}L${coordinate(edge.end.u)} ${coordinate(1 - edge.end.v)}`;
497
+ }
498
+ function renderSvg(layout) {
499
+ const materialName = escapeXml(layout.name);
500
+ const layers = [...layout.layers.values()].filter((layer) => layer.edges.size > 0).map((layer, index) => {
501
+ const name = escapeXml(layer.name);
502
+ const id = `${slugify(layer.name, "mesh")}-${index + 1}`;
503
+ const allEdges = [...layer.edges.values()];
504
+ const boundaryEdges = allEdges.filter((edge) => edge.count === 1);
505
+ const visibleEdges = boundaryEdges.length > 0 ? boundaryEdges : allEdges;
506
+ const paths = visibleEdges.map(edgePath).join("");
507
+ return [
508
+ ` <g id="${id}" data-mesh="${name}" fill="none" stroke="${layer.color}"`,
509
+ ' stroke-width="1" stroke-linejoin="round" vector-effect="non-scaling-stroke">',
510
+ ` <title>${name}</title>`,
511
+ ` <path d="${paths}"/>`,
512
+ " </g>"
513
+ ].join("\n");
514
+ }).join("\n");
515
+ return [
516
+ '<?xml version="1.0" encoding="UTF-8"?>',
517
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${SVG_SIZE}" height="${SVG_SIZE}" viewBox="0 0 ${SVG_SIZE} ${SVG_SIZE}">`,
518
+ ` <title>${materialName} UV layout</title>`,
519
+ " <desc>UV0 island-outline template exported by React Immersive. The SVG canvas maps to the complete 0\u20131 texture area.</desc>",
520
+ ' <rect width="2048" height="2048" fill="none" stroke="#94a3b8" stroke-width="2"/>',
521
+ layers,
522
+ "</svg>"
523
+ ].join("\n");
524
+ }
525
+ function buildUvLayoutFiles(scene, downloadFilename = "model") {
526
+ const layouts = collectUvLayouts(scene);
527
+ const stem = safeFilenameStem(downloadFilename);
528
+ return layouts.map((layout, index) => {
529
+ const materialSlug = slugify(layout.name, `material-${index + 1}`);
530
+ const filename = layouts.length === 1 ? `${stem}-uv-layout.svg` : `${stem}-uv-${materialSlug}-${index + 1}.svg`;
531
+ return {
532
+ filename,
533
+ materialName: layout.name,
534
+ meshCount: [...layout.layers.values()].filter(
535
+ (layer) => layer.edges.size > 0
536
+ ).length,
537
+ svg: renderSvg(layout)
538
+ };
539
+ });
540
+ }
541
+ function downloadBlob(blob, filename) {
542
+ const url = URL.createObjectURL(blob);
543
+ const anchor = document.createElement("a");
544
+ anchor.href = url;
545
+ anchor.download = filename;
546
+ document.body.appendChild(anchor);
547
+ anchor.click();
548
+ document.body.removeChild(anchor);
549
+ URL.revokeObjectURL(url);
550
+ }
551
+ function downloadUvLayouts(scene, downloadFilename = "model") {
552
+ const files = buildUvLayoutFiles(scene, downloadFilename);
553
+ if (files.length === 0) return files;
554
+ if (files.length === 1) {
555
+ downloadBlob(
556
+ new Blob([files[0].svg], { type: "image/svg+xml;charset=utf-8" }),
557
+ files[0].filename
558
+ );
559
+ return files;
560
+ }
561
+ const archive = Object.fromEntries(
562
+ files.map((file) => [file.filename, (0, import_fflate.strToU8)(file.svg)])
563
+ );
564
+ const zipped = (0, import_fflate.zipSync)(archive, { level: 6 });
565
+ downloadBlob(
566
+ new Blob([zipped], { type: "application/zip" }),
567
+ `${safeFilenameStem(downloadFilename)}-uv-layouts.zip`
568
+ );
569
+ return files;
570
+ }
571
+
370
572
  // src/components/ModelViewer/internal/captureImage.ts
371
573
  var import_three2 = require("three");
372
574
  function captureImage(capture, options = {}) {
@@ -3438,19 +3640,31 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3438
3640
  });
3439
3641
  return anchorRef;
3440
3642
  }
3441
- function AnnotationMarker3D({ annotation, occluders, worldPositionsRef, onClick, onHover }) {
3643
+ function AnnotationMarker3D({
3644
+ annotation,
3645
+ occluders,
3646
+ worldPositionsRef,
3647
+ onClick,
3648
+ onHover,
3649
+ showAnnotationOnHover
3650
+ }) {
3442
3651
  const [occluded, setOccluded] = (0, import_react12.useState)(false);
3443
3652
  const anchorRef = useCameraBiasedAnchor(annotation, worldPositionsRef);
3444
3653
  const [isHovered, setIsHovered] = (0, import_react12.useState)(false);
3445
3654
  const [zIndex, setZIndex] = (0, import_react12.useState)(1);
3446
3655
  const clickTimeoutRef = (0, import_react12.useRef)(null);
3447
3656
  const markerRef = (0, import_react12.useRef)(null);
3657
+ const hoverTimeoutRef = (0, import_react12.useRef)(null);
3448
3658
  (0, import_react12.useEffect)(() => {
3449
3659
  return () => {
3450
3660
  if (clickTimeoutRef.current) {
3451
3661
  clearTimeout(clickTimeoutRef.current);
3452
3662
  clickTimeoutRef.current = null;
3453
3663
  }
3664
+ if (hoverTimeoutRef.current) {
3665
+ clearTimeout(hoverTimeoutRef.current);
3666
+ hoverTimeoutRef.current = null;
3667
+ }
3454
3668
  };
3455
3669
  }, []);
3456
3670
  const handleClick = (event) => {
@@ -3469,13 +3683,29 @@ function AnnotationMarker3D({ annotation, occluders, worldPositionsRef, onClick,
3469
3683
  event.stopPropagation();
3470
3684
  setIsHovered(true);
3471
3685
  setZIndex(100);
3686
+ if (hoverTimeoutRef.current) {
3687
+ clearTimeout(hoverTimeoutRef.current);
3688
+ hoverTimeoutRef.current = null;
3689
+ }
3472
3690
  onHover?.(annotation);
3691
+ if (showAnnotationOnHover) {
3692
+ onClick?.(annotation);
3693
+ }
3473
3694
  };
3474
3695
  const handlePointerLeave = (event) => {
3475
3696
  event.stopPropagation();
3476
3697
  setIsHovered(false);
3477
3698
  setZIndex(1);
3478
- onHover?.(null);
3699
+ if (hoverTimeoutRef.current) {
3700
+ clearTimeout(hoverTimeoutRef.current);
3701
+ }
3702
+ hoverTimeoutRef.current = window.setTimeout(() => {
3703
+ onHover?.(null);
3704
+ if (showAnnotationOnHover) {
3705
+ onHover?.(null);
3706
+ }
3707
+ hoverTimeoutRef.current = null;
3708
+ }, 50);
3479
3709
  };
3480
3710
  const handlePointerDown = (event) => {
3481
3711
  event.stopPropagation();
@@ -3600,7 +3830,8 @@ function AnnotationsLayer({
3600
3830
  onAnnotationClick,
3601
3831
  onAnnotationHover,
3602
3832
  onCloseAnnotation,
3603
- hideCloseButton = false
3833
+ hideCloseButton = false,
3834
+ showAnnotationOnHover = false
3604
3835
  }) {
3605
3836
  const invalidate = (0, import_fiber8.useThree)((state) => state.invalidate);
3606
3837
  const scene = (0, import_fiber8.useThree)((state) => state.scene);
@@ -3608,6 +3839,7 @@ function AnnotationsLayer({
3608
3839
  const [isLocked, setIsLocked] = (0, import_react12.useState)(false);
3609
3840
  const groupRef = (0, import_react12.useRef)(null);
3610
3841
  const invalidateFrameRef = (0, import_react12.useRef)(null);
3842
+ const hoverCloseTimeoutRef = (0, import_react12.useRef)(null);
3611
3843
  const occluders = (0, import_react12.useMemo)(() => {
3612
3844
  const list = [];
3613
3845
  scene.traverse((object) => {
@@ -3638,6 +3870,9 @@ function AnnotationsLayer({
3638
3870
  if (invalidateFrameRef.current !== null) {
3639
3871
  window.cancelAnimationFrame(invalidateFrameRef.current);
3640
3872
  }
3873
+ if (hoverCloseTimeoutRef.current) {
3874
+ clearTimeout(hoverCloseTimeoutRef.current);
3875
+ }
3641
3876
  };
3642
3877
  }, []);
3643
3878
  const activeAnnotationToShow = isLocked ? localActiveAnnotation : activeAnnotation || localActiveAnnotation;
@@ -3649,53 +3884,71 @@ function AnnotationsLayer({
3649
3884
  (annotation) => annotation.id === activeAnnotationToShow.id
3650
3885
  ) ?? activeAnnotationToShow;
3651
3886
  }, [activeAnnotationToShow, annotations]);
3652
- const handleAnnotationClick = (0, import_react12.useCallback)((annotation) => {
3653
- if (isLocked && localActiveAnnotation?.id === annotation.id) {
3654
- setIsLocked(false);
3655
- setLocalActiveAnnotation(null);
3656
- onCloseAnnotation?.();
3657
- return;
3658
- }
3659
- setIsLocked(true);
3660
- setLocalActiveAnnotation(annotation);
3661
- onAnnotationClick?.(annotation);
3662
- }, [isLocked, localActiveAnnotation, onAnnotationClick, onCloseAnnotation]);
3663
- const handleAnnotationHover = (0, import_react12.useCallback)((annotation) => {
3664
- if (!isLocked) {
3887
+ const handleAnnotationClick = (0, import_react12.useCallback)(
3888
+ (annotation) => {
3889
+ if (isLocked && localActiveAnnotation?.id === annotation.id) {
3890
+ setIsLocked(false);
3891
+ setLocalActiveAnnotation(null);
3892
+ onCloseAnnotation?.();
3893
+ return;
3894
+ }
3895
+ setIsLocked(true);
3896
+ setLocalActiveAnnotation(annotation);
3897
+ onAnnotationClick?.(annotation);
3898
+ },
3899
+ [isLocked, localActiveAnnotation, onAnnotationClick, onCloseAnnotation]
3900
+ );
3901
+ const handleAnnotationHover = (0, import_react12.useCallback)(
3902
+ (annotation) => {
3665
3903
  onAnnotationHover?.(annotation);
3666
- if (annotation) {
3667
- setLocalActiveAnnotation(annotation);
3668
- } else {
3669
- if (!isLocked) {
3670
- setLocalActiveAnnotation(null);
3904
+ if (showAnnotationOnHover && !isLocked) {
3905
+ if (hoverCloseTimeoutRef.current) {
3906
+ clearTimeout(hoverCloseTimeoutRef.current);
3907
+ hoverCloseTimeoutRef.current = null;
3908
+ }
3909
+ if (annotation) {
3910
+ setLocalActiveAnnotation(annotation);
3911
+ } else {
3912
+ hoverCloseTimeoutRef.current = window.setTimeout(() => {
3913
+ setLocalActiveAnnotation(null);
3914
+ onCloseAnnotation?.();
3915
+ hoverCloseTimeoutRef.current = null;
3916
+ }, 100);
3671
3917
  }
3672
3918
  }
3673
- }
3674
- }, [isLocked, onAnnotationHover]);
3919
+ },
3920
+ [isLocked, onAnnotationHover, showAnnotationOnHover, onCloseAnnotation]
3921
+ );
3675
3922
  const handleClose = (0, import_react12.useCallback)(() => {
3676
3923
  setIsLocked(false);
3677
3924
  setLocalActiveAnnotation(null);
3678
3925
  onCloseAnnotation?.();
3679
3926
  }, [onCloseAnnotation]);
3680
- const handleCanvasClick = (0, import_react12.useCallback)((event) => {
3681
- const target = event.target;
3682
- if (target.closest?.(".ri-annotationMarker") || target.closest?.(".ri-viewAnnotationPopup") || target.closest?.(".annotation-html-wrapper")) {
3683
- return;
3684
- }
3685
- const canvas = canvasRootRef.current;
3686
- if (canvas && canvas.contains(target)) {
3687
- setIsLocked(false);
3688
- setLocalActiveAnnotation(null);
3689
- onCloseAnnotation?.();
3690
- }
3691
- }, [canvasRootRef, onCloseAnnotation]);
3692
- const handleEscapeKey = (0, import_react12.useCallback)((event) => {
3693
- if (event.key === "Escape" && isLocked) {
3694
- setIsLocked(false);
3695
- setLocalActiveAnnotation(null);
3696
- onCloseAnnotation?.();
3697
- }
3698
- }, [isLocked, onCloseAnnotation]);
3927
+ const handleCanvasClick = (0, import_react12.useCallback)(
3928
+ (event) => {
3929
+ const target = event.target;
3930
+ if (target.closest?.(".ri-annotationMarker") || target.closest?.(".ri-viewAnnotationPopup") || target.closest?.(".annotation-html-wrapper")) {
3931
+ return;
3932
+ }
3933
+ const canvas = canvasRootRef.current;
3934
+ if (canvas && canvas.contains(target)) {
3935
+ setIsLocked(false);
3936
+ setLocalActiveAnnotation(null);
3937
+ onCloseAnnotation?.();
3938
+ }
3939
+ },
3940
+ [canvasRootRef, onCloseAnnotation]
3941
+ );
3942
+ const handleEscapeKey = (0, import_react12.useCallback)(
3943
+ (event) => {
3944
+ if (event.key === "Escape" && isLocked) {
3945
+ setIsLocked(false);
3946
+ setLocalActiveAnnotation(null);
3947
+ onCloseAnnotation?.();
3948
+ }
3949
+ },
3950
+ [isLocked, onCloseAnnotation]
3951
+ );
3699
3952
  (0, import_react12.useEffect)(() => {
3700
3953
  const canvas = canvasRootRef.current;
3701
3954
  if (canvas) {
@@ -3745,7 +3998,8 @@ function AnnotationsLayer({
3745
3998
  occluders,
3746
3999
  worldPositionsRef,
3747
4000
  onClick: handleAnnotationClick,
3748
- onHover: handleAnnotationHover
4001
+ onHover: handleAnnotationHover,
4002
+ showAnnotationOnHover
3749
4003
  },
3750
4004
  stableKey
3751
4005
  );
@@ -4099,18 +4353,75 @@ function CinematicCamera({
4099
4353
  sceneRef,
4100
4354
  config,
4101
4355
  playing,
4356
+ resetOnPlay = false,
4102
4357
  onFinished,
4103
- onUserInteract
4358
+ onUserInteract,
4359
+ onStop
4104
4360
  }) {
4105
4361
  const invalidate = (0, import_fiber10.useThree)((state) => state.invalidate);
4106
4362
  const waypoints = (0, import_react15.useMemo)(() => config.waypoints ?? [], [config.waypoints]);
4107
4363
  const hasPath = waypoints.length >= 2;
4108
4364
  const loop = config.loop ?? true;
4109
- const duration = config.duration ?? (hasPath ? Math.max(waypoints.length * 3.5, 8) : 24);
4365
+ const calculatedDuration = (0, import_react15.useMemo)(() => {
4366
+ if (waypoints.length < 2) return 24;
4367
+ let total = 0;
4368
+ for (let i = 0; i < waypoints.length - 1; i++) {
4369
+ total += waypoints[i].segmentDuration ?? 3.5;
4370
+ }
4371
+ if (loop && waypoints.length > 1) {
4372
+ total += waypoints[waypoints.length - 1].segmentDuration ?? 3.5;
4373
+ }
4374
+ return Math.max(total, 4);
4375
+ }, [waypoints, loop]);
4376
+ const duration = config.duration ?? calculatedDuration;
4110
4377
  const signature = (0, import_react15.useMemo)(
4111
- () => waypoints.map((w) => [...w.position, ...w.target].join(",")).join(";"),
4378
+ () => waypoints.map(
4379
+ (waypoint) => [
4380
+ ...waypoint.position,
4381
+ ...waypoint.target,
4382
+ waypoint.azimuthAngle ?? "",
4383
+ waypoint.polarAngle ?? "",
4384
+ waypoint.segmentDuration ?? ""
4385
+ ].join(",")
4386
+ ).join(";"),
4112
4387
  [waypoints]
4113
4388
  );
4389
+ const segmentTimes = (0, import_react15.useMemo)(() => {
4390
+ const times = [];
4391
+ if (waypoints.length < 2) return times;
4392
+ let accumulated = 0;
4393
+ for (let i = 0; i < waypoints.length - 1; i++) {
4394
+ accumulated += waypoints[i].segmentDuration ?? 3.5;
4395
+ times.push(accumulated);
4396
+ }
4397
+ if (loop && waypoints.length > 1) {
4398
+ accumulated += waypoints[waypoints.length - 1].segmentDuration ?? 3.5;
4399
+ times.push(accumulated);
4400
+ }
4401
+ return times;
4402
+ }, [waypoints, loop]);
4403
+ const getCurveParameter = (0, import_react15.useMemo)(() => {
4404
+ return (elapsed) => {
4405
+ if (waypoints.length < 2 || segmentTimes.length === 0) {
4406
+ return elapsed / duration;
4407
+ }
4408
+ const segmentTotal = segmentTimes[segmentTimes.length - 1];
4409
+ const playbackElapsed = loop ? elapsed % duration : Math.min(elapsed, duration);
4410
+ const normalizedElapsed = duration > 0 ? playbackElapsed / duration * segmentTotal : 0;
4411
+ for (let i = 0; i < segmentTimes.length; i++) {
4412
+ if (normalizedElapsed <= segmentTimes[i]) {
4413
+ const prevTime = i === 0 ? 0 : segmentTimes[i - 1];
4414
+ const segmentDuration = segmentTimes[i] - prevTime;
4415
+ const progress = segmentDuration > 0 ? (normalizedElapsed - prevTime) / segmentDuration : 0;
4416
+ const totalSegments = loop ? waypoints.length : waypoints.length - 1;
4417
+ const baseU = i / totalSegments;
4418
+ const segmentU = 1 / totalSegments;
4419
+ return baseU + progress * segmentU;
4420
+ }
4421
+ }
4422
+ return 1;
4423
+ };
4424
+ }, [waypoints.length, segmentTimes, duration, loop]);
4114
4425
  const curves = (0, import_react15.useMemo)(() => {
4115
4426
  if (!hasPath) return null;
4116
4427
  return {
@@ -4128,29 +4439,83 @@ function CinematicCamera({
4128
4439
  )
4129
4440
  };
4130
4441
  }, [signature, hasPath, loop]);
4442
+ const orbitPath = (0, import_react15.useMemo)(() => {
4443
+ if (!hasPath || !waypoints.every(
4444
+ (waypoint) => Number.isFinite(waypoint.azimuthAngle) && Number.isFinite(waypoint.polarAngle)
4445
+ )) {
4446
+ return null;
4447
+ }
4448
+ const points = waypoints.map((waypoint) => ({
4449
+ azimuth: waypoint.azimuthAngle,
4450
+ polar: waypoint.polarAngle,
4451
+ distance: new import_three18.Vector3(...waypoint.position).distanceTo(
4452
+ new import_three18.Vector3(...waypoint.target)
4453
+ )
4454
+ }));
4455
+ for (let index = 1; index < points.length; index += 1) {
4456
+ const previous = points[index - 1].azimuth;
4457
+ const current = points[index].azimuth;
4458
+ points[index].azimuth = current + Math.round((previous - current) / (Math.PI * 2)) * Math.PI * 2;
4459
+ }
4460
+ return points;
4461
+ }, [signature, hasPath]);
4131
4462
  const elapsedRef = (0, import_react15.useRef)(0);
4132
4463
  const enterElapsedRef = (0, import_react15.useRef)(0);
4133
4464
  const enterPos = (0, import_react15.useRef)(new import_three18.Vector3());
4134
4465
  const enterTarget = (0, import_react15.useRef)(new import_three18.Vector3());
4135
4466
  const startedRef = (0, import_react15.useRef)(false);
4136
4467
  const pausedElapsedRef = (0, import_react15.useRef)(0);
4468
+ const hasFinishedRef = (0, import_react15.useRef)(false);
4137
4469
  const orbitRef = (0, import_react15.useRef)(null);
4138
4470
  const tmpPos = (0, import_react15.useMemo)(() => new import_three18.Vector3(), []);
4139
4471
  const tmpTarget = (0, import_react15.useMemo)(() => new import_three18.Vector3(), []);
4472
+ const tmpSpherical = (0, import_react15.useMemo)(() => new import_three18.Spherical(), []);
4473
+ (0, import_react15.useEffect)(() => {
4474
+ elapsedRef.current = 0;
4475
+ pausedElapsedRef.current = 0;
4476
+ enterElapsedRef.current = 0;
4477
+ hasFinishedRef.current = false;
4478
+ }, [signature, loop, duration]);
4479
+ (0, import_react15.useEffect)(() => {
4480
+ if (resetOnPlay) {
4481
+ pausedElapsedRef.current = 0;
4482
+ elapsedRef.current = 0;
4483
+ hasFinishedRef.current = false;
4484
+ }
4485
+ }, [resetOnPlay]);
4140
4486
  (0, import_react15.useEffect)(() => {
4141
4487
  if (!playing) {
4142
4488
  startedRef.current = false;
4143
- if (elapsedRef.current > 0) {
4489
+ if (elapsedRef.current > 0 && !resetOnPlay) {
4144
4490
  pausedElapsedRef.current = elapsedRef.current;
4491
+ } else if (resetOnPlay) {
4492
+ pausedElapsedRef.current = 0;
4493
+ }
4494
+ if (elapsedRef.current > 0 || hasFinishedRef.current) {
4495
+ onStop?.();
4145
4496
  }
4146
4497
  return;
4147
4498
  }
4499
+ if (elapsedRef.current >= duration && !loop) {
4500
+ elapsedRef.current = 0;
4501
+ pausedElapsedRef.current = 0;
4502
+ hasFinishedRef.current = false;
4503
+ }
4148
4504
  const controls = controlsRef.current;
4149
4505
  if (controls) {
4150
4506
  controls.getPosition(enterPos.current);
4151
4507
  controls.getTarget(enterTarget.current);
4152
4508
  }
4153
- elapsedRef.current = pausedElapsedRef.current || 0;
4509
+ if (resetOnPlay || hasFinishedRef.current || pausedElapsedRef.current === 0) {
4510
+ elapsedRef.current = 0;
4511
+ pausedElapsedRef.current = 0;
4512
+ hasFinishedRef.current = false;
4513
+ } else if (pausedElapsedRef.current > 0 && pausedElapsedRef.current < duration) {
4514
+ elapsedRef.current = pausedElapsedRef.current;
4515
+ } else {
4516
+ elapsedRef.current = 0;
4517
+ pausedElapsedRef.current = 0;
4518
+ }
4154
4519
  enterElapsedRef.current = 0;
4155
4520
  if (!hasPath) {
4156
4521
  const scene = sceneRef.current;
@@ -4164,12 +4529,26 @@ function CinematicCamera({
4164
4529
  }
4165
4530
  startedRef.current = true;
4166
4531
  invalidate();
4167
- }, [playing, hasPath, controlsRef, sceneRef, invalidate]);
4532
+ }, [
4533
+ playing,
4534
+ hasPath,
4535
+ controlsRef,
4536
+ sceneRef,
4537
+ invalidate,
4538
+ duration,
4539
+ loop,
4540
+ signature,
4541
+ resetOnPlay,
4542
+ onStop
4543
+ ]);
4168
4544
  (0, import_react15.useEffect)(() => {
4169
4545
  if (!playing || !onUserInteract) return;
4170
4546
  const controls = controlsRef.current;
4171
4547
  if (!controls) return;
4172
- const handle = () => onUserInteract();
4548
+ const handle = () => {
4549
+ if (hasFinishedRef.current) return;
4550
+ onUserInteract();
4551
+ };
4173
4552
  controls.addEventListener("controlstart", handle);
4174
4553
  return () => controls.removeEventListener("controlstart", handle);
4175
4554
  }, [playing, controlsRef, onUserInteract]);
@@ -4179,12 +4558,33 @@ function CinematicCamera({
4179
4558
  if (!controls) return;
4180
4559
  elapsedRef.current += delta;
4181
4560
  enterElapsedRef.current += delta;
4182
- let u = elapsedRef.current / duration;
4183
- const finished = !loop && u >= 1;
4561
+ let u = getCurveParameter(elapsedRef.current);
4562
+ const finished = !loop && elapsedRef.current >= duration;
4184
4563
  u = loop ? u % 1 : Math.min(u, 1);
4185
4564
  if (curves) {
4186
- curves.position.getPointAt(u, tmpPos);
4187
- curves.target.getPointAt(u, tmpTarget);
4565
+ curves.target.getPoint(u, tmpTarget);
4566
+ if (orbitPath) {
4567
+ const segmentCount = loop ? orbitPath.length : orbitPath.length - 1;
4568
+ const scaled = Math.min(u, 1) * segmentCount;
4569
+ const segment = Math.min(Math.floor(scaled), segmentCount - 1);
4570
+ const next = (segment + 1) % orbitPath.length;
4571
+ const localT = u >= 1 ? 1 : scaled - segment;
4572
+ const eased = localT * localT * (3 - 2 * localT);
4573
+ const start = orbitPath[segment];
4574
+ const end = orbitPath[next];
4575
+ const endAzimuth = end.azimuth + Math.round((start.azimuth - end.azimuth) / (Math.PI * 2)) * Math.PI * 2;
4576
+ const azimuth = start.azimuth + (endAzimuth - start.azimuth) * eased;
4577
+ const polar = start.polar + (end.polar - start.polar) * eased;
4578
+ const distance = start.distance + (end.distance - start.distance) * eased;
4579
+ tmpSpherical.set(
4580
+ Math.max(distance, 1e-6),
4581
+ Math.min(Math.max(polar, 1e-6), Math.PI - 1e-6),
4582
+ azimuth
4583
+ );
4584
+ tmpPos.setFromSpherical(tmpSpherical).add(tmpTarget);
4585
+ } else {
4586
+ curves.position.getPoint(u, tmpPos);
4587
+ }
4188
4588
  } else {
4189
4589
  const orbit = orbitRef.current;
4190
4590
  if (!orbit) return;
@@ -4214,6 +4614,8 @@ function CinematicCamera({
4214
4614
  );
4215
4615
  if (finished) {
4216
4616
  startedRef.current = false;
4617
+ hasFinishedRef.current = true;
4618
+ elapsedRef.current = 0;
4217
4619
  pausedElapsedRef.current = 0;
4218
4620
  onFinished?.();
4219
4621
  return;
@@ -5470,7 +5872,8 @@ function ModelViewer({
5470
5872
  usdzUrl,
5471
5873
  arScaleMode = "normalized",
5472
5874
  mobileHandoffUrl,
5473
- showAnnotationNavigation = true
5875
+ showAnnotationNavigation = true,
5876
+ showAnnotationOnHover = false
5474
5877
  }) {
5475
5878
  const { activeTab, activeSubTab } = useBindingBuilderContext();
5476
5879
  const license = useLicense(licenseKey);
@@ -5540,6 +5943,7 @@ function ModelViewer({
5540
5943
  !modelUrl || Boolean(onAutoFit) || Boolean(camera)
5541
5944
  );
5542
5945
  const [downloadMenuOpen, setDownloadMenuOpen] = (0, import_react22.useState)(false);
5946
+ const [uvExportError, setUvExportError] = (0, import_react22.useState)(null);
5543
5947
  const [modelLoaded, setModelLoaded] = (0, import_react22.useState)(false);
5544
5948
  const [modelLoadFailed, setModelLoadFailed] = (0, import_react22.useState)(false);
5545
5949
  const [bindingsMismatch, setBindingsMismatch] = (0, import_react22.useState)(false);
@@ -5594,9 +5998,11 @@ function ModelViewer({
5594
5998
  return { ...base ?? {}, ...cinematic };
5595
5999
  }, [cinematic, resolvedSceneConfig]);
5596
6000
  const [cinematicPlaying, setCinematicPlaying] = (0, import_react22.useState)(false);
6001
+ const [cinematicReset, setCinematicReset] = (0, import_react22.useState)(false);
5597
6002
  const cinematicAutoStartedRef = (0, import_react22.useRef)(false);
5598
6003
  (0, import_react22.useEffect)(() => {
5599
6004
  setCinematicPlaying(false);
6005
+ setCinematicReset(false);
5600
6006
  cinematicAutoStartedRef.current = false;
5601
6007
  }, [modelUrl]);
5602
6008
  (0, import_react22.useEffect)(() => {
@@ -5604,6 +6010,7 @@ function ModelViewer({
5604
6010
  if (modelLoaded && cameraPositioned && !isAutoFraming) {
5605
6011
  cinematicAutoStartedRef.current = true;
5606
6012
  setCinematicPlaying(true);
6013
+ setCinematicReset(false);
5607
6014
  }
5608
6015
  }, [cinematicConfig, modelLoaded, cameraPositioned, isAutoFraming]);
5609
6016
  const xrStore = (0, import_react22.useMemo)(() => {
@@ -6728,6 +7135,17 @@ function ModelViewer({
6728
7135
  console.error("Screenshot failed:", err);
6729
7136
  });
6730
7137
  }, [downloadFilename]);
7138
+ const handleDownloadUvLayout = (0, import_react22.useCallback)(() => {
7139
+ const scene = sceneRef.current;
7140
+ if (!scene) return;
7141
+ const files = downloadUvLayouts(scene, downloadFilename);
7142
+ if (files.length === 0) {
7143
+ setUvExportError("This model does not contain UV coordinates.");
7144
+ return;
7145
+ }
7146
+ setUvExportError(null);
7147
+ setDownloadMenuOpen(false);
7148
+ }, [downloadFilename]);
6731
7149
  const handleReset = (0, import_react22.useCallback)(async () => {
6732
7150
  if (onAutoFit) {
6733
7151
  await onAutoFit();
@@ -6757,6 +7175,20 @@ function ModelViewer({
6757
7175
  console.error("Reset camera error:", error);
6758
7176
  }
6759
7177
  }, [onAutoFit, fitScene]);
7178
+ const handleCinematicPlay = (0, import_react22.useCallback)(() => {
7179
+ setCinematicReset(false);
7180
+ setCinematicPlaying(true);
7181
+ }, []);
7182
+ const handleCinematicPause = (0, import_react22.useCallback)(() => {
7183
+ setCinematicPlaying(false);
7184
+ }, []);
7185
+ const handleCinematicStop = (0, import_react22.useCallback)(() => {
7186
+ setCinematicPlaying(false);
7187
+ setCinematicReset(true);
7188
+ setTimeout(() => {
7189
+ handleReset();
7190
+ }, 100);
7191
+ }, [handleReset]);
6760
7192
  const handleCameraChangeWithAnnotations = (0, import_react22.useCallback)(
6761
7193
  (cameraValue, controls) => {
6762
7194
  handleCameraChangeInternal(cameraValue, controls);
@@ -7054,7 +7486,8 @@ function ModelViewer({
7054
7486
  modelLoaded,
7055
7487
  onAnnotationClick: handleAnnotationMarkerClick,
7056
7488
  onCloseAnnotation: closeViewingAnnotation,
7057
- hideCloseButton: isGuidedTourActive
7489
+ hideCloseButton: isGuidedTourActive,
7490
+ showAnnotationOnHover
7058
7491
  }
7059
7492
  ),
7060
7493
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -7073,8 +7506,14 @@ function ModelViewer({
7073
7506
  sceneRef,
7074
7507
  config: cinematicConfig,
7075
7508
  playing: cinematicPlaying,
7076
- onFinished: () => setCinematicPlaying(false),
7077
- onUserInteract: () => setCinematicPlaying(false)
7509
+ resetOnPlay: cinematicReset,
7510
+ onFinished: () => {
7511
+ setCinematicPlaying(false);
7512
+ setCinematicReset(false);
7513
+ },
7514
+ onUserInteract: () => {
7515
+ setCinematicPlaying(false);
7516
+ }
7078
7517
  }
7079
7518
  ),
7080
7519
  showMeasureTools && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -7302,24 +7741,43 @@ function ModelViewer({
7302
7741
  ]
7303
7742
  }
7304
7743
  ),
7305
- cinematicConfig && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
7744
+ cinematicConfig && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
7306
7745
  "div",
7307
7746
  {
7308
7747
  className: "ri-cinematicTools",
7309
7748
  style: {
7310
7749
  top: `${topLeftRow((xrToolsVisible ? 1 : 0) + (showMeasureTools ? 1 : 0) + (showExplodeControls ? 1 : 0))}px`
7311
7750
  },
7312
- children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
7313
- "button",
7314
- {
7315
- type: "button",
7316
- className: `ri-measureButton ${cinematicPlaying ? "ri-measureButton--active" : ""}`,
7317
- onClick: () => setCinematicPlaying((prev) => !prev),
7318
- title: cinematicPlaying ? "Pause cinematic" : "Play cinematic",
7319
- "aria-pressed": cinematicPlaying,
7320
- children: cinematicPlaying ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react3.Pause, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react3.Play, { size: 16 })
7321
- }
7322
- )
7751
+ children: [
7752
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
7753
+ "button",
7754
+ {
7755
+ type: "button",
7756
+ className: `ri-measureButton ${cinematicPlaying ? "ri-measureButton--active" : ""}`,
7757
+ onClick: () => {
7758
+ if (cinematicPlaying) {
7759
+ setCinematicPlaying(false);
7760
+ } else {
7761
+ setCinematicReset(false);
7762
+ setCinematicPlaying(true);
7763
+ }
7764
+ },
7765
+ title: cinematicPlaying ? "Pause cinematic" : "Play cinematic",
7766
+ "aria-pressed": cinematicPlaying,
7767
+ children: cinematicPlaying ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react3.Pause, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react3.Play, { size: 16 })
7768
+ }
7769
+ ),
7770
+ cinematicPlaying && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
7771
+ "button",
7772
+ {
7773
+ type: "button",
7774
+ className: "ri-measureButton ri-measureButton--stop",
7775
+ onClick: handleCinematicStop,
7776
+ title: "Stop cinematic and reset camera",
7777
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react3.Square, { size: 16 })
7778
+ }
7779
+ )
7780
+ ]
7323
7781
  }
7324
7782
  ),
7325
7783
  (showResetButton || showDownloadButton || showAnnotationNavigation && sortedAnnotations.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "ri-buttonContainer", children: [
@@ -7422,13 +7880,38 @@ function ModelViewer({
7422
7880
  "Download PNG"
7423
7881
  ]
7424
7882
  }
7883
+ ),
7884
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "ri-downloadMenuDivider" }),
7885
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
7886
+ "button",
7887
+ {
7888
+ className: "ri-downloadMenuItem",
7889
+ onClick: handleDownloadUvLayout,
7890
+ disabled: !sceneRef.current,
7891
+ children: [
7892
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react3.FileImage, { size: 14 }),
7893
+ "Download UV Layout"
7894
+ ]
7895
+ }
7896
+ ),
7897
+ uvExportError && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
7898
+ "div",
7899
+ {
7900
+ className: "ri-downloadMenuNotice",
7901
+ role: "status",
7902
+ "aria-live": "polite",
7903
+ children: uvExportError
7904
+ }
7425
7905
  )
7426
7906
  ] }),
7427
7907
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
7428
7908
  "button",
7429
7909
  {
7430
7910
  className: "ri-downloadButton",
7431
- onClick: () => setDownloadMenuOpen((v) => !v),
7911
+ onClick: () => {
7912
+ setUvExportError(null);
7913
+ setDownloadMenuOpen((v) => !v);
7914
+ },
7432
7915
  disabled: isDownloading || !sceneRef.current,
7433
7916
  title: "Download options",
7434
7917
  children: isDownloading ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
@@ -7559,7 +8042,7 @@ function ModelViewer({
7559
8042
  }
7560
8043
 
7561
8044
  // src/components/BindingBuilder/BindingBuilder.tsx
7562
- var import_react38 = require("react");
8045
+ var import_react39 = require("react");
7563
8046
  var import_lucide_react16 = require("lucide-react");
7564
8047
 
7565
8048
  // src/styles/BindingBuilder.css
@@ -7599,14 +8082,16 @@ function buildCameraState(camera, controls) {
7599
8082
  position: toTuple(position),
7600
8083
  target: toTuple(target),
7601
8084
  fov: "fov" in camera && typeof camera.fov === "number" ? camera.fov : null,
7602
- zoom: "zoom" in camera && typeof camera.zoom === "number" ? camera.zoom : 1
8085
+ zoom: "zoom" in camera && typeof camera.zoom === "number" ? camera.zoom : 1,
8086
+ azimuthAngle: controls.azimuthAngle,
8087
+ polarAngle: controls.polarAngle
7603
8088
  };
7604
8089
  }
7605
8090
  function isSameCameraState(left, right) {
7606
8091
  if (!left) {
7607
8092
  return false;
7608
8093
  }
7609
- return left.position[0] === right.position[0] && left.position[1] === right.position[1] && left.position[2] === right.position[2] && left.target[0] === right.target[0] && left.target[1] === right.target[1] && left.target[2] === right.target[2] && left.fov === right.fov && left.zoom === right.zoom;
8094
+ return left.position[0] === right.position[0] && left.position[1] === right.position[1] && left.position[2] === right.position[2] && left.target[0] === right.target[0] && left.target[1] === right.target[1] && left.target[2] === right.target[2] && left.fov === right.fov && left.zoom === right.zoom && left.azimuthAngle === right.azimuthAngle && left.polarAngle === right.polarAngle;
7610
8095
  }
7611
8096
  async function runSmoothCameraTransition2(controls, action, smoothTime = 0.6) {
7612
8097
  const originalSmoothTime = controls.smoothTime;
@@ -7761,15 +8246,27 @@ function useViewerCamera() {
7761
8246
  }
7762
8247
  try {
7763
8248
  markInitialAutoFitOverride(controls);
7764
- await controls.setLookAt(
7765
- state.position[0],
7766
- state.position[1],
7767
- state.position[2],
7768
- state.target[0],
7769
- state.target[1],
7770
- state.target[2],
7771
- enableTransition
7772
- );
8249
+ const transitions = [
8250
+ controls.setLookAt(
8251
+ state.position[0],
8252
+ state.position[1],
8253
+ state.position[2],
8254
+ state.target[0],
8255
+ state.target[1],
8256
+ state.target[2],
8257
+ enableTransition
8258
+ )
8259
+ ];
8260
+ if (state.azimuthAngle != null && state.polarAngle != null) {
8261
+ transitions.push(
8262
+ controls.rotateTo(
8263
+ state.azimuthAngle,
8264
+ state.polarAngle,
8265
+ enableTransition
8266
+ )
8267
+ );
8268
+ }
8269
+ await Promise.all(transitions);
7773
8270
  const camera = controls.camera;
7774
8271
  let projectionChanged = false;
7775
8272
  if (state.fov !== null && "fov" in camera && typeof camera.fov === "number") {
@@ -7893,7 +8390,7 @@ function useViewerAnimations() {
7893
8390
  }
7894
8391
 
7895
8392
  // src/utils/modelFileResolver.ts
7896
- var import_fflate = require("fflate");
8393
+ var import_fflate2 = require("fflate");
7897
8394
  var SUPPORTED_MODEL_EXTENSIONS = [".glb", ".gltf", ".zip"];
7898
8395
  var SUPPORTED_MODEL_MIME_TYPES = [
7899
8396
  "model/gltf-binary",
@@ -7951,7 +8448,7 @@ async function resolveStandaloneGltfFile(file) {
7951
8448
  };
7952
8449
  }
7953
8450
  async function resolveZippedGltfFile(file) {
7954
- const zip = (0, import_fflate.unzipSync)(new Uint8Array(await file.arrayBuffer()));
8451
+ const zip = (0, import_fflate2.unzipSync)(new Uint8Array(await file.arrayBuffer()));
7955
8452
  const entries = /* @__PURE__ */ new Map();
7956
8453
  for (const [rawPath, bytes] of Object.entries(zip)) {
7957
8454
  const path = normalizeZipPath(rawPath);
@@ -7972,13 +8469,13 @@ async function resolveZippedGltfFile(file) {
7972
8469
  if (!gltfBytes) {
7973
8470
  throw new Error("Invalid GLTF zip: no .gltf found.");
7974
8471
  }
7975
- const gltf = parseGltfJson((0, import_fflate.strFromU8)(gltfBytes), gltfPath);
8472
+ const gltf = parseGltfJson((0, import_fflate2.strFromU8)(gltfBytes), gltfPath);
7976
8473
  const rootDir = getDirname(gltfPath);
7977
8474
  const urlsToRevoke = [];
7978
8475
  try {
7979
8476
  rewriteZipUris(gltf.buffers, rootDir, entries, urlsToRevoke);
7980
8477
  rewriteZipUris(gltf.images, rootDir, entries, urlsToRevoke);
7981
- const gltfBlob = new Blob([(0, import_fflate.strToU8)(JSON.stringify(gltf))], {
8478
+ const gltfBlob = new Blob([(0, import_fflate2.strToU8)(JSON.stringify(gltf))], {
7982
8479
  type: "model/gltf+json"
7983
8480
  });
7984
8481
  const gltfUrl = URL.createObjectURL(gltfBlob);
@@ -8077,7 +8574,7 @@ function inferMimeType(path) {
8077
8574
  }
8078
8575
 
8079
8576
  // src/components/BindingBuilder/SceneSettings/SceneSettingsTab.tsx
8080
- var import_react32 = require("react");
8577
+ var import_react33 = require("react");
8081
8578
 
8082
8579
  // src/components/BindingBuilder/SceneSettings/GeneralSceneSettings.tsx
8083
8580
  var import_react27 = require("react");
@@ -8258,9 +8755,10 @@ function SliderRow({
8258
8755
  max,
8259
8756
  step,
8260
8757
  formatValue,
8261
- onChange
8758
+ onChange,
8759
+ disabled = false
8262
8760
  }) {
8263
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("label", { className: "se-field", children: [
8761
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("label", { className: `se-field ${disabled ? "se-field--disabled" : ""}`, children: [
8264
8762
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "se-fieldLabel", children: [
8265
8763
  label,
8266
8764
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "se-fieldValue", children: formatValue ? formatValue(value) : value })
@@ -8274,7 +8772,8 @@ function SliderRow({
8274
8772
  max,
8275
8773
  step,
8276
8774
  value,
8277
- onChange: (e) => onChange(Number(e.target.value))
8775
+ onChange: (e) => onChange(Number(e.target.value)),
8776
+ disabled
8278
8777
  }
8279
8778
  )
8280
8779
  ] });
@@ -11690,6 +12189,7 @@ function AnnotationsTab({ config, onChange }) {
11690
12189
  }
11691
12190
 
11692
12191
  // src/components/BindingBuilder/SceneSettings/CinematicTab.tsx
12192
+ var import_react32 = require("react");
11693
12193
  var import_lucide_react10 = require("lucide-react");
11694
12194
  var import_jsx_runtime31 = require("react/jsx-runtime");
11695
12195
  var DEFAULT_CINEMATIC = {
@@ -11698,6 +12198,7 @@ var DEFAULT_CINEMATIC = {
11698
12198
  loop: true,
11699
12199
  autoPlay: false
11700
12200
  };
12201
+ var DEFAULT_SEGMENT_DURATION = 3.5;
11701
12202
  function CinematicTab({
11702
12203
  config,
11703
12204
  onChange,
@@ -11706,31 +12207,76 @@ function CinematicTab({
11706
12207
  }) {
11707
12208
  const cinematic = config.cinematic ?? DEFAULT_CINEMATIC;
11708
12209
  const { waypoints } = cinematic;
11709
- const update = (patch) => {
12210
+ const calculateTotalDuration = (0, import_react32.useCallback)((wps, loop) => {
12211
+ if (wps.length < 2) return 0;
12212
+ let total = 0;
12213
+ for (let i = 0; i < wps.length - 1; i++) {
12214
+ const segDur = wps[i].segmentDuration ?? DEFAULT_SEGMENT_DURATION;
12215
+ total += segDur;
12216
+ }
12217
+ if (loop && wps.length > 1) {
12218
+ total += wps[wps.length - 1].segmentDuration ?? DEFAULT_SEGMENT_DURATION;
12219
+ }
12220
+ return Math.max(total, 0);
12221
+ }, []);
12222
+ const update = (0, import_react32.useCallback)((patch) => {
11710
12223
  onChange({ ...config, cinematic: { ...cinematic, ...patch } });
11711
- };
11712
- const addWaypoint = () => {
12224
+ }, [config, cinematic, onChange]);
12225
+ const addWaypoint = (0, import_react32.useCallback)(() => {
11713
12226
  if (!cameraState) return;
12227
+ const newWaypoint = {
12228
+ position: [...cameraState.position],
12229
+ target: [...cameraState.target],
12230
+ ...cameraState.azimuthAngle != null ? { azimuthAngle: cameraState.azimuthAngle } : {},
12231
+ ...cameraState.polarAngle != null ? { polarAngle: cameraState.polarAngle } : {},
12232
+ segmentDuration: DEFAULT_SEGMENT_DURATION
12233
+ };
12234
+ const newWaypoints = [...waypoints, newWaypoint];
12235
+ const newDuration = calculateTotalDuration(newWaypoints, cinematic.loop);
11714
12236
  update({
11715
- waypoints: [
11716
- ...waypoints,
11717
- {
11718
- position: [...cameraState.position],
11719
- target: [...cameraState.target]
11720
- }
11721
- ]
12237
+ waypoints: newWaypoints,
12238
+ duration: newDuration
11722
12239
  });
11723
- };
11724
- const deleteWaypoint = (index) => {
11725
- update({ waypoints: waypoints.filter((_, i) => i !== index) });
11726
- };
11727
- const moveWaypoint = (index, delta) => {
12240
+ }, [cameraState, waypoints, cinematic.loop, calculateTotalDuration, update]);
12241
+ const deleteWaypoint = (0, import_react32.useCallback)((index) => {
12242
+ const newWaypoints = waypoints.filter((_, i) => i !== index);
12243
+ const newDuration = calculateTotalDuration(newWaypoints, cinematic.loop);
12244
+ update({
12245
+ waypoints: newWaypoints,
12246
+ duration: newDuration
12247
+ });
12248
+ }, [waypoints, cinematic.loop, calculateTotalDuration, update]);
12249
+ const moveWaypoint = (0, import_react32.useCallback)((index, delta) => {
11728
12250
  const next = index + delta;
11729
12251
  if (next < 0 || next >= waypoints.length) return;
11730
12252
  const reordered = [...waypoints];
11731
12253
  [reordered[index], reordered[next]] = [reordered[next], reordered[index]];
11732
- update({ waypoints: reordered });
11733
- };
12254
+ const newDuration = calculateTotalDuration(reordered, cinematic.loop);
12255
+ update({
12256
+ waypoints: reordered,
12257
+ duration: newDuration
12258
+ });
12259
+ }, [waypoints, cinematic.loop, calculateTotalDuration, update]);
12260
+ const updateWaypoint = (0, import_react32.useCallback)((index, updates) => {
12261
+ const newWaypoints = waypoints.map((wp, i) => {
12262
+ if (i === index) {
12263
+ return { ...wp, ...updates };
12264
+ }
12265
+ return wp;
12266
+ });
12267
+ const newDuration = calculateTotalDuration(newWaypoints, cinematic.loop);
12268
+ update({
12269
+ waypoints: newWaypoints,
12270
+ duration: newDuration
12271
+ });
12272
+ }, [waypoints, cinematic.loop, calculateTotalDuration, update]);
12273
+ const [customizeDurations, setCustomizeDurations] = (0, import_react32.useState)(false);
12274
+ const hasSegmentDurations = waypoints.some((w) => w.segmentDuration !== void 0);
12275
+ const getDuration = (0, import_react32.useCallback)((index) => {
12276
+ if (index >= waypoints.length - 1) return DEFAULT_SEGMENT_DURATION;
12277
+ return waypoints[index]?.segmentDuration ?? DEFAULT_SEGMENT_DURATION;
12278
+ }, [waypoints]);
12279
+ const hasEnoughWaypoints = waypoints.length >= 2;
11734
12280
  return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(Section, { title: "Cinematic Camera", children: [
11735
12281
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "se-cinematicHint", children: "Frame the model in the viewer, then capture it as a waypoint. The camera glides through the waypoints in order \u2014 enable and press play in the viewer to preview." }),
11736
12282
  /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
@@ -11747,62 +12293,165 @@ function CinematicTab({
11747
12293
  ]
11748
12294
  }
11749
12295
  ),
11750
- waypoints.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "bb-emptyAnnotations", children: "No waypoints yet. Position the camera and add your first one." }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "se-waypointList", children: waypoints.map((waypoint, index) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointCard", children: [
11751
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "se-waypointBadge", children: index + 1 }),
11752
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointBody", children: [
11753
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointCoords", children: [
11754
- "pos [",
11755
- waypoint.position.map((n) => n.toFixed(1)).join(", "),
11756
- "]"
11757
- ] }),
11758
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointCoords", children: [
11759
- "look [",
11760
- waypoint.target.map((n) => n.toFixed(1)).join(", "),
11761
- "]"
11762
- ] })
11763
- ] }),
11764
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointActions", children: [
12296
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-customizeToggle", children: [
12297
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("label", { className: "se-toggleLabel", children: [
11765
12298
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
11766
- "button",
11767
- {
11768
- className: "se-waypointAction",
11769
- onClick: () => onPreviewWaypoint?.(waypoint),
11770
- title: "Fly camera to this waypoint",
11771
- disabled: !onPreviewWaypoint,
11772
- children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.MapPin, { size: 15 })
11773
- }
11774
- ),
11775
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
11776
- "button",
11777
- {
11778
- className: "se-waypointAction",
11779
- onClick: () => moveWaypoint(index, -1),
11780
- title: "Move up",
11781
- disabled: index === 0,
11782
- children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.ArrowUp, { size: 15 })
11783
- }
11784
- ),
11785
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
11786
- "button",
12299
+ "input",
11787
12300
  {
11788
- className: "se-waypointAction",
11789
- onClick: () => moveWaypoint(index, 1),
11790
- title: "Move down",
11791
- disabled: index === waypoints.length - 1,
11792
- children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.ArrowDown, { size: 15 })
12301
+ type: "checkbox",
12302
+ checked: customizeDurations,
12303
+ onChange: (e) => setCustomizeDurations(e.target.checked)
11793
12304
  }
11794
12305
  ),
11795
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
11796
- "button",
11797
- {
11798
- className: "se-waypointAction se-waypointAction--delete",
11799
- onClick: () => deleteWaypoint(index),
11800
- title: "Delete waypoint",
11801
- children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.Trash2, { size: 15 })
11802
- }
11803
- )
12306
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.Settings, { size: 14 }),
12307
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { children: "Customize durations between waypoints" })
12308
+ ] }),
12309
+ customizeDurations && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "se-toggleHint", children: [
12310
+ "Set duration for each segment between waypoints: 1\u21922, 2\u21923, etc. Default: ",
12311
+ DEFAULT_SEGMENT_DURATION,
12312
+ "s"
11804
12313
  ] })
11805
- ] }, index)) }),
12314
+ ] }),
12315
+ waypoints.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "bb-emptyAnnotations", children: "No waypoints yet. Position the camera and add your first one." }) : /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
12316
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "se-waypointList", children: waypoints.map((waypoint, index) => {
12317
+ const dur = getDuration(index);
12318
+ const isLast = index === waypoints.length - 1;
12319
+ const isCustom = waypoint.segmentDuration !== void 0 && waypoint.segmentDuration !== DEFAULT_SEGMENT_DURATION;
12320
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { children: [
12321
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointCard", children: [
12322
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "se-waypointBadge", children: index + 1 }),
12323
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointBody", children: [
12324
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointCoords", children: [
12325
+ "pos [",
12326
+ waypoint.position.map((n) => n.toFixed(1)).join(", "),
12327
+ "]"
12328
+ ] }),
12329
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointCoords", children: [
12330
+ "look [",
12331
+ waypoint.target.map((n) => n.toFixed(1)).join(", "),
12332
+ "]"
12333
+ ] }),
12334
+ customizeDurations && !isLast && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: `se-customBadge ${isCustom ? "se-customBadge--custom" : "se-customBadge--default"}`, children: [
12335
+ dur.toFixed(1),
12336
+ "s",
12337
+ !isCustom && " (default)"
12338
+ ] })
12339
+ ] }),
12340
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-waypointActions", children: [
12341
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
12342
+ "button",
12343
+ {
12344
+ className: "se-waypointAction",
12345
+ onClick: () => onPreviewWaypoint?.(waypoint),
12346
+ title: "Fly camera to this waypoint",
12347
+ disabled: !onPreviewWaypoint,
12348
+ children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.MapPin, { size: 15 })
12349
+ }
12350
+ ),
12351
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
12352
+ "button",
12353
+ {
12354
+ className: "se-waypointAction",
12355
+ onClick: () => moveWaypoint(index, -1),
12356
+ title: "Move up",
12357
+ disabled: index === 0,
12358
+ children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.ArrowUp, { size: 15 })
12359
+ }
12360
+ ),
12361
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
12362
+ "button",
12363
+ {
12364
+ className: "se-waypointAction",
12365
+ onClick: () => moveWaypoint(index, 1),
12366
+ title: "Move down",
12367
+ disabled: isLast,
12368
+ children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.ArrowDown, { size: 15 })
12369
+ }
12370
+ ),
12371
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
12372
+ "button",
12373
+ {
12374
+ className: "se-waypointAction se-waypointAction--delete",
12375
+ onClick: () => deleteWaypoint(index),
12376
+ title: "Delete waypoint",
12377
+ children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_lucide_react10.Trash2, { size: 15 })
12378
+ }
12379
+ )
12380
+ ] })
12381
+ ] }),
12382
+ customizeDurations && !isLast && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-segmentBetween", children: [
12383
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-segmentLabel", children: [
12384
+ index + 1,
12385
+ " \u2192 ",
12386
+ index + 2,
12387
+ !waypoint.segmentDuration && " (default)"
12388
+ ] }),
12389
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-segmentControls", children: [
12390
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "se-durationLabel", children: "Duration:" }),
12391
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
12392
+ "input",
12393
+ {
12394
+ type: "range",
12395
+ className: "se-segmentSlider",
12396
+ min: 1,
12397
+ max: 20,
12398
+ step: 0.5,
12399
+ value: dur,
12400
+ onChange: (e) => {
12401
+ const val = parseFloat(e.target.value);
12402
+ updateWaypoint(index, { segmentDuration: val });
12403
+ }
12404
+ }
12405
+ ),
12406
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { className: "se-segmentValue", children: [
12407
+ dur.toFixed(1),
12408
+ "s"
12409
+ ] }),
12410
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-presetButtons", children: [
12411
+ [2, 4, 6, 8].map((preset) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
12412
+ "button",
12413
+ {
12414
+ className: "se-presetButton",
12415
+ onClick: () => updateWaypoint(index, { segmentDuration: preset }),
12416
+ children: [
12417
+ preset,
12418
+ "s"
12419
+ ]
12420
+ },
12421
+ `preset-${index}-${preset}`
12422
+ )),
12423
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
12424
+ "button",
12425
+ {
12426
+ className: "se-presetButton se-presetButton--default",
12427
+ onClick: () => {
12428
+ const newWaypoints = waypoints.map((wp, i) => {
12429
+ if (i === index) {
12430
+ const { segmentDuration, ...rest } = wp;
12431
+ return rest;
12432
+ }
12433
+ return wp;
12434
+ });
12435
+ const newDuration = calculateTotalDuration(newWaypoints, cinematic.loop);
12436
+ update({
12437
+ waypoints: newWaypoints,
12438
+ duration: newDuration
12439
+ });
12440
+ },
12441
+ children: "Reset"
12442
+ }
12443
+ )
12444
+ ] })
12445
+ ] })
12446
+ ] })
12447
+ ] }, `waypoint-wrapper-${index}`);
12448
+ }) }),
12449
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "se-totalDuration", children: [
12450
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "se-totalLabel", children: "Total duration:" }),
12451
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "se-totalValue", children: hasEnoughWaypoints ? (cinematic.duration ?? calculateTotalDuration(waypoints, cinematic.loop)).toFixed(1) + "s" : "-" }),
12452
+ !hasEnoughWaypoints && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "se-totalHint", children: "(need at least 2 waypoints)" })
12453
+ ] })
12454
+ ] }),
11806
12455
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
11807
12456
  ToggleRow,
11808
12457
  {
@@ -11816,7 +12465,11 @@ function CinematicTab({
11816
12465
  {
11817
12466
  label: "Loop",
11818
12467
  checked: cinematic.loop,
11819
- onChange: (loop) => update({ loop })
12468
+ onChange: (loop) => {
12469
+ const newLoop = loop;
12470
+ const newDuration = calculateTotalDuration(waypoints, newLoop);
12471
+ update({ loop: newLoop, duration: newDuration });
12472
+ }
11820
12473
  }
11821
12474
  ),
11822
12475
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
@@ -11827,11 +12480,11 @@ function CinematicTab({
11827
12480
  onChange: (autoPlay) => update({ autoPlay })
11828
12481
  }
11829
12482
  ),
11830
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
12483
+ !customizeDurations && hasEnoughWaypoints && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
11831
12484
  SliderRow,
11832
12485
  {
11833
12486
  label: "Duration",
11834
- value: cinematic.duration ?? Math.max(waypoints.length * 3.5, 8),
12487
+ value: cinematic.duration ?? calculateTotalDuration(waypoints, cinematic.loop),
11835
12488
  min: 4,
11836
12489
  max: 120,
11837
12490
  step: 1,
@@ -11843,7 +12496,7 @@ function CinematicTab({
11843
12496
  }
11844
12497
 
11845
12498
  // src/styles/SceneSettingsTab.css
11846
- styleInject('.se-tabContent {\n display: flex;\n flex-direction: column;\n}\n.se-tabContent * {\n box-sizing: border-box;\n}\n.se-subTabRow {\n display: flex;\n justify-content: space-between;\n gap: 6px;\n padding-bottom: 16px;\n border-bottom: 1px solid rgba(148, 163, 184, 0.1);\n margin-bottom: 4px;\n}\n.se-subTab {\n padding: 8px 12px;\n display: flex;\n justify-items: center;\n border-radius: 10px;\n border: 1px solid rgba(148, 163, 184, 0.18);\n background: rgba(255, 255, 255, 0.04);\n color: #b3bfd2;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n cursor: pointer;\n transition:\n background-color 0.18s ease,\n border-color 0.18s ease,\n color 0.18s ease;\n}\n.se-subTab:hover {\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(148, 163, 184, 0.32);\n}\n.se-subTab--active {\n background: rgba(99, 102, 241, 0.18);\n border-color: rgba(99, 102, 241, 0.4);\n color: #f8fafc;\n}\n.se-generalContent {\n display: flex;\n flex-direction: column;\n padding-top: 0;\n}\n.se-placeholder {\n font-size: 13px;\n color: #93a4c0;\n}\n.se-section {\n margin-bottom: 10px;\n border-radius: 7px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(148, 163, 184, 0.12);\n overflow: hidden;\n padding: 0;\n}\n.se-section:last-child {\n margin-bottom: 0;\n}\n.se-sectionTitle {\n margin: 0;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: #c3cbdb;\n padding: 11px 14px;\n border-bottom: 1px solid rgba(148, 163, 184, 0.1);\n}\n.se-sectionBody {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px;\n}\n.se-field {\n display: flex;\n flex-direction: column;\n gap: 5px;\n}\n.se-fieldInline {\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.se-fieldLabel {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n font-size: 12px;\n font-weight: 700;\n color: #d7dee9;\n}\n.se-fieldValue {\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n color: #93a4c0;\n}\n.se-slider {\n width: 100%;\n accent-color: #2aa8d8;\n cursor: pointer;\n}\n.se-checkbox {\n width: 18px;\n height: 18px;\n accent-color: #2aa8d8;\n cursor: pointer;\n}\n.se-select,\n.se-textInput,\n.se-numberInput {\n background: rgba(15, 23, 42, 0.28);\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 7px;\n color: #f8fafc;\n font-size: 13px;\n padding: 11px 13px;\n outline: none;\n}\n.se-select:focus,\n.se-textInput:focus,\n.se-numberInput:focus {\n outline: none;\n border-color: rgba(42, 168, 216, 0.65);\n box-shadow: 0 0 0 4px rgba(42, 168, 216, 0.12);\n}\n.se-select {\n min-width: 130px;\n text-transform: capitalize;\n}\n.se-textInput {\n width: 100%;\n}\n.se-numberInput {\n width: 100%;\n}\n.se-swatch {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n min-width: 96px;\n height: 30px;\n padding: 0 8px;\n border-radius: 8px;\n border: 1px solid rgba(148, 163, 184, 0.35);\n cursor: pointer;\n box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);\n}\n.se-swatchCode {\n font-size: 11px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n color: #fff;\n mix-blend-mode: difference;\n}\n.se-colorPicker {\n margin-top: 6px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: fit-content;\n}\n.se-colorPicker .react-colorful {\n width: 100%;\n height: 140px;\n}\n.se-inlineColorPicker .react-colorful {\n width: 100%;\n height: 240px;\n}\n.se-colorPickerPopover {\n z-index: 1000;\n padding: 10px;\n border-radius: 8px;\n background: #1b2333;\n border: 1px solid rgba(148, 163, 184, 0.25);\n box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);\n}\n.se-colorPickerPopover .react-colorful {\n width: 200px;\n height: 160px;\n}\n.se-vectorRow {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 8px;\n}\n.se-vectorAxis {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 11px;\n color: #93a4c0;\n}\n.se-panel {\n margin-bottom: 10px;\n overflow: hidden;\n border: 1px solid rgba(148, 163, 184, 0.12);\n border-radius: 7px;\n background: rgba(255, 255, 255, 0.03);\n}\n.se-panel:last-child {\n margin-bottom: 0;\n}\n.se-panelHeader {\n display: flex;\n width: 100%;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n border: none;\n background: rgba(255, 255, 255, 0.03);\n padding: 11px 14px;\n color: #e5e7eb;\n cursor: pointer;\n text-align: left;\n}\n.se-panelHeader:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n.se-panelHeaderMain {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: 8px;\n}\n.se-panelChevron {\n flex-shrink: 0;\n color: #8b97ab;\n transform: rotate(-90deg);\n transition: transform 0.18s ease;\n}\n.se-panelChevron--open {\n transform: rotate(0deg);\n}\n.se-panelTitle {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: #c3cbdb;\n}\n.se-panelHelp {\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n margin-left: 2px;\n border-radius: 999px;\n color: #b9c0ca;\n cursor: help;\n transition:\n background 0.18s ease,\n color 0.18s ease,\n transform 0.18s ease;\n}\n.se-panelHelp:hover {\n background: rgba(255, 255, 255, 0.08);\n color: #eef2ff;\n transform: translateY(-0.5px);\n}\n.se-panelTrailing {\n display: flex;\n align-items: center;\n}\n.se-panelBody {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px;\n border-top: 1px solid rgba(148, 163, 184, 0.1);\n background: transparent;\n}\n.se-iconButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n border: none;\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.08);\n color: #d7dde8;\n cursor: pointer;\n}\n.se-iconButton:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n.se-axisStepperGrid {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 12px;\n}\n.se-axisStepper {\n display: grid;\n grid-template-columns: 40px 1fr 40px;\n align-items: center;\n min-height: 30px;\n border-radius: 10px;\n background: rgba(17, 24, 39, 0.62);\n border: 1px solid rgba(255, 255, 255, 0.05);\n}\n.se-axisStepperButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n border: none;\n background: transparent;\n color: #f3f4f6;\n cursor: pointer;\n}\n.se-axisStepperButton:hover {\n background: rgba(255, 255, 255, 0.04);\n}\n.se-axisStepperLabel {\n text-align: center;\n font-size: 15px;\n color: #f3f4f6;\n}\n.se-segmentedControl {\n display: grid;\n grid-auto-flow: column;\n overflow: hidden;\n border-radius: 10px;\n background: rgba(17, 24, 39, 0.62);\n}\n.se-segmentedOption {\n min-height: 35px;\n border: none;\n background: transparent;\n color: #d1d5db;\n font-size: 14px;\n text-transform: uppercase;\n cursor: pointer;\n}\n.se-segmentedOption--active {\n background: #2aa8d8;\n color: #fff;\n}\n.se-helperCard {\n padding: 32px;\n text-align: center;\n background: rgba(15, 23, 42, 0.44);\n border-radius: 10px;\n border: 1px solid rgba(148, 163, 184, 0.15);\n color: #94a3b8;\n font-size: 14px;\n}\n.se-helperCard strong {\n font-size: 14px;\n}\n.se-helperCard span {\n font-size: 13px;\n line-height: 1.4;\n color: #c7d0df;\n}\n.se-switch {\n position: relative;\n display: inline-flex;\n align-items: center;\n}\n.se-switch input[type=checkbox] {\n position: absolute;\n left: 0;\n top: 0;\n z-index: 10;\n width: 100%;\n height: 100%;\n cursor: pointer;\n opacity: 0;\n}\n.se-switchTrack {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.se-switchTrack::before {\n content: "";\n width: 36px;\n height: 20px;\n background: #4b5563;\n display: inline-block;\n border-radius: 20px;\n transition: background-color 0.2s ease-in;\n}\n.se-switchThumb {\n position: absolute;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n left: 2px;\n top: 2px;\n z-index: 2;\n background: #fff;\n box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);\n transition: 0.2s ease-in;\n}\n.se-switch input[type=checkbox]:hover + .se-switchTrack .se-switchThumb {\n box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.2), 0 3px 8px 0 rgba(0, 0, 0, 0.1);\n}\n.se-switch input[type=checkbox]:checked + .se-switchTrack::before {\n background: #376fcb;\n}\n.se-switch input[type=checkbox]:checked + .se-switchTrack .se-switchThumb {\n background: #4285f4;\n left: 18px;\n}\n.se-switchLabel {\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.06em;\n color: #e5e7eb;\n}\n.se-switch input[type=checkbox]:checked ~ .se-switchTrack .se-switchLabel {\n color: #2aa8d8;\n}\n.se-lightList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.se-lightRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid rgba(148, 163, 184, 0.1);\n background: rgba(255, 255, 255, 0.03);\n cursor: pointer;\n transition: background-color 0.15s ease, border-color 0.15s ease;\n}\n.se-lightRow:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n.se-lightRow--active {\n background: rgba(42, 168, 216, 0.12);\n border-color: rgba(42, 168, 216, 0.35);\n}\n.se-lightRowLabel {\n font-size: 13px;\n color: #d8e0ee;\n}\n.se-lightActions {\n display: flex;\n gap: 8px;\n}\n.se-lightProps {\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding-top: 4px;\n border-top: 1px solid rgba(148, 163, 184, 0.1);\n}\n.se-animationList {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n.se-animationCard {\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding: 16px;\n border-radius: 14px;\n border: 1px solid rgba(148, 163, 184, 0.16);\n background: rgba(15, 23, 42, 0.48);\n}\n.se-animationCard--active {\n border-color: rgba(42, 168, 216, 0.45);\n box-shadow: inset 0 0 0 1px rgba(42, 168, 216, 0.16);\n}\n.se-animationHeader {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n}\n.se-animationHeading {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 6px;\n}\n.se-animationTitleRow {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n.se-animationStatus,\n.se-animationBadge {\n display: inline-flex;\n align-items: center;\n min-height: 22px;\n padding: 0 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.se-animationStatus {\n background: rgba(42, 168, 216, 0.16);\n color: #9fe8ff;\n}\n.se-animationBadge {\n background: rgba(99, 102, 241, 0.18);\n color: #d9ddff;\n}\n.se-animationSource {\n font-size: 12px;\n color: #93a4c0;\n word-break: break-word;\n}\n.se-animationMeta {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n.se-animationMeta span {\n border-radius: 999px;\n background: rgba(255, 255, 255, 0.06);\n padding: 4px 9px;\n font-size: 12px;\n color: #c6d0e2;\n}\n.se-animationDangerButton,\n.se-animationButton,\n.se-animationAutoplayButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.04);\n color: #e2e8f0;\n cursor: pointer;\n transition:\n background-color 0.18s ease,\n border-color 0.18s ease,\n color 0.18s ease;\n}\n.se-animationButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.se-animationDangerButton {\n width: 36px;\n height: 36px;\n color: #fca5a5;\n}\n.se-animationDangerButton:hover,\n.se-animationButton:hover,\n.se-animationAutoplayButton:hover {\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(148, 163, 184, 0.34);\n}\n.se-animationButtonRow {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n}\n.se-animationButton {\n min-height: 36px;\n padding: 0 14px;\n font-size: 13px;\n font-weight: 600;\n}\n.se-animationButton--icon {\n flex: 1;\n min-width: 0;\n padding: 0;\n height: 38px;\n}\n.se-animationSeekRow {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.se-animationSeek {\n flex: 1;\n min-width: 0;\n}\n.se-animationSeekTime {\n flex-shrink: 0;\n min-width: 86px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n color: #97a3b6;\n}\n.se-animationGrid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n}\n.se-animationSpeedRow {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 88px;\n gap: 10px;\n align-items: center;\n}\n.se-animationSpeedInput {\n text-align: right;\n}\n.se-animationAutoplayButton {\n align-self: flex-start;\n min-height: 36px;\n padding: 0 14px;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n.se-animationAutoplayButton--active {\n background: rgba(42, 168, 216, 0.18);\n border-color: rgba(42, 168, 216, 0.42);\n color: #dff8ff;\n}\n.se-removedAnimations {\n overflow: hidden;\n border-radius: 12px;\n border: 1px solid rgba(148, 163, 184, 0.14);\n background: rgba(255, 255, 255, 0.03);\n}\n.se-removedAnimationList {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 14px;\n}\n.se-removedAnimationRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n border-radius: 10px;\n background: rgba(15, 23, 42, 0.42);\n padding: 12px;\n}\n.se-removedAnimationName {\n font-size: 13px;\n font-weight: 600;\n color: #e2e8f0;\n}\n@media (max-width: 900px) {\n .se-animationGrid {\n grid-template-columns: 1fr;\n }\n .se-animationSpeedRow {\n grid-template-columns: 1fr;\n }\n}\n.bb-viewSpotOverlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n backdrop-filter: blur(4px);\n animation: bb-fadeIn 0.2s ease;\n}\n.bb-viewSpotPopup {\n background: #1e1e20;\n border-radius: 12px;\n max-width: 480px;\n width: 90%;\n max-height: 80vh;\n overflow-y: auto;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);\n border: 1px solid #333335;\n animation: bb-slideUp 0.25s ease;\n}\n.bb-viewSpotHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 20px 24px;\n border-bottom: 1px solid #2a2a2c;\n}\n.bb-viewSpotTitle {\n font-size: 18px;\n font-weight: 600;\n color: #e8e8ea;\n margin: 0;\n}\n.bb-viewSpotClose {\n background: none;\n border: none;\n color: #888;\n font-size: 24px;\n cursor: pointer;\n padding: 0 4px;\n line-height: 1;\n transition: color 0.2s;\n}\n.bb-viewSpotClose:hover {\n color: #e8e8ea;\n}\n.bb-viewSpotBody {\n padding: 24px;\n}\n.bb-viewSpotDescription {\n color: #c8c8ca;\n font-size: 14px;\n line-height: 1.6;\n margin-bottom: 20px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.bb-viewSpotPosition {\n background: #141416;\n border-radius: 8px;\n padding: 12px 16px;\n margin-bottom: 24px;\n font-size: 13px;\n color: #a0a0a2;\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n.bb-viewSpotLabel {\n font-weight: 500;\n color: #888;\n}\n.bb-viewSpotCode {\n background: #0a0a0c;\n padding: 4px 10px;\n border-radius: 4px;\n font-family:\n "Monaco",\n "Menlo",\n monospace;\n font-size: 12px;\n color: #8be9fd;\n}\n.bb-viewSpotActions {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n padding-top: 4px;\n}\n.bb-editAnnotationOverlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n}\n.bb-editAnnotationPopup {\n background: #0f141e;\n border-radius: 16px;\n padding: 24px;\n min-width: 200px;\n max-width: 400px;\n width: 90%;\n border: 1px solid rgba(148, 163, 184, 0.18);\n}\n.bb-editAnnotationHeader {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 16px;\n padding-bottom: 12px;\n border-bottom: 1px solid rgba(148, 163, 184, 0.12);\n}\n.bb-editAnnotationTitle {\n color: #f8fafc;\n font-size: 18px;\n font-weight: 600;\n margin: 0;\n}\n.bb-editAnnotationClose {\n background: none;\n border: none;\n color: #94a3b8;\n font-size: 24px;\n cursor: pointer;\n padding: 0 4px;\n}\n.bb-editAnnotationClose:hover {\n color: #f8fafc;\n}\n.bb-editAnnotationBody {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n.bb-editAnnotationActions {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n}\n.se-cinematicHint {\n margin: 0 0 10px;\n font-size: 12px;\n line-height: 1.5;\n color: rgba(255, 255, 255, 0.55);\n}\n.bb-button.se-cinematicAdd {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n width: 100%;\n margin-bottom: 12px;\n background: #135bc0;\n color: white;\n}\n.bb-button.se-cinematicAdd:hover:not(:disabled) {\n background: #1a68d6;\n}\n.se-waypointList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-bottom: 14px;\n}\n.se-waypointCard {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.03);\n}\n.se-waypointBadge {\n flex: none;\n width: 22px;\n height: 22px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n color: #06343c;\n background: #22d3ee;\n}\n.se-waypointBody {\n flex: 1;\n min-width: 0;\n}\n.se-waypointCoords {\n font-size: 11px;\n line-height: 1.4;\n color: rgba(255, 255, 255, 0.6);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.se-waypointActions {\n flex: none;\n display: flex;\n gap: 2px;\n}\n.se-waypointAction {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: rgba(255, 255, 255, 0.6);\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n}\n.se-waypointAction:hover:not(:disabled) {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n.se-waypointAction:disabled {\n opacity: 0.35;\n cursor: default;\n}\n.se-waypointAction--delete:hover:not(:disabled) {\n background: rgba(248, 113, 113, 0.16);\n color: #f87171;\n}\n.bb-cameraWaypoint {\n align-items: flex-start;\n margin-bottom: 8px;\n}\n.bb-cameraWaypoint .bb-cameraLiveBadge,\n.bb-cameraWaypoint .bb-cameraSavedBadge {\n flex: none;\n}\n.bb-cameraWaypointLabel {\n font-size: 11px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.8);\n margin-bottom: 2px;\n}\n.bb-cameraControls--centered {\n justify-content: center;\n margin: 12px 0;\n}\n');
12499
+ styleInject('.se-tabContent {\n display: flex;\n flex-direction: column;\n}\n.se-tabContent * {\n box-sizing: border-box;\n}\n.se-subTabRow {\n display: flex;\n justify-content: space-between;\n gap: 6px;\n padding-bottom: 16px;\n border-bottom: 1px solid rgba(148, 163, 184, 0.1);\n margin-bottom: 4px;\n}\n.se-subTab {\n padding: 8px 12px;\n display: flex;\n justify-items: center;\n border-radius: 10px;\n border: 1px solid rgba(148, 163, 184, 0.18);\n background: rgba(255, 255, 255, 0.04);\n color: #b3bfd2;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n cursor: pointer;\n transition:\n background-color 0.18s ease,\n border-color 0.18s ease,\n color 0.18s ease;\n}\n.se-subTab:hover {\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(148, 163, 184, 0.32);\n}\n.se-subTab--active {\n background: rgba(99, 102, 241, 0.18);\n border-color: rgba(99, 102, 241, 0.4);\n color: #f8fafc;\n}\n.se-generalContent {\n display: flex;\n flex-direction: column;\n padding-top: 0;\n}\n.se-placeholder {\n font-size: 13px;\n color: #93a4c0;\n}\n.se-section {\n margin-bottom: 10px;\n border-radius: 7px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(148, 163, 184, 0.12);\n overflow: hidden;\n padding: 0;\n}\n.se-section:last-child {\n margin-bottom: 0;\n}\n.se-sectionTitle {\n margin: 0;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: #c3cbdb;\n padding: 11px 14px;\n border-bottom: 1px solid rgba(148, 163, 184, 0.1);\n}\n.se-sectionBody {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px;\n}\n.se-field {\n display: flex;\n flex-direction: column;\n gap: 5px;\n}\n.se-fieldInline {\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.se-fieldLabel {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n font-size: 12px;\n font-weight: 700;\n color: #d7dee9;\n}\n.se-fieldValue {\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n color: #93a4c0;\n}\n.se-slider {\n width: 100%;\n accent-color: #2aa8d8;\n cursor: pointer;\n}\n.se-checkbox {\n width: 18px;\n height: 18px;\n accent-color: #2aa8d8;\n cursor: pointer;\n}\n.se-select,\n.se-textInput,\n.se-numberInput {\n background: rgba(15, 23, 42, 0.28);\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 7px;\n color: #f8fafc;\n font-size: 13px;\n padding: 11px 13px;\n outline: none;\n}\n.se-select:focus,\n.se-textInput:focus,\n.se-numberInput:focus {\n outline: none;\n border-color: rgba(42, 168, 216, 0.65);\n box-shadow: 0 0 0 4px rgba(42, 168, 216, 0.12);\n}\n.se-select {\n min-width: 130px;\n text-transform: capitalize;\n}\n.se-textInput {\n width: 100%;\n}\n.se-numberInput {\n width: 100%;\n}\n.se-swatch {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n min-width: 96px;\n height: 30px;\n padding: 0 8px;\n border-radius: 8px;\n border: 1px solid rgba(148, 163, 184, 0.35);\n cursor: pointer;\n box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);\n}\n.se-swatchCode {\n font-size: 11px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n color: #fff;\n mix-blend-mode: difference;\n}\n.se-colorPicker {\n margin-top: 6px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: fit-content;\n}\n.se-colorPicker .react-colorful {\n width: 100%;\n height: 140px;\n}\n.se-inlineColorPicker .react-colorful {\n width: 100%;\n height: 240px;\n}\n.se-colorPickerPopover {\n z-index: 1000;\n padding: 10px;\n border-radius: 8px;\n background: #1b2333;\n border: 1px solid rgba(148, 163, 184, 0.25);\n box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);\n}\n.se-colorPickerPopover .react-colorful {\n width: 200px;\n height: 160px;\n}\n.se-vectorRow {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 8px;\n}\n.se-vectorAxis {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 11px;\n color: #93a4c0;\n}\n.se-panel {\n margin-bottom: 10px;\n overflow: hidden;\n border: 1px solid rgba(148, 163, 184, 0.12);\n border-radius: 7px;\n background: rgba(255, 255, 255, 0.03);\n}\n.se-panel:last-child {\n margin-bottom: 0;\n}\n.se-panelHeader {\n display: flex;\n width: 100%;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n border: none;\n background: rgba(255, 255, 255, 0.03);\n padding: 11px 14px;\n color: #e5e7eb;\n cursor: pointer;\n text-align: left;\n}\n.se-panelHeader:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n.se-panelHeaderMain {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: 8px;\n}\n.se-panelChevron {\n flex-shrink: 0;\n color: #8b97ab;\n transform: rotate(-90deg);\n transition: transform 0.18s ease;\n}\n.se-panelChevron--open {\n transform: rotate(0deg);\n}\n.se-panelTitle {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: #c3cbdb;\n}\n.se-panelHelp {\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n margin-left: 2px;\n border-radius: 999px;\n color: #b9c0ca;\n cursor: help;\n transition:\n background 0.18s ease,\n color 0.18s ease,\n transform 0.18s ease;\n}\n.se-panelHelp:hover {\n background: rgba(255, 255, 255, 0.08);\n color: #eef2ff;\n transform: translateY(-0.5px);\n}\n.se-panelTrailing {\n display: flex;\n align-items: center;\n}\n.se-panelBody {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px;\n border-top: 1px solid rgba(148, 163, 184, 0.1);\n background: transparent;\n}\n.se-iconButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n border: none;\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.08);\n color: #d7dde8;\n cursor: pointer;\n}\n.se-iconButton:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n.se-axisStepperGrid {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 12px;\n}\n.se-axisStepper {\n display: grid;\n grid-template-columns: 40px 1fr 40px;\n align-items: center;\n min-height: 30px;\n border-radius: 10px;\n background: rgba(17, 24, 39, 0.62);\n border: 1px solid rgba(255, 255, 255, 0.05);\n}\n.se-axisStepperButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n border: none;\n background: transparent;\n color: #f3f4f6;\n cursor: pointer;\n}\n.se-axisStepperButton:hover {\n background: rgba(255, 255, 255, 0.04);\n}\n.se-axisStepperLabel {\n text-align: center;\n font-size: 15px;\n color: #f3f4f6;\n}\n.se-segmentedControl {\n display: grid;\n grid-auto-flow: column;\n overflow: hidden;\n border-radius: 10px;\n background: rgba(17, 24, 39, 0.62);\n}\n.se-segmentedOption {\n min-height: 35px;\n border: none;\n background: transparent;\n color: #d1d5db;\n font-size: 14px;\n text-transform: uppercase;\n cursor: pointer;\n}\n.se-segmentedOption--active {\n background: #2aa8d8;\n color: #fff;\n}\n.se-helperCard {\n padding: 32px;\n text-align: center;\n background: rgba(15, 23, 42, 0.44);\n border-radius: 10px;\n border: 1px solid rgba(148, 163, 184, 0.15);\n color: #94a3b8;\n font-size: 14px;\n}\n.se-helperCard strong {\n font-size: 14px;\n}\n.se-helperCard span {\n font-size: 13px;\n line-height: 1.4;\n color: #c7d0df;\n}\n.se-switch {\n position: relative;\n display: inline-flex;\n align-items: center;\n}\n.se-switch input[type=checkbox] {\n position: absolute;\n left: 0;\n top: 0;\n z-index: 10;\n width: 100%;\n height: 100%;\n cursor: pointer;\n opacity: 0;\n}\n.se-switchTrack {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.se-switchTrack::before {\n content: "";\n width: 36px;\n height: 20px;\n background: #4b5563;\n display: inline-block;\n border-radius: 20px;\n transition: background-color 0.2s ease-in;\n}\n.se-switchThumb {\n position: absolute;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n left: 2px;\n top: 2px;\n z-index: 2;\n background: #fff;\n box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);\n transition: 0.2s ease-in;\n}\n.se-switch input[type=checkbox]:hover + .se-switchTrack .se-switchThumb {\n box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.2), 0 3px 8px 0 rgba(0, 0, 0, 0.1);\n}\n.se-switch input[type=checkbox]:checked + .se-switchTrack::before {\n background: #376fcb;\n}\n.se-switch input[type=checkbox]:checked + .se-switchTrack .se-switchThumb {\n background: #4285f4;\n left: 18px;\n}\n.se-switchLabel {\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.06em;\n color: #e5e7eb;\n}\n.se-switch input[type=checkbox]:checked ~ .se-switchTrack .se-switchLabel {\n color: #2aa8d8;\n}\n.se-lightList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.se-lightRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid rgba(148, 163, 184, 0.1);\n background: rgba(255, 255, 255, 0.03);\n cursor: pointer;\n transition: background-color 0.15s ease, border-color 0.15s ease;\n}\n.se-lightRow:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n.se-lightRow--active {\n background: rgba(42, 168, 216, 0.12);\n border-color: rgba(42, 168, 216, 0.35);\n}\n.se-lightRowLabel {\n font-size: 13px;\n color: #d8e0ee;\n}\n.se-lightActions {\n display: flex;\n gap: 8px;\n}\n.se-lightProps {\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding-top: 4px;\n border-top: 1px solid rgba(148, 163, 184, 0.1);\n}\n.se-animationList {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n.se-animationCard {\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding: 16px;\n border-radius: 14px;\n border: 1px solid rgba(148, 163, 184, 0.16);\n background: rgba(15, 23, 42, 0.48);\n}\n.se-animationCard--active {\n border-color: rgba(42, 168, 216, 0.45);\n box-shadow: inset 0 0 0 1px rgba(42, 168, 216, 0.16);\n}\n.se-animationHeader {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n}\n.se-animationHeading {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 6px;\n}\n.se-animationTitleRow {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n.se-animationStatus,\n.se-animationBadge {\n display: inline-flex;\n align-items: center;\n min-height: 22px;\n padding: 0 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.se-animationStatus {\n background: rgba(42, 168, 216, 0.16);\n color: #9fe8ff;\n}\n.se-animationBadge {\n background: rgba(99, 102, 241, 0.18);\n color: #d9ddff;\n}\n.se-animationSource {\n font-size: 12px;\n color: #93a4c0;\n word-break: break-word;\n}\n.se-animationMeta {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n.se-animationMeta span {\n border-radius: 999px;\n background: rgba(255, 255, 255, 0.06);\n padding: 4px 9px;\n font-size: 12px;\n color: #c6d0e2;\n}\n.se-animationDangerButton,\n.se-animationButton,\n.se-animationAutoplayButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.04);\n color: #e2e8f0;\n cursor: pointer;\n transition:\n background-color 0.18s ease,\n border-color 0.18s ease,\n color 0.18s ease;\n}\n.se-animationButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.se-animationDangerButton {\n width: 36px;\n height: 36px;\n color: #fca5a5;\n}\n.se-animationDangerButton:hover,\n.se-animationButton:hover,\n.se-animationAutoplayButton:hover {\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(148, 163, 184, 0.34);\n}\n.se-animationButtonRow {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n}\n.se-animationButton {\n min-height: 36px;\n padding: 0 14px;\n font-size: 13px;\n font-weight: 600;\n}\n.se-animationButton--icon {\n flex: 1;\n min-width: 0;\n padding: 0;\n height: 38px;\n}\n.se-animationSeekRow {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.se-animationSeek {\n flex: 1;\n min-width: 0;\n}\n.se-animationSeekTime {\n flex-shrink: 0;\n min-width: 86px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n color: #97a3b6;\n}\n.se-animationGrid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n}\n.se-animationSpeedRow {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 88px;\n gap: 10px;\n align-items: center;\n}\n.se-animationSpeedInput {\n text-align: right;\n}\n.se-animationAutoplayButton {\n align-self: flex-start;\n min-height: 36px;\n padding: 0 14px;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n.se-animationAutoplayButton--active {\n background: rgba(42, 168, 216, 0.18);\n border-color: rgba(42, 168, 216, 0.42);\n color: #dff8ff;\n}\n.se-removedAnimations {\n overflow: hidden;\n border-radius: 12px;\n border: 1px solid rgba(148, 163, 184, 0.14);\n background: rgba(255, 255, 255, 0.03);\n}\n.se-removedAnimationList {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 14px;\n}\n.se-removedAnimationRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n border-radius: 10px;\n background: rgba(15, 23, 42, 0.42);\n padding: 12px;\n}\n.se-removedAnimationName {\n font-size: 13px;\n font-weight: 600;\n color: #e2e8f0;\n}\n@media (max-width: 900px) {\n .se-animationGrid {\n grid-template-columns: 1fr;\n }\n .se-animationSpeedRow {\n grid-template-columns: 1fr;\n }\n}\n.bb-viewSpotOverlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n backdrop-filter: blur(4px);\n animation: bb-fadeIn 0.2s ease;\n}\n.bb-viewSpotPopup {\n background: #1e1e20;\n border-radius: 12px;\n max-width: 480px;\n width: 90%;\n max-height: 80vh;\n overflow-y: auto;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);\n border: 1px solid #333335;\n animation: bb-slideUp 0.25s ease;\n}\n.bb-viewSpotHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 20px 24px;\n border-bottom: 1px solid #2a2a2c;\n}\n.bb-viewSpotTitle {\n font-size: 18px;\n font-weight: 600;\n color: #e8e8ea;\n margin: 0;\n}\n.bb-viewSpotClose {\n background: none;\n border: none;\n color: #888;\n font-size: 24px;\n cursor: pointer;\n padding: 0 4px;\n line-height: 1;\n transition: color 0.2s;\n}\n.bb-viewSpotClose:hover {\n color: #e8e8ea;\n}\n.bb-viewSpotBody {\n padding: 24px;\n}\n.bb-viewSpotDescription {\n color: #c8c8ca;\n font-size: 14px;\n line-height: 1.6;\n margin-bottom: 20px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.bb-viewSpotPosition {\n background: #141416;\n border-radius: 8px;\n padding: 12px 16px;\n margin-bottom: 24px;\n font-size: 13px;\n color: #a0a0a2;\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n.bb-viewSpotLabel {\n font-weight: 500;\n color: #888;\n}\n.bb-viewSpotCode {\n background: #0a0a0c;\n padding: 4px 10px;\n border-radius: 4px;\n font-family:\n "Monaco",\n "Menlo",\n monospace;\n font-size: 12px;\n color: #8be9fd;\n}\n.bb-viewSpotActions {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n padding-top: 4px;\n}\n.bb-editAnnotationOverlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n}\n.bb-editAnnotationPopup {\n background: #0f141e;\n border-radius: 16px;\n padding: 24px;\n min-width: 200px;\n max-width: 400px;\n width: 90%;\n border: 1px solid rgba(148, 163, 184, 0.18);\n}\n.bb-editAnnotationHeader {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 16px;\n padding-bottom: 12px;\n border-bottom: 1px solid rgba(148, 163, 184, 0.12);\n}\n.bb-editAnnotationTitle {\n color: #f8fafc;\n font-size: 18px;\n font-weight: 600;\n margin: 0;\n}\n.bb-editAnnotationClose {\n background: none;\n border: none;\n color: #94a3b8;\n font-size: 24px;\n cursor: pointer;\n padding: 0 4px;\n}\n.bb-editAnnotationClose:hover {\n color: #f8fafc;\n}\n.bb-editAnnotationBody {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n.bb-editAnnotationActions {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n}\n.se-cinematicHint {\n margin: 0 0 10px;\n font-size: 12px;\n line-height: 1.5;\n color: rgba(255, 255, 255, 0.55);\n}\n.bb-button.se-cinematicAdd {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n width: 100%;\n margin-bottom: 12px;\n background: #135bc0;\n color: white;\n}\n.bb-button.se-cinematicAdd:hover:not(:disabled) {\n background: #1a68d6;\n}\n.se-waypointList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-bottom: 14px;\n}\n.se-waypointCard {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.03);\n}\n.se-waypointBadge {\n flex: none;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n color: #06343c;\n background: #22d3ee;\n}\n.se-waypointBody {\n flex: 1;\n min-width: 0;\n}\n.se-waypointCoords {\n font-size: 11px;\n line-height: 1.4;\n color: rgba(255, 255, 255, 0.5);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.se-waypointActions {\n flex: none;\n display: flex;\n gap: 2px;\n}\n.se-waypointAction {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: rgba(255, 255, 255, 0.5);\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n}\n.se-waypointAction:hover:not(:disabled) {\n background: rgba(255, 255, 255, 0.08);\n color: #fff;\n}\n.se-waypointAction:disabled {\n opacity: 0.35;\n cursor: default;\n}\n.se-waypointAction--delete:hover:not(:disabled) {\n background: rgba(248, 113, 113, 0.16);\n color: #f87171;\n}\n.se-customBadge {\n display: inline-block;\n padding: 1px 8px;\n border-radius: 3px;\n background: rgba(34, 211, 238, 0.15);\n color: #22d3ee;\n font-size: 10px;\n font-weight: 600;\n margin-top: 4px;\n}\n.se-customizeToggle {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 8px 12px;\n background: rgba(255, 255, 255, 0.03);\n border-radius: 8px;\n border: 1px solid rgba(148, 163, 184, 0.08);\n margin: 8px 0 12px;\n}\n.se-toggleLabel {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n font-weight: 500;\n color: #f8fafc;\n cursor: pointer;\n user-select: none;\n}\n.se-toggleLabel input[type=checkbox] {\n width: 16px;\n height: 16px;\n accent-color: #22d3ee;\n cursor: pointer;\n}\n.se-toggleLabel svg {\n color: #22d3ee;\n width: 14px;\n height: 14px;\n}\n.se-toggleHint {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.4);\n padding-left: 28px;\n}\n.se-segmentBetween {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 14px;\n margin: 6px 0 6px 32px;\n background: rgba(255, 255, 255, 0.03);\n border-radius: 6px;\n border-left: 3px solid rgba(34, 211, 238, 0.25);\n}\n.se-segmentLabel {\n font-size: 11px;\n font-weight: 500;\n color: rgba(255, 255, 255, 0.45);\n}\n.se-segmentControls {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n.se-durationLabel {\n font-size: 10px;\n color: rgba(255, 255, 255, 0.35);\n font-weight: 500;\n white-space: nowrap;\n}\n.se-segmentSlider {\n flex: 1;\n min-width: 80px;\n max-width: 150px;\n height: 4px;\n -webkit-appearance: none;\n appearance: none;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 2px;\n outline: none;\n}\n.se-segmentSlider::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: #22d3ee;\n cursor: pointer;\n border: 2px solid rgba(255, 255, 255, 0.2);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n}\n.se-segmentSlider::-moz-range-thumb {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: #22d3ee;\n cursor: pointer;\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n.se-segmentValue {\n font-size: 12px;\n font-weight: 600;\n color: #22d3ee;\n min-width: 36px;\n text-align: center;\n}\n.se-presetButtons {\n display: flex;\n gap: 4px;\n}\n.se-presetButton {\n padding: 2px 8px;\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(148, 163, 184, 0.15);\n border-radius: 4px;\n font-size: 10px;\n color: rgba(255, 255, 255, 0.6);\n cursor: pointer;\n transition: all 0.2s ease;\n font-weight: 500;\n}\n.se-presetButton:hover {\n background: #22d3ee;\n color: #06343c;\n border-color: #22d3ee;\n}\n.se-totalDuration {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 4px;\n margin: 8px 0 12px;\n border-top: 1px solid rgba(148, 163, 184, 0.1);\n padding-top: 12px;\n}\n.se-totalLabel {\n font-size: 12px;\n color: rgba(255, 255, 255, 0.5);\n font-weight: 500;\n}\n.se-totalValue {\n font-size: 14px;\n font-weight: 600;\n color: #22d3ee;\n}\n.se-totalHint {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.35);\n font-weight: 400;\n margin-left: 8px;\n}\n');
11847
12500
 
11848
12501
  // src/components/BindingBuilder/SceneSettings/SceneSettingsTab.tsx
11849
12502
  var import_lucide_react11 = require("lucide-react");
@@ -11882,13 +12535,13 @@ function SceneSettingsTab({
11882
12535
  animationClipDetails = [],
11883
12536
  animationPlayback = FALLBACK_ANIMATION_PLAYBACK
11884
12537
  }) {
11885
- const [internalActiveSubTab, setInternalActiveSubTab] = (0, import_react32.useState)("General");
12538
+ const [internalActiveSubTab, setInternalActiveSubTab] = (0, import_react33.useState)("General");
11886
12539
  const activeSubTab = controlledActiveSubTab ?? internalActiveSubTab;
11887
12540
  const resolvedAnimationPlayback = animationPlayback ?? FALLBACK_ANIMATION_PLAYBACK;
11888
12541
  const { tier } = useBindingBuilderContext();
11889
12542
  const canUseStarterFeatures = hasTier(tier, "starter");
11890
12543
  const canUseGrowthFeatures = hasTier(tier, "growth");
11891
- (0, import_react32.useEffect)(() => {
12544
+ (0, import_react33.useEffect)(() => {
11892
12545
  if (controlledActiveSubTab !== void 0) {
11893
12546
  setInternalActiveSubTab(controlledActiveSubTab);
11894
12547
  }
@@ -12068,7 +12721,7 @@ function EnableToggle2({
12068
12721
  }
12069
12722
 
12070
12723
  // src/components/BindingBuilder/components/MaterialSection.tsx
12071
- var import_react34 = require("react");
12724
+ var import_react35 = require("react");
12072
12725
 
12073
12726
  // src/components/ModelViewer/types/objectBinding.ts
12074
12727
  var MATERIAL_SIDES = ["front", "back", "double"];
@@ -12115,7 +12768,7 @@ function MaterialPanelGroup({
12115
12768
  }
12116
12769
 
12117
12770
  // src/components/BindingBuilder/components/MaterialFields.tsx
12118
- var import_react33 = require("react");
12771
+ var import_react34 = require("react");
12119
12772
  var import_react_colorful4 = require("react-colorful");
12120
12773
  var import_lucide_react13 = require("lucide-react");
12121
12774
  var import_jsx_runtime35 = require("react/jsx-runtime");
@@ -12125,7 +12778,7 @@ function MaterialTexturePathField({
12125
12778
  placeholder,
12126
12779
  onChange
12127
12780
  }) {
12128
- const inputRef = (0, import_react33.useRef)(null);
12781
+ const inputRef = (0, import_react34.useRef)(null);
12129
12782
  return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "bb-field bb-field--full", children: [
12130
12783
  /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("label", { className: "bb-label", children: label }),
12131
12784
  /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "bb-fileField", children: [
@@ -12182,9 +12835,9 @@ function MaterialColorField({
12182
12835
  fallback = "#ffffff",
12183
12836
  onChange
12184
12837
  }) {
12185
- const [open, setOpen] = (0, import_react33.useState)(false);
12838
+ const [open, setOpen] = (0, import_react34.useState)(false);
12186
12839
  const color = value || fallback;
12187
- const anchorRef = (0, import_react33.useRef)(null);
12840
+ const anchorRef = (0, import_react34.useRef)(null);
12188
12841
  return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "bb-field", children: [
12189
12842
  /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "bb-label", children: label }),
12190
12843
  /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "se-fieldInline", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { ref: anchorRef, style: { display: "inline-flex" }, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
@@ -12336,8 +12989,8 @@ function MaterialSection({
12336
12989
  tier: "starter",
12337
12990
  locked: !canUseStarterFeatures
12338
12991
  };
12339
- const [roughnessMode, setRoughnessMode] = (0, import_react34.useState)("roughness");
12340
- const [anisotropyMode, setAnisotropyMode] = (0, import_react34.useState)(
12992
+ const [roughnessMode, setRoughnessMode] = (0, import_react35.useState)("roughness");
12993
+ const [anisotropyMode, setAnisotropyMode] = (0, import_react35.useState)(
12341
12994
  "factor"
12342
12995
  );
12343
12996
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
@@ -13376,7 +14029,7 @@ function MaterialPresetGallery({
13376
14029
  }
13377
14030
 
13378
14031
  // src/components/BindingBuilder/components/ToolbarExportMenu.tsx
13379
- var import_react35 = require("react");
14032
+ var import_react36 = require("react");
13380
14033
  var import_lucide_react14 = require("lucide-react");
13381
14034
  var import_jsx_runtime38 = require("react/jsx-runtime");
13382
14035
  function ToolbarExportMenu({
@@ -13385,9 +14038,9 @@ function ToolbarExportMenu({
13385
14038
  onExportJson,
13386
14039
  onExportTs
13387
14040
  }) {
13388
- const [open, setOpen] = (0, import_react35.useState)(false);
13389
- const wrapperRef = (0, import_react35.useRef)(null);
13390
- (0, import_react35.useEffect)(() => {
14041
+ const [open, setOpen] = (0, import_react36.useState)(false);
14042
+ const wrapperRef = (0, import_react36.useRef)(null);
14043
+ (0, import_react36.useEffect)(() => {
13391
14044
  if (!open) return;
13392
14045
  const close = (event) => {
13393
14046
  if (!wrapperRef.current?.contains(event.target)) {
@@ -13824,10 +14477,10 @@ function CameraPositionPanel({
13824
14477
  }
13825
14478
 
13826
14479
  // src/components/BindingBuilder/components/SceneNodesPanel.tsx
13827
- var import_react36 = require("react");
14480
+ var import_react37 = require("react");
13828
14481
 
13829
14482
  // src/components/BindingBuilder/utils/bindingFactory.ts
13830
- function slugify(value) {
14483
+ function slugify2(value) {
13831
14484
  return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
13832
14485
  }
13833
14486
  function humanizeNodeName(value) {
@@ -13873,7 +14526,7 @@ function buildDefaultActions(type) {
13873
14526
  function createDefaultBinding(nodeName) {
13874
14527
  const type = inferType(nodeName);
13875
14528
  return {
13876
- id: slugify(nodeName),
14529
+ id: slugify2(nodeName),
13877
14530
  modelObjectId: nodeName,
13878
14531
  type,
13879
14532
  label: humanizeNodeName(nodeName),
@@ -13913,9 +14566,9 @@ function SceneNodesPanel({
13913
14566
  isOpen = true,
13914
14567
  onToggle
13915
14568
  }) {
13916
- const [search, setSearch] = (0, import_react36.useState)("");
13917
- const nodeKeys = (0, import_react36.useMemo)(() => Object.keys(bindings), [bindings]);
13918
- const filteredNodeKeys = (0, import_react36.useMemo)(() => {
14569
+ const [search, setSearch] = (0, import_react37.useState)("");
14570
+ const nodeKeys = (0, import_react37.useMemo)(() => Object.keys(bindings), [bindings]);
14571
+ const filteredNodeKeys = (0, import_react37.useMemo)(() => {
13919
14572
  const query = search.trim().toLowerCase();
13920
14573
  if (!query) return nodeKeys;
13921
14574
  return nodeKeys.filter((nodeKey) => {
@@ -14058,7 +14711,7 @@ function mergeAnimationConfig(sceneConfig, animationDetails) {
14058
14711
  }
14059
14712
 
14060
14713
  // src/components/BindingBuilder/hooks/useUndoableState.ts
14061
- var import_react37 = require("react");
14714
+ var import_react38 = require("react");
14062
14715
  var MAX_HISTORY = 100;
14063
14716
  function historyReducer(state, action) {
14064
14717
  switch (action.type) {
@@ -14097,13 +14750,13 @@ function historyReducer(state, action) {
14097
14750
  }
14098
14751
  }
14099
14752
  function useUndoableState(initial, coalesceWindowMs = 600) {
14100
- const [state, dispatch] = (0, import_react37.useReducer)(historyReducer, {
14753
+ const [state, dispatch] = (0, import_react38.useReducer)(historyReducer, {
14101
14754
  past: [],
14102
14755
  present: initial,
14103
14756
  future: []
14104
14757
  });
14105
- const lastSetAt = (0, import_react37.useRef)(0);
14106
- const setValue = (0, import_react37.useCallback)(
14758
+ const lastSetAt = (0, import_react38.useRef)(0);
14759
+ const setValue = (0, import_react38.useCallback)(
14107
14760
  (updater) => {
14108
14761
  const now = Date.now();
14109
14762
  const coalesce = now - lastSetAt.current < coalesceWindowMs;
@@ -14112,15 +14765,15 @@ function useUndoableState(initial, coalesceWindowMs = 600) {
14112
14765
  },
14113
14766
  [coalesceWindowMs]
14114
14767
  );
14115
- const undo = (0, import_react37.useCallback)(() => {
14768
+ const undo = (0, import_react38.useCallback)(() => {
14116
14769
  lastSetAt.current = 0;
14117
14770
  dispatch({ type: "undo" });
14118
14771
  }, []);
14119
- const redo = (0, import_react37.useCallback)(() => {
14772
+ const redo = (0, import_react38.useCallback)(() => {
14120
14773
  lastSetAt.current = 0;
14121
14774
  dispatch({ type: "redo" });
14122
14775
  }, []);
14123
- const reset = (0, import_react37.useCallback)((value) => {
14776
+ const reset = (0, import_react38.useCallback)((value) => {
14124
14777
  lastSetAt.current = 0;
14125
14778
  dispatch({ type: "reset", value });
14126
14779
  }, []);
@@ -14136,7 +14789,7 @@ function useUndoableState(initial, coalesceWindowMs = 600) {
14136
14789
  }
14137
14790
 
14138
14791
  // src/components/BindingBuilder/utils/exportAssets.ts
14139
- var import_fflate2 = require("fflate");
14792
+ var import_fflate3 = require("fflate");
14140
14793
 
14141
14794
  // src/components/BindingBuilder/utils/serialization.ts
14142
14795
  function toBindingsJson(bindings) {
@@ -14212,11 +14865,11 @@ async function downloadExport(filename, content, mimeType, zipFilename, assets)
14212
14865
  downloadTextFile(filename, content, mimeType);
14213
14866
  return;
14214
14867
  }
14215
- const files = { [filename]: (0, import_fflate2.strToU8)(content) };
14868
+ const files = { [filename]: (0, import_fflate3.strToU8)(content) };
14216
14869
  for (const asset of assets) {
14217
14870
  files[asset.path] = new Uint8Array(await asset.blob.arrayBuffer());
14218
14871
  }
14219
- const zipped = (0, import_fflate2.zipSync)(files, { level: 6 });
14872
+ const zipped = (0, import_fflate3.zipSync)(files, { level: 6 });
14220
14873
  const blob = new Blob([zipped], { type: "application/zip" });
14221
14874
  const url = URL.createObjectURL(blob);
14222
14875
  const link = document.createElement("a");
@@ -14366,30 +15019,30 @@ function mergeSceneConfigSection(current, incoming) {
14366
15019
  return next;
14367
15020
  }
14368
15021
  function BindingBuilder({ licenseKey }) {
14369
- const modelInputRef = (0, import_react38.useRef)(null);
14370
- const bindingsInputRef = (0, import_react38.useRef)(null);
14371
- const sceneConfigInputRef = (0, import_react38.useRef)(null);
14372
- const [modelUrl, setModelUrl] = (0, import_react38.useState)(null);
14373
- const [modelName, setModelName] = (0, import_react38.useState)("");
14374
- const [selectedItem, setSelectedItem] = (0, import_react38.useState)(null);
15022
+ const modelInputRef = (0, import_react39.useRef)(null);
15023
+ const bindingsInputRef = (0, import_react39.useRef)(null);
15024
+ const sceneConfigInputRef = (0, import_react39.useRef)(null);
15025
+ const [modelUrl, setModelUrl] = (0, import_react39.useState)(null);
15026
+ const [modelName, setModelName] = (0, import_react39.useState)("");
15027
+ const [selectedItem, setSelectedItem] = (0, import_react39.useState)(null);
14375
15028
  const bindingsHistory = useUndoableState({});
14376
15029
  const bindings = bindingsHistory.value;
14377
15030
  const setBindings = bindingsHistory.setValue;
14378
- const [loading, setLoading] = (0, import_react38.useState)(false);
14379
- const [isExporting, setIsExporting] = (0, import_react38.useState)(false);
14380
- const [statusMessage, setStatusMessage] = (0, import_react38.useState)(
15031
+ const [loading, setLoading] = (0, import_react39.useState)(false);
15032
+ const [isExporting, setIsExporting] = (0, import_react39.useState)(false);
15033
+ const [statusMessage, setStatusMessage] = (0, import_react39.useState)(
14381
15034
  "Load the demo model or upload a GLB/GLTF model to generate bindings."
14382
15035
  );
14383
- const [statusTone, setStatusTone] = (0, import_react38.useState)("default");
14384
- const [statusBannerVisible, setStatusBannerVisible] = (0, import_react38.useState)(false);
14385
- const [metricsDraft, setMetricsDraft] = (0, import_react38.useState)("{}");
14386
- const [metadataDraft, setMetadataDraft] = (0, import_react38.useState)("{}");
14387
- const [metricsError, setMetricsError] = (0, import_react38.useState)(null);
14388
- const [metadataError, setMetadataError] = (0, import_react38.useState)(null);
14389
- const [isPreviewDragActive, setIsPreviewDragActive] = (0, import_react38.useState)(false);
14390
- const [isEditorCollapsed, setIsEditorCollapsed] = (0, import_react38.useState)(false);
14391
- const [isNodesCollapsed, setIsNodesCollapsed] = (0, import_react38.useState)(false);
14392
- const [openObjectSections, setOpenObjectSections] = (0, import_react38.useState)({
15036
+ const [statusTone, setStatusTone] = (0, import_react39.useState)("default");
15037
+ const [statusBannerVisible, setStatusBannerVisible] = (0, import_react39.useState)(false);
15038
+ const [metricsDraft, setMetricsDraft] = (0, import_react39.useState)("{}");
15039
+ const [metadataDraft, setMetadataDraft] = (0, import_react39.useState)("{}");
15040
+ const [metricsError, setMetricsError] = (0, import_react39.useState)(null);
15041
+ const [metadataError, setMetadataError] = (0, import_react39.useState)(null);
15042
+ const [isPreviewDragActive, setIsPreviewDragActive] = (0, import_react39.useState)(false);
15043
+ const [isEditorCollapsed, setIsEditorCollapsed] = (0, import_react39.useState)(false);
15044
+ const [isNodesCollapsed, setIsNodesCollapsed] = (0, import_react39.useState)(false);
15045
+ const [openObjectSections, setOpenObjectSections] = (0, import_react39.useState)({
14393
15046
  identity: true,
14394
15047
  basics: true,
14395
15048
  material: true,
@@ -14398,7 +15051,7 @@ function BindingBuilder({ licenseKey }) {
14398
15051
  metrics: false,
14399
15052
  metadata: false
14400
15053
  });
14401
- const [openMaterialPanels, setOpenMaterialPanels] = (0, import_react38.useState)({
15054
+ const [openMaterialPanels, setOpenMaterialPanels] = (0, import_react39.useState)({
14402
15055
  pbr: true,
14403
15056
  roughness: false,
14404
15057
  emission: false,
@@ -14414,27 +15067,27 @@ function BindingBuilder({ licenseKey }) {
14414
15067
  });
14415
15068
  const toggleObjectSection = (key) => setOpenObjectSections((prev) => ({ ...prev, [key]: !prev[key] }));
14416
15069
  const toggleMaterialPanel = (key) => setOpenMaterialPanels((prev) => ({ ...prev, [key]: !prev[key] }));
14417
- const [activeEditorTab, setActiveEditorTab] = (0, import_react38.useState)(
15070
+ const [activeEditorTab, setActiveEditorTab] = (0, import_react39.useState)(
14418
15071
  "object"
14419
15072
  );
14420
- const [activeSubTab, setActiveSubTab] = (0, import_react38.useState)("General");
14421
- const [pbrWorkflow, setPbrWorkflow] = (0, import_react38.useState)("metalness");
14422
- const [materialPanelEnabled, setMaterialPanelEnabled] = (0, import_react38.useState)(createMaterialPanelEnabledState());
14423
- const materialPanelItemRef = (0, import_react38.useRef)(null);
14424
- const [zoomOnSelected, setZoomOnSelected] = (0, import_react38.useState)(false);
14425
- const [highlightOnHover, setHighlightOnHover] = (0, import_react38.useState)(true);
14426
- const [resetDialogOpen, setResetDialogOpen] = (0, import_react38.useState)(false);
14427
- const [resetTarget, setResetTarget] = (0, import_react38.useState)(
15073
+ const [activeSubTab, setActiveSubTab] = (0, import_react39.useState)("General");
15074
+ const [pbrWorkflow, setPbrWorkflow] = (0, import_react39.useState)("metalness");
15075
+ const [materialPanelEnabled, setMaterialPanelEnabled] = (0, import_react39.useState)(createMaterialPanelEnabledState());
15076
+ const materialPanelItemRef = (0, import_react39.useRef)(null);
15077
+ const [zoomOnSelected, setZoomOnSelected] = (0, import_react39.useState)(false);
15078
+ const [highlightOnHover, setHighlightOnHover] = (0, import_react39.useState)(true);
15079
+ const [resetDialogOpen, setResetDialogOpen] = (0, import_react39.useState)(false);
15080
+ const [resetTarget, setResetTarget] = (0, import_react39.useState)(
14428
15081
  null
14429
15082
  );
14430
15083
  const sceneConfigHistory = useUndoableState(DEFAULT_SCENE_CONFIG);
14431
15084
  const sceneConfig = sceneConfigHistory.value;
14432
15085
  const setSceneConfig = sceneConfigHistory.setValue;
14433
- const [activeAnnotation, setActiveAnnotation] = (0, import_react38.useState)(null);
14434
- const [panelHoveredBinding, setPanelHoveredBinding] = (0, import_react38.useState)(null);
15086
+ const [activeAnnotation, setActiveAnnotation] = (0, import_react39.useState)(null);
15087
+ const [panelHoveredBinding, setPanelHoveredBinding] = (0, import_react39.useState)(null);
14435
15088
  const license = useLicense(licenseKey);
14436
15089
  const tier = normalizeTier(license.tier);
14437
- const bindingBuilderContextValue = (0, import_react38.useMemo)(
15090
+ const bindingBuilderContextValue = (0, import_react39.useMemo)(
14438
15091
  () => ({
14439
15092
  isInsideEditor: true,
14440
15093
  activeTab: activeEditorTab,
@@ -14443,11 +15096,11 @@ function BindingBuilder({ licenseKey }) {
14443
15096
  }),
14444
15097
  [activeEditorTab, activeSubTab, tier]
14445
15098
  );
14446
- const uploadedModelRef = (0, import_react38.useRef)(null);
14447
- const previewDragDepthRef = (0, import_react38.useRef)(0);
14448
- const loadRequestRef = (0, import_react38.useRef)(0);
14449
- const pendingLoadRef = (0, import_react38.useRef)(null);
14450
- const loadedSceneRef = (0, import_react38.useRef)(null);
15099
+ const uploadedModelRef = (0, import_react39.useRef)(null);
15100
+ const previewDragDepthRef = (0, import_react39.useRef)(0);
15101
+ const loadRequestRef = (0, import_react39.useRef)(0);
15102
+ const pendingLoadRef = (0, import_react39.useRef)(null);
15103
+ const loadedSceneRef = (0, import_react39.useRef)(null);
14451
15104
  const {
14452
15105
  cameraState,
14453
15106
  resetView,
@@ -14457,14 +15110,16 @@ function BindingBuilder({ licenseKey }) {
14457
15110
  handleCameraChange,
14458
15111
  handleViewerReady
14459
15112
  } = useViewerCamera();
14460
- const handlePreviewWaypoint = (0, import_react38.useCallback)(
15113
+ const handlePreviewWaypoint = (0, import_react39.useCallback)(
14461
15114
  (waypoint) => {
14462
15115
  setCameraState(
14463
15116
  {
14464
15117
  position: waypoint.position,
14465
15118
  target: waypoint.target,
14466
15119
  fov: cameraState?.fov ?? null,
14467
- zoom: cameraState?.zoom ?? 1
15120
+ zoom: cameraState?.zoom ?? 1,
15121
+ azimuthAngle: waypoint.azimuthAngle,
15122
+ polarAngle: waypoint.polarAngle
14468
15123
  },
14469
15124
  true
14470
15125
  );
@@ -14479,7 +15134,7 @@ function BindingBuilder({ licenseKey }) {
14479
15134
  position: formatVector3(cameraState.position),
14480
15135
  target: formatVector3(cameraState.target)
14481
15136
  } : null;
14482
- const handleAnnotationsChange = (0, import_react38.useCallback)(
15137
+ const handleAnnotationsChange = (0, import_react39.useCallback)(
14483
15138
  (newAnnotations) => {
14484
15139
  setSceneConfig((prev) => {
14485
15140
  const currentIds = prev.annotations.map((a) => a.id).join(",");
@@ -14495,7 +15150,7 @@ function BindingBuilder({ licenseKey }) {
14495
15150
  },
14496
15151
  []
14497
15152
  );
14498
- const handleActiveAnnotationChange = (0, import_react38.useCallback)(
15153
+ const handleActiveAnnotationChange = (0, import_react39.useCallback)(
14499
15154
  (annotation) => {
14500
15155
  setActiveAnnotation(
14501
15156
  annotation ? {
@@ -14505,7 +15160,7 @@ function BindingBuilder({ licenseKey }) {
14505
15160
  },
14506
15161
  []
14507
15162
  );
14508
- (0, import_react38.useEffect)(() => {
15163
+ (0, import_react39.useEffect)(() => {
14509
15164
  if (!selectedBinding) {
14510
15165
  setMetricsDraft("{}");
14511
15166
  setMetadataDraft("{}");
@@ -14518,7 +15173,7 @@ function BindingBuilder({ licenseKey }) {
14518
15173
  setMetricsError(null);
14519
15174
  setMetadataError(null);
14520
15175
  }, [selectedBinding]);
14521
- (0, import_react38.useEffect)(() => {
15176
+ (0, import_react39.useEffect)(() => {
14522
15177
  const derived = createMaterialPanelEnabledState(selectedMaterial);
14523
15178
  if (materialPanelItemRef.current !== selectedItem) {
14524
15179
  materialPanelItemRef.current = selectedItem;
@@ -14537,12 +15192,12 @@ function BindingBuilder({ licenseKey }) {
14537
15192
  return changed ? next : prev;
14538
15193
  });
14539
15194
  }, [selectedItem, selectedMaterial]);
14540
- (0, import_react38.useEffect)(() => {
15195
+ (0, import_react39.useEffect)(() => {
14541
15196
  return () => {
14542
15197
  uploadedModelRef.current?.cleanup();
14543
15198
  };
14544
15199
  }, []);
14545
- (0, import_react38.useEffect)(() => {
15200
+ (0, import_react39.useEffect)(() => {
14546
15201
  const handleKeyDown = (event) => {
14547
15202
  if (event.key.toLowerCase() !== "z" || !(event.metaKey || event.ctrlKey)) {
14548
15203
  return;
@@ -14563,7 +15218,7 @@ function BindingBuilder({ licenseKey }) {
14563
15218
  window.addEventListener("keydown", handleKeyDown);
14564
15219
  return () => window.removeEventListener("keydown", handleKeyDown);
14565
15220
  }, [activeEditorTab, bindingsHistory, sceneConfigHistory]);
14566
- (0, import_react38.useEffect)(() => {
15221
+ (0, import_react39.useEffect)(() => {
14567
15222
  if (loading || statusTone === "default" || statusTone === "success") return;
14568
15223
  setStatusBannerVisible(true);
14569
15224
  const timeout = window.setTimeout(
@@ -14572,8 +15227,8 @@ function BindingBuilder({ licenseKey }) {
14572
15227
  );
14573
15228
  return () => window.clearTimeout(timeout);
14574
15229
  }, [statusMessage, statusTone, loading]);
14575
- const nodeKeys = (0, import_react38.useMemo)(() => Object.keys(bindings), [bindings]);
14576
- const idError = (0, import_react38.useMemo)(() => {
15230
+ const nodeKeys = (0, import_react39.useMemo)(() => Object.keys(bindings), [bindings]);
15231
+ const idError = (0, import_react39.useMemo)(() => {
14577
15232
  if (!selectedItem || !selectedBinding) return null;
14578
15233
  const trimmed = selectedBinding.id.trim();
14579
15234
  if (!trimmed) return "ID is required.";
@@ -14616,7 +15271,7 @@ function BindingBuilder({ licenseKey }) {
14616
15271
  });
14617
15272
  }
14618
15273
  };
14619
- const applyBindingsFromScene = (0, import_react38.useCallback)(
15274
+ const applyBindingsFromScene = (0, import_react39.useCallback)(
14620
15275
  (scene, nextModelName) => {
14621
15276
  const nodeNames = collectNodeNames(scene);
14622
15277
  bindingsHistory.reset(buildBindingsFromNodes(nodeNames));
@@ -14655,7 +15310,7 @@ function BindingBuilder({ licenseKey }) {
14655
15310
  bindingsHistory.reset({});
14656
15311
  setSelectedItem(null);
14657
15312
  };
14658
- const handleViewerModelLoaded = (0, import_react38.useCallback)(
15313
+ const handleViewerModelLoaded = (0, import_react39.useCallback)(
14659
15314
  (scene) => {
14660
15315
  const pending = pendingLoadRef.current;
14661
15316
  if (!pending || pending.requestId !== loadRequestRef.current) return;
@@ -14665,7 +15320,7 @@ function BindingBuilder({ licenseKey }) {
14665
15320
  },
14666
15321
  [applyBindingsFromScene]
14667
15322
  );
14668
- const handleViewerLoadError = (0, import_react38.useCallback)((error) => {
15323
+ const handleViewerLoadError = (0, import_react39.useCallback)((error) => {
14669
15324
  const pending = pendingLoadRef.current;
14670
15325
  if (!pending || pending.requestId !== loadRequestRef.current) return;
14671
15326
  pendingLoadRef.current = null;
@@ -14676,7 +15331,7 @@ function BindingBuilder({ licenseKey }) {
14676
15331
  setStatusTone("error");
14677
15332
  setLoading(false);
14678
15333
  }, []);
14679
- const handleAnimationsReady = (0, import_react38.useCallback)(
15334
+ const handleAnimationsReady = (0, import_react39.useCallback)(
14680
15335
  (controls) => {
14681
15336
  viewerAnimations.handleAnimationsReady(controls);
14682
15337
  const details = controls.clipDetails ?? controls.clips.map((sourceName) => ({ sourceName, duration: 0 }));
@@ -14977,7 +15632,7 @@ function BindingBuilder({ licenseKey }) {
14977
15632
  }));
14978
15633
  setError(null);
14979
15634
  };
14980
- const handleViewerSelect = (0, import_react38.useCallback)(
15635
+ const handleViewerSelect = (0, import_react39.useCallback)(
14981
15636
  (binding) => {
14982
15637
  if (!binding) {
14983
15638
  setSelectedItem(null);
@@ -14990,7 +15645,7 @@ function BindingBuilder({ licenseKey }) {
14990
15645
  },
14991
15646
  [bindings]
14992
15647
  );
14993
- const handleSceneNodeSelect = (0, import_react38.useCallback)(
15648
+ const handleSceneNodeSelect = (0, import_react39.useCallback)(
14994
15649
  async (nodeKey) => {
14995
15650
  setSelectedItem(nodeKey);
14996
15651
  setPanelHoveredBinding(null);
@@ -14998,7 +15653,7 @@ function BindingBuilder({ licenseKey }) {
14998
15653
  },
14999
15654
  [focusObject]
15000
15655
  );
15001
- const saveCameraPositionForSelectedObject = (0, import_react38.useCallback)(() => {
15656
+ const saveCameraPositionForSelectedObject = (0, import_react39.useCallback)(() => {
15002
15657
  if (!selectedItem || !cameraState) {
15003
15658
  return;
15004
15659
  }
@@ -15013,7 +15668,7 @@ function BindingBuilder({ licenseKey }) {
15013
15668
  cameraState: currentCameraState
15014
15669
  }));
15015
15670
  }, [selectedItem, cameraState]);
15016
- const goToSavedCameraPosition = (0, import_react38.useCallback)(() => {
15671
+ const goToSavedCameraPosition = (0, import_react39.useCallback)(() => {
15017
15672
  if (!savedCameraPosition?.position || !savedCameraPosition?.target) return;
15018
15673
  setCameraState(
15019
15674
  {
@@ -15025,28 +15680,28 @@ function BindingBuilder({ licenseKey }) {
15025
15680
  true
15026
15681
  );
15027
15682
  }, [savedCameraPosition, setCameraState, cameraState]);
15028
- const clearSavedCameraPosition = (0, import_react38.useCallback)(() => {
15683
+ const clearSavedCameraPosition = (0, import_react39.useCallback)(() => {
15029
15684
  if (!selectedItem) return;
15030
15685
  updateBinding(selectedItem, (binding) => ({
15031
15686
  ...binding,
15032
15687
  cameraState: void 0
15033
15688
  }));
15034
15689
  }, [selectedItem]);
15035
- const handleResetView = (0, import_react38.useCallback)(async () => {
15690
+ const handleResetView = (0, import_react39.useCallback)(async () => {
15036
15691
  await resetView();
15037
15692
  }, [resetView]);
15038
- const handleFitScene = (0, import_react38.useCallback)(async () => {
15693
+ const handleFitScene = (0, import_react39.useCallback)(async () => {
15039
15694
  await fitScene();
15040
15695
  }, [fitScene]);
15041
- const openResetDialog = (0, import_react38.useCallback)((target) => {
15696
+ const openResetDialog = (0, import_react39.useCallback)((target) => {
15042
15697
  setResetTarget(target);
15043
15698
  setResetDialogOpen(true);
15044
15699
  }, []);
15045
- const closeResetDialog = (0, import_react38.useCallback)(() => {
15700
+ const closeResetDialog = (0, import_react39.useCallback)(() => {
15046
15701
  setResetDialogOpen(false);
15047
15702
  setResetTarget(null);
15048
15703
  }, []);
15049
- const executeReset = (0, import_react38.useCallback)(() => {
15704
+ const executeReset = (0, import_react39.useCallback)(() => {
15050
15705
  if (resetTarget === "bindings") {
15051
15706
  if (!nodeKeys.length) {
15052
15707
  closeResetDialog();
@@ -15177,7 +15832,9 @@ function BindingBuilder({ licenseKey }) {
15177
15832
  // the whole settings panel for no reason.
15178
15833
  activeSubTab === "Cinematic" && cameraState ? {
15179
15834
  position: cameraState.position,
15180
- target: cameraState.target
15835
+ target: cameraState.target,
15836
+ azimuthAngle: cameraState.azimuthAngle,
15837
+ polarAngle: cameraState.polarAngle
15181
15838
  } : null
15182
15839
  ),
15183
15840
  onPreviewWaypoint: handlePreviewWaypoint,
@@ -15602,7 +16259,7 @@ function BindingBuilder({ licenseKey }) {
15602
16259
  }
15603
16260
 
15604
16261
  // src/components/SimpleModelViewer/SimpleModelViewer.tsx
15605
- var import_react39 = require("react");
16262
+ var import_react40 = require("react");
15606
16263
  var import_drei6 = require("@react-three/drei");
15607
16264
  var import_fiber12 = require("@react-three/fiber");
15608
16265
  var import_fiber13 = require("@react-three/fiber");
@@ -15642,7 +16299,7 @@ function applyClippingPlanes(scene, controls) {
15642
16299
  camera.updateProjectionMatrix();
15643
16300
  controls.maxDistance = size * 10;
15644
16301
  }
15645
- var ModelErrorBoundary2 = class extends import_react39.Component {
16302
+ var ModelErrorBoundary2 = class extends import_react40.Component {
15646
16303
  constructor() {
15647
16304
  super(...arguments);
15648
16305
  this.state = { hasError: false };
@@ -15674,12 +16331,12 @@ function InnerModel({
15674
16331
  meshopt: true
15675
16332
  });
15676
16333
  const invalidate = (0, import_fiber12.useThree)((state) => state.invalidate);
15677
- const [hoveredKey, setHoveredKey] = (0, import_react39.useState)(null);
15678
- const scene = (0, import_react39.useMemo)(
16334
+ const [hoveredKey, setHoveredKey] = (0, import_react40.useState)(null);
16335
+ const scene = (0, import_react40.useMemo)(
15679
16336
  () => import_three_stdlib4.SkeletonUtils.clone(rawScene),
15680
16337
  [rawScene]
15681
16338
  );
15682
- const meshMap = (0, import_react39.useMemo)(() => {
16339
+ const meshMap = (0, import_react40.useMemo)(() => {
15683
16340
  const map = {};
15684
16341
  scene.traverse((obj) => {
15685
16342
  if (!isMeshNode2(obj)) return;
@@ -15688,7 +16345,7 @@ function InnerModel({
15688
16345
  });
15689
16346
  return map;
15690
16347
  }, [scene]);
15691
- const perKeyMaterials = (0, import_react39.useMemo)(() => {
16348
+ const perKeyMaterials = (0, import_react40.useMemo)(() => {
15692
16349
  const map = {};
15693
16350
  for (const [key, { ref }] of Object.entries(meshMap)) {
15694
16351
  const original = Array.isArray(ref.material) ? ref.material[0] : ref.material;
@@ -15704,14 +16361,14 @@ function InnerModel({
15704
16361
  }
15705
16362
  return map;
15706
16363
  }, [meshMap]);
15707
- (0, import_react39.useEffect)(
16364
+ (0, import_react40.useEffect)(
15708
16365
  () => () => {
15709
16366
  Object.values(perKeyMaterials).forEach((m) => m.dispose());
15710
16367
  },
15711
16368
  [perKeyMaterials]
15712
16369
  );
15713
- const notifiedUrl = (0, import_react39.useRef)(null);
15714
- (0, import_react39.useEffect)(() => {
16370
+ const notifiedUrl = (0, import_react40.useRef)(null);
16371
+ (0, import_react40.useEffect)(() => {
15715
16372
  if (notifiedUrl.current === url) return;
15716
16373
  notifiedUrl.current = url;
15717
16374
  scene.updateWorldMatrix(true, true);
@@ -15719,7 +16376,7 @@ function InnerModel({
15719
16376
  onMeshesReady(meshMap);
15720
16377
  onModelLoaded(scene);
15721
16378
  }, [url, scene, meshMap, onMeshesReady, onModelLoaded]);
15722
- (0, import_react39.useEffect)(() => {
16379
+ (0, import_react40.useEffect)(() => {
15723
16380
  const controller = new AbortController();
15724
16381
  void buildSceneBVH(scene, controller.signal);
15725
16382
  return () => {
@@ -15727,26 +16384,26 @@ function InnerModel({
15727
16384
  disposeSceneBVH(scene);
15728
16385
  };
15729
16386
  }, [scene]);
15730
- const pointerRaycastEnabled = (0, import_react39.useMemo)(
16387
+ const pointerRaycastEnabled = (0, import_react40.useMemo)(
15731
16388
  () => !exceedsBvhBudget(scene),
15732
16389
  [scene]
15733
16390
  );
15734
- (0, import_react39.useEffect)(() => {
16391
+ (0, import_react40.useEffect)(() => {
15735
16392
  for (const [key, { ref }] of Object.entries(meshMap)) {
15736
16393
  nodeRefsOut.current[key] = ref;
15737
16394
  }
15738
16395
  }, [meshMap, nodeRefsOut]);
15739
- (0, import_react39.useEffect)(() => {
16396
+ (0, import_react40.useEffect)(() => {
15740
16397
  if (!highlightOnHover && hoveredKey !== null) {
15741
16398
  setHoveredKey(null);
15742
16399
  }
15743
16400
  }, [highlightOnHover, hoveredKey]);
15744
- (0, import_react39.useEffect)(() => {
16401
+ (0, import_react40.useEffect)(() => {
15745
16402
  return () => {
15746
16403
  document.body.style.cursor = "default";
15747
16404
  };
15748
16405
  }, []);
15749
- (0, import_react39.useEffect)(() => {
16406
+ (0, import_react40.useEffect)(() => {
15750
16407
  for (const [key, { ref }] of Object.entries(meshMap)) {
15751
16408
  ref.visible = !hiddenKeys.has(key);
15752
16409
  const mat = perKeyMaterials[key];
@@ -15816,11 +16473,11 @@ function CameraFitter({
15816
16473
  refitOnResize,
15817
16474
  onInitialFitComplete
15818
16475
  }) {
15819
- const fitted = (0, import_react39.useRef)(false);
16476
+ const fitted = (0, import_react40.useRef)(false);
15820
16477
  const invalidate = (0, import_fiber12.useThree)((state) => state.invalidate);
15821
16478
  const size = (0, import_fiber12.useThree)((state) => state.size);
15822
- const resizeTimerRef = (0, import_react39.useRef)(null);
15823
- (0, import_react39.useEffect)(() => {
16479
+ const resizeTimerRef = (0, import_react40.useRef)(null);
16480
+ (0, import_react40.useEffect)(() => {
15824
16481
  fitted.current = false;
15825
16482
  invalidate();
15826
16483
  }, [invalidate, scene]);
@@ -15840,7 +16497,7 @@ function CameraFitter({
15840
16497
  }).catch(() => {
15841
16498
  }).finally(onInitialFitComplete);
15842
16499
  });
15843
- (0, import_react39.useEffect)(() => {
16500
+ (0, import_react40.useEffect)(() => {
15844
16501
  if (!refitOnResize || !fitted.current || !scene || !controlsRef.current) {
15845
16502
  return;
15846
16503
  }
@@ -15868,7 +16525,7 @@ function CameraFitter({
15868
16525
  function ExposureController({ exposure }) {
15869
16526
  const gl = (0, import_fiber12.useThree)((state) => state.gl);
15870
16527
  const invalidate = (0, import_fiber12.useThree)((state) => state.invalidate);
15871
- (0, import_react39.useEffect)(() => {
16528
+ (0, import_react40.useEffect)(() => {
15872
16529
  gl.toneMappingExposure = exposure;
15873
16530
  invalidate();
15874
16531
  }, [gl, exposure, invalidate]);
@@ -15976,7 +16633,7 @@ function EnvironmentSection({
15976
16633
  title,
15977
16634
  children
15978
16635
  }) {
15979
- const [collapsed, setCollapsed] = (0, import_react39.useState)(false);
16636
+ const [collapsed, setCollapsed] = (0, import_react40.useState)(false);
15980
16637
  return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "env-section", children: [
15981
16638
  /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
15982
16639
  "button",
@@ -16141,8 +16798,8 @@ function SceneObjectsPanel2({
16141
16798
  onHover,
16142
16799
  onToggleVisibility
16143
16800
  }) {
16144
- const [search, setSearch] = (0, import_react39.useState)("");
16145
- const [rowHovered, setRowHovered] = (0, import_react39.useState)(null);
16801
+ const [search, setSearch] = (0, import_react40.useState)("");
16802
+ const [rowHovered, setRowHovered] = (0, import_react40.useState)(null);
16146
16803
  const entries = Object.values(meshKeys).filter(
16147
16804
  (m) => m.label.toLowerCase().includes(search.toLowerCase())
16148
16805
  );
@@ -16266,35 +16923,35 @@ function SimpleModelViewer({
16266
16923
  onModelLoaded,
16267
16924
  onLoadError
16268
16925
  }) {
16269
- const cameraControlsRef = (0, import_react39.useRef)(null);
16270
- const nodeRefsOut = (0, import_react39.useRef)({});
16271
- const fileInputRef = (0, import_react39.useRef)(null);
16272
- const uploadedModelRef = (0, import_react39.useRef)(null);
16273
- const dragDepthRef = (0, import_react39.useRef)(0);
16274
- const [loadedScene, setLoadedScene] = (0, import_react39.useState)(null);
16275
- const [largeModelTriangles, setLargeModelTriangles] = (0, import_react39.useState)(
16926
+ const cameraControlsRef = (0, import_react40.useRef)(null);
16927
+ const nodeRefsOut = (0, import_react40.useRef)({});
16928
+ const fileInputRef = (0, import_react40.useRef)(null);
16929
+ const uploadedModelRef = (0, import_react40.useRef)(null);
16930
+ const dragDepthRef = (0, import_react40.useRef)(0);
16931
+ const [loadedScene, setLoadedScene] = (0, import_react40.useState)(null);
16932
+ const [largeModelTriangles, setLargeModelTriangles] = (0, import_react40.useState)(
16276
16933
  null
16277
16934
  );
16278
- const [largeModelNoticeDismissed, setLargeModelNoticeDismissed] = (0, import_react39.useState)(false);
16279
- const [meshKeys, setMeshKeys] = (0, import_react39.useState)({});
16280
- const [hiddenKeys, setHiddenKeys] = (0, import_react39.useState)(/* @__PURE__ */ new Set());
16281
- const [selectedKey, setSelectedKey] = (0, import_react39.useState)(null);
16282
- const [panelHoveredKey, setPanelHoveredKey] = (0, import_react39.useState)(null);
16283
- const [uploadedModelUrl, setUploadedModelUrl] = (0, import_react39.useState)(null);
16284
- const [uploadedModelName, setUploadedModelName] = (0, import_react39.useState)(
16935
+ const [largeModelNoticeDismissed, setLargeModelNoticeDismissed] = (0, import_react40.useState)(false);
16936
+ const [meshKeys, setMeshKeys] = (0, import_react40.useState)({});
16937
+ const [hiddenKeys, setHiddenKeys] = (0, import_react40.useState)(/* @__PURE__ */ new Set());
16938
+ const [selectedKey, setSelectedKey] = (0, import_react40.useState)(null);
16939
+ const [panelHoveredKey, setPanelHoveredKey] = (0, import_react40.useState)(null);
16940
+ const [uploadedModelUrl, setUploadedModelUrl] = (0, import_react40.useState)(null);
16941
+ const [uploadedModelName, setUploadedModelName] = (0, import_react40.useState)(
16285
16942
  null
16286
16943
  );
16287
- const [modelLoadFailed, setModelLoadFailed] = (0, import_react39.useState)(false);
16288
- const [isResolvingUpload, setIsResolvingUpload] = (0, import_react39.useState)(false);
16289
- const [initialFitComplete, setInitialFitComplete] = (0, import_react39.useState)(false);
16290
- const [isDragActive, setIsDragActive] = (0, import_react39.useState)(false);
16291
- const [isPanelOpen, setIsPanelOpen] = (0, import_react39.useState)(
16944
+ const [modelLoadFailed, setModelLoadFailed] = (0, import_react40.useState)(false);
16945
+ const [isResolvingUpload, setIsResolvingUpload] = (0, import_react40.useState)(false);
16946
+ const [initialFitComplete, setInitialFitComplete] = (0, import_react40.useState)(false);
16947
+ const [isDragActive, setIsDragActive] = (0, import_react40.useState)(false);
16948
+ const [isPanelOpen, setIsPanelOpen] = (0, import_react40.useState)(
16292
16949
  () => typeof window === "undefined" ? true : window.innerWidth > 900
16293
16950
  );
16294
- const [isEnvPanelOpen, setIsEnvPanelOpen] = (0, import_react39.useState)(
16951
+ const [isEnvPanelOpen, setIsEnvPanelOpen] = (0, import_react40.useState)(
16295
16952
  () => typeof window === "undefined" ? true : window.innerWidth > 900
16296
16953
  );
16297
- const [envSettings, setEnvSettings] = (0, import_react39.useState)({
16954
+ const [envSettings, setEnvSettings] = (0, import_react40.useState)({
16298
16955
  backgroundEnabled,
16299
16956
  backgroundColor,
16300
16957
  autoRotate,
@@ -16306,7 +16963,7 @@ function SimpleModelViewer({
16306
16963
  directionalColor
16307
16964
  });
16308
16965
  const activeModelUrl = enableModelUpload ? uploadedModelUrl : modelUrl;
16309
- const resetModelState = (0, import_react39.useCallback)(() => {
16966
+ const resetModelState = (0, import_react40.useCallback)(() => {
16310
16967
  setLoadedScene(null);
16311
16968
  setLargeModelTriangles(null);
16312
16969
  setLargeModelNoticeDismissed(false);
@@ -16317,11 +16974,11 @@ function SimpleModelViewer({
16317
16974
  setModelLoadFailed(false);
16318
16975
  setInitialFitComplete(false);
16319
16976
  }, []);
16320
- const revokeUploadedModelUrl = (0, import_react39.useCallback)(() => {
16977
+ const revokeUploadedModelUrl = (0, import_react40.useCallback)(() => {
16321
16978
  uploadedModelRef.current?.cleanup();
16322
16979
  uploadedModelRef.current = null;
16323
16980
  }, []);
16324
- const clearUploadedModel = (0, import_react39.useCallback)(() => {
16981
+ const clearUploadedModel = (0, import_react40.useCallback)(() => {
16325
16982
  revokeUploadedModelUrl();
16326
16983
  setUploadedModelUrl(null);
16327
16984
  setUploadedModelName(null);
@@ -16350,7 +17007,7 @@ function SimpleModelViewer({
16350
17007
  resetModelState,
16351
17008
  revokeUploadedModelUrl
16352
17009
  ]);
16353
- const handleUploadedModel = (0, import_react39.useCallback)(
17010
+ const handleUploadedModel = (0, import_react40.useCallback)(
16354
17011
  async (file) => {
16355
17012
  setIsResolvingUpload(true);
16356
17013
  try {
@@ -16369,7 +17026,7 @@ function SimpleModelViewer({
16369
17026
  },
16370
17027
  [onLoadError, resetModelState, revokeUploadedModelUrl]
16371
17028
  );
16372
- const handleFileSelection = (0, import_react39.useCallback)(
17029
+ const handleFileSelection = (0, import_react40.useCallback)(
16373
17030
  (files) => {
16374
17031
  const file = files?.[0];
16375
17032
  if (!file) return;
@@ -16377,29 +17034,29 @@ function SimpleModelViewer({
16377
17034
  },
16378
17035
  [handleUploadedModel]
16379
17036
  );
16380
- (0, import_react39.useEffect)(() => {
17037
+ (0, import_react40.useEffect)(() => {
16381
17038
  return () => {
16382
17039
  revokeUploadedModelUrl();
16383
17040
  };
16384
17041
  }, [revokeUploadedModelUrl]);
16385
- const handleEnvSettingChange = (0, import_react39.useCallback)(
17042
+ const handleEnvSettingChange = (0, import_react40.useCallback)(
16386
17043
  (key, value) => {
16387
17044
  setEnvSettings((prev) => ({ ...prev, [key]: value }));
16388
17045
  },
16389
17046
  []
16390
17047
  );
16391
- (0, import_react39.useEffect)(() => {
17048
+ (0, import_react40.useEffect)(() => {
16392
17049
  setEnvSettings((prev) => ({ ...prev, backgroundColor }));
16393
17050
  }, [backgroundColor]);
16394
- (0, import_react39.useEffect)(() => {
17051
+ (0, import_react40.useEffect)(() => {
16395
17052
  resetModelState();
16396
17053
  }, [activeModelUrl, resetModelState]);
16397
- const handleMeshesReady = (0, import_react39.useCallback)((meshes) => {
17054
+ const handleMeshesReady = (0, import_react40.useCallback)((meshes) => {
16398
17055
  setMeshKeys(meshes);
16399
17056
  setHiddenKeys(/* @__PURE__ */ new Set());
16400
17057
  setSelectedKey(null);
16401
17058
  }, []);
16402
- const handleModelLoaded = (0, import_react39.useCallback)(
17059
+ const handleModelLoaded = (0, import_react40.useCallback)(
16403
17060
  (scene) => {
16404
17061
  setLoadedScene(scene);
16405
17062
  setModelLoadFailed(false);
@@ -16410,14 +17067,14 @@ function SimpleModelViewer({
16410
17067
  },
16411
17068
  [onModelLoaded]
16412
17069
  );
16413
- const handleLoadError = (0, import_react39.useCallback)(
17070
+ const handleLoadError = (0, import_react40.useCallback)(
16414
17071
  (error) => {
16415
17072
  setModelLoadFailed(true);
16416
17073
  onLoadError?.(error);
16417
17074
  },
16418
17075
  [onLoadError]
16419
17076
  );
16420
- const focusMesh = (0, import_react39.useCallback)(
17077
+ const focusMesh = (0, import_react40.useCallback)(
16421
17078
  (key) => {
16422
17079
  setSelectedKey(key);
16423
17080
  if (!zoomOnSelected) return;
@@ -16433,14 +17090,14 @@ function SimpleModelViewer({
16433
17090
  },
16434
17091
  [zoomOnSelected]
16435
17092
  );
16436
- const handleToggleVisibility = (0, import_react39.useCallback)((key) => {
17093
+ const handleToggleVisibility = (0, import_react40.useCallback)((key) => {
16437
17094
  setHiddenKeys((prev) => {
16438
17095
  const next = new Set(prev);
16439
17096
  next.has(key) ? next.delete(key) : next.add(key);
16440
17097
  return next;
16441
17098
  });
16442
17099
  }, []);
16443
- const handleDragEnter = (0, import_react39.useCallback)(
17100
+ const handleDragEnter = (0, import_react40.useCallback)(
16444
17101
  (event) => {
16445
17102
  if (!enableModelUpload || !supportsDraggedFiles(event.dataTransfer)) {
16446
17103
  return;
@@ -16452,7 +17109,7 @@ function SimpleModelViewer({
16452
17109
  },
16453
17110
  [enableModelUpload]
16454
17111
  );
16455
- const handleDragOver = (0, import_react39.useCallback)(
17112
+ const handleDragOver = (0, import_react40.useCallback)(
16456
17113
  (event) => {
16457
17114
  if (!enableModelUpload || !supportsDraggedFiles(event.dataTransfer)) {
16458
17115
  return;
@@ -16464,7 +17121,7 @@ function SimpleModelViewer({
16464
17121
  },
16465
17122
  [enableModelUpload]
16466
17123
  );
16467
- const handleDragLeave = (0, import_react39.useCallback)(
17124
+ const handleDragLeave = (0, import_react40.useCallback)(
16468
17125
  (event) => {
16469
17126
  if (!enableModelUpload || !supportsDraggedFiles(event.dataTransfer)) {
16470
17127
  return;
@@ -16478,7 +17135,7 @@ function SimpleModelViewer({
16478
17135
  },
16479
17136
  [enableModelUpload]
16480
17137
  );
16481
- const handleDrop = (0, import_react39.useCallback)(
17138
+ const handleDrop = (0, import_react40.useCallback)(
16482
17139
  (event) => {
16483
17140
  if (!enableModelUpload || !supportsDraggedFiles(event.dataTransfer)) {
16484
17141
  return;
@@ -16571,7 +17228,7 @@ function SimpleModelViewer({
16571
17228
  raycaster: { firstHitOnly: true },
16572
17229
  style: { background: envSettings.backgroundColor },
16573
17230
  camera: { position: [0, 0, 5], fov: 45, near: 0.1, far: 1e3 },
16574
- children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_react39.Suspense, { fallback: null, children: [
17231
+ children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_react40.Suspense, { fallback: null, children: [
16575
17232
  /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
16576
17233
  "ambientLight",
16577
17234
  {
@@ -16589,7 +17246,7 @@ function SimpleModelViewer({
16589
17246
  }
16590
17247
  ),
16591
17248
  /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("directionalLight", { position: [-5, 5, 5], intensity: 0.5 }),
16592
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_react39.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
17249
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_react40.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
16593
17250
  import_drei6.Environment,
16594
17251
  {
16595
17252
  preset: envSettings.envPreset,
@@ -16608,7 +17265,7 @@ function SimpleModelViewer({
16608
17265
  ModelErrorBoundary2,
16609
17266
  {
16610
17267
  onLoadError: handleLoadError,
16611
- children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_react39.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
17268
+ children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_react40.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
16612
17269
  InnerModel,
16613
17270
  {
16614
17271
  url: activeModelUrl,
@@ -16707,18 +17364,18 @@ function SimpleModelViewer({
16707
17364
  }
16708
17365
 
16709
17366
  // src/hooks/useViewerSelection.ts
16710
- var import_react40 = require("react");
17367
+ var import_react41 = require("react");
16711
17368
  function useViewerSelection(initialSelectedObjectBinding = null) {
16712
- const [selectedObjectBinding, setSelectedObjectBinding] = (0, import_react40.useState)(
17369
+ const [selectedObjectBinding, setSelectedObjectBinding] = (0, import_react41.useState)(
16713
17370
  initialSelectedObjectBinding
16714
17371
  );
16715
- const handleObjectSelect = (0, import_react40.useCallback)(
17372
+ const handleObjectSelect = (0, import_react41.useCallback)(
16716
17373
  (binding) => {
16717
17374
  setSelectedObjectBinding(binding);
16718
17375
  },
16719
17376
  [setSelectedObjectBinding]
16720
17377
  );
16721
- const clearSelection = (0, import_react40.useCallback)(() => {
17378
+ const clearSelection = (0, import_react41.useCallback)(() => {
16722
17379
  setSelectedObjectBinding(null);
16723
17380
  }, []);
16724
17381
  return {
@@ -16730,9 +17387,9 @@ function useViewerSelection(initialSelectedObjectBinding = null) {
16730
17387
  }
16731
17388
 
16732
17389
  // src/hooks/useObjectBinding.ts
16733
- var import_react41 = require("react");
17390
+ var import_react42 = require("react");
16734
17391
  function useObjectBinding(objectBindings, objectId) {
16735
- return (0, import_react41.useMemo)(() => {
17392
+ return (0, import_react42.useMemo)(() => {
16736
17393
  if (!objectId) {
16737
17394
  return null;
16738
17395
  }
@@ -16747,23 +17404,23 @@ function useObjectBinding(objectBindings, objectId) {
16747
17404
  }
16748
17405
 
16749
17406
  // src/hooks/useObjectBindingIds.ts
16750
- var import_react42 = require("react");
17407
+ var import_react43 = require("react");
16751
17408
  function useObjectBindingIds(objectBindings) {
16752
- return (0, import_react42.useMemo)(
17409
+ return (0, import_react43.useMemo)(
16753
17410
  () => Object.values(objectBindings).map((binding) => binding.modelObjectId),
16754
17411
  [objectBindings]
16755
17412
  );
16756
17413
  }
16757
17414
 
16758
17415
  // src/hooks/useObjectVisibility.ts
16759
- var import_react43 = require("react");
17416
+ var import_react44 = require("react");
16760
17417
  function getHiddenObjects(objectBindings) {
16761
17418
  return Object.fromEntries(
16762
17419
  Object.entries(objectBindings).filter(([, binding]) => binding.visible === false).map(([bindingKey]) => [bindingKey, true])
16763
17420
  );
16764
17421
  }
16765
17422
  function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16766
- const updateVisibility = (0, import_react43.useCallback)(
17423
+ const updateVisibility = (0, import_react44.useCallback)(
16767
17424
  (objectId, visible) => {
16768
17425
  const bindingKey = resolveBindingKey(objectBindings, objectId);
16769
17426
  if (!bindingKey) return;
@@ -16779,15 +17436,15 @@ function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16779
17436
  },
16780
17437
  [objectBindings, onObjectBindingsChange]
16781
17438
  );
16782
- const hideObject = (0, import_react43.useCallback)(
17439
+ const hideObject = (0, import_react44.useCallback)(
16783
17440
  (objectId) => updateVisibility(objectId, false),
16784
17441
  [updateVisibility]
16785
17442
  );
16786
- const showObject = (0, import_react43.useCallback)(
17443
+ const showObject = (0, import_react44.useCallback)(
16787
17444
  (objectId) => updateVisibility(objectId, true),
16788
17445
  [updateVisibility]
16789
17446
  );
16790
- const toggleObjectVisibility = (0, import_react43.useCallback)(
17447
+ const toggleObjectVisibility = (0, import_react44.useCallback)(
16791
17448
  (objectId) => {
16792
17449
  const bindingKey = resolveBindingKey(objectBindings, objectId);
16793
17450
  if (!bindingKey) return;
@@ -16803,7 +17460,7 @@ function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16803
17460
  },
16804
17461
  [objectBindings, onObjectBindingsChange]
16805
17462
  );
16806
- const clearHiddenObjects = (0, import_react43.useCallback)(() => {
17463
+ const clearHiddenObjects = (0, import_react44.useCallback)(() => {
16807
17464
  onObjectBindingsChange(
16808
17465
  Object.fromEntries(
16809
17466
  Object.entries(objectBindings).map(([bindingKey, binding]) => [
@@ -16813,17 +17470,17 @@ function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16813
17470
  )
16814
17471
  );
16815
17472
  }, [objectBindings, onObjectBindingsChange]);
16816
- const hiddenObjects = (0, import_react43.useMemo)(
17473
+ const hiddenObjects = (0, import_react44.useMemo)(
16817
17474
  () => getHiddenObjects(objectBindings),
16818
17475
  [objectBindings]
16819
17476
  );
16820
- const hiddenObjectIds = (0, import_react43.useMemo)(
17477
+ const hiddenObjectIds = (0, import_react44.useMemo)(
16821
17478
  () => Object.entries(hiddenObjects).map(
16822
17479
  ([bindingKey]) => objectBindings[bindingKey]?.id ?? bindingKey
16823
17480
  ),
16824
17481
  [hiddenObjects, objectBindings]
16825
17482
  );
16826
- const isObjectHidden = (0, import_react43.useCallback)(
17483
+ const isObjectHidden = (0, import_react44.useCallback)(
16827
17484
  (objectId) => {
16828
17485
  const bindingKey = resolveBindingKey(objectBindings, objectId);
16829
17486
  return bindingKey ? objectBindings[bindingKey]?.visible === false : false;
@@ -16842,10 +17499,10 @@ function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16842
17499
  }
16843
17500
 
16844
17501
  // src/hooks/useViewerHover.ts
16845
- var import_react44 = require("react");
17502
+ var import_react45 = require("react");
16846
17503
  function useViewerHover() {
16847
- const [hoveredObjectBinding, setHoveredObjectBinding] = (0, import_react44.useState)(null);
16848
- const handleHoveredObject = (0, import_react44.useCallback)(
17504
+ const [hoveredObjectBinding, setHoveredObjectBinding] = (0, import_react45.useState)(null);
17505
+ const handleHoveredObject = (0, import_react45.useCallback)(
16849
17506
  (binding) => {
16850
17507
  setHoveredObjectBinding(binding);
16851
17508
  },
@@ -16858,18 +17515,18 @@ function useViewerHover() {
16858
17515
  }
16859
17516
 
16860
17517
  // src/hooks/useViewerModel.ts
16861
- var import_react45 = require("react");
17518
+ var import_react46 = require("react");
16862
17519
  var import_three22 = require("three");
16863
17520
  function toTuple2(vector) {
16864
17521
  return [vector.x, vector.y, vector.z];
16865
17522
  }
16866
17523
  function useViewerModel() {
16867
- const [isLoading, setIsLoading] = (0, import_react45.useState)(true);
16868
- const [isReady, setIsReady] = (0, import_react45.useState)(false);
16869
- const [error, setError] = (0, import_react45.useState)(null);
16870
- const [bounds, setBounds] = (0, import_react45.useState)(null);
16871
- const [objectCount, setObjectCount] = (0, import_react45.useState)(0);
16872
- const handleModelLoaded = (0, import_react45.useCallback)((scene) => {
17524
+ const [isLoading, setIsLoading] = (0, import_react46.useState)(true);
17525
+ const [isReady, setIsReady] = (0, import_react46.useState)(false);
17526
+ const [error, setError] = (0, import_react46.useState)(null);
17527
+ const [bounds, setBounds] = (0, import_react46.useState)(null);
17528
+ const [objectCount, setObjectCount] = (0, import_react46.useState)(0);
17529
+ const handleModelLoaded = (0, import_react46.useCallback)((scene) => {
16873
17530
  const box = new import_three22.Box3().setFromObject(scene);
16874
17531
  const min = box.min.clone();
16875
17532
  const max = box.max.clone();
@@ -16894,14 +17551,14 @@ function useViewerModel() {
16894
17551
  setIsLoading(false);
16895
17552
  setIsReady(true);
16896
17553
  }, []);
16897
- const handleLoadError = (0, import_react45.useCallback)((nextError) => {
17554
+ const handleLoadError = (0, import_react46.useCallback)((nextError) => {
16898
17555
  setError(nextError);
16899
17556
  setBounds(null);
16900
17557
  setObjectCount(0);
16901
17558
  setIsLoading(false);
16902
17559
  setIsReady(false);
16903
17560
  }, []);
16904
- const resetModelState = (0, import_react45.useCallback)(() => {
17561
+ const resetModelState = (0, import_react46.useCallback)(() => {
16905
17562
  setIsLoading(true);
16906
17563
  setIsReady(false);
16907
17564
  setError(null);
@@ -16921,10 +17578,10 @@ function useViewerModel() {
16921
17578
  }
16922
17579
 
16923
17580
  // src/hooks/useViewerActions.ts
16924
- var import_react46 = require("react");
17581
+ var import_react47 = require("react");
16925
17582
  function useViewerActions(objectBindings, options = {}) {
16926
17583
  const { onAction, onObjectBindingsChange } = options;
16927
- const getActionsForObject = (0, import_react46.useCallback)(
17584
+ const getActionsForObject = (0, import_react47.useCallback)(
16928
17585
  (objectId) => {
16929
17586
  const bindingKey = resolveBindingKey(objectBindings, objectId);
16930
17587
  if (!bindingKey) {
@@ -16934,7 +17591,7 @@ function useViewerActions(objectBindings, options = {}) {
16934
17591
  },
16935
17592
  [objectBindings]
16936
17593
  );
16937
- const runAction = (0, import_react46.useCallback)(
17594
+ const runAction = (0, import_react47.useCallback)(
16938
17595
  (objectId, actionId) => {
16939
17596
  const bindingKey = resolveBindingKey(objectBindings, objectId);
16940
17597
  if (!bindingKey) {
@@ -16976,7 +17633,7 @@ function useViewerActions(objectBindings, options = {}) {
16976
17633
  }
16977
17634
 
16978
17635
  // src/hooks/useShareableViewerState.ts
16979
- var import_react47 = require("react");
17636
+ var import_react48 = require("react");
16980
17637
  function serializeState(cameraState, objectBindings, selectedObjectBinding) {
16981
17638
  const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
16982
17639
  return {
@@ -16996,8 +17653,8 @@ function useShareableViewerState({
16996
17653
  debounceMs = 400,
16997
17654
  autoSync = true
16998
17655
  }) {
16999
- const lastWrittenRawRef = (0, import_react47.useRef)(null);
17000
- const buildHash = (0, import_react47.useCallback)(() => {
17656
+ const lastWrittenRawRef = (0, import_react48.useRef)(null);
17657
+ const buildHash = (0, import_react48.useCallback)(() => {
17001
17658
  const state = serializeState(
17002
17659
  cameraState,
17003
17660
  objectBindings,
@@ -17005,12 +17662,12 @@ function useShareableViewerState({
17005
17662
  );
17006
17663
  return `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
17007
17664
  }, [cameraState, objectBindings, selectedObjectBinding, paramKey]);
17008
- const getShareUrl = (0, import_react47.useCallback)(() => {
17665
+ const getShareUrl = (0, import_react48.useCallback)(() => {
17009
17666
  const url = new URL(window.location.href);
17010
17667
  url.hash = buildHash();
17011
17668
  return url.toString();
17012
17669
  }, [buildHash]);
17013
- const syncUrl = (0, import_react47.useCallback)(() => {
17670
+ const syncUrl = (0, import_react48.useCallback)(() => {
17014
17671
  const url = new URL(window.location.href);
17015
17672
  url.hash = buildHash();
17016
17673
  window.history.replaceState(null, "", url.toString());
@@ -17018,7 +17675,7 @@ function useShareableViewerState({
17018
17675
  paramKey
17019
17676
  );
17020
17677
  }, [buildHash, paramKey]);
17021
- const latestStateRef = (0, import_react47.useRef)({
17678
+ const latestStateRef = (0, import_react48.useRef)({
17022
17679
  cameraState,
17023
17680
  objectBindings,
17024
17681
  selectedObjectBinding
@@ -17028,9 +17685,9 @@ function useShareableViewerState({
17028
17685
  objectBindings,
17029
17686
  selectedObjectBinding
17030
17687
  };
17031
- const [restoreAttempted, setRestoreAttempted] = (0, import_react47.useState)(false);
17032
- const lastSyncedHashRef = (0, import_react47.useRef)(null);
17033
- (0, import_react47.useEffect)(() => {
17688
+ const [restoreAttempted, setRestoreAttempted] = (0, import_react48.useState)(false);
17689
+ const lastSyncedHashRef = (0, import_react48.useRef)(null);
17690
+ (0, import_react48.useEffect)(() => {
17034
17691
  if (!autoSync || !restoreAttempted) return;
17035
17692
  const tick = () => {
17036
17693
  const current = latestStateRef.current;
@@ -17052,9 +17709,9 @@ function useShareableViewerState({
17052
17709
  const id = window.setInterval(tick, debounceMs);
17053
17710
  return () => window.clearInterval(id);
17054
17711
  }, [autoSync, restoreAttempted, debounceMs, paramKey]);
17055
- const lastRestoredRawRef = (0, import_react47.useRef)(null);
17056
- const restoringRawRef = (0, import_react47.useRef)(null);
17057
- const restoreFromUrl = (0, import_react47.useCallback)(async () => {
17712
+ const lastRestoredRawRef = (0, import_react48.useRef)(null);
17713
+ const restoringRawRef = (0, import_react48.useRef)(null);
17714
+ const restoreFromUrl = (0, import_react48.useCallback)(async () => {
17058
17715
  setRestoreAttempted(true);
17059
17716
  const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
17060
17717
  const raw = new URLSearchParams(rawHash).get(paramKey);
@@ -17107,7 +17764,7 @@ function useShareableViewerState({
17107
17764
  setCameraState,
17108
17765
  setSelectedObjectBinding
17109
17766
  ]);
17110
- (0, import_react47.useEffect)(() => {
17767
+ (0, import_react48.useEffect)(() => {
17111
17768
  const handleHashChange = () => {
17112
17769
  void restoreFromUrl();
17113
17770
  };