@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.mjs CHANGED
@@ -200,7 +200,7 @@ function styleInject(css, { insertAt } = {}) {
200
200
  }
201
201
 
202
202
  // src/styles/ModelViewer.css
203
- styleInject('.ri-viewer--license-checking,\n.ri-viewer--license-invalid {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background-color: #f9f9f9;\n}\n.ri-viewer {\n --ri-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --ri-viewer-bg: #1c1c1d;\n --ri-viewer-text: #111827;\n --ri-panel-bg: #ffffff;\n --ri-panel-border: #e5e7eb;\n --ri-panel-title: #111827;\n --ri-panel-muted-text: #4b5563;\n --ri-panel-subtle-text: #9ca3af;\n --ri-panel-surface: #f9fafb;\n --ri-panel-surface-hover: #f3f4f6;\n --ri-panel-surface-border: #e5e7eb;\n --ri-panel-button-text: #1f2937;\n --ri-panel-accent: #2563eb;\n --ri-panel-accent-contrast: #ffffff;\n --ri-license-overlay-bg: rgba(0, 0, 0, 0.04);\n --ri-license-overlay-border: rgba(0, 0, 0, 0.08);\n --ri-license-overlay-text: #888;\n --ri-license-overlay-error-bg: rgba(192, 57, 43, 0.06);\n --ri-license-overlay-error-border: rgba(192, 57, 43, 0.2);\n --ri-license-overlay-error-text: #c0392b;\n --ri-sidepanel-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --ri-sidepanel-bg: var(--ri-databinding-bg, #111318);\n --ri-sidepanel-open-btn-bg: rgba(20, 20, 20, 0.82);\n --ri-sidepanel-open-btn-bg-hover: #27282b7e;\n --ri-sidepanel-border: rgba(255, 255, 255, 0.06);\n --ri-sidepanel-divider: rgba(255, 255, 255, 0.04);\n --ri-sidepanel-border-strong: rgba(255, 255, 255, 0.08);\n --ri-sidepanel-border-active: rgba(255, 255, 255, 0.18);\n --ri-sidepanel-shadow-left: 4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-sidepanel-shadow-right: -4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-sidepanel-text: var(--ri-databinding-text, #f1f5f9);\n --ri-sidepanel-text-emphasis: var(--ri-databinding-text-hover, #cbd5e1);\n --ri-sidepanel-text-muted: var(--ri-databinding-text-muted, #94a3b8);\n --ri-sidepanel-text-subtle: var(--ri-databinding-text-subtle, #64748b);\n --ri-sidepanel-placeholder: var(--ri-databinding-text-faint, #475569);\n --ri-sidepanel-surface: rgba(255, 255, 255, 0.05);\n --ri-sidepanel-surface-hover: rgba(255, 255, 255, 0.1);\n --ri-sidepanel-accent: #3b82f6;\n --ri-sidepanel-accent-soft: rgba(59, 130, 246, 0.12);\n --ri-databinding-bg: #111318;\n --ri-databinding-border: rgba(255, 255, 255, 0.06);\n --ri-databinding-border-strong: rgba(255, 255, 255, 0.08);\n --ri-databinding-border-hover: rgba(255, 255, 255, 0.15);\n --ri-databinding-shadow: 4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-databinding-font-family:\n "Inter",\n "Segoe UI",\n sans-serif;\n --ri-databinding-mono-font-family:\n "JetBrains Mono",\n "Fira Code",\n "Menlo",\n monospace;\n --ri-databinding-text: #f1f5f9;\n --ri-databinding-text-muted: #94a3b8;\n --ri-databinding-text-subtle: #64748b;\n --ri-databinding-text-faint: #475569;\n --ri-databinding-text-hover: #cbd5e1;\n --ri-databinding-surface: rgba(255, 255, 255, 0.03);\n --ri-databinding-surface-hover: rgba(255, 255, 255, 0.06);\n --ri-databinding-accent: #93c5fd;\n --ri-databinding-accent-glow: rgba(147, 197, 253, 0.8);\n --ri-databinding-accent-border: rgba(96, 165, 250, 0.55);\n --ri-databinding-accent-gradient:\n linear-gradient(\n \n 135deg,\n #1e3a8a 0%,\n #1d4ed8 60%,\n #2563eb 100% );\n --ri-databinding-accent-gradient-active:\n linear-gradient(\n \n 135deg,\n #1e3a8a 0%,\n #1e40af 100% );\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n background: var(--ri-viewer-bg);\n position: relative;\n color: var(--ri-viewer-text);\n font-family: var(--ri-font-family);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-synthesis: none;\n}\n.ri-viewer * {\n box-sizing: border-box;\n}\n.ri-viewerLeft {\n display: flex;\n flex: 1;\n min-height: 0;\n min-width: 0;\n position: relative;\n}\n.ri-viewerRight {\n display: flex;\n flex-shrink: 0;\n min-width: 0;\n}\n.ri-panel {\n width: 280px;\n min-height: 0;\n background: var(--ri-panel-bg);\n border-right: 1px solid var(--ri-panel-border);\n display: flex;\n flex-direction: column;\n padding: 20px;\n overflow-y: auto;\n flex-shrink: 0;\n transition: margin-left 0.3s ease, transform 0.3s ease;\n}\n.ri-panel--open {\n transform: translateX(0);\n margin-left: 0;\n}\n.ri-panel--closed {\n transform: translateX(-100%);\n margin-left: -280px;\n}\n.ri-panelHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 12px;\n}\n.ri-panelTitle {\n margin: 0;\n font-size: 15px;\n font-weight: 700;\n color: var(--ri-panel-title);\n line-height: 1.4;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ri-closeButton {\n border: 0;\n background: transparent;\n color: var(--ri-panel-subtle-text);\n font-size: 22px;\n line-height: 1;\n padding: 0;\n cursor: pointer;\n flex-shrink: 0;\n transition: color 0.2s ease;\n}\n.ri-closeButton:hover {\n color: var(--ri-panel-muted-text);\n}\n.ri-json {\n margin: 0 0 16px;\n padding: 12px;\n background: var(--ri-panel-surface);\n border: 1px solid var(--ri-panel-surface-border);\n border-radius: 12px;\n font-size: 12px;\n line-height: 1.55;\n color: var(--ri-panel-button-text);\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ri-divider {\n border-top: 1px solid var(--ri-panel-border);\n margin-bottom: 12px;\n}\n.ri-sectionLabel {\n margin-bottom: 8px;\n font-size: 11px;\n font-weight: 700;\n color: var(--ri-panel-subtle-text);\n text-transform: uppercase;\n letter-spacing: 0.16em;\n}\n.ri-actionList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.ri-actionButton {\n width: 100%;\n border-radius: 8px;\n border: 1px solid var(--ri-panel-surface-border);\n background: var(--ri-panel-surface);\n color: var(--ri-panel-button-text);\n padding: 10px 12px;\n text-align: left;\n font-size: 14px;\n line-height: 1.4;\n cursor: pointer;\n transition:\n background-color 0.2s ease,\n border-color 0.2s ease,\n color 0.2s ease;\n}\n.ri-actionButton:hover {\n background: var(--ri-panel-surface-hover);\n border-color: var(--ri-panel-border);\n}\n.ri-actionButton--active {\n background: var(--ri-panel-accent);\n border-color: var(--ri-panel-accent);\n color: var(--ri-panel-accent-contrast);\n}\n.ri-actionButton--active:hover {\n background: var(--ri-panel-accent);\n border-color: var(--ri-panel-accent);\n}\n.ri-canvasArea {\n flex: 1 1 auto;\n position: relative;\n min-width: 0;\n}\n.ri-canvasArea--measuring {\n cursor: crosshair;\n}\n.ri-canvasArea canvas {\n opacity: 1;\n transition: opacity 0.18s ease;\n}\n.ri-canvasArea--positioning canvas {\n opacity: 0;\n transition: none;\n}\n.ri-bindingsMismatchBanner {\n position: absolute;\n top: 12px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 12px 7px 10px;\n border-radius: 8px;\n background: rgba(30, 20, 0, 0.88);\n border: 1px solid rgba(251, 191, 36, 0.35);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n color: rgb(251, 191, 36);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n white-space: nowrap;\n pointer-events: auto;\n max-width: calc(100% - 32px);\n white-space: normal;\n text-align: center;\n}\n.ri-bindingsMismatchBanner__icon {\n flex-shrink: 0;\n}\n.ri-bindingsMismatchBanner__dismiss {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 4px;\n padding: 2px;\n background: none;\n border: none;\n border-radius: 4px;\n color: rgba(251, 191, 36, 0.7);\n cursor: pointer;\n transition: color 0.15s ease, background 0.15s ease;\n}\n.ri-bindingsMismatchBanner__dismiss:hover {\n color: rgb(251, 191, 36);\n background: rgba(251, 191, 36, 0.12);\n}\n.ri-measureTools {\n position: absolute;\n top: 16px;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-measureButton {\n width: 34px;\n height: 34px;\n padding: 0;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: rgba(255, 255, 255, 0.75);\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease;\n}\n.ri-measureButton:hover {\n background: rgba(40, 40, 40, 0.92);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.25);\n}\n.ri-measureButton:active {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-measureButton--active {\n background: #22d3ee;\n border-color: #22d3ee;\n color: #06343c;\n}\n.ri-measureButton--active:hover {\n background: #38dbf0;\n color: #06343c;\n}\n.ri-measureHint {\n padding: 6px 10px;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n white-space: nowrap;\n}\n.ri-explodeTools {\n position: absolute;\n top: 16px;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-explodeSlider {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 34px;\n padding: 0 12px;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n}\n.ri-explodeSlider input[type=range] {\n width: 120px;\n accent-color: #22d3ee;\n cursor: pointer;\n}\n.ri-cinematicTools {\n position: absolute;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-measureDot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #22d3ee;\n border: 2px solid #fff;\n box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);\n pointer-events: none;\n}\n.ri-measureLabel {\n padding: 3px 7px;\n border-radius: 6px;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n color: #06343c;\n background: #22d3ee;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n user-select: none;\n}\n.ri-xrTools {\n position: absolute;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-xrButton {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 34px;\n padding: 0 10px;\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 8px;\n cursor: pointer;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n transition: background 0.15s ease, border-color 0.15s ease;\n}\n.ri-xrButton:hover {\n background: rgba(40, 40, 40, 0.92);\n border-color: rgba(255, 255, 255, 0.3);\n}\n.ri-mobileHandoffBackdrop {\n position: absolute;\n z-index: 30;\n inset: 0;\n display: grid;\n place-items: center;\n padding: 20px;\n background: rgba(2, 6, 23, 0.68);\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n}\n.ri-mobileHandoffDialog {\n position: relative;\n display: flex;\n width: min(100%, 300px);\n flex-direction: column;\n align-items: center;\n padding: 16px;\n text-align: center;\n color: #e2e8f0;\n background: #0f172a;\n border: 1px solid rgba(148, 163, 184, 0.3);\n border-radius: 16px;\n box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);\n}\n.ri-mobileHandoffDialog > svg {\n color: #67e8f9;\n}\n.ri-mobileHandoffDialog h2 {\n margin: 12px 0 0;\n font-size: 18px;\n line-height: 1.25;\n color: #fff;\n}\n.ri-mobileHandoffDialog p {\n margin: 8px 0 16px;\n font-size: 13px;\n line-height: 1.5;\n color: #cbd5e1;\n}\n.ri-mobileHandoffQr {\n display: grid;\n place-items: center;\n padding: 10px;\n background: #fff;\n border-radius: 10px;\n}\n.ri-mobileHandoffDialog a {\n margin-top: 16px;\n font-size: 13px;\n font-weight: 600;\n color: #a5f3fc;\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n.ri-mobileHandoffClose {\n position: absolute;\n top: 10px;\n right: 10px;\n display: grid;\n width: 30px;\n height: 30px;\n place-items: center;\n padding: 0;\n color: #cbd5e1;\n background: transparent;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.ri-mobileHandoffClose:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n}\n.ri-arHintOverlay {\n position: absolute;\n top: 18px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n width: min(calc(100% - 32px), 360px);\n pointer-events: none;\n}\n.ri-arHint {\n margin: 0 auto;\n width: fit-content;\n max-width: 100%;\n padding: 6px 12px;\n font-size: 11px;\n font-weight: 500;\n line-height: 1.2;\n text-align: center;\n color: rgba(255, 255, 255, 0.92);\n background: rgba(20, 20, 20, 0.58);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 999px;\n user-select: none;\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n.ri-arOverlay {\n position: absolute;\n bottom: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n width: min(calc(100% - 32px), 420px);\n pointer-events: none;\n}\n.ri-arControls {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n}\n.ri-arStatus,\n.ri-arActionButton {\n max-width: 100%;\n padding: 8px 16px;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n text-align: center;\n white-space: nowrap;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 19px;\n user-select: none;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n}\n.ri-arStatus {\n pointer-events: none;\n}\n.ri-arActionButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n cursor: pointer;\n pointer-events: auto;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n transition:\n background 0.15s ease,\n border-color 0.15s ease,\n color 0.15s ease,\n transform 0.15s ease;\n}\n.ri-arActionButton svg {\n flex-shrink: 0;\n}\n.ri-arActionButton:hover {\n background: rgba(40, 40, 40, 0.92);\n border-color: rgba(255, 255, 255, 0.3);\n}\n.ri-arActionButton:active {\n transform: scale(0.98);\n}\n.ri-arActionButton--primary {\n color: #06343c;\n background: #22d3ee;\n border-color: transparent;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n}\n.ri-arActionButton--primary:hover {\n background: #67e8f9;\n border-color: transparent;\n}\n.ri-arActionButton.ri-arActionButton--active,\n.ri-arActionButton[aria-pressed=true],\n.ri-arActionButton.ri-arActionButton--active:hover,\n.ri-arActionButton[aria-pressed=true]:hover,\n.ri-arActionButton.ri-arActionButton--active:active,\n.ri-arActionButton[aria-pressed=true]:active {\n color: #06343c;\n background: #22d3ee;\n border-color: transparent;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n}\n.ri-hiddenInput {\n display: none;\n}\n.ri-popover {\n position: absolute;\n z-index: 50;\n background: #ffffff;\n border: 1px solid #e5e7eb;\n border-radius: 16px;\n box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);\n color: #111827;\n}\n.ri-popover--inline {\n position: static;\n z-index: auto;\n width: 100%;\n border-radius: 10px;\n box-shadow: none;\n}\n.ri-popover--inline .react-colorful {\n width: 100%;\n}\n.ri-popover--inline {\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text, #f1f5f9);\n}\n.ri-popover--inline .ri-popoverHeader {\n border-bottom-color: var( --ri-databinding-border-strong, rgba(255, 255, 255, 0.08) );\n}\n.ri-popover--inline .ri-popoverLabel {\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-colorCode {\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-secondaryButton {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text, #f1f5f9);\n}\n.ri-popover--inline .ri-secondaryButton:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n.ri-popover--inline .ri-previewFrame {\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n}\n.ri-popover--inline .ri-emptyState {\n border-color: var(--ri-databinding-border-hover, rgba(255, 255, 255, 0.15));\n color: var(--ri-databinding-text-subtle, #64748b);\n}\n.ri-popover--inline .ri-uploadButton {\n background: var(--ri-databinding-accent-gradient, #2563eb);\n color: #ffffff;\n}\n.ri-popover--inline .ri-uploadButton:hover {\n background: var(--ri-databinding-accent-gradient-active, #1d4ed8);\n}\n.ri-popover--inline .ri-removeButton {\n background: transparent;\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-removeButton:hover {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n}\n.ri-popover--inline .ri-formatHint {\n color: var(--ri-databinding-text-subtle, #64748b);\n}\n.ri-popoverHeader {\n padding: 12px 16px;\n}\n.ri-popoverLabel {\n margin: 0 0 8px;\n font-size: 11px;\n font-weight: 700;\n color: #6b7280;\n text-transform: uppercase;\n letter-spacing: 0.16em;\n}\n.ri-inlineRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n margin-top: 8px;\n}\n.ri-colorCode {\n font-size: 12px;\n color: #9ca3af;\n font-family:\n ui-monospace,\n SFMono-Regular,\n Menlo,\n Monaco,\n Consolas,\n monospace;\n text-transform: uppercase;\n}\n.ri-secondaryButton {\n border: 1px solid #e5e7eb;\n background: #f3f4f6;\n color: #374151;\n border-radius: 8px;\n padding: 6px 10px;\n font-size: 12px;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-secondaryButton:hover {\n background: #e5e7eb;\n}\n.ri-materialBody {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding-left: 12px;\n padding-right: 12px;\n padding-bottom: 12px;\n}\n.ri-previewFrame {\n position: relative;\n height: 112px;\n border-radius: 12px;\n overflow: hidden;\n border: 1px solid #e5e7eb;\n}\n.ri-previewImage {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n.ri-previewCaption {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n padding: 4px 8px;\n color: #ffffff;\n font-size: 10px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ri-emptyState {\n height: 112px;\n border-radius: 12px;\n border: 2px dashed #e5e7eb;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n color: #d1d5db;\n text-align: center;\n}\n.ri-uploadButton {\n width: 100%;\n border: 0;\n border-radius: 10px;\n background: #111827;\n color: #ffffff;\n padding: 10px 12px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-uploadButton:hover {\n background: #374151;\n}\n.ri-removeButton {\n width: 100%;\n border-radius: 10px;\n border: 1px solid #e5e7eb;\n background: #ffffff;\n color: #6b7280;\n padding: 8px 12px;\n font-size: 12px;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-removeButton:hover {\n background: #f9fafb;\n}\n.ri-formatHint {\n margin: 0;\n text-align: center;\n font-size: 10px;\n color: #d1d5db;\n}\n.ri-buttonContainer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n position: absolute;\n bottom: 12px;\n left: 12px;\n right: 12px;\n z-index: 10;\n}\n.ri-tourControls {\n display: flex;\n gap: 6px;\n align-items: center;\n flex-shrink: 0;\n}\n.ri-rightControls {\n display: flex;\n gap: 6px;\n align-items: center;\n flex-shrink: 0;\n margin-left: auto;\n}\n.ri-rightControls:only-child {\n margin-left: auto;\n}\n.ri-navButtonSingle,\n.ri-tourButton,\n.ri-resetButton,\n.ri-downloadButton {\n height: 34px;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: rgba(255, 255, 255, 0.75);\n border-radius: 8px;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n cursor: pointer;\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease,\n transform 0.15s ease;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n.ri-navButtonSingle:hover:not(:disabled),\n.ri-tourButton:hover:not(:disabled),\n.ri-resetButton:hover:not(:disabled) {\n background: rgba(40, 40, 40, 0.92);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.25);\n}\n.ri-navButtonSingle:active:not(:disabled),\n.ri-tourButton:active:not(:disabled),\n.ri-resetButton:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-navButtonSingle:disabled,\n.ri-tourButton:disabled,\n.ri-resetButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ri-navButtonSingle {\n padding: 0 16px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 8px;\n white-space: nowrap;\n}\n.ri-tourButton {\n padding: 0 12px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 4px;\n white-space: nowrap;\n}\n.ri-tourButton svg {\n width: 15px;\n height: 15px;\n flex-shrink: 0;\n}\n.ri-tourButton--stop {\n border-color: rgba(255, 68, 68, 0.4);\n color: #ff6b6b;\n}\n.ri-tourButton--stop:hover:not(:disabled) {\n background: rgba(255, 68, 68, 0.15);\n border-color: rgba(255, 68, 68, 0.6);\n color: #ff6b6b;\n}\n.ri-tourButton--stop:active:not(:disabled) {\n background: #ff4444;\n color: #fff;\n border-color: #ff4444;\n}\n.ri-tourButton--next {\n border-color: rgba(56, 219, 240, 0.3);\n color: #38dbf0;\n}\n.ri-tourButton--next:hover:not(:disabled) {\n background: rgba(56, 219, 240, 0.12);\n border-color: rgba(56, 219, 240, 0.5);\n color: #38dbf0;\n}\n.ri-tourButton--next:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-resetButton {\n width: 34px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.ri-resetButton svg {\n width: 15px;\n height: 15px;\n pointer-events: none;\n flex-shrink: 0;\n}\n.ri-resetButton:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.5);\n outline-offset: 2px;\n}\n.ri-downloadWrapper {\n position: relative;\n display: inline-flex;\n flex-direction: column;\n align-items: stretch;\n}\n.ri-downloadButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 0 16px;\n font-size: 13px;\n font-weight: 500;\n white-space: nowrap;\n}\n.ri-downloadButton:hover:not(:disabled) {\n background: rgba(0, 0, 0, 0.72);\n border-color: rgba(255, 255, 255, 0.3);\n color: #fff;\n}\n.ri-downloadButton:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-downloadButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ri-downloadButton__spinner {\n display: inline-block;\n width: 13px;\n height: 13px;\n border: 2px solid rgba(255, 255, 255, 0.3);\n border-top-color: #fff;\n border-radius: 50%;\n animation: ri-spin 0.7s linear infinite;\n flex-shrink: 0;\n}\n.ri-downloadMenu {\n position: absolute;\n bottom: calc(100% + 6px);\n right: 0;\n min-width: 168px;\n background: rgba(20, 20, 20, 0.92);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 10px;\n overflow: hidden;\n z-index: 100;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n animation: ri-menuReveal 0.12s ease;\n}\n@keyframes ri-menuReveal {\n from {\n opacity: 0;\n transform: translateY(4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ri-downloadMenuItem {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 10px 14px;\n font-size: 13px;\n font-weight: 400;\n color: #fff;\n background: transparent;\n border: none;\n cursor: pointer;\n text-align: left;\n transition: background 0.1s ease;\n}\n.ri-downloadMenuItem:hover:not(:disabled) {\n background: rgba(255, 255, 255, 0.1);\n}\n.ri-downloadMenuItem:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.ri-downloadMenuDivider {\n height: 1px;\n background: rgba(255, 255, 255, 0.1);\n margin: 0 10px;\n}\n@keyframes ri-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.ri-annotationMarker {\n position: relative;\n width: 36px;\n height: 36px;\n border-radius: 50%;\n background: rgba(20, 20, 20, 0.82);\n border: 2px solid rgba(255, 255, 255, 0.9);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n font-weight: 700;\n cursor: pointer;\n z-index: 20;\n user-select: none;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);\n pointer-events: auto;\n transition: opacity 0.3s ease, transform 0.3s ease;\n}\n.ri-annotationMarker:hover {\n transform: scale(1.08);\n box-shadow: 0 12px 28px rgba(15, 23, 42, 0.18);\n background: rgba(40, 40, 40, 0.92);\n}\n.ri-annotationPopup {\n position: absolute;\n width: min(270px, calc(100vw - 32px));\n background: rgba(20, 20, 20, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 16px;\n padding: 16px;\n z-index: 30;\n display: flex;\n flex-direction: column;\n gap: 12px;\n box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n}\n.ri-annotationInput,\n.ri-annotationTextarea {\n width: 100%;\n border: 1px solid rgba(255, 255, 255, 0.12);\n background: rgba(255, 255, 255, 0.05);\n color: #e5e7eb;\n border-radius: 10px;\n padding: 10px 12px;\n outline: none;\n font-family: inherit;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n}\n.ri-annotationInput {\n font-size: 14px;\n font-weight: 500;\n}\n.ri-annotationTextarea {\n font-size: 13px;\n resize: vertical;\n min-height: 110px;\n}\n.ri-annotationInput:focus,\n.ri-annotationTextarea:focus {\n border-color: #38dbf0;\n box-shadow: 0 0 0 3px rgba(56, 219, 240, 0.12);\n}\n.ri-annotationActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.ri-annotationOkBtn {\n border: 0;\n border-radius: 8px;\n background: rgba(56, 219, 240, 0.8);\n color: #06343c;\n padding: 8px 18px;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.2s ease, opacity 0.2s ease;\n}\n.ri-annotationOkBtn:hover:not(:disabled) {\n background: #38dbf0;\n}\n.ri-annotationOkBtn:disabled,\n.ri-annotationOkBtn.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.ri-annotationCancelBtn {\n border: 1px solid rgba(255, 255, 255, 0.12);\n background: transparent;\n color: #9ca3af;\n border-radius: 8px;\n padding: 8px 12px;\n font-size: 13px;\n cursor: pointer;\n transition: background-color 0.2s ease, border-color 0.2s ease;\n}\n.ri-annotationCancelBtn:hover {\n background: rgba(255, 255, 255, 0.05);\n border-color: rgba(255, 255, 255, 0.2);\n}\n.ri-annotationIcons {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 12px;\n color: #9ca3af;\n}\n.ri-annotationIcons svg {\n flex-shrink: 0;\n}\n.ri-viewAnnotationPopup {\n position: relative;\n background: rgba(20, 20, 20, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n padding: 12px 16px;\n min-width: 200px;\n max-width: min(280px, calc(100vw - 48px));\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n animation: ri-viewAnnotationFadeIn 0.15s ease-out;\n margin-left: 24px;\n margin-top: -20px;\n isolation: isolate;\n z-index: 1000;\n}\n.ri-viewAnnotationHeader {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n.ri-viewAnnotationTitle {\n margin: 0;\n color: #e2e8f0;\n font-size: 14px;\n font-weight: 600;\n}\n.ri-viewAnnotationClose {\n background: transparent;\n border: none;\n color: #b7bdc6;\n cursor: pointer;\n font-size: 18px;\n padding: 0 4px;\n transition: color 0.2s;\n line-height: 1;\n}\n.ri-viewAnnotationClose:hover {\n color: #e2e8f0;\n}\n.ri-viewAnnotationDescription {\n margin: 0;\n color: #cbd5e1;\n font-size: 13px;\n line-height: 1.4;\n word-wrap: break-word;\n}\n@keyframes ri-viewAnnotationFadeIn {\n from {\n opacity: 0;\n transform: translateY(-6px) scale(0.96);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n.ri-annotationNavSingle {\n position: absolute;\n bottom: 120px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n pointer-events: none;\n}\n.ri-annotationNavSingle > * {\n pointer-events: auto;\n}\n.ri-navBadge {\n background: rgba(255, 255, 255, 0.1);\n padding: 2px 10px;\n border-radius: 4px;\n font-size: 12px;\n font-weight: 400;\n color: rgba(255, 255, 255, 0.7);\n}\n.ri-annotationTitleHint {\n background: rgba(0, 0, 0, 0.7);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n color: white;\n padding: 8px 20px;\n border-radius: 8px;\n font-size: 13px;\n border: 1px solid rgba(255, 255, 255, 0.08);\n text-align: center;\n max-width: 350px;\n animation: slideUp 0.3s ease;\n}\n.ri-annotationHintTitle {\n font-weight: 600;\n color: #e5e7eb;\n}\n.ri-annotationHintDesc {\n font-size: 12px;\n opacity: 0.6;\n margin-top: 2px;\n color: #d1d5db;\n}\n@keyframes slideUp {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n@media (max-width: 768px) {\n .ri-xrTools {\n gap: 6px;\n }\n .ri-xrButton {\n height: 34px;\n gap: 6px;\n padding: 0 8px;\n font-size: 11px;\n }\n .ri-xrButton svg {\n width: 14px;\n height: 14px;\n }\n .ri-xrButtonPrefix {\n display: none;\n }\n .ri-annotationNavSingle {\n bottom: 100px;\n }\n .ri-navButtonSingle {\n padding: 8px 20px;\n font-size: 13px;\n min-width: 110px;\n }\n .ri-annotationTitleHint {\n padding: 6px 14px;\n font-size: 12px;\n max-width: 250px;\n }\n .ri-viewAnnotationPopup {\n min-width: 160px;\n max-width: 220px;\n padding: 10px 12px;\n }\n .ri-viewAnnotationTitle {\n font-size: 13px;\n }\n .ri-viewAnnotationDescription {\n font-size: 12px;\n }\n .ri-buttonContainer {\n flex-wrap: wrap;\n gap: 6px;\n }\n .ri-tourControls {\n flex-wrap: wrap;\n }\n .ri-rightControls {\n flex-wrap: wrap;\n }\n}\n@media (max-width: 480px) {\n .ri-xrTools {\n gap: 5px;\n }\n .ri-xrButton {\n height: 34px;\n gap: 5px;\n font-size: 10px;\n }\n .ri-xrButton svg {\n width: 13px;\n height: 13px;\n }\n .ri-annotationNavSingle {\n bottom: 80px;\n }\n .ri-navButtonSingle {\n padding: 6px 16px;\n font-size: 12px;\n min-width: 90px;\n gap: 6px;\n }\n .ri-annotationTitleHint {\n padding: 4px 10px;\n font-size: 11px;\n max-width: 200px;\n }\n .ri-navBadge {\n font-size: 10px;\n padding: 1px 6px;\n }\n .ri-tourButton {\n padding: 0 8px;\n font-size: 12px;\n }\n .ri-tourButton span {\n display: none;\n }\n .ri-downloadButton {\n padding: 0 10px;\n font-size: 12px;\n }\n .ri-downloadButton span {\n display: none;\n }\n}\n.ri-canvasArea .bb-loadingOverlay {\n position: absolute;\n inset: 0;\n z-index: 12;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 24px;\n text-align: center;\n border-radius: 14px;\n background: rgba(15, 23, 42, 0.46);\n backdrop-filter: blur(1px);\n}\n.ri-canvasArea .bb-loadingSpinner {\n width: 40px;\n height: 40px;\n border-radius: 999px;\n border: 3px solid rgba(226, 232, 240, 0.24);\n border-top-color: #93c5fd;\n animation: bb-spin 0.8s linear infinite;\n}\n.ri-canvasArea .bb-loadingMessage {\n max-width: 320px;\n color: #f7f8f9;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.5;\n}\n@keyframes bb-spin {\n to {\n transform: rotate(360deg);\n }\n}\n.ri-largeModelBanner {\n position: absolute;\n top: 12px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 12px 7px 10px;\n border-radius: 8px;\n background: rgba(30, 20, 0, 0.88);\n border: 1px solid rgba(251, 191, 36, 0.35);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n color: rgb(251, 191, 36);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n pointer-events: auto;\n max-width: calc(100% - 32px);\n white-space: normal;\n text-align: center;\n}\n.ri-largeModelBanner--offset {\n top: 64px;\n}\n.ri-largeModelBanner__icon {\n flex-shrink: 0;\n}\n.ri-largeModelBanner__dismiss {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 4px;\n padding: 2px;\n background: none;\n border: none;\n border-radius: 4px;\n color: rgba(251, 191, 36, 0.7);\n cursor: pointer;\n transition: color 0.15s ease, background 0.15s ease;\n}\n.ri-largeModelBanner__dismiss:hover {\n color: rgb(251, 191, 36);\n background: rgba(251, 191, 36, 0.12);\n}\n');
203
+ styleInject('.ri-viewer--license-checking,\n.ri-viewer--license-invalid {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background-color: #f9f9f9;\n}\n.ri-viewer {\n --ri-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --ri-viewer-bg: #1c1c1d;\n --ri-viewer-text: #111827;\n --ri-panel-bg: #ffffff;\n --ri-panel-border: #e5e7eb;\n --ri-panel-title: #111827;\n --ri-panel-muted-text: #4b5563;\n --ri-panel-subtle-text: #9ca3af;\n --ri-panel-surface: #f9fafb;\n --ri-panel-surface-hover: #f3f4f6;\n --ri-panel-surface-border: #e5e7eb;\n --ri-panel-button-text: #1f2937;\n --ri-panel-accent: #2563eb;\n --ri-panel-accent-contrast: #ffffff;\n --ri-license-overlay-bg: rgba(0, 0, 0, 0.04);\n --ri-license-overlay-border: rgba(0, 0, 0, 0.08);\n --ri-license-overlay-text: #888;\n --ri-license-overlay-error-bg: rgba(192, 57, 43, 0.06);\n --ri-license-overlay-error-border: rgba(192, 57, 43, 0.2);\n --ri-license-overlay-error-text: #c0392b;\n --ri-sidepanel-font-family:\n Inter,\n "Segoe UI",\n Helvetica,\n Arial,\n sans-serif;\n --ri-sidepanel-bg: var(--ri-databinding-bg, #111318);\n --ri-sidepanel-open-btn-bg: rgba(20, 20, 20, 0.82);\n --ri-sidepanel-open-btn-bg-hover: #27282b7e;\n --ri-sidepanel-border: rgba(255, 255, 255, 0.06);\n --ri-sidepanel-divider: rgba(255, 255, 255, 0.04);\n --ri-sidepanel-border-strong: rgba(255, 255, 255, 0.08);\n --ri-sidepanel-border-active: rgba(255, 255, 255, 0.18);\n --ri-sidepanel-shadow-left: 4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-sidepanel-shadow-right: -4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-sidepanel-text: var(--ri-databinding-text, #f1f5f9);\n --ri-sidepanel-text-emphasis: var(--ri-databinding-text-hover, #cbd5e1);\n --ri-sidepanel-text-muted: var(--ri-databinding-text-muted, #94a3b8);\n --ri-sidepanel-text-subtle: var(--ri-databinding-text-subtle, #64748b);\n --ri-sidepanel-placeholder: var(--ri-databinding-text-faint, #475569);\n --ri-sidepanel-surface: rgba(255, 255, 255, 0.05);\n --ri-sidepanel-surface-hover: rgba(255, 255, 255, 0.1);\n --ri-sidepanel-accent: #3b82f6;\n --ri-sidepanel-accent-soft: rgba(59, 130, 246, 0.12);\n --ri-databinding-bg: #111318;\n --ri-databinding-border: rgba(255, 255, 255, 0.06);\n --ri-databinding-border-strong: rgba(255, 255, 255, 0.08);\n --ri-databinding-border-hover: rgba(255, 255, 255, 0.15);\n --ri-databinding-shadow: 4px 0 24px rgba(0, 0, 0, 0.35);\n --ri-databinding-font-family:\n "Inter",\n "Segoe UI",\n sans-serif;\n --ri-databinding-mono-font-family:\n "JetBrains Mono",\n "Fira Code",\n "Menlo",\n monospace;\n --ri-databinding-text: #f1f5f9;\n --ri-databinding-text-muted: #94a3b8;\n --ri-databinding-text-subtle: #64748b;\n --ri-databinding-text-faint: #475569;\n --ri-databinding-text-hover: #cbd5e1;\n --ri-databinding-surface: rgba(255, 255, 255, 0.03);\n --ri-databinding-surface-hover: rgba(255, 255, 255, 0.06);\n --ri-databinding-accent: #93c5fd;\n --ri-databinding-accent-glow: rgba(147, 197, 253, 0.8);\n --ri-databinding-accent-border: rgba(96, 165, 250, 0.55);\n --ri-databinding-accent-gradient:\n linear-gradient(\n \n 135deg,\n #1e3a8a 0%,\n #1d4ed8 60%,\n #2563eb 100% );\n --ri-databinding-accent-gradient-active:\n linear-gradient(\n \n 135deg,\n #1e3a8a 0%,\n #1e40af 100% );\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n background: var(--ri-viewer-bg);\n position: relative;\n color: var(--ri-viewer-text);\n font-family: var(--ri-font-family);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-synthesis: none;\n}\n.ri-viewer * {\n box-sizing: border-box;\n}\n.ri-viewerLeft {\n display: flex;\n flex: 1;\n min-height: 0;\n min-width: 0;\n position: relative;\n}\n.ri-viewerRight {\n display: flex;\n flex-shrink: 0;\n min-width: 0;\n}\n.ri-panel {\n width: 280px;\n min-height: 0;\n background: var(--ri-panel-bg);\n border-right: 1px solid var(--ri-panel-border);\n display: flex;\n flex-direction: column;\n padding: 20px;\n overflow-y: auto;\n flex-shrink: 0;\n transition: margin-left 0.3s ease, transform 0.3s ease;\n}\n.ri-panel--open {\n transform: translateX(0);\n margin-left: 0;\n}\n.ri-panel--closed {\n transform: translateX(-100%);\n margin-left: -280px;\n}\n.ri-panelHeader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 12px;\n}\n.ri-panelTitle {\n margin: 0;\n font-size: 15px;\n font-weight: 700;\n color: var(--ri-panel-title);\n line-height: 1.4;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ri-closeButton {\n border: 0;\n background: transparent;\n color: var(--ri-panel-subtle-text);\n font-size: 22px;\n line-height: 1;\n padding: 0;\n cursor: pointer;\n flex-shrink: 0;\n transition: color 0.2s ease;\n}\n.ri-closeButton:hover {\n color: var(--ri-panel-muted-text);\n}\n.ri-json {\n margin: 0 0 16px;\n padding: 12px;\n background: var(--ri-panel-surface);\n border: 1px solid var(--ri-panel-surface-border);\n border-radius: 12px;\n font-size: 12px;\n line-height: 1.55;\n color: var(--ri-panel-button-text);\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.ri-divider {\n border-top: 1px solid var(--ri-panel-border);\n margin-bottom: 12px;\n}\n.ri-sectionLabel {\n margin-bottom: 8px;\n font-size: 11px;\n font-weight: 700;\n color: var(--ri-panel-subtle-text);\n text-transform: uppercase;\n letter-spacing: 0.16em;\n}\n.ri-actionList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.ri-actionButton {\n width: 100%;\n border-radius: 8px;\n border: 1px solid var(--ri-panel-surface-border);\n background: var(--ri-panel-surface);\n color: var(--ri-panel-button-text);\n padding: 10px 12px;\n text-align: left;\n font-size: 14px;\n line-height: 1.4;\n cursor: pointer;\n transition:\n background-color 0.2s ease,\n border-color 0.2s ease,\n color 0.2s ease;\n}\n.ri-actionButton:hover {\n background: var(--ri-panel-surface-hover);\n border-color: var(--ri-panel-border);\n}\n.ri-actionButton--active {\n background: var(--ri-panel-accent);\n border-color: var(--ri-panel-accent);\n color: var(--ri-panel-accent-contrast);\n}\n.ri-actionButton--active:hover {\n background: var(--ri-panel-accent);\n border-color: var(--ri-panel-accent);\n}\n.ri-canvasArea {\n flex: 1 1 auto;\n position: relative;\n min-width: 0;\n}\n.ri-canvasArea--measuring {\n cursor: crosshair;\n}\n.ri-canvasArea canvas {\n opacity: 1;\n transition: opacity 0.18s ease;\n}\n.ri-canvasArea--positioning canvas {\n opacity: 0;\n transition: none;\n}\n.ri-bindingsMismatchBanner {\n position: absolute;\n top: 12px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 7px 12px 7px 10px;\n border-radius: 8px;\n background: rgba(30, 20, 0, 0.88);\n border: 1px solid rgba(251, 191, 36, 0.35);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n color: rgb(251, 191, 36);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n white-space: nowrap;\n pointer-events: auto;\n max-width: calc(100% - 32px);\n white-space: normal;\n text-align: center;\n}\n.ri-bindingsMismatchBanner__icon {\n flex-shrink: 0;\n}\n.ri-bindingsMismatchBanner__dismiss {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 4px;\n padding: 2px;\n background: none;\n border: none;\n border-radius: 4px;\n color: rgba(251, 191, 36, 0.7);\n cursor: pointer;\n transition: color 0.15s ease, background 0.15s ease;\n}\n.ri-bindingsMismatchBanner__dismiss:hover {\n color: rgb(251, 191, 36);\n background: rgba(251, 191, 36, 0.12);\n}\n.ri-measureTools {\n position: absolute;\n top: 16px;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-measureButton {\n width: 34px;\n height: 34px;\n padding: 0;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: rgba(255, 255, 255, 0.75);\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease;\n}\n.ri-measureButton:hover {\n background: rgba(40, 40, 40, 0.92);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.25);\n}\n.ri-measureButton:active {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-measureButton--active {\n background: #22d3ee;\n border-color: #22d3ee;\n color: #06343c;\n}\n.ri-measureButton--active:hover {\n background: #38dbf0;\n color: #06343c;\n}\n.ri-measureHint {\n padding: 6px 10px;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n white-space: nowrap;\n}\n.ri-explodeTools {\n position: absolute;\n top: 16px;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-explodeSlider {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 34px;\n padding: 0 12px;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 8px;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n}\n.ri-explodeSlider input[type=range] {\n width: 120px;\n accent-color: #22d3ee;\n cursor: pointer;\n}\n.ri-cinematicTools {\n position: absolute;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-measureDot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #22d3ee;\n border: 2px solid #fff;\n box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);\n pointer-events: none;\n}\n.ri-measureLabel {\n padding: 3px 7px;\n border-radius: 6px;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n color: #06343c;\n background: #22d3ee;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n user-select: none;\n}\n.ri-xrTools {\n position: absolute;\n left: 16px;\n z-index: 10;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.ri-xrButton {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 34px;\n padding: 0 10px;\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 8px;\n cursor: pointer;\n touch-action: manipulation;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n transition: background 0.15s ease, border-color 0.15s ease;\n}\n.ri-xrButton:hover {\n background: rgba(40, 40, 40, 0.92);\n border-color: rgba(255, 255, 255, 0.3);\n}\n.ri-mobileHandoffBackdrop {\n position: absolute;\n z-index: 30;\n inset: 0;\n display: grid;\n place-items: center;\n padding: 20px;\n background: rgba(2, 6, 23, 0.68);\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n}\n.ri-mobileHandoffDialog {\n position: relative;\n display: flex;\n width: min(100%, 300px);\n flex-direction: column;\n align-items: center;\n padding: 16px;\n text-align: center;\n color: #e2e8f0;\n background: #0f172a;\n border: 1px solid rgba(148, 163, 184, 0.3);\n border-radius: 16px;\n box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);\n}\n.ri-mobileHandoffDialog > svg {\n color: #67e8f9;\n}\n.ri-mobileHandoffDialog h2 {\n margin: 12px 0 0;\n font-size: 18px;\n line-height: 1.25;\n color: #fff;\n}\n.ri-mobileHandoffDialog p {\n margin: 8px 0 16px;\n font-size: 13px;\n line-height: 1.5;\n color: #cbd5e1;\n}\n.ri-mobileHandoffQr {\n display: grid;\n place-items: center;\n padding: 10px;\n background: #fff;\n border-radius: 10px;\n}\n.ri-mobileHandoffDialog a {\n margin-top: 16px;\n font-size: 13px;\n font-weight: 600;\n color: #a5f3fc;\n text-decoration: underline;\n text-underline-offset: 3px;\n}\n.ri-mobileHandoffClose {\n position: absolute;\n top: 10px;\n right: 10px;\n display: grid;\n width: 30px;\n height: 30px;\n place-items: center;\n padding: 0;\n color: #cbd5e1;\n background: transparent;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.ri-mobileHandoffClose:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n}\n.ri-arHintOverlay {\n position: absolute;\n top: 18px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n width: min(calc(100% - 32px), 360px);\n pointer-events: none;\n}\n.ri-arHint {\n margin: 0 auto;\n width: fit-content;\n max-width: 100%;\n padding: 6px 12px;\n font-size: 11px;\n font-weight: 500;\n line-height: 1.2;\n text-align: center;\n color: rgba(255, 255, 255, 0.92);\n background: rgba(20, 20, 20, 0.58);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 999px;\n user-select: none;\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n}\n.ri-arOverlay {\n position: absolute;\n bottom: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10;\n width: min(calc(100% - 32px), 420px);\n pointer-events: none;\n}\n.ri-arControls {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n}\n.ri-arStatus,\n.ri-arActionButton {\n max-width: 100%;\n padding: 8px 16px;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n text-align: center;\n white-space: nowrap;\n color: #fff;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.16);\n border-radius: 19px;\n user-select: none;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n}\n.ri-arStatus {\n pointer-events: none;\n}\n.ri-arActionButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n cursor: pointer;\n pointer-events: auto;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n transition:\n background 0.15s ease,\n border-color 0.15s ease,\n color 0.15s ease,\n transform 0.15s ease;\n}\n.ri-arActionButton svg {\n flex-shrink: 0;\n}\n.ri-arActionButton:hover {\n background: rgba(40, 40, 40, 0.92);\n border-color: rgba(255, 255, 255, 0.3);\n}\n.ri-arActionButton:active {\n transform: scale(0.98);\n}\n.ri-arActionButton--primary {\n color: #06343c;\n background: #22d3ee;\n border-color: transparent;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n}\n.ri-arActionButton--primary:hover {\n background: #67e8f9;\n border-color: transparent;\n}\n.ri-arActionButton.ri-arActionButton--active,\n.ri-arActionButton[aria-pressed=true],\n.ri-arActionButton.ri-arActionButton--active:hover,\n.ri-arActionButton[aria-pressed=true]:hover,\n.ri-arActionButton.ri-arActionButton--active:active,\n.ri-arActionButton[aria-pressed=true]:active {\n color: #06343c;\n background: #22d3ee;\n border-color: transparent;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);\n}\n.ri-hiddenInput {\n display: none;\n}\n.ri-popover {\n position: absolute;\n z-index: 50;\n background: #ffffff;\n border: 1px solid #e5e7eb;\n border-radius: 16px;\n box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);\n color: #111827;\n}\n.ri-popover--inline {\n position: static;\n z-index: auto;\n width: 100%;\n border-radius: 10px;\n box-shadow: none;\n}\n.ri-popover--inline .react-colorful {\n width: 100%;\n}\n.ri-popover--inline {\n background: var(--ri-databinding-surface, rgba(255, 255, 255, 0.03));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text, #f1f5f9);\n}\n.ri-popover--inline .ri-popoverHeader {\n border-bottom-color: var( --ri-databinding-border-strong, rgba(255, 255, 255, 0.08) );\n}\n.ri-popover--inline .ri-popoverLabel {\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-colorCode {\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-secondaryButton {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text, #f1f5f9);\n}\n.ri-popover--inline .ri-secondaryButton:hover {\n background: rgba(255, 255, 255, 0.12);\n}\n.ri-popover--inline .ri-previewFrame {\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n}\n.ri-popover--inline .ri-emptyState {\n border-color: var(--ri-databinding-border-hover, rgba(255, 255, 255, 0.15));\n color: var(--ri-databinding-text-subtle, #64748b);\n}\n.ri-popover--inline .ri-uploadButton {\n background: var(--ri-databinding-accent-gradient, #2563eb);\n color: #ffffff;\n}\n.ri-popover--inline .ri-uploadButton:hover {\n background: var(--ri-databinding-accent-gradient-active, #1d4ed8);\n}\n.ri-popover--inline .ri-removeButton {\n background: transparent;\n border-color: var(--ri-databinding-border-strong, rgba(255, 255, 255, 0.08));\n color: var(--ri-databinding-text-muted, #94a3b8);\n}\n.ri-popover--inline .ri-removeButton:hover {\n background: var(--ri-databinding-surface-hover, rgba(255, 255, 255, 0.06));\n}\n.ri-popover--inline .ri-formatHint {\n color: var(--ri-databinding-text-subtle, #64748b);\n}\n.ri-popoverHeader {\n padding: 12px 16px;\n}\n.ri-popoverLabel {\n margin: 0 0 8px;\n font-size: 11px;\n font-weight: 700;\n color: #6b7280;\n text-transform: uppercase;\n letter-spacing: 0.16em;\n}\n.ri-inlineRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n margin-top: 8px;\n}\n.ri-colorCode {\n font-size: 12px;\n color: #9ca3af;\n font-family:\n ui-monospace,\n SFMono-Regular,\n Menlo,\n Monaco,\n Consolas,\n monospace;\n text-transform: uppercase;\n}\n.ri-secondaryButton {\n border: 1px solid #e5e7eb;\n background: #f3f4f6;\n color: #374151;\n border-radius: 8px;\n padding: 6px 10px;\n font-size: 12px;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-secondaryButton:hover {\n background: #e5e7eb;\n}\n.ri-materialBody {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding-left: 12px;\n padding-right: 12px;\n padding-bottom: 12px;\n}\n.ri-previewFrame {\n position: relative;\n height: 112px;\n border-radius: 12px;\n overflow: hidden;\n border: 1px solid #e5e7eb;\n}\n.ri-previewImage {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n.ri-previewCaption {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n padding: 4px 8px;\n color: #ffffff;\n font-size: 10px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ri-emptyState {\n height: 112px;\n border-radius: 12px;\n border: 2px dashed #e5e7eb;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n color: #d1d5db;\n text-align: center;\n}\n.ri-uploadButton {\n width: 100%;\n border: 0;\n border-radius: 10px;\n background: #111827;\n color: #ffffff;\n padding: 10px 12px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-uploadButton:hover {\n background: #374151;\n}\n.ri-removeButton {\n width: 100%;\n border-radius: 10px;\n border: 1px solid #e5e7eb;\n background: #ffffff;\n color: #6b7280;\n padding: 8px 12px;\n font-size: 12px;\n cursor: pointer;\n transition: background-color 0.2s ease;\n}\n.ri-removeButton:hover {\n background: #f9fafb;\n}\n.ri-formatHint {\n margin: 0;\n text-align: center;\n font-size: 10px;\n color: #d1d5db;\n}\n.ri-buttonContainer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n position: absolute;\n bottom: 12px;\n left: 12px;\n right: 12px;\n z-index: 10;\n}\n.ri-tourControls {\n display: flex;\n gap: 6px;\n align-items: center;\n flex-shrink: 0;\n}\n.ri-rightControls {\n display: flex;\n gap: 6px;\n align-items: center;\n flex-shrink: 0;\n margin-left: auto;\n}\n.ri-rightControls:only-child {\n margin-left: auto;\n}\n.ri-navButtonSingle,\n.ri-tourButton,\n.ri-resetButton,\n.ri-downloadButton {\n height: 34px;\n background: rgba(20, 20, 20, 0.82);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: rgba(255, 255, 255, 0.75);\n border-radius: 8px;\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n cursor: pointer;\n transition:\n background 0.15s ease,\n color 0.15s ease,\n border-color 0.15s ease,\n transform 0.15s ease;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n.ri-navButtonSingle:hover:not(:disabled),\n.ri-tourButton:hover:not(:disabled),\n.ri-resetButton:hover:not(:disabled) {\n background: rgba(40, 40, 40, 0.92);\n color: #fff;\n border-color: rgba(255, 255, 255, 0.25);\n}\n.ri-navButtonSingle:active:not(:disabled),\n.ri-tourButton:active:not(:disabled),\n.ri-resetButton:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-navButtonSingle:disabled,\n.ri-tourButton:disabled,\n.ri-resetButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ri-navButtonSingle {\n padding: 0 16px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 8px;\n white-space: nowrap;\n}\n.ri-tourButton {\n padding: 0 12px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 4px;\n white-space: nowrap;\n}\n.ri-tourButton svg {\n width: 15px;\n height: 15px;\n flex-shrink: 0;\n}\n.ri-tourButton--stop {\n border-color: rgba(255, 68, 68, 0.4);\n color: #ff6b6b;\n}\n.ri-tourButton--stop:hover:not(:disabled) {\n background: rgba(255, 68, 68, 0.15);\n border-color: rgba(255, 68, 68, 0.6);\n color: #ff6b6b;\n}\n.ri-tourButton--stop:active:not(:disabled) {\n background: #ff4444;\n color: #fff;\n border-color: #ff4444;\n}\n.ri-tourButton--next {\n border-color: rgba(56, 219, 240, 0.3);\n color: #38dbf0;\n}\n.ri-tourButton--next:hover:not(:disabled) {\n background: rgba(56, 219, 240, 0.12);\n border-color: rgba(56, 219, 240, 0.5);\n color: #38dbf0;\n}\n.ri-tourButton--next:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-resetButton {\n width: 34px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.ri-resetButton svg {\n width: 15px;\n height: 15px;\n pointer-events: none;\n flex-shrink: 0;\n}\n.ri-resetButton:focus-visible {\n outline: 2px solid rgba(255, 255, 255, 0.5);\n outline-offset: 2px;\n}\n.ri-downloadWrapper {\n position: relative;\n display: inline-flex;\n flex-direction: column;\n align-items: stretch;\n}\n.ri-downloadButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 0 16px;\n font-size: 13px;\n font-weight: 500;\n white-space: nowrap;\n}\n.ri-downloadButton:hover:not(:disabled) {\n background: rgba(0, 0, 0, 0.72);\n border-color: rgba(255, 255, 255, 0.3);\n color: #fff;\n}\n.ri-downloadButton:active:not(:disabled) {\n background: #38dbf0;\n color: #06343c;\n border-color: #38dbf0;\n}\n.ri-downloadButton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ri-downloadButton__spinner {\n display: inline-block;\n width: 13px;\n height: 13px;\n border: 2px solid rgba(255, 255, 255, 0.3);\n border-top-color: #fff;\n border-radius: 50%;\n animation: ri-spin 0.7s linear infinite;\n flex-shrink: 0;\n}\n.ri-downloadMenu {\n position: absolute;\n bottom: calc(100% + 6px);\n right: 0;\n min-width: 168px;\n background: rgba(20, 20, 20, 0.92);\n border: 1px solid rgba(255, 255, 255, 0.14);\n border-radius: 10px;\n overflow: hidden;\n z-index: 100;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n animation: ri-menuReveal 0.12s ease;\n}\n@keyframes ri-menuReveal {\n from {\n opacity: 0;\n transform: translateY(4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ri-downloadMenuItem {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 10px 14px;\n font-size: 13px;\n font-weight: 400;\n color: #fff;\n background: transparent;\n border: none;\n cursor: pointer;\n text-align: left;\n transition: background 0.1s ease;\n 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');
204
204
 
