@liveroom-tech/react-immersive 1.0.1 → 1.0.3
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/README.md +3 -1
- package/dist/index.d.mts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +275 -4
- package/dist/index.mjs +281 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1018,6 +1018,8 @@ Example:
|
|
|
1018
1018
|
/>
|
|
1019
1019
|
```
|
|
1020
1020
|
|
|
1021
|
+
Passing an explicit `camera` counts as choosing the initial view yourself, so the mount-time auto-fit (see [`onAutoFit`](#onautofit)) is skipped automatically and your `position`/`fov` stick. Provide `onAutoFit` as well only if you want to run your _own_ fit-to-scene logic instead.
|
|
1022
|
+
|
|
1021
1023
|
### `backgroundColor`
|
|
1022
1024
|
|
|
1023
1025
|
Optional background color for the viewer canvas.
|
|
@@ -1273,7 +1275,7 @@ true;
|
|
|
1273
1275
|
|
|
1274
1276
|
### `onAutoFit`
|
|
1275
1277
|
|
|
1276
|
-
Optional async callback fired once the model has loaded and the scene is ready. When provided, the viewer calls it so consumers can run their own fit-to-scene behavior (e.g. an animated fit); if omitted, the viewer falls back to its internal `fitScene
|
|
1278
|
+
Optional async callback fired once the model has loaded and the scene is ready. When provided, the viewer calls it so consumers can run their own fit-to-scene behavior (e.g. an animated fit); if omitted, the viewer falls back to its internal `fitScene` — unless an explicit [`camera`](#camera) prop was given, in which case that position is treated as the chosen initial view and `fitScene` is skipped.
|
|
1277
1279
|
|
|
1278
1280
|
Current callback shape:
|
|
1279
1281
|
|
package/dist/index.d.mts
CHANGED
|
@@ -577,12 +577,13 @@ type ModelViewerProps = {
|
|
|
577
577
|
ktx2TranscoderPath?: string | false;
|
|
578
578
|
meshopt?: boolean;
|
|
579
579
|
showMeasureTools?: boolean;
|
|
580
|
+
showExplodeControls?: boolean;
|
|
580
581
|
measurementUnit?: string;
|
|
581
582
|
enableXR?: boolean;
|
|
582
583
|
usdzUrl?: string;
|
|
583
584
|
showAnnotationNavigation?: boolean;
|
|
584
585
|
};
|
|
585
|
-
declare function ModelViewer({ modelUrl, licenseKey, objectBindings, selectedObject, panelHoveredObject, onObjectBindingsChange, onObjectSelect, onObjectHover, onModelLoaded, onLoadError, onAction, onHiddenObjectsChange, onCameraChange, onViewerReady, onTextureUpload, onAnimationsReady, onAnnotationsChange, activeAnnotation: controlledActiveAnnotation, onActiveAnnotationChange, lights, camera, backgroundColor, shadows, showObjectBindingDataPanel, customObjectBindingDataPanel, customSceneObjectsPanel, showSceneObjectsPanel, showDownloadButton, downloadFilename, showResetButton, showDownloadButtons, showLoadingOverlay, showMouseController, mouseControllerPosition, mouseControllerOpacity, moveSensitivity, zoomSensitivity, sceneConfig, disableZoom, zoomOnSelected, highlightOnHover, onAutoFit, refitOnResize, renderMode, maxDpr, performanceProfile, dracoDecoderPath, ktx2TranscoderPath, meshopt, showMeasureTools, measurementUnit, enableXR, usdzUrl, showAnnotationNavigation, }: ModelViewerProps): react_jsx_runtime.JSX.Element;
|
|
586
|
+
declare function ModelViewer({ modelUrl, licenseKey, objectBindings, selectedObject, panelHoveredObject, onObjectBindingsChange, onObjectSelect, onObjectHover, onModelLoaded, onLoadError, onAction, onHiddenObjectsChange, onCameraChange, onViewerReady, onTextureUpload, onAnimationsReady, onAnnotationsChange, activeAnnotation: controlledActiveAnnotation, onActiveAnnotationChange, lights, camera, backgroundColor, shadows, showObjectBindingDataPanel, customObjectBindingDataPanel, customSceneObjectsPanel, showSceneObjectsPanel, showDownloadButton, downloadFilename, showResetButton, showDownloadButtons, showLoadingOverlay, showMouseController, mouseControllerPosition, mouseControllerOpacity, moveSensitivity, zoomSensitivity, sceneConfig, disableZoom, zoomOnSelected, highlightOnHover, onAutoFit, refitOnResize, renderMode, maxDpr, performanceProfile, dracoDecoderPath, ktx2TranscoderPath, meshopt, showMeasureTools, showExplodeControls, measurementUnit, enableXR, usdzUrl, showAnnotationNavigation, }: ModelViewerProps): react_jsx_runtime.JSX.Element;
|
|
586
587
|
|
|
587
588
|
declare function BindingBuilder({ licenseKey }: {
|
|
588
589
|
licenseKey: string;
|
|
@@ -669,4 +670,22 @@ type ViewerActionsProps = {
|
|
|
669
670
|
};
|
|
670
671
|
declare function useViewerActions(objectBindings: Record<string, ObjectBinding>, options?: ViewerActionsOptions): ViewerActionsProps;
|
|
671
672
|
|
|
672
|
-
|
|
673
|
+
type UseShareableViewerStateOptions = {
|
|
674
|
+
cameraState: ViewerCameraState | null;
|
|
675
|
+
setCameraState: (state: ViewerCameraState, enableTransition?: boolean) => Promise<boolean>;
|
|
676
|
+
objectBindings: Record<string, ObjectBinding>;
|
|
677
|
+
onObjectBindingsChange: (next: Record<string, ObjectBinding>) => void;
|
|
678
|
+
selectedObjectBinding: ObjectBinding | null;
|
|
679
|
+
setSelectedObjectBinding: (binding: ObjectBinding | null) => void;
|
|
680
|
+
paramKey?: string;
|
|
681
|
+
debounceMs?: number;
|
|
682
|
+
autoSync?: boolean;
|
|
683
|
+
};
|
|
684
|
+
type ShareableViewerStateProps = {
|
|
685
|
+
getShareUrl: () => string;
|
|
686
|
+
syncUrl: () => void;
|
|
687
|
+
restoreFromUrl: () => Promise<boolean>;
|
|
688
|
+
};
|
|
689
|
+
declare function useShareableViewerState({ cameraState, setCameraState, objectBindings, onObjectBindingsChange, selectedObjectBinding, setSelectedObjectBinding, paramKey, debounceMs, autoSync, }: UseShareableViewerStateOptions): ShareableViewerStateProps;
|
|
690
|
+
|
|
691
|
+
export { type AnimationClipInfo, type AnimationControls, type AnimationLoopMode, type AnimationPlaybackState, BindingBuilder, type CaptureImageOptions, type CustomObjectBindingDataPanelProps, type CustomSceneObjectsPanelProps, type HiddenObjectsState, type HomeCameraState, MATERIAL_BLENDING_MODES, MATERIAL_SIDES, ModelViewer, type ModelViewerProps, type ObjectActionEvent, type ObjectBinding, type ObjectBindingAction, type ObjectBindingMaterial, type ObjectBindingMaterialBlendingMode, type ObjectBindingMaterialSide, type ObjectBindingTextureSlot, type ObjectVisibilityProps, type SceneAnimationClipConfig, type SceneAnimationConfig, type SceneConfig, SimpleModelViewer, type SimpleModelViewerProps, type ViewerActionsOptions, type ViewerActionsProps, type ViewerAnimationsProps, type ViewerCameraProps, type ViewerCameraState, type ViewerModelBounds, type ViewerModelProps, type ViewerReadyState, type ViewerSelectionProps, useObjectBinding, useObjectBindingIds, useObjectVisibility, useShareableViewerState, useViewerActions, useViewerAnimations, useViewerCamera, useViewerHover, useViewerModel, useViewerSelection };
|
package/dist/index.d.ts
CHANGED
|
@@ -577,12 +577,13 @@ type ModelViewerProps = {
|
|
|
577
577
|
ktx2TranscoderPath?: string | false;
|
|
578
578
|
meshopt?: boolean;
|
|
579
579
|
showMeasureTools?: boolean;
|
|
580
|
+
showExplodeControls?: boolean;
|
|
580
581
|
measurementUnit?: string;
|
|
581
582
|
enableXR?: boolean;
|
|
582
583
|
usdzUrl?: string;
|
|
583
584
|
showAnnotationNavigation?: boolean;
|
|
584
585
|
};
|
|
585
|
-
declare function ModelViewer({ modelUrl, licenseKey, objectBindings, selectedObject, panelHoveredObject, onObjectBindingsChange, onObjectSelect, onObjectHover, onModelLoaded, onLoadError, onAction, onHiddenObjectsChange, onCameraChange, onViewerReady, onTextureUpload, onAnimationsReady, onAnnotationsChange, activeAnnotation: controlledActiveAnnotation, onActiveAnnotationChange, lights, camera, backgroundColor, shadows, showObjectBindingDataPanel, customObjectBindingDataPanel, customSceneObjectsPanel, showSceneObjectsPanel, showDownloadButton, downloadFilename, showResetButton, showDownloadButtons, showLoadingOverlay, showMouseController, mouseControllerPosition, mouseControllerOpacity, moveSensitivity, zoomSensitivity, sceneConfig, disableZoom, zoomOnSelected, highlightOnHover, onAutoFit, refitOnResize, renderMode, maxDpr, performanceProfile, dracoDecoderPath, ktx2TranscoderPath, meshopt, showMeasureTools, measurementUnit, enableXR, usdzUrl, showAnnotationNavigation, }: ModelViewerProps): react_jsx_runtime.JSX.Element;
|
|
586
|
+
declare function ModelViewer({ modelUrl, licenseKey, objectBindings, selectedObject, panelHoveredObject, onObjectBindingsChange, onObjectSelect, onObjectHover, onModelLoaded, onLoadError, onAction, onHiddenObjectsChange, onCameraChange, onViewerReady, onTextureUpload, onAnimationsReady, onAnnotationsChange, activeAnnotation: controlledActiveAnnotation, onActiveAnnotationChange, lights, camera, backgroundColor, shadows, showObjectBindingDataPanel, customObjectBindingDataPanel, customSceneObjectsPanel, showSceneObjectsPanel, showDownloadButton, downloadFilename, showResetButton, showDownloadButtons, showLoadingOverlay, showMouseController, mouseControllerPosition, mouseControllerOpacity, moveSensitivity, zoomSensitivity, sceneConfig, disableZoom, zoomOnSelected, highlightOnHover, onAutoFit, refitOnResize, renderMode, maxDpr, performanceProfile, dracoDecoderPath, ktx2TranscoderPath, meshopt, showMeasureTools, showExplodeControls, measurementUnit, enableXR, usdzUrl, showAnnotationNavigation, }: ModelViewerProps): react_jsx_runtime.JSX.Element;
|
|
586
587
|
|
|
587
588
|
declare function BindingBuilder({ licenseKey }: {
|
|
588
589
|
licenseKey: string;
|
|
@@ -669,4 +670,22 @@ type ViewerActionsProps = {
|
|
|
669
670
|
};
|
|
670
671
|
declare function useViewerActions(objectBindings: Record<string, ObjectBinding>, options?: ViewerActionsOptions): ViewerActionsProps;
|
|
671
672
|
|
|
672
|
-
|
|
673
|
+
type UseShareableViewerStateOptions = {
|
|
674
|
+
cameraState: ViewerCameraState | null;
|
|
675
|
+
setCameraState: (state: ViewerCameraState, enableTransition?: boolean) => Promise<boolean>;
|
|
676
|
+
objectBindings: Record<string, ObjectBinding>;
|
|
677
|
+
onObjectBindingsChange: (next: Record<string, ObjectBinding>) => void;
|
|
678
|
+
selectedObjectBinding: ObjectBinding | null;
|
|
679
|
+
setSelectedObjectBinding: (binding: ObjectBinding | null) => void;
|
|
680
|
+
paramKey?: string;
|
|
681
|
+
debounceMs?: number;
|
|
682
|
+
autoSync?: boolean;
|
|
683
|
+
};
|
|
684
|
+
type ShareableViewerStateProps = {
|
|
685
|
+
getShareUrl: () => string;
|
|
686
|
+
syncUrl: () => void;
|
|
687
|
+
restoreFromUrl: () => Promise<boolean>;
|
|
688
|
+
};
|
|
689
|
+
declare function useShareableViewerState({ cameraState, setCameraState, objectBindings, onObjectBindingsChange, selectedObjectBinding, setSelectedObjectBinding, paramKey, debounceMs, autoSync, }: UseShareableViewerStateOptions): ShareableViewerStateProps;
|
|
690
|
+
|
|
691
|
+
export { type AnimationClipInfo, type AnimationControls, type AnimationLoopMode, type AnimationPlaybackState, BindingBuilder, type CaptureImageOptions, type CustomObjectBindingDataPanelProps, type CustomSceneObjectsPanelProps, type HiddenObjectsState, type HomeCameraState, MATERIAL_BLENDING_MODES, MATERIAL_SIDES, ModelViewer, type ModelViewerProps, type ObjectActionEvent, type ObjectBinding, type ObjectBindingAction, type ObjectBindingMaterial, type ObjectBindingMaterialBlendingMode, type ObjectBindingMaterialSide, type ObjectBindingTextureSlot, type ObjectVisibilityProps, type SceneAnimationClipConfig, type SceneAnimationConfig, type SceneConfig, SimpleModelViewer, type SimpleModelViewerProps, type ViewerActionsOptions, type ViewerActionsProps, type ViewerAnimationsProps, type ViewerCameraProps, type ViewerCameraState, type ViewerModelBounds, type ViewerModelProps, type ViewerReadyState, type ViewerSelectionProps, useObjectBinding, useObjectBindingIds, useObjectVisibility, useShareableViewerState, useViewerActions, useViewerAnimations, useViewerCamera, useViewerHover, useViewerModel, useViewerSelection };
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
useObjectBinding: () => useObjectBinding,
|
|
39
39
|
useObjectBindingIds: () => useObjectBindingIds,
|
|
40
40
|
useObjectVisibility: () => useObjectVisibility,
|
|
41
|
+
useShareableViewerState: () => useShareableViewerState,
|
|
41
42
|
useViewerActions: () => useViewerActions,
|
|
42
43
|
useViewerAnimations: () => useViewerAnimations,
|
|
43
44
|
useViewerCamera: () => useViewerCamera,
|
|
@@ -77,7 +78,7 @@ function styleInject(css, { insertAt } = {}) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// src/styles/ModelViewer.css
|
|
80
|
-
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: #141824;\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: #f1f5f9;\n --ri-sidepanel-text-emphasis: #ffffff;\n --ri-sidepanel-text-muted: #94a3b8;\n --ri-sidepanel-text-subtle: #64748b;\n --ri-sidepanel-placeholder: #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-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 top: 16px;\n left: 50%;\n transform: translateX(-50%);\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: 10px;\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-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-popoverHeader {\n padding: 12px 16px;\n border-bottom: 1px solid #f3f4f6;\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: 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 padding: 8px 12px;\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: 30px;\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 top: 12px;\n gap: 5px;\n }\n .ri-xrButton {\n height: 28px;\n gap: 5px;\n padding: 0 7px;\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');
|
|
81
|
+
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: #141824;\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: #f1f5f9;\n --ri-sidepanel-text-emphasis: #ffffff;\n --ri-sidepanel-text-muted: #94a3b8;\n --ri-sidepanel-text-subtle: #64748b;\n --ri-sidepanel-placeholder: #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-explodeTools--stacked {\n top: 58px;\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-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 top: 16px;\n left: 50%;\n transform: translateX(-50%);\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: 10px;\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-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-popoverHeader {\n padding: 12px 16px;\n border-bottom: 1px solid #f3f4f6;\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: 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 padding: 8px 12px;\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: 30px;\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 top: 12px;\n gap: 5px;\n }\n .ri-xrButton {\n height: 28px;\n gap: 5px;\n padding: 0 7px;\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');
|
|
81
82
|
|
|
82
83
|
// src/components/LicenseOverlay.tsx
|
|
83
84
|
var import_lucide_react = require("lucide-react");
|
|
@@ -2478,7 +2479,8 @@ function Model({
|
|
|
2478
2479
|
sceneConfig,
|
|
2479
2480
|
zoomOnSelected: _zoomOnSelected = true,
|
|
2480
2481
|
highlightOnHover = true,
|
|
2481
|
-
decoderConfig
|
|
2482
|
+
decoderConfig,
|
|
2483
|
+
explodeFactor = 0
|
|
2482
2484
|
}) {
|
|
2483
2485
|
const gltf = useConfiguredGLTF(url, decoderConfig);
|
|
2484
2486
|
const invalidate = (0, import_fiber7.useThree)((state) => state.invalidate);
|
|
@@ -3014,6 +3016,84 @@ function Model({
|
|
|
3014
3016
|
const settled = Math.abs(rotation.x - targetRotation[0]) < 1e-4 && Math.abs(rotation.y - targetRotation[1]) < 1e-4 && Math.abs(rotation.z - targetRotation[2]) < 1e-4;
|
|
3015
3017
|
if (!settled) invalidate();
|
|
3016
3018
|
});
|
|
3019
|
+
const explodePartsRef = (0, import_react11.useRef)([]);
|
|
3020
|
+
const explodeStrengthRef = (0, import_react11.useRef)(0);
|
|
3021
|
+
const currentExplodeRef = (0, import_react11.useRef)(0);
|
|
3022
|
+
const explodeTmp = (0, import_react11.useMemo)(() => new import_three13.Vector3(), []);
|
|
3023
|
+
(0, import_react11.useEffect)(() => {
|
|
3024
|
+
for (const part of explodePartsRef.current) {
|
|
3025
|
+
part.node.position.copy(part.restLocalPos);
|
|
3026
|
+
}
|
|
3027
|
+
currentExplodeRef.current = 0;
|
|
3028
|
+
scene.updateWorldMatrix(true, true);
|
|
3029
|
+
const sceneBox = new import_three13.Box3().setFromObject(scene);
|
|
3030
|
+
const modelCenter = sceneBox.getCenter(new import_three13.Vector3());
|
|
3031
|
+
const radius = sceneBox.getSize(new import_three13.Vector3()).length() * 0.5;
|
|
3032
|
+
explodeStrengthRef.current = Number.isFinite(radius) ? radius : 0;
|
|
3033
|
+
const targets = [];
|
|
3034
|
+
const targetSet = /* @__PURE__ */ new Set();
|
|
3035
|
+
for (const [nodeName, binding] of Object.entries(objectBindings)) {
|
|
3036
|
+
const node = nodeMap[binding.modelObjectId ?? nodeName];
|
|
3037
|
+
if (!node || targetSet.has(node)) continue;
|
|
3038
|
+
targetSet.add(node);
|
|
3039
|
+
targets.push(node);
|
|
3040
|
+
}
|
|
3041
|
+
const parts = [];
|
|
3042
|
+
const partCenter = new import_three13.Vector3();
|
|
3043
|
+
for (const node of targets) {
|
|
3044
|
+
if (!node.parent) continue;
|
|
3045
|
+
let ancestor = node.parent;
|
|
3046
|
+
let nested = false;
|
|
3047
|
+
while (ancestor) {
|
|
3048
|
+
if (targetSet.has(ancestor)) {
|
|
3049
|
+
nested = true;
|
|
3050
|
+
break;
|
|
3051
|
+
}
|
|
3052
|
+
ancestor = ancestor.parent;
|
|
3053
|
+
}
|
|
3054
|
+
if (nested) continue;
|
|
3055
|
+
const box = new import_three13.Box3().setFromObject(node);
|
|
3056
|
+
if (box.isEmpty()) continue;
|
|
3057
|
+
box.getCenter(partCenter);
|
|
3058
|
+
const worldDir = partCenter.clone().sub(modelCenter);
|
|
3059
|
+
if (worldDir.lengthSq() < 1e-8) continue;
|
|
3060
|
+
worldDir.normalize();
|
|
3061
|
+
parts.push({
|
|
3062
|
+
node,
|
|
3063
|
+
restLocalPos: node.position.clone(),
|
|
3064
|
+
restWorldPos: node.getWorldPosition(new import_three13.Vector3()),
|
|
3065
|
+
worldDir,
|
|
3066
|
+
parentInverse: new import_three13.Matrix4().copy(node.parent.matrixWorld).invert()
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
explodePartsRef.current = parts;
|
|
3070
|
+
invalidate();
|
|
3071
|
+
}, [scene, nodeMap, nodeBindingSignature, invalidate]);
|
|
3072
|
+
(0, import_react11.useEffect)(() => {
|
|
3073
|
+
invalidate();
|
|
3074
|
+
}, [explodeFactor, invalidate]);
|
|
3075
|
+
(0, import_fiber7.useFrame)((_, delta) => {
|
|
3076
|
+
const parts = explodePartsRef.current;
|
|
3077
|
+
if (parts.length === 0) return;
|
|
3078
|
+
const target = Math.min(Math.max(explodeFactor, 0), 1);
|
|
3079
|
+
const current = currentExplodeRef.current;
|
|
3080
|
+
if (Math.abs(current - target) < 1e-4) {
|
|
3081
|
+
if (current === target) return;
|
|
3082
|
+
currentExplodeRef.current = target;
|
|
3083
|
+
} else {
|
|
3084
|
+
currentExplodeRef.current = import_three13.MathUtils.lerp(
|
|
3085
|
+
current,
|
|
3086
|
+
target,
|
|
3087
|
+
1 - Math.exp(-6 * delta)
|
|
3088
|
+
);
|
|
3089
|
+
invalidate();
|
|
3090
|
+
}
|
|
3091
|
+
const distance = currentExplodeRef.current * explodeStrengthRef.current;
|
|
3092
|
+
for (const part of parts) {
|
|
3093
|
+
explodeTmp.copy(part.worldDir).multiplyScalar(distance).add(part.restWorldPos).applyMatrix4(part.parentInverse);
|
|
3094
|
+
part.node.position.copy(explodeTmp);
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3017
3097
|
const resolveBinding = (object) => {
|
|
3018
3098
|
let node = object;
|
|
3019
3099
|
while (node) {
|
|
@@ -4960,6 +5040,7 @@ function ModelViewer({
|
|
|
4960
5040
|
ktx2TranscoderPath,
|
|
4961
5041
|
meshopt = true,
|
|
4962
5042
|
showMeasureTools = false,
|
|
5043
|
+
showExplodeControls = false,
|
|
4963
5044
|
measurementUnit = "m",
|
|
4964
5045
|
enableXR = false,
|
|
4965
5046
|
usdzUrl,
|
|
@@ -5059,6 +5140,12 @@ function ModelViewer({
|
|
|
5059
5140
|
measureModeRef.current = measureMode;
|
|
5060
5141
|
const measurePointCounter = (0, import_react21.useRef)(0);
|
|
5061
5142
|
const measureDownRef = (0, import_react21.useRef)(null);
|
|
5143
|
+
const [explodeFactor, setExplodeFactor] = (0, import_react21.useState)(0);
|
|
5144
|
+
const [explodeOpen, setExplodeOpen] = (0, import_react21.useState)(false);
|
|
5145
|
+
(0, import_react21.useEffect)(() => {
|
|
5146
|
+
setExplodeFactor(0);
|
|
5147
|
+
setExplodeOpen(false);
|
|
5148
|
+
}, [modelUrl]);
|
|
5062
5149
|
const xrStore = (0, import_react21.useMemo)(() => {
|
|
5063
5150
|
if (!enableXR) return null;
|
|
5064
5151
|
const xr = navigator.xr;
|
|
@@ -6386,7 +6473,8 @@ function ModelViewer({
|
|
|
6386
6473
|
sceneConfig: resolvedSceneConfig,
|
|
6387
6474
|
zoomOnSelected,
|
|
6388
6475
|
highlightOnHover,
|
|
6389
|
-
decoderConfig
|
|
6476
|
+
decoderConfig,
|
|
6477
|
+
explodeFactor
|
|
6390
6478
|
}
|
|
6391
6479
|
),
|
|
6392
6480
|
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
@@ -6481,6 +6569,43 @@ function ModelViewer({
|
|
|
6481
6569
|
),
|
|
6482
6570
|
measureMode && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "ri-measureHint", children: "Click points on the model to measure" })
|
|
6483
6571
|
] }),
|
|
6572
|
+
showExplodeControls && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
6573
|
+
"div",
|
|
6574
|
+
{
|
|
6575
|
+
className: `ri-explodeTools ${showMeasureTools ? "ri-explodeTools--stacked" : ""}`,
|
|
6576
|
+
children: [
|
|
6577
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
6578
|
+
"button",
|
|
6579
|
+
{
|
|
6580
|
+
type: "button",
|
|
6581
|
+
className: `ri-measureButton ${explodeOpen || explodeFactor > 0 ? "ri-measureButton--active" : ""}`,
|
|
6582
|
+
onClick: () => setExplodeOpen((prev) => !prev),
|
|
6583
|
+
title: "Exploded view",
|
|
6584
|
+
"aria-pressed": explodeOpen,
|
|
6585
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_lucide_react3.Expand, { size: 16 })
|
|
6586
|
+
}
|
|
6587
|
+
),
|
|
6588
|
+
explodeOpen && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
6589
|
+
"div",
|
|
6590
|
+
{
|
|
6591
|
+
className: "ri-explodeSlider",
|
|
6592
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
6593
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
6594
|
+
"input",
|
|
6595
|
+
{
|
|
6596
|
+
type: "range",
|
|
6597
|
+
min: 0,
|
|
6598
|
+
max: 100,
|
|
6599
|
+
value: Math.round(explodeFactor * 100),
|
|
6600
|
+
onChange: (e) => setExplodeFactor(Number(e.target.value) / 100),
|
|
6601
|
+
"aria-label": "Explode amount"
|
|
6602
|
+
}
|
|
6603
|
+
)
|
|
6604
|
+
}
|
|
6605
|
+
)
|
|
6606
|
+
]
|
|
6607
|
+
}
|
|
6608
|
+
),
|
|
6484
6609
|
enableXR && (xrStore && (arSupported || vrSupported) || quickLookSupported) && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "ri-xrTools", children: [
|
|
6485
6610
|
!arSupported && quickLookSupported && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
6486
6611
|
"button",
|
|
@@ -14502,7 +14627,8 @@ function BindingBuilder({ licenseKey }) {
|
|
|
14502
14627
|
disableZoom: false,
|
|
14503
14628
|
zoomOnSelected,
|
|
14504
14629
|
highlightOnHover,
|
|
14505
|
-
panelHoveredObject: highlightOnHover ? panelHoveredBinding : null
|
|
14630
|
+
panelHoveredObject: highlightOnHover ? panelHoveredBinding : null,
|
|
14631
|
+
showExplodeControls: true
|
|
14506
14632
|
}
|
|
14507
14633
|
) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
|
|
14508
14634
|
"div",
|
|
@@ -15895,6 +16021,150 @@ function useViewerActions(objectBindings, options = {}) {
|
|
|
15895
16021
|
runAction
|
|
15896
16022
|
};
|
|
15897
16023
|
}
|
|
16024
|
+
|
|
16025
|
+
// src/hooks/useShareableViewerState.ts
|
|
16026
|
+
var import_react46 = require("react");
|
|
16027
|
+
function serializeState(cameraState, objectBindings, selectedObjectBinding) {
|
|
16028
|
+
const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
|
|
16029
|
+
return {
|
|
16030
|
+
camera: cameraState ?? void 0,
|
|
16031
|
+
hidden,
|
|
16032
|
+
selected: selectedObjectBinding?.id ?? null
|
|
16033
|
+
};
|
|
16034
|
+
}
|
|
16035
|
+
function useShareableViewerState({
|
|
16036
|
+
cameraState,
|
|
16037
|
+
setCameraState,
|
|
16038
|
+
objectBindings,
|
|
16039
|
+
onObjectBindingsChange,
|
|
16040
|
+
selectedObjectBinding,
|
|
16041
|
+
setSelectedObjectBinding,
|
|
16042
|
+
paramKey = "view",
|
|
16043
|
+
debounceMs = 400,
|
|
16044
|
+
autoSync = true
|
|
16045
|
+
}) {
|
|
16046
|
+
const lastWrittenRawRef = (0, import_react46.useRef)(null);
|
|
16047
|
+
const buildHash = (0, import_react46.useCallback)(() => {
|
|
16048
|
+
const state = serializeState(
|
|
16049
|
+
cameraState,
|
|
16050
|
+
objectBindings,
|
|
16051
|
+
selectedObjectBinding
|
|
16052
|
+
);
|
|
16053
|
+
return `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
|
|
16054
|
+
}, [cameraState, objectBindings, selectedObjectBinding, paramKey]);
|
|
16055
|
+
const getShareUrl = (0, import_react46.useCallback)(() => {
|
|
16056
|
+
const url = new URL(window.location.href);
|
|
16057
|
+
url.hash = buildHash();
|
|
16058
|
+
return url.toString();
|
|
16059
|
+
}, [buildHash]);
|
|
16060
|
+
const syncUrl = (0, import_react46.useCallback)(() => {
|
|
16061
|
+
const url = new URL(window.location.href);
|
|
16062
|
+
url.hash = buildHash();
|
|
16063
|
+
window.history.replaceState(null, "", url.toString());
|
|
16064
|
+
lastWrittenRawRef.current = new URLSearchParams(url.hash.slice(1)).get(
|
|
16065
|
+
paramKey
|
|
16066
|
+
);
|
|
16067
|
+
}, [buildHash, paramKey]);
|
|
16068
|
+
const latestStateRef = (0, import_react46.useRef)({
|
|
16069
|
+
cameraState,
|
|
16070
|
+
objectBindings,
|
|
16071
|
+
selectedObjectBinding
|
|
16072
|
+
});
|
|
16073
|
+
latestStateRef.current = {
|
|
16074
|
+
cameraState,
|
|
16075
|
+
objectBindings,
|
|
16076
|
+
selectedObjectBinding
|
|
16077
|
+
};
|
|
16078
|
+
const [restoreAttempted, setRestoreAttempted] = (0, import_react46.useState)(false);
|
|
16079
|
+
const lastSyncedHashRef = (0, import_react46.useRef)(null);
|
|
16080
|
+
(0, import_react46.useEffect)(() => {
|
|
16081
|
+
if (!autoSync || !restoreAttempted) return;
|
|
16082
|
+
const tick = () => {
|
|
16083
|
+
const current = latestStateRef.current;
|
|
16084
|
+
const state = serializeState(
|
|
16085
|
+
current.cameraState,
|
|
16086
|
+
current.objectBindings,
|
|
16087
|
+
current.selectedObjectBinding
|
|
16088
|
+
);
|
|
16089
|
+
const hash = `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
|
|
16090
|
+
if (hash === lastSyncedHashRef.current) return;
|
|
16091
|
+
lastSyncedHashRef.current = hash;
|
|
16092
|
+
const url = new URL(window.location.href);
|
|
16093
|
+
url.hash = hash;
|
|
16094
|
+
window.history.replaceState(null, "", url.toString());
|
|
16095
|
+
lastWrittenRawRef.current = new URLSearchParams(url.hash.slice(1)).get(
|
|
16096
|
+
paramKey
|
|
16097
|
+
);
|
|
16098
|
+
};
|
|
16099
|
+
const id = window.setInterval(tick, debounceMs);
|
|
16100
|
+
return () => window.clearInterval(id);
|
|
16101
|
+
}, [autoSync, restoreAttempted, debounceMs, paramKey]);
|
|
16102
|
+
const lastRestoredRawRef = (0, import_react46.useRef)(null);
|
|
16103
|
+
const restoringRawRef = (0, import_react46.useRef)(null);
|
|
16104
|
+
const restoreFromUrl = (0, import_react46.useCallback)(async () => {
|
|
16105
|
+
setRestoreAttempted(true);
|
|
16106
|
+
const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
16107
|
+
const raw = new URLSearchParams(rawHash).get(paramKey);
|
|
16108
|
+
if (!raw) {
|
|
16109
|
+
lastRestoredRawRef.current = null;
|
|
16110
|
+
return false;
|
|
16111
|
+
}
|
|
16112
|
+
if (raw === lastWrittenRawRef.current || raw === lastRestoredRawRef.current || raw === restoringRawRef.current) {
|
|
16113
|
+
return false;
|
|
16114
|
+
}
|
|
16115
|
+
let state;
|
|
16116
|
+
try {
|
|
16117
|
+
state = JSON.parse(raw);
|
|
16118
|
+
} catch {
|
|
16119
|
+
return false;
|
|
16120
|
+
}
|
|
16121
|
+
restoringRawRef.current = raw;
|
|
16122
|
+
try {
|
|
16123
|
+
if (state.camera) {
|
|
16124
|
+
await setCameraState(state.camera, false);
|
|
16125
|
+
}
|
|
16126
|
+
if (state.hidden) {
|
|
16127
|
+
const hiddenIds = new Set(state.hidden);
|
|
16128
|
+
onObjectBindingsChange(
|
|
16129
|
+
Object.fromEntries(
|
|
16130
|
+
Object.entries(objectBindings).map(([bindingKey, binding]) => [
|
|
16131
|
+
bindingKey,
|
|
16132
|
+
{ ...binding, visible: !hiddenIds.has(binding.id) }
|
|
16133
|
+
])
|
|
16134
|
+
)
|
|
16135
|
+
);
|
|
16136
|
+
}
|
|
16137
|
+
if (state.selected !== void 0) {
|
|
16138
|
+
const bindingKey = state.selected ? resolveBindingKey(objectBindings, state.selected) : null;
|
|
16139
|
+
setSelectedObjectBinding(
|
|
16140
|
+
bindingKey ? objectBindings[bindingKey] : null
|
|
16141
|
+
);
|
|
16142
|
+
}
|
|
16143
|
+
lastRestoredRawRef.current = raw;
|
|
16144
|
+
return true;
|
|
16145
|
+
} finally {
|
|
16146
|
+
if (restoringRawRef.current === raw) {
|
|
16147
|
+
restoringRawRef.current = null;
|
|
16148
|
+
}
|
|
16149
|
+
}
|
|
16150
|
+
}, [
|
|
16151
|
+
objectBindings,
|
|
16152
|
+
onObjectBindingsChange,
|
|
16153
|
+
paramKey,
|
|
16154
|
+
setCameraState,
|
|
16155
|
+
setSelectedObjectBinding
|
|
16156
|
+
]);
|
|
16157
|
+
(0, import_react46.useEffect)(() => {
|
|
16158
|
+
const handleHashChange = () => {
|
|
16159
|
+
void restoreFromUrl();
|
|
16160
|
+
};
|
|
16161
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
16162
|
+
return () => {
|
|
16163
|
+
window.removeEventListener("hashchange", handleHashChange);
|
|
16164
|
+
};
|
|
16165
|
+
}, [restoreFromUrl]);
|
|
16166
|
+
return { getShareUrl, syncUrl, restoreFromUrl };
|
|
16167
|
+
}
|
|
15898
16168
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15899
16169
|
0 && (module.exports = {
|
|
15900
16170
|
BindingBuilder,
|
|
@@ -15905,6 +16175,7 @@ function useViewerActions(objectBindings, options = {}) {
|
|
|
15905
16175
|
useObjectBinding,
|
|
15906
16176
|
useObjectBindingIds,
|
|
15907
16177
|
useObjectVisibility,
|
|
16178
|
+
useShareableViewerState,
|
|
15908
16179
|
useViewerActions,
|
|
15909
16180
|
useViewerAnimations,
|
|
15910
16181
|
useViewerCamera,
|
package/dist/index.mjs
CHANGED
|
@@ -42,7 +42,7 @@ function styleInject(css, { insertAt } = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/styles/ModelViewer.css
|
|
45
|
-
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: #141824;\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: #f1f5f9;\n --ri-sidepanel-text-emphasis: #ffffff;\n --ri-sidepanel-text-muted: #94a3b8;\n --ri-sidepanel-text-subtle: #64748b;\n --ri-sidepanel-placeholder: #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-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 top: 16px;\n left: 50%;\n transform: translateX(-50%);\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: 10px;\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-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-popoverHeader {\n padding: 12px 16px;\n border-bottom: 1px solid #f3f4f6;\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: 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 padding: 8px 12px;\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: 30px;\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 top: 12px;\n gap: 5px;\n }\n .ri-xrButton {\n height: 28px;\n gap: 5px;\n padding: 0 7px;\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');
|
|
45
|
+
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: #141824;\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: #f1f5f9;\n --ri-sidepanel-text-emphasis: #ffffff;\n --ri-sidepanel-text-muted: #94a3b8;\n --ri-sidepanel-text-subtle: #64748b;\n --ri-sidepanel-placeholder: #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-explodeTools--stacked {\n top: 58px;\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-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 top: 16px;\n left: 50%;\n transform: translateX(-50%);\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: 10px;\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-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-popoverHeader {\n padding: 12px 16px;\n border-bottom: 1px solid #f3f4f6;\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: 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 padding: 8px 12px;\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: 30px;\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 top: 12px;\n gap: 5px;\n }\n .ri-xrButton {\n height: 28px;\n gap: 5px;\n padding: 0 7px;\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');
|
|
46
46
|
|
|
47
47
|
// src/components/LicenseOverlay.tsx
|
|
48
48
|
import { AlertTriangle } from "lucide-react";
|
|
@@ -1874,6 +1874,7 @@ import {
|
|
|
1874
1874
|
LoopRepeat,
|
|
1875
1875
|
Box3,
|
|
1876
1876
|
Vector3 as Vector32,
|
|
1877
|
+
Matrix4,
|
|
1877
1878
|
MathUtils as MathUtils2
|
|
1878
1879
|
} from "three";
|
|
1879
1880
|
import { SkeletonUtils } from "three-stdlib";
|
|
@@ -2509,7 +2510,8 @@ function Model({
|
|
|
2509
2510
|
sceneConfig,
|
|
2510
2511
|
zoomOnSelected: _zoomOnSelected = true,
|
|
2511
2512
|
highlightOnHover = true,
|
|
2512
|
-
decoderConfig
|
|
2513
|
+
decoderConfig,
|
|
2514
|
+
explodeFactor = 0
|
|
2513
2515
|
}) {
|
|
2514
2516
|
const gltf = useConfiguredGLTF(url, decoderConfig);
|
|
2515
2517
|
const invalidate = useThree7((state) => state.invalidate);
|
|
@@ -3045,6 +3047,84 @@ function Model({
|
|
|
3045
3047
|
const settled = Math.abs(rotation.x - targetRotation[0]) < 1e-4 && Math.abs(rotation.y - targetRotation[1]) < 1e-4 && Math.abs(rotation.z - targetRotation[2]) < 1e-4;
|
|
3046
3048
|
if (!settled) invalidate();
|
|
3047
3049
|
});
|
|
3050
|
+
const explodePartsRef = useRef5([]);
|
|
3051
|
+
const explodeStrengthRef = useRef5(0);
|
|
3052
|
+
const currentExplodeRef = useRef5(0);
|
|
3053
|
+
const explodeTmp = useMemo7(() => new Vector32(), []);
|
|
3054
|
+
useEffect6(() => {
|
|
3055
|
+
for (const part of explodePartsRef.current) {
|
|
3056
|
+
part.node.position.copy(part.restLocalPos);
|
|
3057
|
+
}
|
|
3058
|
+
currentExplodeRef.current = 0;
|
|
3059
|
+
scene.updateWorldMatrix(true, true);
|
|
3060
|
+
const sceneBox = new Box3().setFromObject(scene);
|
|
3061
|
+
const modelCenter = sceneBox.getCenter(new Vector32());
|
|
3062
|
+
const radius = sceneBox.getSize(new Vector32()).length() * 0.5;
|
|
3063
|
+
explodeStrengthRef.current = Number.isFinite(radius) ? radius : 0;
|
|
3064
|
+
const targets = [];
|
|
3065
|
+
const targetSet = /* @__PURE__ */ new Set();
|
|
3066
|
+
for (const [nodeName, binding] of Object.entries(objectBindings)) {
|
|
3067
|
+
const node = nodeMap[binding.modelObjectId ?? nodeName];
|
|
3068
|
+
if (!node || targetSet.has(node)) continue;
|
|
3069
|
+
targetSet.add(node);
|
|
3070
|
+
targets.push(node);
|
|
3071
|
+
}
|
|
3072
|
+
const parts = [];
|
|
3073
|
+
const partCenter = new Vector32();
|
|
3074
|
+
for (const node of targets) {
|
|
3075
|
+
if (!node.parent) continue;
|
|
3076
|
+
let ancestor = node.parent;
|
|
3077
|
+
let nested = false;
|
|
3078
|
+
while (ancestor) {
|
|
3079
|
+
if (targetSet.has(ancestor)) {
|
|
3080
|
+
nested = true;
|
|
3081
|
+
break;
|
|
3082
|
+
}
|
|
3083
|
+
ancestor = ancestor.parent;
|
|
3084
|
+
}
|
|
3085
|
+
if (nested) continue;
|
|
3086
|
+
const box = new Box3().setFromObject(node);
|
|
3087
|
+
if (box.isEmpty()) continue;
|
|
3088
|
+
box.getCenter(partCenter);
|
|
3089
|
+
const worldDir = partCenter.clone().sub(modelCenter);
|
|
3090
|
+
if (worldDir.lengthSq() < 1e-8) continue;
|
|
3091
|
+
worldDir.normalize();
|
|
3092
|
+
parts.push({
|
|
3093
|
+
node,
|
|
3094
|
+
restLocalPos: node.position.clone(),
|
|
3095
|
+
restWorldPos: node.getWorldPosition(new Vector32()),
|
|
3096
|
+
worldDir,
|
|
3097
|
+
parentInverse: new Matrix4().copy(node.parent.matrixWorld).invert()
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
explodePartsRef.current = parts;
|
|
3101
|
+
invalidate();
|
|
3102
|
+
}, [scene, nodeMap, nodeBindingSignature, invalidate]);
|
|
3103
|
+
useEffect6(() => {
|
|
3104
|
+
invalidate();
|
|
3105
|
+
}, [explodeFactor, invalidate]);
|
|
3106
|
+
useFrame4((_, delta) => {
|
|
3107
|
+
const parts = explodePartsRef.current;
|
|
3108
|
+
if (parts.length === 0) return;
|
|
3109
|
+
const target = Math.min(Math.max(explodeFactor, 0), 1);
|
|
3110
|
+
const current = currentExplodeRef.current;
|
|
3111
|
+
if (Math.abs(current - target) < 1e-4) {
|
|
3112
|
+
if (current === target) return;
|
|
3113
|
+
currentExplodeRef.current = target;
|
|
3114
|
+
} else {
|
|
3115
|
+
currentExplodeRef.current = MathUtils2.lerp(
|
|
3116
|
+
current,
|
|
3117
|
+
target,
|
|
3118
|
+
1 - Math.exp(-6 * delta)
|
|
3119
|
+
);
|
|
3120
|
+
invalidate();
|
|
3121
|
+
}
|
|
3122
|
+
const distance = currentExplodeRef.current * explodeStrengthRef.current;
|
|
3123
|
+
for (const part of parts) {
|
|
3124
|
+
explodeTmp.copy(part.worldDir).multiplyScalar(distance).add(part.restWorldPos).applyMatrix4(part.parentInverse);
|
|
3125
|
+
part.node.position.copy(explodeTmp);
|
|
3126
|
+
}
|
|
3127
|
+
});
|
|
3048
3128
|
const resolveBinding = (object) => {
|
|
3049
3129
|
let node = object;
|
|
3050
3130
|
while (node) {
|
|
@@ -3592,7 +3672,7 @@ import {
|
|
|
3592
3672
|
} from "react";
|
|
3593
3673
|
import { useXR as useXR2, useXRHitTest, useXRInputSourceEvent } from "@react-three/xr";
|
|
3594
3674
|
import { useFrame as useFrame6 } from "@react-three/fiber";
|
|
3595
|
-
import { Matrix4, Vector3 as Vector35 } from "three";
|
|
3675
|
+
import { Matrix4 as Matrix42, Vector3 as Vector35 } from "three";
|
|
3596
3676
|
import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3597
3677
|
var TARGET_SIZE = 1;
|
|
3598
3678
|
var FORWARD_DISTANCE = 1;
|
|
@@ -3631,7 +3711,7 @@ var XRModelPresenter = forwardRef2(
|
|
|
3631
3711
|
const rotationY = useRef7(0);
|
|
3632
3712
|
const placementMode = useRef7(true);
|
|
3633
3713
|
const modelPlaced = useRef7(false);
|
|
3634
|
-
const hitMatrix = useMemo10(() => new
|
|
3714
|
+
const hitMatrix = useMemo10(() => new Matrix42(), []);
|
|
3635
3715
|
const hitPosition = useRef7(new Vector35());
|
|
3636
3716
|
const hasHit = useRef7(false);
|
|
3637
3717
|
const lastARState = useRef7(null);
|
|
@@ -4792,7 +4872,8 @@ import {
|
|
|
4792
4872
|
Square,
|
|
4793
4873
|
ChevronRight,
|
|
4794
4874
|
RectangleGoggles,
|
|
4795
|
-
X
|
|
4875
|
+
X,
|
|
4876
|
+
Expand
|
|
4796
4877
|
} from "lucide-react";
|
|
4797
4878
|
|
|
4798
4879
|
// src/components/BindingBuilder/BindingBuilderContext.tsx
|
|
@@ -5018,6 +5099,7 @@ function ModelViewer({
|
|
|
5018
5099
|
ktx2TranscoderPath,
|
|
5019
5100
|
meshopt = true,
|
|
5020
5101
|
showMeasureTools = false,
|
|
5102
|
+
showExplodeControls = false,
|
|
5021
5103
|
measurementUnit = "m",
|
|
5022
5104
|
enableXR = false,
|
|
5023
5105
|
usdzUrl,
|
|
@@ -5117,6 +5199,12 @@ function ModelViewer({
|
|
|
5117
5199
|
measureModeRef.current = measureMode;
|
|
5118
5200
|
const measurePointCounter = useRef9(0);
|
|
5119
5201
|
const measureDownRef = useRef9(null);
|
|
5202
|
+
const [explodeFactor, setExplodeFactor] = useState10(0);
|
|
5203
|
+
const [explodeOpen, setExplodeOpen] = useState10(false);
|
|
5204
|
+
useEffect13(() => {
|
|
5205
|
+
setExplodeFactor(0);
|
|
5206
|
+
setExplodeOpen(false);
|
|
5207
|
+
}, [modelUrl]);
|
|
5120
5208
|
const xrStore = useMemo12(() => {
|
|
5121
5209
|
if (!enableXR) return null;
|
|
5122
5210
|
const xr = navigator.xr;
|
|
@@ -6444,7 +6532,8 @@ function ModelViewer({
|
|
|
6444
6532
|
sceneConfig: resolvedSceneConfig,
|
|
6445
6533
|
zoomOnSelected,
|
|
6446
6534
|
highlightOnHover,
|
|
6447
|
-
decoderConfig
|
|
6535
|
+
decoderConfig,
|
|
6536
|
+
explodeFactor
|
|
6448
6537
|
}
|
|
6449
6538
|
),
|
|
6450
6539
|
/* @__PURE__ */ jsx19(
|
|
@@ -6539,6 +6628,43 @@ function ModelViewer({
|
|
|
6539
6628
|
),
|
|
6540
6629
|
measureMode && /* @__PURE__ */ jsx19("span", { className: "ri-measureHint", children: "Click points on the model to measure" })
|
|
6541
6630
|
] }),
|
|
6631
|
+
showExplodeControls && /* @__PURE__ */ jsxs15(
|
|
6632
|
+
"div",
|
|
6633
|
+
{
|
|
6634
|
+
className: `ri-explodeTools ${showMeasureTools ? "ri-explodeTools--stacked" : ""}`,
|
|
6635
|
+
children: [
|
|
6636
|
+
/* @__PURE__ */ jsx19(
|
|
6637
|
+
"button",
|
|
6638
|
+
{
|
|
6639
|
+
type: "button",
|
|
6640
|
+
className: `ri-measureButton ${explodeOpen || explodeFactor > 0 ? "ri-measureButton--active" : ""}`,
|
|
6641
|
+
onClick: () => setExplodeOpen((prev) => !prev),
|
|
6642
|
+
title: "Exploded view",
|
|
6643
|
+
"aria-pressed": explodeOpen,
|
|
6644
|
+
children: /* @__PURE__ */ jsx19(Expand, { size: 16 })
|
|
6645
|
+
}
|
|
6646
|
+
),
|
|
6647
|
+
explodeOpen && /* @__PURE__ */ jsx19(
|
|
6648
|
+
"div",
|
|
6649
|
+
{
|
|
6650
|
+
className: "ri-explodeSlider",
|
|
6651
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
6652
|
+
children: /* @__PURE__ */ jsx19(
|
|
6653
|
+
"input",
|
|
6654
|
+
{
|
|
6655
|
+
type: "range",
|
|
6656
|
+
min: 0,
|
|
6657
|
+
max: 100,
|
|
6658
|
+
value: Math.round(explodeFactor * 100),
|
|
6659
|
+
onChange: (e) => setExplodeFactor(Number(e.target.value) / 100),
|
|
6660
|
+
"aria-label": "Explode amount"
|
|
6661
|
+
}
|
|
6662
|
+
)
|
|
6663
|
+
}
|
|
6664
|
+
)
|
|
6665
|
+
]
|
|
6666
|
+
}
|
|
6667
|
+
),
|
|
6542
6668
|
enableXR && (xrStore && (arSupported || vrSupported) || quickLookSupported) && /* @__PURE__ */ jsxs15("div", { className: "ri-xrTools", children: [
|
|
6543
6669
|
!arSupported && quickLookSupported && /* @__PURE__ */ jsxs15(
|
|
6544
6670
|
"button",
|
|
@@ -14578,7 +14704,8 @@ function BindingBuilder({ licenseKey }) {
|
|
|
14578
14704
|
disableZoom: false,
|
|
14579
14705
|
zoomOnSelected,
|
|
14580
14706
|
highlightOnHover,
|
|
14581
|
-
panelHoveredObject: highlightOnHover ? panelHoveredBinding : null
|
|
14707
|
+
panelHoveredObject: highlightOnHover ? panelHoveredBinding : null,
|
|
14708
|
+
showExplodeControls: true
|
|
14582
14709
|
}
|
|
14583
14710
|
) : /* @__PURE__ */ jsx44(
|
|
14584
14711
|
"div",
|
|
@@ -14678,7 +14805,7 @@ import {
|
|
|
14678
14805
|
Check,
|
|
14679
14806
|
Settings,
|
|
14680
14807
|
ChevronDown as ChevronDown6,
|
|
14681
|
-
Boxes as
|
|
14808
|
+
Boxes as Boxes3,
|
|
14682
14809
|
Trash2 as Trash24,
|
|
14683
14810
|
Upload as Upload2
|
|
14684
14811
|
} from "lucide-react";
|
|
@@ -15220,7 +15347,7 @@ function SceneObjectsPanel2({
|
|
|
15220
15347
|
{
|
|
15221
15348
|
className: `right-panel-wrapper ${isOpen ? "right-panel-wrapper--open" : "right-panel-wrapper--closed"}`,
|
|
15222
15349
|
children: [
|
|
15223
|
-
!isOpen && /* @__PURE__ */ jsx45("button", { className: "side-panel__open-btn", onClick: () => onToggle(true), children: /* @__PURE__ */ jsx45(
|
|
15350
|
+
!isOpen && /* @__PURE__ */ jsx45("button", { className: "side-panel__open-btn", onClick: () => onToggle(true), children: /* @__PURE__ */ jsx45(Boxes3, { size: 20, strokeWidth: 1.8 }) }),
|
|
15224
15351
|
/* @__PURE__ */ jsxs40(
|
|
15225
15352
|
"div",
|
|
15226
15353
|
{
|
|
@@ -15991,6 +16118,150 @@ function useViewerActions(objectBindings, options = {}) {
|
|
|
15991
16118
|
runAction
|
|
15992
16119
|
};
|
|
15993
16120
|
}
|
|
16121
|
+
|
|
16122
|
+
// src/hooks/useShareableViewerState.ts
|
|
16123
|
+
import { useCallback as useCallback15, useEffect as useEffect20, useRef as useRef18, useState as useState29 } from "react";
|
|
16124
|
+
function serializeState(cameraState, objectBindings, selectedObjectBinding) {
|
|
16125
|
+
const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
|
|
16126
|
+
return {
|
|
16127
|
+
camera: cameraState ?? void 0,
|
|
16128
|
+
hidden,
|
|
16129
|
+
selected: selectedObjectBinding?.id ?? null
|
|
16130
|
+
};
|
|
16131
|
+
}
|
|
16132
|
+
function useShareableViewerState({
|
|
16133
|
+
cameraState,
|
|
16134
|
+
setCameraState,
|
|
16135
|
+
objectBindings,
|
|
16136
|
+
onObjectBindingsChange,
|
|
16137
|
+
selectedObjectBinding,
|
|
16138
|
+
setSelectedObjectBinding,
|
|
16139
|
+
paramKey = "view",
|
|
16140
|
+
debounceMs = 400,
|
|
16141
|
+
autoSync = true
|
|
16142
|
+
}) {
|
|
16143
|
+
const lastWrittenRawRef = useRef18(null);
|
|
16144
|
+
const buildHash = useCallback15(() => {
|
|
16145
|
+
const state = serializeState(
|
|
16146
|
+
cameraState,
|
|
16147
|
+
objectBindings,
|
|
16148
|
+
selectedObjectBinding
|
|
16149
|
+
);
|
|
16150
|
+
return `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
|
|
16151
|
+
}, [cameraState, objectBindings, selectedObjectBinding, paramKey]);
|
|
16152
|
+
const getShareUrl = useCallback15(() => {
|
|
16153
|
+
const url = new URL(window.location.href);
|
|
16154
|
+
url.hash = buildHash();
|
|
16155
|
+
return url.toString();
|
|
16156
|
+
}, [buildHash]);
|
|
16157
|
+
const syncUrl = useCallback15(() => {
|
|
16158
|
+
const url = new URL(window.location.href);
|
|
16159
|
+
url.hash = buildHash();
|
|
16160
|
+
window.history.replaceState(null, "", url.toString());
|
|
16161
|
+
lastWrittenRawRef.current = new URLSearchParams(url.hash.slice(1)).get(
|
|
16162
|
+
paramKey
|
|
16163
|
+
);
|
|
16164
|
+
}, [buildHash, paramKey]);
|
|
16165
|
+
const latestStateRef = useRef18({
|
|
16166
|
+
cameraState,
|
|
16167
|
+
objectBindings,
|
|
16168
|
+
selectedObjectBinding
|
|
16169
|
+
});
|
|
16170
|
+
latestStateRef.current = {
|
|
16171
|
+
cameraState,
|
|
16172
|
+
objectBindings,
|
|
16173
|
+
selectedObjectBinding
|
|
16174
|
+
};
|
|
16175
|
+
const [restoreAttempted, setRestoreAttempted] = useState29(false);
|
|
16176
|
+
const lastSyncedHashRef = useRef18(null);
|
|
16177
|
+
useEffect20(() => {
|
|
16178
|
+
if (!autoSync || !restoreAttempted) return;
|
|
16179
|
+
const tick = () => {
|
|
16180
|
+
const current = latestStateRef.current;
|
|
16181
|
+
const state = serializeState(
|
|
16182
|
+
current.cameraState,
|
|
16183
|
+
current.objectBindings,
|
|
16184
|
+
current.selectedObjectBinding
|
|
16185
|
+
);
|
|
16186
|
+
const hash = `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
|
|
16187
|
+
if (hash === lastSyncedHashRef.current) return;
|
|
16188
|
+
lastSyncedHashRef.current = hash;
|
|
16189
|
+
const url = new URL(window.location.href);
|
|
16190
|
+
url.hash = hash;
|
|
16191
|
+
window.history.replaceState(null, "", url.toString());
|
|
16192
|
+
lastWrittenRawRef.current = new URLSearchParams(url.hash.slice(1)).get(
|
|
16193
|
+
paramKey
|
|
16194
|
+
);
|
|
16195
|
+
};
|
|
16196
|
+
const id = window.setInterval(tick, debounceMs);
|
|
16197
|
+
return () => window.clearInterval(id);
|
|
16198
|
+
}, [autoSync, restoreAttempted, debounceMs, paramKey]);
|
|
16199
|
+
const lastRestoredRawRef = useRef18(null);
|
|
16200
|
+
const restoringRawRef = useRef18(null);
|
|
16201
|
+
const restoreFromUrl = useCallback15(async () => {
|
|
16202
|
+
setRestoreAttempted(true);
|
|
16203
|
+
const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
16204
|
+
const raw = new URLSearchParams(rawHash).get(paramKey);
|
|
16205
|
+
if (!raw) {
|
|
16206
|
+
lastRestoredRawRef.current = null;
|
|
16207
|
+
return false;
|
|
16208
|
+
}
|
|
16209
|
+
if (raw === lastWrittenRawRef.current || raw === lastRestoredRawRef.current || raw === restoringRawRef.current) {
|
|
16210
|
+
return false;
|
|
16211
|
+
}
|
|
16212
|
+
let state;
|
|
16213
|
+
try {
|
|
16214
|
+
state = JSON.parse(raw);
|
|
16215
|
+
} catch {
|
|
16216
|
+
return false;
|
|
16217
|
+
}
|
|
16218
|
+
restoringRawRef.current = raw;
|
|
16219
|
+
try {
|
|
16220
|
+
if (state.camera) {
|
|
16221
|
+
await setCameraState(state.camera, false);
|
|
16222
|
+
}
|
|
16223
|
+
if (state.hidden) {
|
|
16224
|
+
const hiddenIds = new Set(state.hidden);
|
|
16225
|
+
onObjectBindingsChange(
|
|
16226
|
+
Object.fromEntries(
|
|
16227
|
+
Object.entries(objectBindings).map(([bindingKey, binding]) => [
|
|
16228
|
+
bindingKey,
|
|
16229
|
+
{ ...binding, visible: !hiddenIds.has(binding.id) }
|
|
16230
|
+
])
|
|
16231
|
+
)
|
|
16232
|
+
);
|
|
16233
|
+
}
|
|
16234
|
+
if (state.selected !== void 0) {
|
|
16235
|
+
const bindingKey = state.selected ? resolveBindingKey(objectBindings, state.selected) : null;
|
|
16236
|
+
setSelectedObjectBinding(
|
|
16237
|
+
bindingKey ? objectBindings[bindingKey] : null
|
|
16238
|
+
);
|
|
16239
|
+
}
|
|
16240
|
+
lastRestoredRawRef.current = raw;
|
|
16241
|
+
return true;
|
|
16242
|
+
} finally {
|
|
16243
|
+
if (restoringRawRef.current === raw) {
|
|
16244
|
+
restoringRawRef.current = null;
|
|
16245
|
+
}
|
|
16246
|
+
}
|
|
16247
|
+
}, [
|
|
16248
|
+
objectBindings,
|
|
16249
|
+
onObjectBindingsChange,
|
|
16250
|
+
paramKey,
|
|
16251
|
+
setCameraState,
|
|
16252
|
+
setSelectedObjectBinding
|
|
16253
|
+
]);
|
|
16254
|
+
useEffect20(() => {
|
|
16255
|
+
const handleHashChange = () => {
|
|
16256
|
+
void restoreFromUrl();
|
|
16257
|
+
};
|
|
16258
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
16259
|
+
return () => {
|
|
16260
|
+
window.removeEventListener("hashchange", handleHashChange);
|
|
16261
|
+
};
|
|
16262
|
+
}, [restoreFromUrl]);
|
|
16263
|
+
return { getShareUrl, syncUrl, restoreFromUrl };
|
|
16264
|
+
}
|
|
15994
16265
|
export {
|
|
15995
16266
|
BindingBuilder,
|
|
15996
16267
|
MATERIAL_BLENDING_MODES,
|
|
@@ -16000,6 +16271,7 @@ export {
|
|
|
16000
16271
|
useObjectBinding,
|
|
16001
16272
|
useObjectBindingIds,
|
|
16002
16273
|
useObjectVisibility,
|
|
16274
|
+
useShareableViewerState,
|
|
16003
16275
|
useViewerActions,
|
|
16004
16276
|
useViewerAnimations,
|
|
16005
16277
|
useViewerCamera,
|