205
205
  // src/components/LicenseOverlay.tsx
206
206
  import { AlertTriangle } from "lucide-react";
@@ -325,6 +325,208 @@ function exportSceneMesh({
325
325
  });
326
326
  }
327
327
 
328
+ // src/components/ModelViewer/internal/exportUvLayout.ts
329
+ import { strToU8, zipSync } from "fflate";
330
+ var SVG_SIZE = 2048;
331
+ var MESH_COLORS = [
332
+ "#00e5ff",
333
+ "#ff4fd8",
334
+ "#ffe45e",
335
+ "#7cff6b",
336
+ "#ff784f",
337
+ "#a78bfa",
338
+ "#38bdf8",
339
+ "#fb7185"
340
+ ];
341
+ function escapeXml(value) {
342
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
343
+ }
344
+ function slugify(value, fallback) {
345
+ const slug = value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
346
+ return slug || fallback;
347
+ }
348
+ function safeFilenameStem(value) {
349
+ return slugify(value, "model");
350
+ }
351
+ function getVertexIndex(geometry, elementIndex) {
352
+ return geometry.index ? Math.round(geometry.index.getX(elementIndex)) : elementIndex;
353
+ }
354
+ function readTriangle(geometry, uv, elementIndex) {
355
+ const a = getVertexIndex(geometry, elementIndex);
356
+ const b = getVertexIndex(geometry, elementIndex + 1);
357
+ const c = getVertexIndex(geometry, elementIndex + 2);
358
+ if (a >= uv.count || b >= uv.count || c >= uv.count) return null;
359
+ const values = [
360
+ { index: a, u: uv.getX(a), v: uv.getY(a) },
361
+ { index: b, u: uv.getX(b), v: uv.getY(b) },
362
+ { index: c, u: uv.getX(c), v: uv.getY(c) }
363
+ ];
364
+ return values.every(
365
+ (vertex) => Number.isFinite(vertex.u) && Number.isFinite(vertex.v)
366
+ ) ? values : null;
367
+ }
368
+ function uvPointKey(vertex) {
369
+ return `${vertex.u.toFixed(6)},${vertex.v.toFixed(6)}`;
370
+ }
371
+ function addEdge(edges, start, end, indexed) {
372
+ let startKey;
373
+ let endKey;
374
+ if (indexed) {
375
+ startKey = start.index;
376
+ endKey = end.index;
377
+ } else {
378
+ startKey = uvPointKey(start);
379
+ endKey = uvPointKey(end);
380
+ }
381
+ const key = startKey < endKey ? `${startKey}|${endKey}` : `${endKey}|${startKey}`;
382
+ const existing = edges.get(key);
383
+ if (existing) {
384
+ existing.count += 1;
385
+ } else {
386
+ edges.set(key, { count: 1, start, end });
387
+ }
388
+ }
389
+ function addTriangleEdges(edges, triangle, indexed) {
390
+ addEdge(edges, triangle[0], triangle[1], indexed);
391
+ addEdge(edges, triangle[1], triangle[2], indexed);
392
+ addEdge(edges, triangle[2], triangle[0], indexed);
393
+ }
394
+ function materialForIndex(material, materialIndex) {
395
+ if (!Array.isArray(material)) return material;
396
+ return material[materialIndex] ?? null;
397
+ }
398
+ function collectUvLayouts(scene) {
399
+ const layouts = /* @__PURE__ */ new Map();
400
+ let unnamedMeshIndex = 0;
401
+ let unnamedMaterialIndex = 0;
402
+ let meshColorIndex = 0;
403
+ scene.traverse((node) => {
404
+ const mesh = node;
405
+ if (!mesh.isMesh || !mesh.geometry || !mesh.material) return;
406
+ const geometry = mesh.geometry;
407
+ const uv = geometry.getAttribute("uv");
408
+ if (!uv || uv.itemSize < 2 || uv.count < 3) return;
409
+ unnamedMeshIndex += 1;
410
+ const meshName = mesh.name || `Mesh ${unnamedMeshIndex}`;
411
+ const elementCount = geometry.index?.count ?? uv.count;
412
+ const groups = geometry.groups.length > 0 ? geometry.groups : [{ start: 0, count: elementCount, materialIndex: 0 }];
413
+ for (const group of groups) {
414
+ const materialIndex = group.materialIndex ?? 0;
415
+ const material = materialForIndex(mesh.material, materialIndex);
416
+ if (!material) continue;
417
+ let layout = layouts.get(material.uuid);
418
+ if (!layout) {
419
+ unnamedMaterialIndex += 1;
420
+ layout = {
421
+ name: material.name || `Material ${unnamedMaterialIndex}`,
422
+ layers: /* @__PURE__ */ new Map()
423
+ };
424
+ layouts.set(material.uuid, layout);
425
+ }
426
+ let layer = layout.layers.get(mesh.uuid);
427
+ if (!layer) {
428
+ layer = {
429
+ name: meshName,
430
+ color: MESH_COLORS[meshColorIndex % MESH_COLORS.length],
431
+ edges: /* @__PURE__ */ new Map()
432
+ };
433
+ meshColorIndex += 1;
434
+ layout.layers.set(mesh.uuid, layer);
435
+ }
436
+ const start = Math.max(0, group.start);
437
+ const end = Math.min(elementCount, start + group.count);
438
+ for (let elementIndex = start; elementIndex + 2 < end; elementIndex += 3) {
439
+ const triangle = readTriangle(geometry, uv, elementIndex);
440
+ if (triangle) {
441
+ addTriangleEdges(layer.edges, triangle, Boolean(geometry.index));
442
+ }
443
+ }
444
+ }
445
+ });
446
+ return [...layouts.values()].filter(
447
+ (layout) => [...layout.layers.values()].some((layer) => layer.edges.size > 0)
448
+ );
449
+ }
450
+ function coordinate(value) {
451
+ return (value * SVG_SIZE).toFixed(2);
452
+ }
453
+ function edgePath(edge) {
454
+ return `M${coordinate(edge.start.u)} ${coordinate(1 - edge.start.v)}L${coordinate(edge.end.u)} ${coordinate(1 - edge.end.v)}`;
455
+ }
456
+ function renderSvg(layout) {
457
+ const materialName = escapeXml(layout.name);
458
+ const layers = [...layout.layers.values()].filter((layer) => layer.edges.size > 0).map((layer, index) => {
459
+ const name = escapeXml(layer.name);
460
+ const id = `${slugify(layer.name, "mesh")}-${index + 1}`;
461
+ const allEdges = [...layer.edges.values()];
462
+ const boundaryEdges = allEdges.filter((edge) => edge.count === 1);
463
+ const visibleEdges = boundaryEdges.length > 0 ? boundaryEdges : allEdges;
464
+ const paths = visibleEdges.map(edgePath).join("");
465
+ return [
466
+ ` <g id="${id}" data-mesh="${name}" fill="none" stroke="${layer.color}"`,
467
+ ' stroke-width="1" stroke-linejoin="round" vector-effect="non-scaling-stroke">',
468
+ ` <title>${name}</title>`,
469
+ ` <path d="${paths}"/>`,
470
+ " </g>"
471
+ ].join("\n");
472
+ }).join("\n");
473
+ return [
474
+ '<?xml version="1.0" encoding="UTF-8"?>',
475
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${SVG_SIZE}" height="${SVG_SIZE}" viewBox="0 0 ${SVG_SIZE} ${SVG_SIZE}">`,
476
+ ` <title>${materialName} UV layout</title>`,
477
+ " <desc>UV0 island-outline template exported by React Immersive. The SVG canvas maps to the complete 0\u20131 texture area.</desc>",
478
+ ' <rect width="2048" height="2048" fill="none" stroke="#94a3b8" stroke-width="2"/>',
479
+ layers,
480
+ "</svg>"
481
+ ].join("\n");
482
+ }
483
+ function buildUvLayoutFiles(scene, downloadFilename = "model") {
484
+ const layouts = collectUvLayouts(scene);
485
+ const stem = safeFilenameStem(downloadFilename);
486
+ return layouts.map((layout, index) => {
487
+ const materialSlug = slugify(layout.name, `material-${index + 1}`);
488
+ const filename = layouts.length === 1 ? `${stem}-uv-layout.svg` : `${stem}-uv-${materialSlug}-${index + 1}.svg`;
489
+ return {
490
+ filename,
491
+ materialName: layout.name,
492
+ meshCount: [...layout.layers.values()].filter(
493
+ (layer) => layer.edges.size > 0
494
+ ).length,
495
+ svg: renderSvg(layout)
496
+ };
497
+ });
498
+ }
499
+ function downloadBlob(blob, filename) {
500
+ const url = URL.createObjectURL(blob);
501
+ const anchor = document.createElement("a");
502
+ anchor.href = url;
503
+ anchor.download = filename;
504
+ document.body.appendChild(anchor);
505
+ anchor.click();
506
+ document.body.removeChild(anchor);
507
+ URL.revokeObjectURL(url);
508
+ }
509
+ function downloadUvLayouts(scene, downloadFilename = "model") {
510
+ const files = buildUvLayoutFiles(scene, downloadFilename);
511
+ if (files.length === 0) return files;
512
+ if (files.length === 1) {
513
+ downloadBlob(
514
+ new Blob([files[0].svg], { type: "image/svg+xml;charset=utf-8" }),
515
+ files[0].filename
516
+ );
517
+ return files;
518
+ }
519
+ const archive = Object.fromEntries(
520
+ files.map((file) => [file.filename, strToU8(file.svg)])
521
+ );
522
+ const zipped = zipSync(archive, { level: 6 });
523
+ downloadBlob(
524
+ new Blob([zipped], { type: "application/zip" }),
525
+ `${safeFilenameStem(downloadFilename)}-uv-layouts.zip`
526
+ );
527
+ return files;
528
+ }
529
+
328
530
  // src/components/ModelViewer/internal/captureImage.ts
329
531
  import { PerspectiveCamera, Vector2 } from "three";
330
532
  function captureImage(capture, options = {}) {
@@ -3465,19 +3667,31 @@ function useCameraBiasedAnchor(annotation, worldPositionsRef) {
3465
3667
  });
3466
3668
  return anchorRef;
3467
3669
  }
3468
- function AnnotationMarker3D({ annotation, occluders, worldPositionsRef, onClick, onHover }) {
3670
+ function AnnotationMarker3D({
3671
+ annotation,
3672
+ occluders,
3673
+ worldPositionsRef,
3674
+ onClick,
3675
+ onHover,
3676
+ showAnnotationOnHover
3677
+ }) {
3469
3678
  const [occluded, setOccluded] = useState4(false);
3470
3679
  const anchorRef = useCameraBiasedAnchor(annotation, worldPositionsRef);
3471
3680
  const [isHovered, setIsHovered] = useState4(false);
3472
3681
  const [zIndex, setZIndex] = useState4(1);
3473
3682
  const clickTimeoutRef = useRef6(null);
3474
3683
  const markerRef = useRef6(null);
3684
+ const hoverTimeoutRef = useRef6(null);
3475
3685
  useEffect8(() => {
3476
3686
  return () => {
3477
3687
  if (clickTimeoutRef.current) {
3478
3688
  clearTimeout(clickTimeoutRef.current);
3479
3689
  clickTimeoutRef.current = null;
3480
3690
  }
3691
+ if (hoverTimeoutRef.current) {
3692
+ clearTimeout(hoverTimeoutRef.current);
3693
+ hoverTimeoutRef.current = null;
3694
+ }
3481
3695
  };
3482
3696
  }, []);
3483
3697
  const handleClick = (event) => {
@@ -3496,13 +3710,29 @@ function AnnotationMarker3D({ annotation, occluders, worldPositionsRef, onClick,
3496
3710
  event.stopPropagation();
3497
3711
  setIsHovered(true);
3498
3712
  setZIndex(100);
3713
+ if (hoverTimeoutRef.current) {
3714
+ clearTimeout(hoverTimeoutRef.current);
3715
+ hoverTimeoutRef.current = null;
3716
+ }
3499
3717
  onHover?.(annotation);
3718
+ if (showAnnotationOnHover) {
3719
+ onClick?.(annotation);
3720
+ }
3500
3721
  };
3501
3722
  const handlePointerLeave = (event) => {
3502
3723
  event.stopPropagation();
3503
3724
  setIsHovered(false);
3504
3725
  setZIndex(1);
3505
- onHover?.(null);
3726
+ if (hoverTimeoutRef.current) {
3727
+ clearTimeout(hoverTimeoutRef.current);
3728
+ }
3729
+ hoverTimeoutRef.current = window.setTimeout(() => {
3730
+ onHover?.(null);
3731
+ if (showAnnotationOnHover) {
3732
+ onHover?.(null);
3733
+ }
3734
+ hoverTimeoutRef.current = null;
3735
+ }, 50);
3506
3736
  };
3507
3737
  const handlePointerDown = (event) => {
3508
3738
  event.stopPropagation();
@@ -3627,7 +3857,8 @@ function AnnotationsLayer({
3627
3857
  onAnnotationClick,
3628
3858
  onAnnotationHover,
3629
3859
  onCloseAnnotation,
3630
- hideCloseButton = false
3860
+ hideCloseButton = false,
3861
+ showAnnotationOnHover = false
3631
3862
  }) {
3632
3863
  const invalidate = useThree8((state) => state.invalidate);
3633
3864
  const scene = useThree8((state) => state.scene);
@@ -3635,6 +3866,7 @@ function AnnotationsLayer({
3635
3866
  const [isLocked, setIsLocked] = useState4(false);
3636
3867
  const groupRef = useRef6(null);
3637
3868
  const invalidateFrameRef = useRef6(null);
3869
+ const hoverCloseTimeoutRef = useRef6(null);
3638
3870
  const occluders = useMemo8(() => {
3639
3871
  const list = [];
3640
3872
  scene.traverse((object) => {
@@ -3665,6 +3897,9 @@ function AnnotationsLayer({
3665
3897
  if (invalidateFrameRef.current !== null) {
3666
3898
  window.cancelAnimationFrame(invalidateFrameRef.current);
3667
3899
  }
3900
+ if (hoverCloseTimeoutRef.current) {
3901
+ clearTimeout(hoverCloseTimeoutRef.current);
3902
+ }
3668
3903
  };
3669
3904
  }, []);
3670
3905
  const activeAnnotationToShow = isLocked ? localActiveAnnotation : activeAnnotation || localActiveAnnotation;
@@ -3676,53 +3911,71 @@ function AnnotationsLayer({
3676
3911
  (annotation) => annotation.id === activeAnnotationToShow.id
3677
3912
  ) ?? activeAnnotationToShow;
3678
3913
  }, [activeAnnotationToShow, annotations]);
3679
- const handleAnnotationClick = useCallback((annotation) => {
3680
- if (isLocked && localActiveAnnotation?.id === annotation.id) {
3681
- setIsLocked(false);
3682
- setLocalActiveAnnotation(null);
3683
- onCloseAnnotation?.();
3684
- return;
3685
- }
3686
- setIsLocked(true);
3687
- setLocalActiveAnnotation(annotation);
3688
- onAnnotationClick?.(annotation);
3689
- }, [isLocked, localActiveAnnotation, onAnnotationClick, onCloseAnnotation]);
3690
- const handleAnnotationHover = useCallback((annotation) => {
3691
- if (!isLocked) {
3914
+ const handleAnnotationClick = useCallback(
3915
+ (annotation) => {
3916
+ if (isLocked && localActiveAnnotation?.id === annotation.id) {
3917
+ setIsLocked(false);
3918
+ setLocalActiveAnnotation(null);
3919
+ onCloseAnnotation?.();
3920
+ return;
3921
+ }
3922
+ setIsLocked(true);
3923
+ setLocalActiveAnnotation(annotation);
3924
+ onAnnotationClick?.(annotation);
3925
+ },
3926
+ [isLocked, localActiveAnnotation, onAnnotationClick, onCloseAnnotation]
3927
+ );
3928
+ const handleAnnotationHover = useCallback(
3929
+ (annotation) => {
3692
3930
  onAnnotationHover?.(annotation);
3693
- if (annotation) {
3694
- setLocalActiveAnnotation(annotation);
3695
- } else {
3696
- if (!isLocked) {
3697
- setLocalActiveAnnotation(null);
3931
+ if (showAnnotationOnHover && !isLocked) {
3932
+ if (hoverCloseTimeoutRef.current) {
3933
+ clearTimeout(hoverCloseTimeoutRef.current);
3934
+ hoverCloseTimeoutRef.current = null;
3935
+ }
3936
+ if (annotation) {
3937
+ setLocalActiveAnnotation(annotation);
3938
+ } else {
3939
+ hoverCloseTimeoutRef.current = window.setTimeout(() => {
3940
+ setLocalActiveAnnotation(null);
3941
+ onCloseAnnotation?.();
3942
+ hoverCloseTimeoutRef.current = null;
3943
+ }, 100);
3698
3944
  }
3699
3945
  }
3700
- }
3701
- }, [isLocked, onAnnotationHover]);
3946
+ },
3947
+ [isLocked, onAnnotationHover, showAnnotationOnHover, onCloseAnnotation]
3948
+ );
3702
3949
  const handleClose = useCallback(() => {
3703
3950
  setIsLocked(false);
3704
3951
  setLocalActiveAnnotation(null);
3705
3952
  onCloseAnnotation?.();
3706
3953
  }, [onCloseAnnotation]);
3707
- const handleCanvasClick = useCallback((event) => {
3708
- const target = event.target;
3709
- if (target.closest?.(".ri-annotationMarker") || target.closest?.(".ri-viewAnnotationPopup") || target.closest?.(".annotation-html-wrapper")) {
3710
- return;
3711
- }
3712
- const canvas = canvasRootRef.current;
3713
- if (canvas && canvas.contains(target)) {
3714
- setIsLocked(false);
3715
- setLocalActiveAnnotation(null);
3716
- onCloseAnnotation?.();
3717
- }
3718
- }, [canvasRootRef, onCloseAnnotation]);
3719
- const handleEscapeKey = useCallback((event) => {
3720
- if (event.key === "Escape" && isLocked) {
3721
- setIsLocked(false);
3722
- setLocalActiveAnnotation(null);
3723
- onCloseAnnotation?.();
3724
- }
3725
- }, [isLocked, onCloseAnnotation]);
3954
+ const handleCanvasClick = useCallback(
3955
+ (event) => {
3956
+ const target = event.target;
3957
+ if (target.closest?.(".ri-annotationMarker") || target.closest?.(".ri-viewAnnotationPopup") || target.closest?.(".annotation-html-wrapper")) {
3958
+ return;
3959
+ }
3960
+ const canvas = canvasRootRef.current;
3961
+ if (canvas && canvas.contains(target)) {
3962
+ setIsLocked(false);
3963
+ setLocalActiveAnnotation(null);
3964
+ onCloseAnnotation?.();
3965
+ }
3966
+ },
3967
+ [canvasRootRef, onCloseAnnotation]
3968
+ );
3969
+ const handleEscapeKey = useCallback(
3970
+ (event) => {
3971
+ if (event.key === "Escape" && isLocked) {
3972
+ setIsLocked(false);
3973
+ setLocalActiveAnnotation(null);
3974
+ onCloseAnnotation?.();
3975
+ }
3976
+ },
3977
+ [isLocked, onCloseAnnotation]
3978
+ );
3726
3979
  useEffect8(() => {
3727
3980
  const canvas = canvasRootRef.current;
3728
3981
  if (canvas) {
@@ -3772,7 +4025,8 @@ function AnnotationsLayer({
3772
4025
  occluders,
3773
4026
  worldPositionsRef,
3774
4027
  onClick: handleAnnotationClick,
3775
- onHover: handleAnnotationHover
4028
+ onHover: handleAnnotationHover,
4029
+ showAnnotationOnHover
3776
4030
  },
3777
4031
  stableKey
3778
4032
  );
@@ -4130,6 +4384,7 @@ import {
4130
4384
  Box3 as Box33,
4131
4385
  CatmullRomCurve3,
4132
4386
  Sphere,
4387
+ Spherical,
4133
4388
  Vector3 as Vector36
4134
4389
  } from "three";
4135
4390
  var ENTER_BLEND_SECONDS = 1.2;
@@ -4138,18 +4393,75 @@ function CinematicCamera({
4138
4393
  sceneRef,
4139
4394
  config,
4140
4395
  playing,
4396
+ resetOnPlay = false,
4141
4397
  onFinished,
4142
- onUserInteract
4398
+ onUserInteract,
4399
+ onStop
4143
4400
  }) {
4144
4401
  const invalidate = useThree9((state) => state.invalidate);
4145
4402
  const waypoints = useMemo11(() => config.waypoints ?? [], [config.waypoints]);
4146
4403
  const hasPath = waypoints.length >= 2;
4147
4404
  const loop = config.loop ?? true;
4148
- const duration = config.duration ?? (hasPath ? Math.max(waypoints.length * 3.5, 8) : 24);
4405
+ const calculatedDuration = useMemo11(() => {
4406
+ if (waypoints.length < 2) return 24;
4407
+ let total = 0;
4408
+ for (let i = 0; i < waypoints.length - 1; i++) {
4409
+ total += waypoints[i].segmentDuration ?? 3.5;
4410
+ }
4411
+ if (loop && waypoints.length > 1) {
4412
+ total += waypoints[waypoints.length - 1].segmentDuration ?? 3.5;
4413
+ }
4414
+ return Math.max(total, 4);
4415
+ }, [waypoints, loop]);
4416
+ const duration = config.duration ?? calculatedDuration;
4149
4417
  const signature = useMemo11(
4150
- () => waypoints.map((w) => [...w.position, ...w.target].join(",")).join(";"),
4418
+ () => waypoints.map(
4419
+ (waypoint) => [
4420
+ ...waypoint.position,
4421
+ ...waypoint.target,
4422
+ waypoint.azimuthAngle ?? "",
4423
+ waypoint.polarAngle ?? "",
4424
+ waypoint.segmentDuration ?? ""
4425
+ ].join(",")
4426
+ ).join(";"),
4151
4427
  [waypoints]
4152
4428
  );
4429
+ const segmentTimes = useMemo11(() => {
4430
+ const times = [];
4431
+ if (waypoints.length < 2) return times;
4432
+ let accumulated = 0;
4433
+ for (let i = 0; i < waypoints.length - 1; i++) {
4434
+ accumulated += waypoints[i].segmentDuration ?? 3.5;
4435
+ times.push(accumulated);
4436
+ }
4437
+ if (loop && waypoints.length > 1) {
4438
+ accumulated += waypoints[waypoints.length - 1].segmentDuration ?? 3.5;
4439
+ times.push(accumulated);
4440
+ }
4441
+ return times;
4442
+ }, [waypoints, loop]);
4443
+ const getCurveParameter = useMemo11(() => {
4444
+ return (elapsed) => {
4445
+ if (waypoints.length < 2 || segmentTimes.length === 0) {
4446
+ return elapsed / duration;
4447
+ }
4448
+ const segmentTotal = segmentTimes[segmentTimes.length - 1];
4449
+ const playbackElapsed = loop ? elapsed % duration : Math.min(elapsed, duration);
4450
+ const normalizedElapsed = duration > 0 ? playbackElapsed / duration * segmentTotal : 0;
4451
+ for (let i = 0; i < segmentTimes.length; i++) {
4452
+ if (normalizedElapsed <= segmentTimes[i]) {
4453
+ const prevTime = i === 0 ? 0 : segmentTimes[i - 1];
4454
+ const segmentDuration = segmentTimes[i] - prevTime;
4455
+ const progress = segmentDuration > 0 ? (normalizedElapsed - prevTime) / segmentDuration : 0;
4456
+ const totalSegments = loop ? waypoints.length : waypoints.length - 1;
4457
+ const baseU = i / totalSegments;
4458
+ const segmentU = 1 / totalSegments;
4459
+ return baseU + progress * segmentU;
4460
+ }
4461
+ }
4462
+ return 1;
4463
+ };
4464
+ }, [waypoints.length, segmentTimes, duration, loop]);
4153
4465
  const curves = useMemo11(() => {
4154
4466
  if (!hasPath) return null;
4155
4467
  return {
@@ -4167,29 +4479,83 @@ function CinematicCamera({
4167
4479
  )
4168
4480
  };
4169
4481
  }, [signature, hasPath, loop]);
4482
+ const orbitPath = useMemo11(() => {
4483
+ if (!hasPath || !waypoints.every(
4484
+ (waypoint) => Number.isFinite(waypoint.azimuthAngle) && Number.isFinite(waypoint.polarAngle)
4485
+ )) {
4486
+ return null;
4487
+ }
4488
+ const points = waypoints.map((waypoint) => ({
4489
+ azimuth: waypoint.azimuthAngle,
4490
+ polar: waypoint.polarAngle,
4491
+ distance: new Vector36(...waypoint.position).distanceTo(
4492
+ new Vector36(...waypoint.target)
4493
+ )
4494
+ }));
4495
+ for (let index = 1; index < points.length; index += 1) {
4496
+ const previous = points[index - 1].azimuth;
4497
+ const current = points[index].azimuth;
4498
+ points[index].azimuth = current + Math.round((previous - current) / (Math.PI * 2)) * Math.PI * 2;
4499
+ }
4500
+ return points;
4501
+ }, [signature, hasPath]);
4170
4502
  const elapsedRef = useRef8(0);
4171
4503
  const enterElapsedRef = useRef8(0);
4172
4504
  const enterPos = useRef8(new Vector36());
4173
4505
  const enterTarget = useRef8(new Vector36());
4174
4506
  const startedRef = useRef8(false);
4175
4507
  const pausedElapsedRef = useRef8(0);
4508
+ const hasFinishedRef = useRef8(false);
4176
4509
  const orbitRef = useRef8(null);
4177
4510
  const tmpPos = useMemo11(() => new Vector36(), []);
4178
4511
  const tmpTarget = useMemo11(() => new Vector36(), []);
4512
+ const tmpSpherical = useMemo11(() => new Spherical(), []);
4513
+ useEffect11(() => {
4514
+ elapsedRef.current = 0;
4515
+ pausedElapsedRef.current = 0;
4516
+ enterElapsedRef.current = 0;
4517
+ hasFinishedRef.current = false;
4518
+ }, [signature, loop, duration]);
4519
+ useEffect11(() => {
4520
+ if (resetOnPlay) {
4521
+ pausedElapsedRef.current = 0;
4522
+ elapsedRef.current = 0;
4523
+ hasFinishedRef.current = false;
4524
+ }
4525
+ }, [resetOnPlay]);
4179
4526
  useEffect11(() => {
4180
4527
  if (!playing) {
4181
4528
  startedRef.current = false;
4182
- if (elapsedRef.current > 0) {
4529
+ if (elapsedRef.current > 0 && !resetOnPlay) {
4183
4530
  pausedElapsedRef.current = elapsedRef.current;
4531
+ } else if (resetOnPlay) {
4532
+ pausedElapsedRef.current = 0;
4533
+ }
4534
+ if (elapsedRef.current > 0 || hasFinishedRef.current) {
4535
+ onStop?.();
4184
4536
  }
4185
4537
  return;
4186
4538
  }
4539
+ if (elapsedRef.current >= duration && !loop) {
4540
+ elapsedRef.current = 0;
4541
+ pausedElapsedRef.current = 0;
4542
+ hasFinishedRef.current = false;
4543
+ }
4187
4544
  const controls = controlsRef.current;
4188
4545
  if (controls) {
4189
4546
  controls.getPosition(enterPos.current);
4190
4547
  controls.getTarget(enterTarget.current);
4191
4548
  }
4192
- elapsedRef.current = pausedElapsedRef.current || 0;
4549
+ if (resetOnPlay || hasFinishedRef.current || pausedElapsedRef.current === 0) {
4550
+ elapsedRef.current = 0;
4551
+ pausedElapsedRef.current = 0;
4552
+ hasFinishedRef.current = false;
4553
+ } else if (pausedElapsedRef.current > 0 && pausedElapsedRef.current < duration) {
4554
+ elapsedRef.current = pausedElapsedRef.current;
4555
+ } else {
4556
+ elapsedRef.current = 0;
4557
+ pausedElapsedRef.current = 0;
4558
+ }
4193
4559
  enterElapsedRef.current = 0;
4194
4560
  if (!hasPath) {
4195
4561
  const scene = sceneRef.current;
@@ -4203,12 +4569,26 @@ function CinematicCamera({
4203
4569
  }
4204
4570
  startedRef.current = true;
4205
4571
  invalidate();
4206
- }, [playing, hasPath, controlsRef, sceneRef, invalidate]);
4572
+ }, [
4573
+ playing,
4574
+ hasPath,
4575
+ controlsRef,
4576
+ sceneRef,
4577
+ invalidate,
4578
+ duration,
4579
+ loop,
4580
+ signature,
4581
+ resetOnPlay,
4582
+ onStop
4583
+ ]);
4207
4584
  useEffect11(() => {
4208
4585
  if (!playing || !onUserInteract) return;
4209
4586
  const controls = controlsRef.current;
4210
4587
  if (!controls) return;
4211
- const handle = () => onUserInteract();
4588
+ const handle = () => {
4589
+ if (hasFinishedRef.current) return;
4590
+ onUserInteract();
4591
+ };
4212
4592
  controls.addEventListener("controlstart", handle);
4213
4593
  return () => controls.removeEventListener("controlstart", handle);
4214
4594
  }, [playing, controlsRef, onUserInteract]);
@@ -4218,12 +4598,33 @@ function CinematicCamera({
4218
4598
  if (!controls) return;
4219
4599
  elapsedRef.current += delta;
4220
4600
  enterElapsedRef.current += delta;
4221
- let u = elapsedRef.current / duration;
4222
- const finished = !loop && u >= 1;
4601
+ let u = getCurveParameter(elapsedRef.current);
4602
+ const finished = !loop && elapsedRef.current >= duration;
4223
4603
  u = loop ? u % 1 : Math.min(u, 1);
4224
4604
  if (curves) {
4225
- curves.position.getPointAt(u, tmpPos);
4226
- curves.target.getPointAt(u, tmpTarget);
4605
+ curves.target.getPoint(u, tmpTarget);
4606
+ if (orbitPath) {
4607
+ const segmentCount = loop ? orbitPath.length : orbitPath.length - 1;
4608
+ const scaled = Math.min(u, 1) * segmentCount;
4609
+ const segment = Math.min(Math.floor(scaled), segmentCount - 1);
4610
+ const next = (segment + 1) % orbitPath.length;
4611
+ const localT = u >= 1 ? 1 : scaled - segment;
4612
+ const eased = localT * localT * (3 - 2 * localT);
4613
+ const start = orbitPath[segment];
4614
+ const end = orbitPath[next];
4615
+ const endAzimuth = end.azimuth + Math.round((start.azimuth - end.azimuth) / (Math.PI * 2)) * Math.PI * 2;
4616
+ const azimuth = start.azimuth + (endAzimuth - start.azimuth) * eased;
4617
+ const polar = start.polar + (end.polar - start.polar) * eased;
4618
+ const distance = start.distance + (end.distance - start.distance) * eased;
4619
+ tmpSpherical.set(
4620
+ Math.max(distance, 1e-6),
4621
+ Math.min(Math.max(polar, 1e-6), Math.PI - 1e-6),
4622
+ azimuth
4623
+ );
4624
+ tmpPos.setFromSpherical(tmpSpherical).add(tmpTarget);
4625
+ } else {
4626
+ curves.position.getPoint(u, tmpPos);
4627
+ }
4227
4628
  } else {
4228
4629
  const orbit = orbitRef.current;
4229
4630
  if (!orbit) return;
@@ -4253,6 +4654,8 @@ function CinematicCamera({
4253
4654
  );
4254
4655
  if (finished) {
4255
4656
  startedRef.current = false;
4657
+ hasFinishedRef.current = true;
4658
+ elapsedRef.current = 0;
4256
4659
  pausedElapsedRef.current = 0;
4257
4660
  onFinished?.();
4258
4661
  return;
@@ -5276,6 +5679,7 @@ import {
5276
5679
  Camera as CameraIcon,
5277
5680
  Download,
5278
5681
  Eraser,
5682
+ FileImage,
5279
5683
  MapPin,
5280
5684
  RotateCcw,
5281
5685
  ChevronLeft,
@@ -5529,7 +5933,8 @@ function ModelViewer({
5529
5933
  usdzUrl,
5530
5934
  arScaleMode = "normalized",
5531
5935
  mobileHandoffUrl,
5532
- showAnnotationNavigation = true
5936
+ showAnnotationNavigation = true,
5937
+ showAnnotationOnHover = false
5533
5938
  }) {
5534
5939
  const { activeTab, activeSubTab } = useBindingBuilderContext();
5535
5940
  const license = useLicense(licenseKey);
@@ -5599,6 +6004,7 @@ function ModelViewer({
5599
6004
  !modelUrl || Boolean(onAutoFit) || Boolean(camera)
5600
6005
  );
5601
6006
  const [downloadMenuOpen, setDownloadMenuOpen] = useState10(false);
6007
+ const [uvExportError, setUvExportError] = useState10(null);
5602
6008
  const [modelLoaded, setModelLoaded] = useState10(false);
5603
6009
  const [modelLoadFailed, setModelLoadFailed] = useState10(false);
5604
6010
  const [bindingsMismatch, setBindingsMismatch] = useState10(false);
@@ -5653,9 +6059,11 @@ function ModelViewer({
5653
6059
  return { ...base ?? {}, ...cinematic };
5654
6060
  }, [cinematic, resolvedSceneConfig]);
5655
6061
  const [cinematicPlaying, setCinematicPlaying] = useState10(false);
6062
+ const [cinematicReset, setCinematicReset] = useState10(false);
5656
6063
  const cinematicAutoStartedRef = useRef11(false);
5657
6064
  useEffect15(() => {
5658
6065
  setCinematicPlaying(false);
6066
+ setCinematicReset(false);
5659
6067
  cinematicAutoStartedRef.current = false;
5660
6068
  }, [modelUrl]);
5661
6069
  useEffect15(() => {
@@ -5663,6 +6071,7 @@ function ModelViewer({
5663
6071
  if (modelLoaded && cameraPositioned && !isAutoFraming) {
5664
6072
  cinematicAutoStartedRef.current = true;
5665
6073
  setCinematicPlaying(true);
6074
+ setCinematicReset(false);
5666
6075
  }
5667
6076
  }, [cinematicConfig, modelLoaded, cameraPositioned, isAutoFraming]);
5668
6077
  const xrStore = useMemo13(() => {
@@ -6787,6 +7196,17 @@ function ModelViewer({
6787
7196
  console.error("Screenshot failed:", err);
6788
7197
  });
6789
7198
  }, [downloadFilename]);
7199
+ const handleDownloadUvLayout = useCallback4(() => {
7200
+ const scene = sceneRef.current;
7201
+ if (!scene) return;
7202
+ const files = downloadUvLayouts(scene, downloadFilename);
7203
+ if (files.length === 0) {
7204
+ setUvExportError("This model does not contain UV coordinates.");
7205
+ return;
7206
+ }
7207
+ setUvExportError(null);
7208
+ setDownloadMenuOpen(false);
7209
+ }, [downloadFilename]);
6790
7210
  const handleReset = useCallback4(async () => {
6791
7211
  if (onAutoFit) {
6792
7212
  await onAutoFit();
@@ -6816,6 +7236,20 @@ function ModelViewer({
6816
7236
  console.error("Reset camera error:", error);
6817
7237
  }
6818
7238
  }, [onAutoFit, fitScene]);
7239
+ const handleCinematicPlay = useCallback4(() => {
7240
+ setCinematicReset(false);
7241
+ setCinematicPlaying(true);
7242
+ }, []);
7243
+ const handleCinematicPause = useCallback4(() => {
7244
+ setCinematicPlaying(false);
7245
+ }, []);
7246
+ const handleCinematicStop = useCallback4(() => {
7247
+ setCinematicPlaying(false);
7248
+ setCinematicReset(true);
7249
+ setTimeout(() => {
7250
+ handleReset();
7251
+ }, 100);
7252
+ }, [handleReset]);
6819
7253
  const handleCameraChangeWithAnnotations = useCallback4(
6820
7254
  (cameraValue, controls) => {
6821
7255
  handleCameraChangeInternal(cameraValue, controls);
@@ -7113,7 +7547,8 @@ function ModelViewer({
7113
7547
  modelLoaded,
7114
7548
  onAnnotationClick: handleAnnotationMarkerClick,
7115
7549
  onCloseAnnotation: closeViewingAnnotation,
7116
- hideCloseButton: isGuidedTourActive
7550
+ hideCloseButton: isGuidedTourActive,
7551
+ showAnnotationOnHover
7117
7552
  }
7118
7553
  ),
7119
7554
  /* @__PURE__ */ jsx19(
@@ -7132,8 +7567,14 @@ function ModelViewer({
7132
7567
  sceneRef,
7133
7568
  config: cinematicConfig,
7134
7569
  playing: cinematicPlaying,
7135
- onFinished: () => setCinematicPlaying(false),
7136
- onUserInteract: () => setCinematicPlaying(false)
7570
+ resetOnPlay: cinematicReset,
7571
+ onFinished: () => {
7572
+ setCinematicPlaying(false);
7573
+ setCinematicReset(false);
7574
+ },
7575
+ onUserInteract: () => {
7576
+ setCinematicPlaying(false);
7577
+ }
7137
7578
  }
7138
7579
  ),
7139
7580
  showMeasureTools && /* @__PURE__ */ jsx19(
@@ -7361,24 +7802,43 @@ function ModelViewer({
7361
7802
  ]
7362
7803
  }
7363
7804
  ),
7364
- cinematicConfig && /* @__PURE__ */ jsx19(
7805
+ cinematicConfig && /* @__PURE__ */ jsxs15(
7365
7806
  "div",
7366
7807
  {
7367
7808
  className: "ri-cinematicTools",
7368
7809
  style: {
7369
7810
  top: `${topLeftRow((xrToolsVisible ? 1 : 0) + (showMeasureTools ? 1 : 0) + (showExplodeControls ? 1 : 0))}px`
7370
7811
  },
7371
- children: /* @__PURE__ */ jsx19(
7372
- "button",
7373
- {
7374
- type: "button",
7375
- className: `ri-measureButton ${cinematicPlaying ? "ri-measureButton--active" : ""}`,
7376
- onClick: () => setCinematicPlaying((prev) => !prev),
7377
- title: cinematicPlaying ? "Pause cinematic" : "Play cinematic",
7378
- "aria-pressed": cinematicPlaying,
7379
- children: cinematicPlaying ? /* @__PURE__ */ jsx19(Pause, { size: 16 }) : /* @__PURE__ */ jsx19(Play, { size: 16 })
7380
- }
7381
- )
7812
+ children: [
7813
+ /* @__PURE__ */ jsx19(
7814
+ "button",
7815
+ {
7816
+ type: "button",
7817
+ className: `ri-measureButton ${cinematicPlaying ? "ri-measureButton--active" : ""}`,
7818
+ onClick: () => {
7819
+ if (cinematicPlaying) {
7820
+ setCinematicPlaying(false);
7821
+ } else {
7822
+ setCinematicReset(false);
7823
+ setCinematicPlaying(true);
7824
+ }
7825
+ },
7826
+ title: cinematicPlaying ? "Pause cinematic" : "Play cinematic",
7827
+ "aria-pressed": cinematicPlaying,
7828
+ children: cinematicPlaying ? /* @__PURE__ */ jsx19(Pause, { size: 16 }) : /* @__PURE__ */ jsx19(Play, { size: 16 })
7829
+ }
7830
+ ),
7831
+ cinematicPlaying && /* @__PURE__ */ jsx19(
7832
+ "button",
7833
+ {
7834
+ type: "button",
7835
+ className: "ri-measureButton ri-measureButton--stop",
7836
+ onClick: handleCinematicStop,
7837
+ title: "Stop cinematic and reset camera",
7838
+ children: /* @__PURE__ */ jsx19(Square, { size: 16 })
7839
+ }
7840
+ )
7841
+ ]
7382
7842
  }
7383
7843
  ),
7384
7844
  (showResetButton || showDownloadButton || showAnnotationNavigation && sortedAnnotations.length > 0) && /* @__PURE__ */ jsxs15("div", { className: "ri-buttonContainer", children: [
@@ -7481,13 +7941,38 @@ function ModelViewer({
7481
7941
  "Download PNG"
7482
7942
  ]
7483
7943
  }
7944
+ ),
7945
+ /* @__PURE__ */ jsx19("div", { className: "ri-downloadMenuDivider" }),
7946
+ /* @__PURE__ */ jsxs15(
7947
+ "button",
7948
+ {
7949
+ className: "ri-downloadMenuItem",
7950
+ onClick: handleDownloadUvLayout,
7951
+ disabled: !sceneRef.current,
7952
+ children: [
7953
+ /* @__PURE__ */ jsx19(FileImage, { size: 14 }),
7954
+ "Download UV Layout"
7955
+ ]
7956
+ }
7957
+ ),
7958
+ uvExportError && /* @__PURE__ */ jsx19(
7959
+ "div",
7960
+ {
7961
+ className: "ri-downloadMenuNotice",
7962
+ role: "status",
7963
+ "aria-live": "polite",
7964
+ children: uvExportError
7965
+ }
7484
7966
  )
7485
7967
  ] }),
7486
7968
  /* @__PURE__ */ jsx19(
7487
7969
  "button",
7488
7970
  {
7489
7971
  className: "ri-downloadButton",
7490
- onClick: () => setDownloadMenuOpen((v) => !v),
7972
+ onClick: () => {
7973
+ setUvExportError(null);
7974
+ setDownloadMenuOpen((v) => !v);
7975
+ },
7491
7976
  disabled: isDownloading || !sceneRef.current,
7492
7977
  title: "Download options",
7493
7978
  children: isDownloading ? /* @__PURE__ */ jsxs15(Fragment7, { children: [
@@ -7618,7 +8103,7 @@ function ModelViewer({
7618
8103
  }
7619
8104
 
7620
8105
  // src/components/BindingBuilder/BindingBuilder.tsx
7621
- import { useCallback as useCallback8, useEffect as useEffect20, useMemo as useMemo16, useRef as useRef18, useState as useState24 } from "react";
8106
+ import { useCallback as useCallback9, useEffect as useEffect20, useMemo as useMemo16, useRef as useRef18, useState as useState25 } from "react";
7622
8107
  import {
7623
8108
  Redo2,
7624
8109
  Undo2,
@@ -7665,14 +8150,16 @@ function buildCameraState(camera, controls) {
7665
8150
  position: toTuple(position),
7666
8151
  target: toTuple(target),
7667
8152
  fov: "fov" in camera && typeof camera.fov === "number" ? camera.fov : null,
7668
- zoom: "zoom" in camera && typeof camera.zoom === "number" ? camera.zoom : 1
8153
+ zoom: "zoom" in camera && typeof camera.zoom === "number" ? camera.zoom : 1,
8154
+ azimuthAngle: controls.azimuthAngle,
8155
+ polarAngle: controls.polarAngle
7669
8156
  };
7670
8157
  }
7671
8158
  function isSameCameraState(left, right) {
7672
8159
  if (!left) {
7673
8160
  return false;
7674
8161
  }
7675
- 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;
8162
+ 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;
7676
8163
  }
7677
8164
  async function runSmoothCameraTransition2(controls, action, smoothTime = 0.6) {
7678
8165
  const originalSmoothTime = controls.smoothTime;
@@ -7827,15 +8314,27 @@ function useViewerCamera() {
7827
8314
  }
7828
8315
  try {
7829
8316
  markInitialAutoFitOverride(controls);
7830
- await controls.setLookAt(
7831
- state.position[0],
7832
- state.position[1],
7833
- state.position[2],
7834
- state.target[0],
7835
- state.target[1],
7836
- state.target[2],
7837
- enableTransition
7838
- );
8317
+ const transitions = [
8318
+ controls.setLookAt(
8319
+ state.position[0],
8320
+ state.position[1],
8321
+ state.position[2],
8322
+ state.target[0],
8323
+ state.target[1],
8324
+ state.target[2],
8325
+ enableTransition
8326
+ )
8327
+ ];
8328
+ if (state.azimuthAngle != null && state.polarAngle != null) {
8329
+ transitions.push(
8330
+ controls.rotateTo(
8331
+ state.azimuthAngle,
8332
+ state.polarAngle,
8333
+ enableTransition
8334
+ )
8335
+ );
8336
+ }
8337
+ await Promise.all(transitions);
7839
8338
  const camera = controls.camera;
7840
8339
  let projectionChanged = false;
7841
8340
  if (state.fov !== null && "fov" in camera && typeof camera.fov === "number") {
@@ -7959,7 +8458,7 @@ function useViewerAnimations() {
7959
8458
  }
7960
8459
 
7961
8460
  // src/utils/modelFileResolver.ts
7962
- import { strFromU8, strToU8, unzipSync } from "fflate";
8461
+ import { strFromU8, strToU8 as strToU82, unzipSync } from "fflate";
7963
8462
  var SUPPORTED_MODEL_EXTENSIONS = [".glb", ".gltf", ".zip"];
7964
8463
  var SUPPORTED_MODEL_MIME_TYPES = [
7965
8464
  "model/gltf-binary",
@@ -8044,7 +8543,7 @@ async function resolveZippedGltfFile(file) {
8044
8543
  try {
8045
8544
  rewriteZipUris(gltf.buffers, rootDir, entries, urlsToRevoke);
8046
8545
  rewriteZipUris(gltf.images, rootDir, entries, urlsToRevoke);
8047
- const gltfBlob = new Blob([strToU8(JSON.stringify(gltf))], {
8546
+ const gltfBlob = new Blob([strToU82(JSON.stringify(gltf))], {
8048
8547
  type: "model/gltf+json"
8049
8548
  });
8050
8549
  const gltfUrl = URL.createObjectURL(gltfBlob);
@@ -8143,7 +8642,7 @@ function inferMimeType(path) {
8143
8642
  }
8144
8643
 
8145
8644
  // src/components/BindingBuilder/SceneSettings/SceneSettingsTab.tsx
8146
- import { useState as useState19, useEffect as useEffect18 } from "react";
8645
+ import { useState as useState20, useEffect as useEffect18 } from "react";
8147
8646
 
8148
8647
  // src/components/BindingBuilder/SceneSettings/GeneralSceneSettings.tsx
8149
8648
  import { useState as useState14 } from "react";
@@ -8329,9 +8828,10 @@ function SliderRow({
8329
8828
  max,
8330
8829
  step,
8331
8830
  formatValue,
8332
- onChange
8831
+ onChange,
8832
+ disabled = false
8333
8833
  }) {
8334
- return /* @__PURE__ */ jsxs16("label", { className: "se-field", children: [
8834
+ return /* @__PURE__ */ jsxs16("label", { className: `se-field ${disabled ? "se-field--disabled" : ""}`, children: [
8335
8835
  /* @__PURE__ */ jsxs16("span", { className: "se-fieldLabel", children: [
8336
8836
  label,
8337
8837
  /* @__PURE__ */ jsx20("span", { className: "se-fieldValue", children: formatValue ? formatValue(value) : value })
@@ -8345,7 +8845,8 @@ function SliderRow({
8345
8845
  max,
8346
8846
  step,
8347
8847
  value,
8348
- onChange: (e) => onChange(Number(e.target.value))
8848
+ onChange: (e) => onChange(Number(e.target.value)),
8849
+ disabled
8349
8850
  }
8350
8851
  )
8351
8852
  ] });
@@ -11761,14 +12262,16 @@ function AnnotationsTab({ config, onChange }) {
11761
12262
  }
11762
12263
 
11763
12264
  // src/components/BindingBuilder/SceneSettings/CinematicTab.tsx
11764
- import { ArrowDown, ArrowUp, MapPin as MapPin2, Plus as Plus2, Trash2 as Trash24 } from "lucide-react";
11765
- import { jsx as jsx31, jsxs as jsxs27 } from "react/jsx-runtime";
12265
+ import { useState as useState19, useCallback as useCallback7 } from "react";
12266
+ import { ArrowDown, ArrowUp, MapPin as MapPin2, Plus as Plus2, Trash2 as Trash24, Settings } from "lucide-react";
12267
+ import { Fragment as Fragment10, jsx as jsx31, jsxs as jsxs27 } from "react/jsx-runtime";
11766
12268
  var DEFAULT_CINEMATIC = {
11767
12269
  enabled: false,
11768
12270
  waypoints: [],
11769
12271
  loop: true,
11770
12272
  autoPlay: false
11771
12273
  };
12274
+ var DEFAULT_SEGMENT_DURATION = 3.5;
11772
12275
  function CinematicTab({
11773
12276
  config,
11774
12277
  onChange,
@@ -11777,31 +12280,76 @@ function CinematicTab({
11777
12280
  }) {
11778
12281
  const cinematic = config.cinematic ?? DEFAULT_CINEMATIC;
11779
12282
  const { waypoints } = cinematic;
11780
- const update = (patch) => {
12283
+ const calculateTotalDuration = useCallback7((wps, loop) => {
12284
+ if (wps.length < 2) return 0;
12285
+ let total = 0;
12286
+ for (let i = 0; i < wps.length - 1; i++) {
12287
+ const segDur = wps[i].segmentDuration ?? DEFAULT_SEGMENT_DURATION;
12288
+ total += segDur;
12289
+ }
12290
+ if (loop && wps.length > 1) {
12291
+ total += wps[wps.length - 1].segmentDuration ?? DEFAULT_SEGMENT_DURATION;
12292
+ }
12293
+ return Math.max(total, 0);
12294
+ }, []);
12295
+ const update = useCallback7((patch) => {
11781
12296
  onChange({ ...config, cinematic: { ...cinematic, ...patch } });
11782
- };
11783
- const addWaypoint = () => {
12297
+ }, [config, cinematic, onChange]);
12298
+ const addWaypoint = useCallback7(() => {
11784
12299
  if (!cameraState) return;
12300
+ const newWaypoint = {
12301
+ position: [...cameraState.position],
12302
+ target: [...cameraState.target],
12303
+ ...cameraState.azimuthAngle != null ? { azimuthAngle: cameraState.azimuthAngle } : {},
12304
+ ...cameraState.polarAngle != null ? { polarAngle: cameraState.polarAngle } : {},
12305
+ segmentDuration: DEFAULT_SEGMENT_DURATION
12306
+ };
12307
+ const newWaypoints = [...waypoints, newWaypoint];
12308
+ const newDuration = calculateTotalDuration(newWaypoints, cinematic.loop);
11785
12309
  update({
11786
- waypoints: [
11787
- ...waypoints,
11788
- {
11789
- position: [...cameraState.position],
11790
- target: [...cameraState.target]
11791
- }
11792
- ]
12310
+ waypoints: newWaypoints,
12311
+ duration: newDuration
11793
12312
  });
11794
- };
11795
- const deleteWaypoint = (index) => {
11796
- update({ waypoints: waypoints.filter((_, i) => i !== index) });
11797
- };
11798
- const moveWaypoint = (index, delta) => {
12313
+ }, [cameraState, waypoints, cinematic.loop, calculateTotalDuration, update]);
12314
+ const deleteWaypoint = useCallback7((index) => {
12315
+ const newWaypoints = waypoints.filter((_, i) => i !== index);
12316
+ const newDuration = calculateTotalDuration(newWaypoints, cinematic.loop);
12317
+ update({
12318
+ waypoints: newWaypoints,
12319
+ duration: newDuration
12320
+ });
12321
+ }, [waypoints, cinematic.loop, calculateTotalDuration, update]);
12322
+ const moveWaypoint = useCallback7((index, delta) => {
11799
12323
  const next = index + delta;
11800
12324
  if (next < 0 || next >= waypoints.length) return;
11801
12325
  const reordered = [...waypoints];
11802
12326
  [reordered[index], reordered[next]] = [reordered[next], reordered[index]];
11803
- update({ waypoints: reordered });
11804
- };
12327
+ const newDuration = calculateTotalDuration(reordered, cinematic.loop);
12328
+ update({
12329
+ waypoints: reordered,
12330
+ duration: newDuration
12331
+ });
12332
+ }, [waypoints, cinematic.loop, calculateTotalDuration, update]);
12333
+ const updateWaypoint = useCallback7((index, updates) => {
12334
+ const newWaypoints = waypoints.map((wp, i) => {
12335
+ if (i === index) {
12336
+ return { ...wp, ...updates };
12337
+ }
12338
+ return wp;
12339
+ });
12340
+ const newDuration = calculateTotalDuration(newWaypoints, cinematic.loop);
12341
+ update({
12342
+ waypoints: newWaypoints,
12343
+ duration: newDuration
12344
+ });
12345
+ }, [waypoints, cinematic.loop, calculateTotalDuration, update]);
12346
+ const [customizeDurations, setCustomizeDurations] = useState19(false);
12347
+ const hasSegmentDurations = waypoints.some((w) => w.segmentDuration !== void 0);
12348
+ const getDuration = useCallback7((index) => {
12349
+ if (index >= waypoints.length - 1) return DEFAULT_SEGMENT_DURATION;
12350
+ return waypoints[index]?.segmentDuration ?? DEFAULT_SEGMENT_DURATION;
12351
+ }, [waypoints]);
12352
+ const hasEnoughWaypoints = waypoints.length >= 2;
11805
12353
  return /* @__PURE__ */ jsxs27(Section, { title: "Cinematic Camera", children: [
11806
12354
  /* @__PURE__ */ jsx31("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." }),
11807
12355
  /* @__PURE__ */ jsxs27(
@@ -11818,62 +12366,165 @@ function CinematicTab({
11818
12366
  ]
11819
12367
  }
11820
12368
  ),
11821
- waypoints.length === 0 ? /* @__PURE__ */ jsx31("div", { className: "bb-emptyAnnotations", children: "No waypoints yet. Position the camera and add your first one." }) : /* @__PURE__ */ jsx31("div", { className: "se-waypointList", children: waypoints.map((waypoint, index) => /* @__PURE__ */ jsxs27("div", { className: "se-waypointCard", children: [
11822
- /* @__PURE__ */ jsx31("div", { className: "se-waypointBadge", children: index + 1 }),
11823
- /* @__PURE__ */ jsxs27("div", { className: "se-waypointBody", children: [
11824
- /* @__PURE__ */ jsxs27("div", { className: "se-waypointCoords", children: [
11825
- "pos [",
11826
- waypoint.position.map((n) => n.toFixed(1)).join(", "),
11827
- "]"
11828
- ] }),
11829
- /* @__PURE__ */ jsxs27("div", { className: "se-waypointCoords", children: [
11830
- "look [",
11831
- waypoint.target.map((n) => n.toFixed(1)).join(", "),
11832
- "]"
11833
- ] })
11834
- ] }),
11835
- /* @__PURE__ */ jsxs27("div", { className: "se-waypointActions", children: [
11836
- /* @__PURE__ */ jsx31(
11837
- "button",
11838
- {
11839
- className: "se-waypointAction",
11840
- onClick: () => onPreviewWaypoint?.(waypoint),
11841
- title: "Fly camera to this waypoint",
11842
- disabled: !onPreviewWaypoint,
11843
- children: /* @__PURE__ */ jsx31(MapPin2, { size: 15 })
11844
- }
11845
- ),
12369
+ /* @__PURE__ */ jsxs27("div", { className: "se-customizeToggle", children: [
12370
+ /* @__PURE__ */ jsxs27("label", { className: "se-toggleLabel", children: [
11846
12371
  /* @__PURE__ */ jsx31(
11847
- "button",
11848
- {
11849
- className: "se-waypointAction",
11850
- onClick: () => moveWaypoint(index, -1),
11851
- title: "Move up",
11852
- disabled: index === 0,
11853
- children: /* @__PURE__ */ jsx31(ArrowUp, { size: 15 })
11854
- }
11855
- ),
11856
- /* @__PURE__ */ jsx31(
11857
- "button",
12372
+ "input",
11858
12373
  {
11859
- className: "se-waypointAction",
11860
- onClick: () => moveWaypoint(index, 1),
11861
- title: "Move down",
11862
- disabled: index === waypoints.length - 1,
11863
- children: /* @__PURE__ */ jsx31(ArrowDown, { size: 15 })
12374
+ type: "checkbox",
12375
+ checked: customizeDurations,
12376
+ onChange: (e) => setCustomizeDurations(e.target.checked)
11864
12377
  }
11865
12378
  ),
11866
- /* @__PURE__ */ jsx31(
11867
- "button",
11868
- {
11869
- className: "se-waypointAction se-waypointAction--delete",
11870
- onClick: () => deleteWaypoint(index),
11871
- title: "Delete waypoint",
11872
- children: /* @__PURE__ */ jsx31(Trash24, { size: 15 })
11873
- }
11874
- )
12379
+ /* @__PURE__ */ jsx31(Settings, { size: 14 }),
12380
+ /* @__PURE__ */ jsx31("span", { children: "Customize durations between waypoints" })
12381
+ ] }),
12382
+ customizeDurations && /* @__PURE__ */ jsxs27("span", { className: "se-toggleHint", children: [
12383
+ "Set duration for each segment between waypoints: 1\u21922, 2\u21923, etc. Default: ",
12384
+ DEFAULT_SEGMENT_DURATION,
12385
+ "s"
12386
+ ] })
12387
+ ] }),
12388
+ waypoints.length === 0 ? /* @__PURE__ */ jsx31("div", { className: "bb-emptyAnnotations", children: "No waypoints yet. Position the camera and add your first one." }) : /* @__PURE__ */ jsxs27(Fragment10, { children: [
12389
+ /* @__PURE__ */ jsx31("div", { className: "se-waypointList", children: waypoints.map((waypoint, index) => {
12390
+ const dur = getDuration(index);
12391
+ const isLast = index === waypoints.length - 1;
12392
+ const isCustom = waypoint.segmentDuration !== void 0 && waypoint.segmentDuration !== DEFAULT_SEGMENT_DURATION;
12393
+ return /* @__PURE__ */ jsxs27("div", { children: [
12394
+ /* @__PURE__ */ jsxs27("div", { className: "se-waypointCard", children: [
12395
+ /* @__PURE__ */ jsx31("div", { className: "se-waypointBadge", children: index + 1 }),
12396
+ /* @__PURE__ */ jsxs27("div", { className: "se-waypointBody", children: [
12397
+ /* @__PURE__ */ jsxs27("div", { className: "se-waypointCoords", children: [
12398
+ "pos [",
12399
+ waypoint.position.map((n) => n.toFixed(1)).join(", "),
12400
+ "]"
12401
+ ] }),
12402
+ /* @__PURE__ */ jsxs27("div", { className: "se-waypointCoords", children: [
12403
+ "look [",
12404
+ waypoint.target.map((n) => n.toFixed(1)).join(", "),
12405
+ "]"
12406
+ ] }),
12407
+ customizeDurations && !isLast && /* @__PURE__ */ jsxs27("div", { className: `se-customBadge ${isCustom ? "se-customBadge--custom" : "se-customBadge--default"}`, children: [
12408
+ dur.toFixed(1),
12409
+ "s",
12410
+ !isCustom && " (default)"
12411
+ ] })
12412
+ ] }),
12413
+ /* @__PURE__ */ jsxs27("div", { className: "se-waypointActions", children: [
12414
+ /* @__PURE__ */ jsx31(
12415
+ "button",
12416
+ {
12417
+ className: "se-waypointAction",
12418
+ onClick: () => onPreviewWaypoint?.(waypoint),
12419
+ title: "Fly camera to this waypoint",
12420
+ disabled: !onPreviewWaypoint,
12421
+ children: /* @__PURE__ */ jsx31(MapPin2, { size: 15 })
12422
+ }
12423
+ ),
12424
+ /* @__PURE__ */ jsx31(
12425
+ "button",
12426
+ {
12427
+ className: "se-waypointAction",
12428
+ onClick: () => moveWaypoint(index, -1),
12429
+ title: "Move up",
12430
+ disabled: index === 0,
12431
+ children: /* @__PURE__ */ jsx31(ArrowUp, { size: 15 })
12432
+ }
12433
+ ),
12434
+ /* @__PURE__ */ jsx31(
12435
+ "button",
12436
+ {
12437
+ className: "se-waypointAction",
12438
+ onClick: () => moveWaypoint(index, 1),
12439
+ title: "Move down",
12440
+ disabled: isLast,
12441
+ children: /* @__PURE__ */ jsx31(ArrowDown, { size: 15 })
12442
+ }
12443
+ ),
12444
+ /* @__PURE__ */ jsx31(
12445
+ "button",
12446
+ {
12447
+ className: "se-waypointAction se-waypointAction--delete",
12448
+ onClick: () => deleteWaypoint(index),
12449
+ title: "Delete waypoint",
12450
+ children: /* @__PURE__ */ jsx31(Trash24, { size: 15 })
12451
+ }
12452
+ )
12453
+ ] })
12454
+ ] }),
12455
+ customizeDurations && !isLast && /* @__PURE__ */ jsxs27("div", { className: "se-segmentBetween", children: [
12456
+ /* @__PURE__ */ jsxs27("div", { className: "se-segmentLabel", children: [
12457
+ index + 1,
12458
+ " \u2192 ",
12459
+ index + 2,
12460
+ !waypoint.segmentDuration && " (default)"
12461
+ ] }),
12462
+ /* @__PURE__ */ jsxs27("div", { className: "se-segmentControls", children: [
12463
+ /* @__PURE__ */ jsx31("span", { className: "se-durationLabel", children: "Duration:" }),
12464
+ /* @__PURE__ */ jsx31(
12465
+ "input",
12466
+ {
12467
+ type: "range",
12468
+ className: "se-segmentSlider",
12469
+ min: 1,
12470
+ max: 20,
12471
+ step: 0.5,
12472
+ value: dur,
12473
+ onChange: (e) => {
12474
+ const val = parseFloat(e.target.value);
12475
+ updateWaypoint(index, { segmentDuration: val });
12476
+ }
12477
+ }
12478
+ ),
12479
+ /* @__PURE__ */ jsxs27("span", { className: "se-segmentValue", children: [
12480
+ dur.toFixed(1),
12481
+ "s"
12482
+ ] }),
12483
+ /* @__PURE__ */ jsxs27("div", { className: "se-presetButtons", children: [
12484
+ [2, 4, 6, 8].map((preset) => /* @__PURE__ */ jsxs27(
12485
+ "button",
12486
+ {
12487
+ className: "se-presetButton",
12488
+ onClick: () => updateWaypoint(index, { segmentDuration: preset }),
12489
+ children: [
12490
+ preset,
12491
+ "s"
12492
+ ]
12493
+ },
12494
+ `preset-${index}-${preset}`
12495
+ )),
12496
+ /* @__PURE__ */ jsx31(
12497
+ "button",
12498
+ {
12499
+ className: "se-presetButton se-presetButton--default",
12500
+ onClick: () => {
12501
+ const newWaypoints = waypoints.map((wp, i) => {
12502
+ if (i === index) {
12503
+ const { segmentDuration, ...rest } = wp;
12504
+ return rest;
12505
+ }
12506
+ return wp;
12507
+ });
12508
+ const newDuration = calculateTotalDuration(newWaypoints, cinematic.loop);
12509
+ update({
12510
+ waypoints: newWaypoints,
12511
+ duration: newDuration
12512
+ });
12513
+ },
12514
+ children: "Reset"
12515
+ }
12516
+ )
12517
+ ] })
12518
+ ] })
12519
+ ] })
12520
+ ] }, `waypoint-wrapper-${index}`);
12521
+ }) }),
12522
+ /* @__PURE__ */ jsxs27("div", { className: "se-totalDuration", children: [
12523
+ /* @__PURE__ */ jsx31("span", { className: "se-totalLabel", children: "Total duration:" }),
12524
+ /* @__PURE__ */ jsx31("span", { className: "se-totalValue", children: hasEnoughWaypoints ? (cinematic.duration ?? calculateTotalDuration(waypoints, cinematic.loop)).toFixed(1) + "s" : "-" }),
12525
+ !hasEnoughWaypoints && /* @__PURE__ */ jsx31("span", { className: "se-totalHint", children: "(need at least 2 waypoints)" })
11875
12526
  ] })
11876
- ] }, index)) }),
12527
+ ] }),
11877
12528
  /* @__PURE__ */ jsx31(
11878
12529
  ToggleRow,
11879
12530
  {
@@ -11887,7 +12538,11 @@ function CinematicTab({
11887
12538
  {
11888
12539
  label: "Loop",
11889
12540
  checked: cinematic.loop,
11890
- onChange: (loop) => update({ loop })
12541
+ onChange: (loop) => {
12542
+ const newLoop = loop;
12543
+ const newDuration = calculateTotalDuration(waypoints, newLoop);
12544
+ update({ loop: newLoop, duration: newDuration });
12545
+ }
11891
12546
  }
11892
12547
  ),
11893
12548
  /* @__PURE__ */ jsx31(
@@ -11898,11 +12553,11 @@ function CinematicTab({
11898
12553
  onChange: (autoPlay) => update({ autoPlay })
11899
12554
  }
11900
12555
  ),
11901
- /* @__PURE__ */ jsx31(
12556
+ !customizeDurations && hasEnoughWaypoints && /* @__PURE__ */ jsx31(
11902
12557
  SliderRow,
11903
12558
  {
11904
12559
  label: "Duration",
11905
- value: cinematic.duration ?? Math.max(waypoints.length * 3.5, 8),
12560
+ value: cinematic.duration ?? calculateTotalDuration(waypoints, cinematic.loop),
11906
12561
  min: 4,
11907
12562
  max: 120,
11908
12563
  step: 1,
@@ -11914,7 +12569,7 @@ function CinematicTab({
11914
12569
  }
11915
12570
 
11916
12571
  // src/styles/SceneSettingsTab.css
11917
- 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');
12572
+ 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');
11918
12573
 
11919
12574
  // src/components/BindingBuilder/SceneSettings/SceneSettingsTab.tsx
11920
12575
  import {
@@ -11960,7 +12615,7 @@ function SceneSettingsTab({
11960
12615
  animationClipDetails = [],
11961
12616
  animationPlayback = FALLBACK_ANIMATION_PLAYBACK
11962
12617
  }) {
11963
- const [internalActiveSubTab, setInternalActiveSubTab] = useState19("General");
12618
+ const [internalActiveSubTab, setInternalActiveSubTab] = useState20("General");
11964
12619
  const activeSubTab = controlledActiveSubTab ?? internalActiveSubTab;
11965
12620
  const resolvedAnimationPlayback = animationPlayback ?? FALLBACK_ANIMATION_PLAYBACK;
11966
12621
  const { tier } = useBindingBuilderContext();
@@ -12146,7 +12801,7 @@ function EnableToggle2({
12146
12801
  }
12147
12802
 
12148
12803
  // src/components/BindingBuilder/components/MaterialSection.tsx
12149
- import { useState as useState21 } from "react";
12804
+ import { useState as useState22 } from "react";
12150
12805
 
12151
12806
  // src/components/ModelViewer/types/objectBinding.ts
12152
12807
  var MATERIAL_SIDES = ["front", "back", "double"];
@@ -12193,7 +12848,7 @@ function MaterialPanelGroup({
12193
12848
  }
12194
12849
 
12195
12850
  // src/components/BindingBuilder/components/MaterialFields.tsx
12196
- import { useRef as useRef15, useState as useState20 } from "react";
12851
+ import { useRef as useRef15, useState as useState21 } from "react";
12197
12852
  import { HexColorInput as HexColorInput3, HexColorPicker as HexColorPicker4 } from "react-colorful";
12198
12853
  import { FileUp } from "lucide-react";
12199
12854
  import { jsx as jsx35, jsxs as jsxs30 } from "react/jsx-runtime";
@@ -12260,7 +12915,7 @@ function MaterialColorField({
12260
12915
  fallback = "#ffffff",
12261
12916
  onChange
12262
12917
  }) {
12263
- const [open, setOpen] = useState20(false);
12918
+ const [open, setOpen] = useState21(false);
12264
12919
  const color = value || fallback;
12265
12920
  const anchorRef = useRef15(null);
12266
12921
  return /* @__PURE__ */ jsxs30("div", { className: "bb-field", children: [
@@ -12389,7 +13044,7 @@ function MaterialSliderField({
12389
13044
  }
12390
13045
 
12391
13046
  // src/components/BindingBuilder/components/MaterialSection.tsx
12392
- import { Fragment as Fragment10, jsx as jsx36, jsxs as jsxs31 } from "react/jsx-runtime";
13047
+ import { Fragment as Fragment11, jsx as jsx36, jsxs as jsxs31 } from "react/jsx-runtime";
12393
13048
  function MaterialSection({
12394
13049
  selectedItem,
12395
13050
  selectedMaterial,
@@ -12414,11 +13069,11 @@ function MaterialSection({
12414
13069
  tier: "starter",
12415
13070
  locked: !canUseStarterFeatures
12416
13071
  };
12417
- const [roughnessMode, setRoughnessMode] = useState21("roughness");
12418
- const [anisotropyMode, setAnisotropyMode] = useState21(
13072
+ const [roughnessMode, setRoughnessMode] = useState22("roughness");
13073
+ const [anisotropyMode, setAnisotropyMode] = useState22(
12419
13074
  "factor"
12420
13075
  );
12421
- return /* @__PURE__ */ jsxs31(Fragment10, { children: [
13076
+ return /* @__PURE__ */ jsxs31(Fragment11, { children: [
12422
13077
  /* @__PURE__ */ jsx36(
12423
13078
  MaterialPanelGroup,
12424
13079
  {
@@ -12457,7 +13112,7 @@ function MaterialSection({
12457
13112
  }))
12458
13113
  }
12459
13114
  ),
12460
- pbrWorkflow === "metalness" ? /* @__PURE__ */ jsxs31(Fragment10, { children: [
13115
+ pbrWorkflow === "metalness" ? /* @__PURE__ */ jsxs31(Fragment11, { children: [
12461
13116
  /* @__PURE__ */ jsx36(
12462
13117
  MaterialSliderField,
12463
13118
  {
@@ -12481,7 +13136,7 @@ function MaterialSection({
12481
13136
  onChange: (value) => setSelectedTexturePath(selectedItem, "metalnessMap", value)
12482
13137
  }
12483
13138
  )
12484
- ] }) : /* @__PURE__ */ jsxs31(Fragment10, { children: [
13139
+ ] }) : /* @__PURE__ */ jsxs31(Fragment11, { children: [
12485
13140
  /* @__PURE__ */ jsx36(
12486
13141
  MaterialSliderField,
12487
13142
  {
@@ -13454,7 +14109,7 @@ function MaterialPresetGallery({
13454
14109
  }
13455
14110
 
13456
14111
  // src/components/BindingBuilder/components/ToolbarExportMenu.tsx
13457
- import { useEffect as useEffect19, useRef as useRef16, useState as useState22 } from "react";
14112
+ import { useEffect as useEffect19, useRef as useRef16, useState as useState23 } from "react";
13458
14113
  import { ChevronDown as ChevronDown5, Download as Download2, FileCode2, FileJson } from "lucide-react";
13459
14114
  import { jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
13460
14115
  function ToolbarExportMenu({
@@ -13463,7 +14118,7 @@ function ToolbarExportMenu({
13463
14118
  onExportJson,
13464
14119
  onExportTs
13465
14120
  }) {
13466
- const [open, setOpen] = useState22(false);
14121
+ const [open, setOpen] = useState23(false);
13467
14122
  const wrapperRef = useRef16(null);
13468
14123
  useEffect19(() => {
13469
14124
  if (!open) return;
@@ -13797,7 +14452,7 @@ function ActionsPanel({
13797
14452
 
13798
14453
  // src/components/BindingBuilder/components/CameraPositionPanel.tsx
13799
14454
  import { MapPin as MapPin3, Trash2 as Trash25 } from "lucide-react";
13800
- import { Fragment as Fragment11, jsx as jsx42, jsxs as jsxs37 } from "react/jsx-runtime";
14455
+ import { Fragment as Fragment12, jsx as jsx42, jsxs as jsxs37 } from "react/jsx-runtime";
13801
14456
  function CameraPositionPanel({
13802
14457
  selectedBinding,
13803
14458
  open,
@@ -13816,7 +14471,7 @@ function CameraPositionPanel({
13816
14471
  /* @__PURE__ */ jsx42("span", { className: "bb-cameraLiveBadge", children: "LIVE" }),
13817
14472
  /* @__PURE__ */ jsxs37("div", { className: "se-waypointBody", children: [
13818
14473
  /* @__PURE__ */ jsx42("div", { className: "bb-cameraWaypointLabel", children: "Current view" }),
13819
- cameraState ? /* @__PURE__ */ jsxs37(Fragment11, { children: [
14474
+ cameraState ? /* @__PURE__ */ jsxs37(Fragment12, { children: [
13820
14475
  /* @__PURE__ */ jsxs37("div", { className: "se-waypointCoords", children: [
13821
14476
  "pos ",
13822
14477
  formattedCameraState?.position
@@ -13902,10 +14557,10 @@ function CameraPositionPanel({
13902
14557
  }
13903
14558
 
13904
14559
  // src/components/BindingBuilder/components/SceneNodesPanel.tsx
13905
- import { useMemo as useMemo15, useState as useState23 } from "react";
14560
+ import { useMemo as useMemo15, useState as useState24 } from "react";
13906
14561
 
13907
14562
  // src/components/BindingBuilder/utils/bindingFactory.ts
13908
- function slugify(value) {
14563
+ function slugify2(value) {
13909
14564
  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();
13910
14565
  }
13911
14566
  function humanizeNodeName(value) {
@@ -13951,7 +14606,7 @@ function buildDefaultActions(type) {
13951
14606
  function createDefaultBinding(nodeName) {
13952
14607
  const type = inferType(nodeName);
13953
14608
  return {
13954
- id: slugify(nodeName),
14609
+ id: slugify2(nodeName),
13955
14610
  modelObjectId: nodeName,
13956
14611
  type,
13957
14612
  label: humanizeNodeName(nodeName),
@@ -13982,7 +14637,7 @@ function collectNodeNames(scene) {
13982
14637
  }
13983
14638
 
13984
14639
  // src/components/BindingBuilder/components/SceneNodesPanel.tsx
13985
- import { Fragment as Fragment12, jsx as jsx43, jsxs as jsxs38 } from "react/jsx-runtime";
14640
+ import { Fragment as Fragment13, jsx as jsx43, jsxs as jsxs38 } from "react/jsx-runtime";
13986
14641
  function SceneNodesPanel({
13987
14642
  bindings,
13988
14643
  selectedItem,
@@ -13991,7 +14646,7 @@ function SceneNodesPanel({
13991
14646
  isOpen = true,
13992
14647
  onToggle
13993
14648
  }) {
13994
- const [search, setSearch] = useState23("");
14649
+ const [search, setSearch] = useState24("");
13995
14650
  const nodeKeys = useMemo15(() => Object.keys(bindings), [bindings]);
13996
14651
  const filteredNodeKeys = useMemo15(() => {
13997
14652
  const query = search.trim().toLowerCase();
@@ -14018,7 +14673,7 @@ function SceneNodesPanel({
14018
14673
  children: "\u2190"
14019
14674
  }
14020
14675
  ),
14021
- isOpen && /* @__PURE__ */ jsxs38(Fragment12, { children: [
14676
+ isOpen && /* @__PURE__ */ jsxs38(Fragment13, { children: [
14022
14677
  /* @__PURE__ */ jsxs38("div", { children: [
14023
14678
  /* @__PURE__ */ jsx43("div", { className: "bb-sectionKicker", children: "Scene Nodes" }),
14024
14679
  /* @__PURE__ */ jsxs38("div", { className: "bb-sectionHeading", children: [
@@ -14136,7 +14791,7 @@ function mergeAnimationConfig(sceneConfig, animationDetails) {
14136
14791
  }
14137
14792
 
14138
14793
  // src/components/BindingBuilder/hooks/useUndoableState.ts
14139
- import { useCallback as useCallback7, useReducer, useRef as useRef17 } from "react";
14794
+ import { useCallback as useCallback8, useReducer, useRef as useRef17 } from "react";
14140
14795
  var MAX_HISTORY = 100;
14141
14796
  function historyReducer(state, action) {
14142
14797
  switch (action.type) {
@@ -14181,7 +14836,7 @@ function useUndoableState(initial, coalesceWindowMs = 600) {
14181
14836
  future: []
14182
14837
  });
14183
14838
  const lastSetAt = useRef17(0);
14184
- const setValue = useCallback7(
14839
+ const setValue = useCallback8(
14185
14840
  (updater) => {
14186
14841
  const now = Date.now();
14187
14842
  const coalesce = now - lastSetAt.current < coalesceWindowMs;
@@ -14190,15 +14845,15 @@ function useUndoableState(initial, coalesceWindowMs = 600) {
14190
14845
  },
14191
14846
  [coalesceWindowMs]
14192
14847
  );
14193
- const undo = useCallback7(() => {
14848
+ const undo = useCallback8(() => {
14194
14849
  lastSetAt.current = 0;
14195
14850
  dispatch({ type: "undo" });
14196
14851
  }, []);
14197
- const redo = useCallback7(() => {
14852
+ const redo = useCallback8(() => {
14198
14853
  lastSetAt.current = 0;
14199
14854
  dispatch({ type: "redo" });
14200
14855
  }, []);
14201
- const reset = useCallback7((value) => {
14856
+ const reset = useCallback8((value) => {
14202
14857
  lastSetAt.current = 0;
14203
14858
  dispatch({ type: "reset", value });
14204
14859
  }, []);
@@ -14214,7 +14869,7 @@ function useUndoableState(initial, coalesceWindowMs = 600) {
14214
14869
  }
14215
14870
 
14216
14871
  // src/components/BindingBuilder/utils/exportAssets.ts
14217
- import { strToU8 as strToU82, zipSync } from "fflate";
14872
+ import { strToU8 as strToU83, zipSync as zipSync2 } from "fflate";
14218
14873
 
14219
14874
  // src/components/BindingBuilder/utils/serialization.ts
14220
14875
  function toBindingsJson(bindings) {
@@ -14290,11 +14945,11 @@ async function downloadExport(filename, content, mimeType, zipFilename, assets)
14290
14945
  downloadTextFile(filename, content, mimeType);
14291
14946
  return;
14292
14947
  }
14293
- const files = { [filename]: strToU82(content) };
14948
+ const files = { [filename]: strToU83(content) };
14294
14949
  for (const asset of assets) {
14295
14950
  files[asset.path] = new Uint8Array(await asset.blob.arrayBuffer());
14296
14951
  }
14297
- const zipped = zipSync(files, { level: 6 });
14952
+ const zipped = zipSync2(files, { level: 6 });
14298
14953
  const blob = new Blob([zipped], { type: "application/zip" });
14299
14954
  const url = URL.createObjectURL(blob);
14300
14955
  const link = document.createElement("a");
@@ -14427,7 +15082,7 @@ function removeMaterialPanelFields(material, panel) {
14427
15082
  }
14428
15083
 
14429
15084
  // src/components/BindingBuilder/BindingBuilder.tsx
14430
- import { Fragment as Fragment13, jsx as jsx45, jsxs as jsxs40 } from "react/jsx-runtime";
15085
+ import { Fragment as Fragment14, jsx as jsx45, jsxs as jsxs40 } from "react/jsx-runtime";
14431
15086
  var DEMO_MODEL_URL = "/room.glb";
14432
15087
  function isPlainObject(value) {
14433
15088
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -14447,27 +15102,27 @@ function BindingBuilder({ licenseKey }) {
14447
15102
  const modelInputRef = useRef18(null);
14448
15103
  const bindingsInputRef = useRef18(null);
14449
15104
  const sceneConfigInputRef = useRef18(null);
14450
- const [modelUrl, setModelUrl] = useState24(null);
14451
- const [modelName, setModelName] = useState24("");
14452
- const [selectedItem, setSelectedItem] = useState24(null);
15105
+ const [modelUrl, setModelUrl] = useState25(null);
15106
+ const [modelName, setModelName] = useState25("");
15107
+ const [selectedItem, setSelectedItem] = useState25(null);
14453
15108
  const bindingsHistory = useUndoableState({});
14454
15109
  const bindings = bindingsHistory.value;
14455
15110
  const setBindings = bindingsHistory.setValue;
14456
- const [loading, setLoading] = useState24(false);
14457
- const [isExporting, setIsExporting] = useState24(false);
14458
- const [statusMessage, setStatusMessage] = useState24(
15111
+ const [loading, setLoading] = useState25(false);
15112
+ const [isExporting, setIsExporting] = useState25(false);
15113
+ const [statusMessage, setStatusMessage] = useState25(
14459
15114
  "Load the demo model or upload a GLB/GLTF model to generate bindings."
14460
15115
  );
14461
- const [statusTone, setStatusTone] = useState24("default");
14462
- const [statusBannerVisible, setStatusBannerVisible] = useState24(false);
14463
- const [metricsDraft, setMetricsDraft] = useState24("{}");
14464
- const [metadataDraft, setMetadataDraft] = useState24("{}");
14465
- const [metricsError, setMetricsError] = useState24(null);
14466
- const [metadataError, setMetadataError] = useState24(null);
14467
- const [isPreviewDragActive, setIsPreviewDragActive] = useState24(false);
14468
- const [isEditorCollapsed, setIsEditorCollapsed] = useState24(false);
14469
- const [isNodesCollapsed, setIsNodesCollapsed] = useState24(false);
14470
- const [openObjectSections, setOpenObjectSections] = useState24({
15116
+ const [statusTone, setStatusTone] = useState25("default");
15117
+ const [statusBannerVisible, setStatusBannerVisible] = useState25(false);
15118
+ const [metricsDraft, setMetricsDraft] = useState25("{}");
15119
+ const [metadataDraft, setMetadataDraft] = useState25("{}");
15120
+ const [metricsError, setMetricsError] = useState25(null);
15121
+ const [metadataError, setMetadataError] = useState25(null);
15122
+ const [isPreviewDragActive, setIsPreviewDragActive] = useState25(false);
15123
+ const [isEditorCollapsed, setIsEditorCollapsed] = useState25(false);
15124
+ const [isNodesCollapsed, setIsNodesCollapsed] = useState25(false);
15125
+ const [openObjectSections, setOpenObjectSections] = useState25({
14471
15126
  identity: true,
14472
15127
  basics: true,
14473
15128
  material: true,
@@ -14476,7 +15131,7 @@ function BindingBuilder({ licenseKey }) {
14476
15131
  metrics: false,
14477
15132
  metadata: false
14478
15133
  });
14479
- const [openMaterialPanels, setOpenMaterialPanels] = useState24({
15134
+ const [openMaterialPanels, setOpenMaterialPanels] = useState25({
14480
15135
  pbr: true,
14481
15136
  roughness: false,
14482
15137
  emission: false,
@@ -14492,24 +15147,24 @@ function BindingBuilder({ licenseKey }) {
14492
15147
  });
14493
15148
  const toggleObjectSection = (key) => setOpenObjectSections((prev) => ({ ...prev, [key]: !prev[key] }));
14494
15149
  const toggleMaterialPanel = (key) => setOpenMaterialPanels((prev) => ({ ...prev, [key]: !prev[key] }));
14495
- const [activeEditorTab, setActiveEditorTab] = useState24(
15150
+ const [activeEditorTab, setActiveEditorTab] = useState25(
14496
15151
  "object"
14497
15152
  );
14498
- const [activeSubTab, setActiveSubTab] = useState24("General");
14499
- const [pbrWorkflow, setPbrWorkflow] = useState24("metalness");
14500
- const [materialPanelEnabled, setMaterialPanelEnabled] = useState24(createMaterialPanelEnabledState());
15153
+ const [activeSubTab, setActiveSubTab] = useState25("General");
15154
+ const [pbrWorkflow, setPbrWorkflow] = useState25("metalness");
15155
+ const [materialPanelEnabled, setMaterialPanelEnabled] = useState25(createMaterialPanelEnabledState());
14501
15156
  const materialPanelItemRef = useRef18(null);
14502
- const [zoomOnSelected, setZoomOnSelected] = useState24(false);
14503
- const [highlightOnHover, setHighlightOnHover] = useState24(true);
14504
- const [resetDialogOpen, setResetDialogOpen] = useState24(false);
14505
- const [resetTarget, setResetTarget] = useState24(
15157
+ const [zoomOnSelected, setZoomOnSelected] = useState25(false);
15158
+ const [highlightOnHover, setHighlightOnHover] = useState25(true);
15159
+ const [resetDialogOpen, setResetDialogOpen] = useState25(false);
15160
+ const [resetTarget, setResetTarget] = useState25(
14506
15161
  null
14507
15162
  );
14508
15163
  const sceneConfigHistory = useUndoableState(DEFAULT_SCENE_CONFIG);
14509
15164
  const sceneConfig = sceneConfigHistory.value;
14510
15165
  const setSceneConfig = sceneConfigHistory.setValue;
14511
- const [activeAnnotation, setActiveAnnotation] = useState24(null);
14512
- const [panelHoveredBinding, setPanelHoveredBinding] = useState24(null);
15166
+ const [activeAnnotation, setActiveAnnotation] = useState25(null);
15167
+ const [panelHoveredBinding, setPanelHoveredBinding] = useState25(null);
14513
15168
  const license = useLicense(licenseKey);
14514
15169
  const tier = normalizeTier(license.tier);
14515
15170
  const bindingBuilderContextValue = useMemo16(
@@ -14535,14 +15190,16 @@ function BindingBuilder({ licenseKey }) {
14535
15190
  handleCameraChange,
14536
15191
  handleViewerReady
14537
15192
  } = useViewerCamera();
14538
- const handlePreviewWaypoint = useCallback8(
15193
+ const handlePreviewWaypoint = useCallback9(
14539
15194
  (waypoint) => {
14540
15195
  setCameraState(
14541
15196
  {
14542
15197
  position: waypoint.position,
14543
15198
  target: waypoint.target,
14544
15199
  fov: cameraState?.fov ?? null,
14545
- zoom: cameraState?.zoom ?? 1
15200
+ zoom: cameraState?.zoom ?? 1,
15201
+ azimuthAngle: waypoint.azimuthAngle,
15202
+ polarAngle: waypoint.polarAngle
14546
15203
  },
14547
15204
  true
14548
15205
  );
@@ -14557,7 +15214,7 @@ function BindingBuilder({ licenseKey }) {
14557
15214
  position: formatVector3(cameraState.position),
14558
15215
  target: formatVector3(cameraState.target)
14559
15216
  } : null;
14560
- const handleAnnotationsChange = useCallback8(
15217
+ const handleAnnotationsChange = useCallback9(
14561
15218
  (newAnnotations) => {
14562
15219
  setSceneConfig((prev) => {
14563
15220
  const currentIds = prev.annotations.map((a) => a.id).join(",");
@@ -14573,7 +15230,7 @@ function BindingBuilder({ licenseKey }) {
14573
15230
  },
14574
15231
  []
14575
15232
  );
14576
- const handleActiveAnnotationChange = useCallback8(
15233
+ const handleActiveAnnotationChange = useCallback9(
14577
15234
  (annotation) => {
14578
15235
  setActiveAnnotation(
14579
15236
  annotation ? {
@@ -14694,7 +15351,7 @@ function BindingBuilder({ licenseKey }) {
14694
15351
  });
14695
15352
  }
14696
15353
  };
14697
- const applyBindingsFromScene = useCallback8(
15354
+ const applyBindingsFromScene = useCallback9(
14698
15355
  (scene, nextModelName) => {
14699
15356
  const nodeNames = collectNodeNames(scene);
14700
15357
  bindingsHistory.reset(buildBindingsFromNodes(nodeNames));
@@ -14733,7 +15390,7 @@ function BindingBuilder({ licenseKey }) {
14733
15390
  bindingsHistory.reset({});
14734
15391
  setSelectedItem(null);
14735
15392
  };
14736
- const handleViewerModelLoaded = useCallback8(
15393
+ const handleViewerModelLoaded = useCallback9(
14737
15394
  (scene) => {
14738
15395
  const pending = pendingLoadRef.current;
14739
15396
  if (!pending || pending.requestId !== loadRequestRef.current) return;
@@ -14743,7 +15400,7 @@ function BindingBuilder({ licenseKey }) {
14743
15400
  },
14744
15401
  [applyBindingsFromScene]
14745
15402
  );
14746
- const handleViewerLoadError = useCallback8((error) => {
15403
+ const handleViewerLoadError = useCallback9((error) => {
14747
15404
  const pending = pendingLoadRef.current;
14748
15405
  if (!pending || pending.requestId !== loadRequestRef.current) return;
14749
15406
  pendingLoadRef.current = null;
@@ -14754,7 +15411,7 @@ function BindingBuilder({ licenseKey }) {
14754
15411
  setStatusTone("error");
14755
15412
  setLoading(false);
14756
15413
  }, []);
14757
- const handleAnimationsReady = useCallback8(
15414
+ const handleAnimationsReady = useCallback9(
14758
15415
  (controls) => {
14759
15416
  viewerAnimations.handleAnimationsReady(controls);
14760
15417
  const details = controls.clipDetails ?? controls.clips.map((sourceName) => ({ sourceName, duration: 0 }));
@@ -15055,7 +15712,7 @@ function BindingBuilder({ licenseKey }) {
15055
15712
  }));
15056
15713
  setError(null);
15057
15714
  };
15058
- const handleViewerSelect = useCallback8(
15715
+ const handleViewerSelect = useCallback9(
15059
15716
  (binding) => {
15060
15717
  if (!binding) {
15061
15718
  setSelectedItem(null);
@@ -15068,7 +15725,7 @@ function BindingBuilder({ licenseKey }) {
15068
15725
  },
15069
15726
  [bindings]
15070
15727
  );
15071
- const handleSceneNodeSelect = useCallback8(
15728
+ const handleSceneNodeSelect = useCallback9(
15072
15729
  async (nodeKey) => {
15073
15730
  setSelectedItem(nodeKey);
15074
15731
  setPanelHoveredBinding(null);
@@ -15076,7 +15733,7 @@ function BindingBuilder({ licenseKey }) {
15076
15733
  },
15077
15734
  [focusObject]
15078
15735
  );
15079
- const saveCameraPositionForSelectedObject = useCallback8(() => {
15736
+ const saveCameraPositionForSelectedObject = useCallback9(() => {
15080
15737
  if (!selectedItem || !cameraState) {
15081
15738
  return;
15082
15739
  }
@@ -15091,7 +15748,7 @@ function BindingBuilder({ licenseKey }) {
15091
15748
  cameraState: currentCameraState
15092
15749
  }));
15093
15750
  }, [selectedItem, cameraState]);
15094
- const goToSavedCameraPosition = useCallback8(() => {
15751
+ const goToSavedCameraPosition = useCallback9(() => {
15095
15752
  if (!savedCameraPosition?.position || !savedCameraPosition?.target) return;
15096
15753
  setCameraState(
15097
15754
  {
@@ -15103,28 +15760,28 @@ function BindingBuilder({ licenseKey }) {
15103
15760
  true
15104
15761
  );
15105
15762
  }, [savedCameraPosition, setCameraState, cameraState]);
15106
- const clearSavedCameraPosition = useCallback8(() => {
15763
+ const clearSavedCameraPosition = useCallback9(() => {
15107
15764
  if (!selectedItem) return;
15108
15765
  updateBinding(selectedItem, (binding) => ({
15109
15766
  ...binding,
15110
15767
  cameraState: void 0
15111
15768
  }));
15112
15769
  }, [selectedItem]);
15113
- const handleResetView = useCallback8(async () => {
15770
+ const handleResetView = useCallback9(async () => {
15114
15771
  await resetView();
15115
15772
  }, [resetView]);
15116
- const handleFitScene = useCallback8(async () => {
15773
+ const handleFitScene = useCallback9(async () => {
15117
15774
  await fitScene();
15118
15775
  }, [fitScene]);
15119
- const openResetDialog = useCallback8((target) => {
15776
+ const openResetDialog = useCallback9((target) => {
15120
15777
  setResetTarget(target);
15121
15778
  setResetDialogOpen(true);
15122
15779
  }, []);
15123
- const closeResetDialog = useCallback8(() => {
15780
+ const closeResetDialog = useCallback9(() => {
15124
15781
  setResetDialogOpen(false);
15125
15782
  setResetTarget(null);
15126
15783
  }, []);
15127
- const executeReset = useCallback8(() => {
15784
+ const executeReset = useCallback9(() => {
15128
15785
  if (resetTarget === "bindings") {
15129
15786
  if (!nodeKeys.length) {
15130
15787
  closeResetDialog();
@@ -15221,7 +15878,7 @@ function BindingBuilder({ licenseKey }) {
15221
15878
  }
15222
15879
  )
15223
15880
  ] }),
15224
- !isEditorCollapsed && /* @__PURE__ */ jsxs40(Fragment13, { children: [
15881
+ !isEditorCollapsed && /* @__PURE__ */ jsxs40(Fragment14, { children: [
15225
15882
  /* @__PURE__ */ jsxs40("div", { className: "bb-tabRow", children: [
15226
15883
  /* @__PURE__ */ jsx45(
15227
15884
  "button",
@@ -15242,7 +15899,7 @@ function BindingBuilder({ licenseKey }) {
15242
15899
  }
15243
15900
  )
15244
15901
  ] }),
15245
- /* @__PURE__ */ jsx45("section", { className: "bb-editor", children: activeEditorTab === "scene" ? /* @__PURE__ */ jsx45(Fragment13, { children: modelUrl ? /* @__PURE__ */ jsx45(
15902
+ /* @__PURE__ */ jsx45("section", { className: "bb-editor", children: activeEditorTab === "scene" ? /* @__PURE__ */ jsx45(Fragment14, { children: modelUrl ? /* @__PURE__ */ jsx45(
15246
15903
  SceneSettingsTab,
15247
15904
  {
15248
15905
  config: sceneConfig,
@@ -15255,14 +15912,16 @@ function BindingBuilder({ licenseKey }) {
15255
15912
  // the whole settings panel for no reason.
15256
15913
  activeSubTab === "Cinematic" && cameraState ? {
15257
15914
  position: cameraState.position,
15258
- target: cameraState.target
15915
+ target: cameraState.target,
15916
+ azimuthAngle: cameraState.azimuthAngle,
15917
+ polarAngle: cameraState.polarAngle
15259
15918
  } : null
15260
15919
  ),
15261
15920
  onPreviewWaypoint: handlePreviewWaypoint,
15262
15921
  animationClipDetails: viewerAnimations.clipDetails,
15263
15922
  animationPlayback: viewerAnimations
15264
15923
  }
15265
- ) : /* @__PURE__ */ jsx45("div", { className: "bb-emptyEditor", children: "Upload a model to edit its scene settings." }) }) : !selectedBinding || !selectedItem ? /* @__PURE__ */ jsx45("div", { className: "bb-emptyEditor", children: "Select a generated node binding to edit its values." }) : /* @__PURE__ */ jsxs40(Fragment13, { children: [
15924
+ ) : /* @__PURE__ */ jsx45("div", { className: "bb-emptyEditor", children: "Upload a model to edit its scene settings." }) }) : !selectedBinding || !selectedItem ? /* @__PURE__ */ jsx45("div", { className: "bb-emptyEditor", children: "Select a generated node binding to edit its values." }) : /* @__PURE__ */ jsxs40(Fragment14, { children: [
15266
15925
  /* @__PURE__ */ jsx45("div", { className: "bb-editorHeader", children: /* @__PURE__ */ jsxs40("div", { children: [
15267
15926
  /* @__PURE__ */ jsx45("div", { className: "bb-sectionKicker", children: "Editing Binding" }),
15268
15927
  /* @__PURE__ */ jsx45("h2", { className: "bb-editorTitle", children: selectedBinding.label || selectedItem }),
@@ -15682,8 +16341,8 @@ function BindingBuilder({ licenseKey }) {
15682
16341
  // src/components/SimpleModelViewer/SimpleModelViewer.tsx
15683
16342
  import {
15684
16343
  useRef as useRef19,
15685
- useState as useState25,
15686
- useCallback as useCallback9,
16344
+ useState as useState26,
16345
+ useCallback as useCallback10,
15687
16346
  useEffect as useEffect21,
15688
16347
  useMemo as useMemo17,
15689
16348
  Component as Component3,
@@ -15706,14 +16365,14 @@ styleInject('.smv-root {\n --smv-panel-font-family:\n Inter,\n "Segoe UI"
15706
16365
  import {
15707
16366
  X as X3,
15708
16367
  Check,
15709
- Settings,
16368
+ Settings as Settings2,
15710
16369
  ChevronDown as ChevronDown6,
15711
16370
  Boxes as Boxes2,
15712
16371
  Trash2 as Trash26,
15713
16372
  Upload as Upload2,
15714
16373
  AlertTriangle as AlertTriangle4
15715
16374
  } from "lucide-react";
15716
- import { Fragment as Fragment14, jsx as jsx46, jsxs as jsxs41 } from "react/jsx-runtime";
16375
+ import { Fragment as Fragment15, jsx as jsx46, jsxs as jsxs41 } from "react/jsx-runtime";
15717
16376
  var HIGHLIGHT2 = { color: "#93c5fd", intensity: 0.05 };
15718
16377
  var ENV_PRESETS = [
15719
16378
  "apartment",
@@ -15773,7 +16432,7 @@ function InnerModel({
15773
16432
  meshopt: true
15774
16433
  });
15775
16434
  const invalidate = useThree11((state) => state.invalidate);
15776
- const [hoveredKey, setHoveredKey] = useState25(null);
16435
+ const [hoveredKey, setHoveredKey] = useState26(null);
15777
16436
  const scene = useMemo17(
15778
16437
  () => SkeletonUtils2.clone(rawScene),
15779
16438
  [rawScene]
@@ -16075,7 +16734,7 @@ function EnvironmentSection({
16075
16734
  title,
16076
16735
  children
16077
16736
  }) {
16078
- const [collapsed, setCollapsed] = useState25(false);
16737
+ const [collapsed, setCollapsed] = useState26(false);
16079
16738
  return /* @__PURE__ */ jsxs41("div", { className: "env-section", children: [
16080
16739
  /* @__PURE__ */ jsxs41(
16081
16740
  "button",
@@ -16114,7 +16773,7 @@ function EnvironmentPanel({
16114
16773
  {
16115
16774
  className: "side-panel__open-btn side-panel__open-btn--left",
16116
16775
  onClick: () => onToggle(true),
16117
- children: /* @__PURE__ */ jsx46(Settings, { size: 20, strokeWidth: 1.8 })
16776
+ children: /* @__PURE__ */ jsx46(Settings2, { size: 20, strokeWidth: 1.8 })
16118
16777
  }
16119
16778
  ),
16120
16779
  /* @__PURE__ */ jsxs41(
@@ -16240,8 +16899,8 @@ function SceneObjectsPanel2({
16240
16899
  onHover,
16241
16900
  onToggleVisibility
16242
16901
  }) {
16243
- const [search, setSearch] = useState25("");
16244
- const [rowHovered, setRowHovered] = useState25(null);
16902
+ const [search, setSearch] = useState26("");
16903
+ const [rowHovered, setRowHovered] = useState26(null);
16245
16904
  const entries = Object.values(meshKeys).filter(
16246
16905
  (m) => m.label.toLowerCase().includes(search.toLowerCase())
16247
16906
  );
@@ -16370,30 +17029,30 @@ function SimpleModelViewer({
16370
17029
  const fileInputRef = useRef19(null);
16371
17030
  const uploadedModelRef = useRef19(null);
16372
17031
  const dragDepthRef = useRef19(0);
16373
- const [loadedScene, setLoadedScene] = useState25(null);
16374
- const [largeModelTriangles, setLargeModelTriangles] = useState25(
17032
+ const [loadedScene, setLoadedScene] = useState26(null);
17033
+ const [largeModelTriangles, setLargeModelTriangles] = useState26(
16375
17034
  null
16376
17035
  );
16377
- const [largeModelNoticeDismissed, setLargeModelNoticeDismissed] = useState25(false);
16378
- const [meshKeys, setMeshKeys] = useState25({});
16379
- const [hiddenKeys, setHiddenKeys] = useState25(/* @__PURE__ */ new Set());
16380
- const [selectedKey, setSelectedKey] = useState25(null);
16381
- const [panelHoveredKey, setPanelHoveredKey] = useState25(null);
16382
- const [uploadedModelUrl, setUploadedModelUrl] = useState25(null);
16383
- const [uploadedModelName, setUploadedModelName] = useState25(
17036
+ const [largeModelNoticeDismissed, setLargeModelNoticeDismissed] = useState26(false);
17037
+ const [meshKeys, setMeshKeys] = useState26({});
17038
+ const [hiddenKeys, setHiddenKeys] = useState26(/* @__PURE__ */ new Set());
17039
+ const [selectedKey, setSelectedKey] = useState26(null);
17040
+ const [panelHoveredKey, setPanelHoveredKey] = useState26(null);
17041
+ const [uploadedModelUrl, setUploadedModelUrl] = useState26(null);
17042
+ const [uploadedModelName, setUploadedModelName] = useState26(
16384
17043
  null
16385
17044
  );
16386
- const [modelLoadFailed, setModelLoadFailed] = useState25(false);
16387
- const [isResolvingUpload, setIsResolvingUpload] = useState25(false);
16388
- const [initialFitComplete, setInitialFitComplete] = useState25(false);
16389
- const [isDragActive, setIsDragActive] = useState25(false);
16390
- const [isPanelOpen, setIsPanelOpen] = useState25(
17045
+ const [modelLoadFailed, setModelLoadFailed] = useState26(false);
17046
+ const [isResolvingUpload, setIsResolvingUpload] = useState26(false);
17047
+ const [initialFitComplete, setInitialFitComplete] = useState26(false);
17048
+ const [isDragActive, setIsDragActive] = useState26(false);
17049
+ const [isPanelOpen, setIsPanelOpen] = useState26(
16391
17050
  () => typeof window === "undefined" ? true : window.innerWidth > 900
16392
17051
  );
16393
- const [isEnvPanelOpen, setIsEnvPanelOpen] = useState25(
17052
+ const [isEnvPanelOpen, setIsEnvPanelOpen] = useState26(
16394
17053
  () => typeof window === "undefined" ? true : window.innerWidth > 900
16395
17054
  );
16396
- const [envSettings, setEnvSettings] = useState25({
17055
+ const [envSettings, setEnvSettings] = useState26({
16397
17056
  backgroundEnabled,
16398
17057
  backgroundColor,
16399
17058
  autoRotate,
@@ -16405,7 +17064,7 @@ function SimpleModelViewer({
16405
17064
  directionalColor
16406
17065
  });
16407
17066
  const activeModelUrl = enableModelUpload ? uploadedModelUrl : modelUrl;
16408
- const resetModelState = useCallback9(() => {
17067
+ const resetModelState = useCallback10(() => {
16409
17068
  setLoadedScene(null);
16410
17069
  setLargeModelTriangles(null);
16411
17070
  setLargeModelNoticeDismissed(false);
@@ -16416,11 +17075,11 @@ function SimpleModelViewer({
16416
17075
  setModelLoadFailed(false);
16417
17076
  setInitialFitComplete(false);
16418
17077
  }, []);
16419
- const revokeUploadedModelUrl = useCallback9(() => {
17078
+ const revokeUploadedModelUrl = useCallback10(() => {
16420
17079
  uploadedModelRef.current?.cleanup();
16421
17080
  uploadedModelRef.current = null;
16422
17081
  }, []);
16423
- const clearUploadedModel = useCallback9(() => {
17082
+ const clearUploadedModel = useCallback10(() => {
16424
17083
  revokeUploadedModelUrl();
16425
17084
  setUploadedModelUrl(null);
16426
17085
  setUploadedModelName(null);
@@ -16449,7 +17108,7 @@ function SimpleModelViewer({
16449
17108
  resetModelState,
16450
17109
  revokeUploadedModelUrl
16451
17110
  ]);
16452
- const handleUploadedModel = useCallback9(
17111
+ const handleUploadedModel = useCallback10(
16453
17112
  async (file) => {
16454
17113
  setIsResolvingUpload(true);
16455
17114
  try {
@@ -16468,7 +17127,7 @@ function SimpleModelViewer({
16468
17127
  },
16469
17128
  [onLoadError, resetModelState, revokeUploadedModelUrl]
16470
17129
  );
16471
- const handleFileSelection = useCallback9(
17130
+ const handleFileSelection = useCallback10(
16472
17131
  (files) => {
16473
17132
  const file = files?.[0];
16474
17133
  if (!file) return;
@@ -16481,7 +17140,7 @@ function SimpleModelViewer({
16481
17140
  revokeUploadedModelUrl();
16482
17141
  };
16483
17142
  }, [revokeUploadedModelUrl]);
16484
- const handleEnvSettingChange = useCallback9(
17143
+ const handleEnvSettingChange = useCallback10(
16485
17144
  (key, value) => {
16486
17145
  setEnvSettings((prev) => ({ ...prev, [key]: value }));
16487
17146
  },
@@ -16493,12 +17152,12 @@ function SimpleModelViewer({
16493
17152
  useEffect21(() => {
16494
17153
  resetModelState();
16495
17154
  }, [activeModelUrl, resetModelState]);
16496
- const handleMeshesReady = useCallback9((meshes) => {
17155
+ const handleMeshesReady = useCallback10((meshes) => {
16497
17156
  setMeshKeys(meshes);
16498
17157
  setHiddenKeys(/* @__PURE__ */ new Set());
16499
17158
  setSelectedKey(null);
16500
17159
  }, []);
16501
- const handleModelLoaded = useCallback9(
17160
+ const handleModelLoaded = useCallback10(
16502
17161
  (scene) => {
16503
17162
  setLoadedScene(scene);
16504
17163
  setModelLoadFailed(false);
@@ -16509,14 +17168,14 @@ function SimpleModelViewer({
16509
17168
  },
16510
17169
  [onModelLoaded]
16511
17170
  );
16512
- const handleLoadError = useCallback9(
17171
+ const handleLoadError = useCallback10(
16513
17172
  (error) => {
16514
17173
  setModelLoadFailed(true);
16515
17174
  onLoadError?.(error);
16516
17175
  },
16517
17176
  [onLoadError]
16518
17177
  );
16519
- const focusMesh = useCallback9(
17178
+ const focusMesh = useCallback10(
16520
17179
  (key) => {
16521
17180
  setSelectedKey(key);
16522
17181
  if (!zoomOnSelected) return;
@@ -16532,14 +17191,14 @@ function SimpleModelViewer({
16532
17191
  },
16533
17192
  [zoomOnSelected]
16534
17193
  );
16535
- const handleToggleVisibility = useCallback9((key) => {
17194
+ const handleToggleVisibility = useCallback10((key) => {
16536
17195
  setHiddenKeys((prev) => {
16537
17196
  const next = new Set(prev);
16538
17197
  next.has(key) ? next.delete(key) : next.add(key);
16539
17198
  return next;
16540
17199
  });
16541
17200
  }, []);
16542
- const handleDragEnter = useCallback9(
17201
+ const handleDragEnter = useCallback10(
16543
17202
  (event) => {
16544
17203
  if (!enableModelUpload || !supportsDraggedFiles(event.dataTransfer)) {
16545
17204
  return;
@@ -16551,7 +17210,7 @@ function SimpleModelViewer({
16551
17210
  },
16552
17211
  [enableModelUpload]
16553
17212
  );
16554
- const handleDragOver = useCallback9(
17213
+ const handleDragOver = useCallback10(
16555
17214
  (event) => {
16556
17215
  if (!enableModelUpload || !supportsDraggedFiles(event.dataTransfer)) {
16557
17216
  return;
@@ -16563,7 +17222,7 @@ function SimpleModelViewer({
16563
17222
  },
16564
17223
  [enableModelUpload]
16565
17224
  );
16566
- const handleDragLeave = useCallback9(
17225
+ const handleDragLeave = useCallback10(
16567
17226
  (event) => {
16568
17227
  if (!enableModelUpload || !supportsDraggedFiles(event.dataTransfer)) {
16569
17228
  return;
@@ -16577,7 +17236,7 @@ function SimpleModelViewer({
16577
17236
  },
16578
17237
  [enableModelUpload]
16579
17238
  );
16580
- const handleDrop = useCallback9(
17239
+ const handleDrop = useCallback10(
16581
17240
  (event) => {
16582
17241
  if (!enableModelUpload || !supportsDraggedFiles(event.dataTransfer)) {
16583
17242
  return;
@@ -16631,7 +17290,7 @@ function SimpleModelViewer({
16631
17290
  }
16632
17291
  )
16633
17292
  ] }),
16634
- enableModelUpload ? /* @__PURE__ */ jsxs41(Fragment14, { children: [
17293
+ enableModelUpload ? /* @__PURE__ */ jsxs41(Fragment15, { children: [
16635
17294
  /* @__PURE__ */ jsx46(
16636
17295
  "input",
16637
17296
  {
@@ -16645,7 +17304,7 @@ function SimpleModelViewer({
16645
17304
  }
16646
17305
  }
16647
17306
  ),
16648
- activeModelUrl ? /* @__PURE__ */ jsx46("div", { className: "smv-upload-toolbar", children: /* @__PURE__ */ jsxs41(Fragment14, { children: [
17307
+ activeModelUrl ? /* @__PURE__ */ jsx46("div", { className: "smv-upload-toolbar", children: /* @__PURE__ */ jsxs41(Fragment15, { children: [
16649
17308
  /* @__PURE__ */ jsx46("span", { className: "smv-upload-toolbar__name", children: uploadedModelName ?? "Uploaded model" }),
16650
17309
  /* @__PURE__ */ jsxs41(
16651
17310
  "button",
@@ -16806,18 +17465,18 @@ function SimpleModelViewer({
16806
17465
  }
16807
17466
 
16808
17467
  // src/hooks/useViewerSelection.ts
16809
- import { useCallback as useCallback10, useState as useState26 } from "react";
17468
+ import { useCallback as useCallback11, useState as useState27 } from "react";
16810
17469
  function useViewerSelection(initialSelectedObjectBinding = null) {
16811
- const [selectedObjectBinding, setSelectedObjectBinding] = useState26(
17470
+ const [selectedObjectBinding, setSelectedObjectBinding] = useState27(
16812
17471
  initialSelectedObjectBinding
16813
17472
  );
16814
- const handleObjectSelect = useCallback10(
17473
+ const handleObjectSelect = useCallback11(
16815
17474
  (binding) => {
16816
17475
  setSelectedObjectBinding(binding);
16817
17476
  },
16818
17477
  [setSelectedObjectBinding]
16819
17478
  );
16820
- const clearSelection = useCallback10(() => {
17479
+ const clearSelection = useCallback11(() => {
16821
17480
  setSelectedObjectBinding(null);
16822
17481
  }, []);
16823
17482
  return {
@@ -16855,14 +17514,14 @@ function useObjectBindingIds(objectBindings) {
16855
17514
  }
16856
17515
 
16857
17516
  // src/hooks/useObjectVisibility.ts
16858
- import { useCallback as useCallback11, useMemo as useMemo20 } from "react";
17517
+ import { useCallback as useCallback12, useMemo as useMemo20 } from "react";
16859
17518
  function getHiddenObjects(objectBindings) {
16860
17519
  return Object.fromEntries(
16861
17520
  Object.entries(objectBindings).filter(([, binding]) => binding.visible === false).map(([bindingKey]) => [bindingKey, true])
16862
17521
  );
16863
17522
  }
16864
17523
  function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16865
- const updateVisibility = useCallback11(
17524
+ const updateVisibility = useCallback12(
16866
17525
  (objectId, visible) => {
16867
17526
  const bindingKey = resolveBindingKey(objectBindings, objectId);
16868
17527
  if (!bindingKey) return;
@@ -16878,15 +17537,15 @@ function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16878
17537
  },
16879
17538
  [objectBindings, onObjectBindingsChange]
16880
17539
  );
16881
- const hideObject = useCallback11(
17540
+ const hideObject = useCallback12(
16882
17541
  (objectId) => updateVisibility(objectId, false),
16883
17542
  [updateVisibility]
16884
17543
  );
16885
- const showObject = useCallback11(
17544
+ const showObject = useCallback12(
16886
17545
  (objectId) => updateVisibility(objectId, true),
16887
17546
  [updateVisibility]
16888
17547
  );
16889
- const toggleObjectVisibility = useCallback11(
17548
+ const toggleObjectVisibility = useCallback12(
16890
17549
  (objectId) => {
16891
17550
  const bindingKey = resolveBindingKey(objectBindings, objectId);
16892
17551
  if (!bindingKey) return;
@@ -16902,7 +17561,7 @@ function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16902
17561
  },
16903
17562
  [objectBindings, onObjectBindingsChange]
16904
17563
  );
16905
- const clearHiddenObjects = useCallback11(() => {
17564
+ const clearHiddenObjects = useCallback12(() => {
16906
17565
  onObjectBindingsChange(
16907
17566
  Object.fromEntries(
16908
17567
  Object.entries(objectBindings).map(([bindingKey, binding]) => [
@@ -16922,7 +17581,7 @@ function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16922
17581
  ),
16923
17582
  [hiddenObjects, objectBindings]
16924
17583
  );
16925
- const isObjectHidden = useCallback11(
17584
+ const isObjectHidden = useCallback12(
16926
17585
  (objectId) => {
16927
17586
  const bindingKey = resolveBindingKey(objectBindings, objectId);
16928
17587
  return bindingKey ? objectBindings[bindingKey]?.visible === false : false;
@@ -16941,10 +17600,10 @@ function useObjectVisibility(objectBindings, onObjectBindingsChange) {
16941
17600
  }
16942
17601
 
16943
17602
  // src/hooks/useViewerHover.ts
16944
- import { useCallback as useCallback12, useState as useState27 } from "react";
17603
+ import { useCallback as useCallback13, useState as useState28 } from "react";
16945
17604
  function useViewerHover() {
16946
- const [hoveredObjectBinding, setHoveredObjectBinding] = useState27(null);
16947
- const handleHoveredObject = useCallback12(
17605
+ const [hoveredObjectBinding, setHoveredObjectBinding] = useState28(null);
17606
+ const handleHoveredObject = useCallback13(
16948
17607
  (binding) => {
16949
17608
  setHoveredObjectBinding(binding);
16950
17609
  },
@@ -16957,18 +17616,18 @@ function useViewerHover() {
16957
17616
  }
16958
17617
 
16959
17618
  // src/hooks/useViewerModel.ts
16960
- import { useCallback as useCallback13, useState as useState28 } from "react";
17619
+ import { useCallback as useCallback14, useState as useState29 } from "react";
16961
17620
  import { Box3 as Box36, Vector3 as Vector310 } from "three";
16962
17621
  function toTuple2(vector) {
16963
17622
  return [vector.x, vector.y, vector.z];
16964
17623
  }
16965
17624
  function useViewerModel() {
16966
- const [isLoading, setIsLoading] = useState28(true);
16967
- const [isReady, setIsReady] = useState28(false);
16968
- const [error, setError] = useState28(null);
16969
- const [bounds, setBounds] = useState28(null);
16970
- const [objectCount, setObjectCount] = useState28(0);
16971
- const handleModelLoaded = useCallback13((scene) => {
17625
+ const [isLoading, setIsLoading] = useState29(true);
17626
+ const [isReady, setIsReady] = useState29(false);
17627
+ const [error, setError] = useState29(null);
17628
+ const [bounds, setBounds] = useState29(null);
17629
+ const [objectCount, setObjectCount] = useState29(0);
17630
+ const handleModelLoaded = useCallback14((scene) => {
16972
17631
  const box = new Box36().setFromObject(scene);
16973
17632
  const min = box.min.clone();
16974
17633
  const max = box.max.clone();
@@ -16993,14 +17652,14 @@ function useViewerModel() {
16993
17652
  setIsLoading(false);
16994
17653
  setIsReady(true);
16995
17654
  }, []);
16996
- const handleLoadError = useCallback13((nextError) => {
17655
+ const handleLoadError = useCallback14((nextError) => {
16997
17656
  setError(nextError);
16998
17657
  setBounds(null);
16999
17658
  setObjectCount(0);
17000
17659
  setIsLoading(false);
17001
17660
  setIsReady(false);
17002
17661
  }, []);
17003
- const resetModelState = useCallback13(() => {
17662
+ const resetModelState = useCallback14(() => {
17004
17663
  setIsLoading(true);
17005
17664
  setIsReady(false);
17006
17665
  setError(null);
@@ -17020,10 +17679,10 @@ function useViewerModel() {
17020
17679
  }
17021
17680
 
17022
17681
  // src/hooks/useViewerActions.ts
17023
- import { useCallback as useCallback14 } from "react";
17682
+ import { useCallback as useCallback15 } from "react";
17024
17683
  function useViewerActions(objectBindings, options = {}) {
17025
17684
  const { onAction, onObjectBindingsChange } = options;
17026
- const getActionsForObject = useCallback14(
17685
+ const getActionsForObject = useCallback15(
17027
17686
  (objectId) => {
17028
17687
  const bindingKey = resolveBindingKey(objectBindings, objectId);
17029
17688
  if (!bindingKey) {
@@ -17033,7 +17692,7 @@ function useViewerActions(objectBindings, options = {}) {
17033
17692
  },
17034
17693
  [objectBindings]
17035
17694
  );
17036
- const runAction = useCallback14(
17695
+ const runAction = useCallback15(
17037
17696
  (objectId, actionId) => {
17038
17697
  const bindingKey = resolveBindingKey(objectBindings, objectId);
17039
17698
  if (!bindingKey) {
@@ -17075,7 +17734,7 @@ function useViewerActions(objectBindings, options = {}) {
17075
17734
  }
17076
17735
 
17077
17736
  // src/hooks/useShareableViewerState.ts
17078
- import { useCallback as useCallback15, useEffect as useEffect22, useRef as useRef20, useState as useState29 } from "react";
17737
+ import { useCallback as useCallback16, useEffect as useEffect22, useRef as useRef20, useState as useState30 } from "react";
17079
17738
  function serializeState(cameraState, objectBindings, selectedObjectBinding) {
17080
17739
  const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
17081
17740
  return {
@@ -17096,7 +17755,7 @@ function useShareableViewerState({
17096
17755
  autoSync = true
17097
17756
  }) {
17098
17757
  const lastWrittenRawRef = useRef20(null);
17099
- const buildHash = useCallback15(() => {
17758
+ const buildHash = useCallback16(() => {
17100
17759
  const state = serializeState(
17101
17760
  cameraState,
17102
17761
  objectBindings,
@@ -17104,12 +17763,12 @@ function useShareableViewerState({
17104
17763
  );
17105
17764
  return `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
17106
17765
  }, [cameraState, objectBindings, selectedObjectBinding, paramKey]);
17107
- const getShareUrl = useCallback15(() => {
17766
+ const getShareUrl = useCallback16(() => {
17108
17767
  const url = new URL(window.location.href);
17109
17768
  url.hash = buildHash();
17110
17769
  return url.toString();
17111
17770
  }, [buildHash]);
17112
- const syncUrl = useCallback15(() => {
17771
+ const syncUrl = useCallback16(() => {
17113
17772
  const url = new URL(window.location.href);
17114
17773
  url.hash = buildHash();
17115
17774
  window.history.replaceState(null, "", url.toString());
@@ -17127,7 +17786,7 @@ function useShareableViewerState({
17127
17786
  objectBindings,
17128
17787
  selectedObjectBinding
17129
17788
  };
17130
- const [restoreAttempted, setRestoreAttempted] = useState29(false);
17789
+ const [restoreAttempted, setRestoreAttempted] = useState30(false);
17131
17790
  const lastSyncedHashRef = useRef20(null);
17132
17791
  useEffect22(() => {
17133
17792
  if (!autoSync || !restoreAttempted) return;
@@ -17153,7 +17812,7 @@ function useShareableViewerState({
17153
17812
  }, [autoSync, restoreAttempted, debounceMs, paramKey]);
17154
17813
  const lastRestoredRawRef = useRef20(null);
17155
17814
  const restoringRawRef = useRef20(null);
17156
- const restoreFromUrl = useCallback15(async () => {
17815
+ const restoreFromUrl = useCallback16(async () => {
17157
17816
  setRestoreAttempted(true);
17158
17817
  const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
17159
17818
  const raw = new URLSearchParams(rawHash).get(paramKey);