@liveroom-tech/react-immersive 1.0.1 → 1.0.2

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 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
@@ -669,4 +669,22 @@ type ViewerActionsProps = {
669
669
  };
670
670
  declare function useViewerActions(objectBindings: Record<string, ObjectBinding>, options?: ViewerActionsOptions): ViewerActionsProps;
671
671
 
672
- 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, useViewerActions, useViewerAnimations, useViewerCamera, useViewerHover, useViewerModel, useViewerSelection };
672
+ type UseShareableViewerStateOptions = {
673
+ cameraState: ViewerCameraState | null;
674
+ setCameraState: (state: ViewerCameraState, enableTransition?: boolean) => Promise<boolean>;
675
+ objectBindings: Record<string, ObjectBinding>;
676
+ onObjectBindingsChange: (next: Record<string, ObjectBinding>) => void;
677
+ selectedObjectBinding: ObjectBinding | null;
678
+ setSelectedObjectBinding: (binding: ObjectBinding | null) => void;
679
+ paramKey?: string;
680
+ debounceMs?: number;
681
+ autoSync?: boolean;
682
+ };
683
+ type ShareableViewerStateProps = {
684
+ getShareUrl: () => string;
685
+ syncUrl: () => void;
686
+ restoreFromUrl: () => Promise<boolean>;
687
+ };
688
+ declare function useShareableViewerState({ cameraState, setCameraState, objectBindings, onObjectBindingsChange, selectedObjectBinding, setSelectedObjectBinding, paramKey, debounceMs, autoSync, }: UseShareableViewerStateOptions): ShareableViewerStateProps;
689
+
690
+ 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
@@ -669,4 +669,22 @@ type ViewerActionsProps = {
669
669
  };
670
670
  declare function useViewerActions(objectBindings: Record<string, ObjectBinding>, options?: ViewerActionsOptions): ViewerActionsProps;
671
671
 
672
- 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, useViewerActions, useViewerAnimations, useViewerCamera, useViewerHover, useViewerModel, useViewerSelection };
672
+ type UseShareableViewerStateOptions = {
673
+ cameraState: ViewerCameraState | null;
674
+ setCameraState: (state: ViewerCameraState, enableTransition?: boolean) => Promise<boolean>;
675
+ objectBindings: Record<string, ObjectBinding>;
676
+ onObjectBindingsChange: (next: Record<string, ObjectBinding>) => void;
677
+ selectedObjectBinding: ObjectBinding | null;
678
+ setSelectedObjectBinding: (binding: ObjectBinding | null) => void;
679
+ paramKey?: string;
680
+ debounceMs?: number;
681
+ autoSync?: boolean;
682
+ };
683
+ type ShareableViewerStateProps = {
684
+ getShareUrl: () => string;
685
+ syncUrl: () => void;
686
+ restoreFromUrl: () => Promise<boolean>;
687
+ };
688
+ declare function useShareableViewerState({ cameraState, setCameraState, objectBindings, onObjectBindingsChange, selectedObjectBinding, setSelectedObjectBinding, paramKey, debounceMs, autoSync, }: UseShareableViewerStateOptions): ShareableViewerStateProps;
689
+
690
+ 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,
@@ -15895,6 +15896,150 @@ function useViewerActions(objectBindings, options = {}) {
15895
15896
  runAction
15896
15897
  };
15897
15898
  }
15899
+
15900
+ // src/hooks/useShareableViewerState.ts
15901
+ var import_react46 = require("react");
15902
+ function serializeState(cameraState, objectBindings, selectedObjectBinding) {
15903
+ const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
15904
+ return {
15905
+ camera: cameraState ?? void 0,
15906
+ hidden,
15907
+ selected: selectedObjectBinding?.id ?? null
15908
+ };
15909
+ }
15910
+ function useShareableViewerState({
15911
+ cameraState,
15912
+ setCameraState,
15913
+ objectBindings,
15914
+ onObjectBindingsChange,
15915
+ selectedObjectBinding,
15916
+ setSelectedObjectBinding,
15917
+ paramKey = "view",
15918
+ debounceMs = 400,
15919
+ autoSync = true
15920
+ }) {
15921
+ const lastWrittenRawRef = (0, import_react46.useRef)(null);
15922
+ const buildHash = (0, import_react46.useCallback)(() => {
15923
+ const state = serializeState(
15924
+ cameraState,
15925
+ objectBindings,
15926
+ selectedObjectBinding
15927
+ );
15928
+ return `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
15929
+ }, [cameraState, objectBindings, selectedObjectBinding, paramKey]);
15930
+ const getShareUrl = (0, import_react46.useCallback)(() => {
15931
+ const url = new URL(window.location.href);
15932
+ url.hash = buildHash();
15933
+ return url.toString();
15934
+ }, [buildHash]);
15935
+ const syncUrl = (0, import_react46.useCallback)(() => {
15936
+ const url = new URL(window.location.href);
15937
+ url.hash = buildHash();
15938
+ window.history.replaceState(null, "", url.toString());
15939
+ lastWrittenRawRef.current = new URLSearchParams(url.hash.slice(1)).get(
15940
+ paramKey
15941
+ );
15942
+ }, [buildHash, paramKey]);
15943
+ const latestStateRef = (0, import_react46.useRef)({
15944
+ cameraState,
15945
+ objectBindings,
15946
+ selectedObjectBinding
15947
+ });
15948
+ latestStateRef.current = {
15949
+ cameraState,
15950
+ objectBindings,
15951
+ selectedObjectBinding
15952
+ };
15953
+ const [restoreAttempted, setRestoreAttempted] = (0, import_react46.useState)(false);
15954
+ const lastSyncedHashRef = (0, import_react46.useRef)(null);
15955
+ (0, import_react46.useEffect)(() => {
15956
+ if (!autoSync || !restoreAttempted) return;
15957
+ const tick = () => {
15958
+ const current = latestStateRef.current;
15959
+ const state = serializeState(
15960
+ current.cameraState,
15961
+ current.objectBindings,
15962
+ current.selectedObjectBinding
15963
+ );
15964
+ const hash = `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
15965
+ if (hash === lastSyncedHashRef.current) return;
15966
+ lastSyncedHashRef.current = hash;
15967
+ const url = new URL(window.location.href);
15968
+ url.hash = hash;
15969
+ window.history.replaceState(null, "", url.toString());
15970
+ lastWrittenRawRef.current = new URLSearchParams(url.hash.slice(1)).get(
15971
+ paramKey
15972
+ );
15973
+ };
15974
+ const id = window.setInterval(tick, debounceMs);
15975
+ return () => window.clearInterval(id);
15976
+ }, [autoSync, restoreAttempted, debounceMs, paramKey]);
15977
+ const lastRestoredRawRef = (0, import_react46.useRef)(null);
15978
+ const restoringRawRef = (0, import_react46.useRef)(null);
15979
+ const restoreFromUrl = (0, import_react46.useCallback)(async () => {
15980
+ setRestoreAttempted(true);
15981
+ const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
15982
+ const raw = new URLSearchParams(rawHash).get(paramKey);
15983
+ if (!raw) {
15984
+ lastRestoredRawRef.current = null;
15985
+ return false;
15986
+ }
15987
+ if (raw === lastWrittenRawRef.current || raw === lastRestoredRawRef.current || raw === restoringRawRef.current) {
15988
+ return false;
15989
+ }
15990
+ let state;
15991
+ try {
15992
+ state = JSON.parse(raw);
15993
+ } catch {
15994
+ return false;
15995
+ }
15996
+ restoringRawRef.current = raw;
15997
+ try {
15998
+ if (state.camera) {
15999
+ await setCameraState(state.camera, false);
16000
+ }
16001
+ if (state.hidden) {
16002
+ const hiddenIds = new Set(state.hidden);
16003
+ onObjectBindingsChange(
16004
+ Object.fromEntries(
16005
+ Object.entries(objectBindings).map(([bindingKey, binding]) => [
16006
+ bindingKey,
16007
+ { ...binding, visible: !hiddenIds.has(binding.id) }
16008
+ ])
16009
+ )
16010
+ );
16011
+ }
16012
+ if (state.selected !== void 0) {
16013
+ const bindingKey = state.selected ? resolveBindingKey(objectBindings, state.selected) : null;
16014
+ setSelectedObjectBinding(
16015
+ bindingKey ? objectBindings[bindingKey] : null
16016
+ );
16017
+ }
16018
+ lastRestoredRawRef.current = raw;
16019
+ return true;
16020
+ } finally {
16021
+ if (restoringRawRef.current === raw) {
16022
+ restoringRawRef.current = null;
16023
+ }
16024
+ }
16025
+ }, [
16026
+ objectBindings,
16027
+ onObjectBindingsChange,
16028
+ paramKey,
16029
+ setCameraState,
16030
+ setSelectedObjectBinding
16031
+ ]);
16032
+ (0, import_react46.useEffect)(() => {
16033
+ const handleHashChange = () => {
16034
+ void restoreFromUrl();
16035
+ };
16036
+ window.addEventListener("hashchange", handleHashChange);
16037
+ return () => {
16038
+ window.removeEventListener("hashchange", handleHashChange);
16039
+ };
16040
+ }, [restoreFromUrl]);
16041
+ return { getShareUrl, syncUrl, restoreFromUrl };
16042
+ }
15898
16043
  // Annotate the CommonJS export names for ESM import in node:
15899
16044
  0 && (module.exports = {
15900
16045
  BindingBuilder,
@@ -15905,6 +16050,7 @@ function useViewerActions(objectBindings, options = {}) {
15905
16050
  useObjectBinding,
15906
16051
  useObjectBindingIds,
15907
16052
  useObjectVisibility,
16053
+ useShareableViewerState,
15908
16054
  useViewerActions,
15909
16055
  useViewerAnimations,
15910
16056
  useViewerCamera,
package/dist/index.mjs CHANGED
@@ -15991,6 +15991,150 @@ function useViewerActions(objectBindings, options = {}) {
15991
15991
  runAction
15992
15992
  };
15993
15993
  }
15994
+
15995
+ // src/hooks/useShareableViewerState.ts
15996
+ import { useCallback as useCallback15, useEffect as useEffect20, useRef as useRef18, useState as useState29 } from "react";
15997
+ function serializeState(cameraState, objectBindings, selectedObjectBinding) {
15998
+ const hidden = Object.values(objectBindings).filter((binding) => binding.visible === false).map((binding) => binding.id);
15999
+ return {
16000
+ camera: cameraState ?? void 0,
16001
+ hidden,
16002
+ selected: selectedObjectBinding?.id ?? null
16003
+ };
16004
+ }
16005
+ function useShareableViewerState({
16006
+ cameraState,
16007
+ setCameraState,
16008
+ objectBindings,
16009
+ onObjectBindingsChange,
16010
+ selectedObjectBinding,
16011
+ setSelectedObjectBinding,
16012
+ paramKey = "view",
16013
+ debounceMs = 400,
16014
+ autoSync = true
16015
+ }) {
16016
+ const lastWrittenRawRef = useRef18(null);
16017
+ const buildHash = useCallback15(() => {
16018
+ const state = serializeState(
16019
+ cameraState,
16020
+ objectBindings,
16021
+ selectedObjectBinding
16022
+ );
16023
+ return `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
16024
+ }, [cameraState, objectBindings, selectedObjectBinding, paramKey]);
16025
+ const getShareUrl = useCallback15(() => {
16026
+ const url = new URL(window.location.href);
16027
+ url.hash = buildHash();
16028
+ return url.toString();
16029
+ }, [buildHash]);
16030
+ const syncUrl = useCallback15(() => {
16031
+ const url = new URL(window.location.href);
16032
+ url.hash = buildHash();
16033
+ window.history.replaceState(null, "", url.toString());
16034
+ lastWrittenRawRef.current = new URLSearchParams(url.hash.slice(1)).get(
16035
+ paramKey
16036
+ );
16037
+ }, [buildHash, paramKey]);
16038
+ const latestStateRef = useRef18({
16039
+ cameraState,
16040
+ objectBindings,
16041
+ selectedObjectBinding
16042
+ });
16043
+ latestStateRef.current = {
16044
+ cameraState,
16045
+ objectBindings,
16046
+ selectedObjectBinding
16047
+ };
16048
+ const [restoreAttempted, setRestoreAttempted] = useState29(false);
16049
+ const lastSyncedHashRef = useRef18(null);
16050
+ useEffect20(() => {
16051
+ if (!autoSync || !restoreAttempted) return;
16052
+ const tick = () => {
16053
+ const current = latestStateRef.current;
16054
+ const state = serializeState(
16055
+ current.cameraState,
16056
+ current.objectBindings,
16057
+ current.selectedObjectBinding
16058
+ );
16059
+ const hash = `${paramKey}=${encodeURIComponent(JSON.stringify(state))}`;
16060
+ if (hash === lastSyncedHashRef.current) return;
16061
+ lastSyncedHashRef.current = hash;
16062
+ const url = new URL(window.location.href);
16063
+ url.hash = hash;
16064
+ window.history.replaceState(null, "", url.toString());
16065
+ lastWrittenRawRef.current = new URLSearchParams(url.hash.slice(1)).get(
16066
+ paramKey
16067
+ );
16068
+ };
16069
+ const id = window.setInterval(tick, debounceMs);
16070
+ return () => window.clearInterval(id);
16071
+ }, [autoSync, restoreAttempted, debounceMs, paramKey]);
16072
+ const lastRestoredRawRef = useRef18(null);
16073
+ const restoringRawRef = useRef18(null);
16074
+ const restoreFromUrl = useCallback15(async () => {
16075
+ setRestoreAttempted(true);
16076
+ const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
16077
+ const raw = new URLSearchParams(rawHash).get(paramKey);
16078
+ if (!raw) {
16079
+ lastRestoredRawRef.current = null;
16080
+ return false;
16081
+ }
16082
+ if (raw === lastWrittenRawRef.current || raw === lastRestoredRawRef.current || raw === restoringRawRef.current) {
16083
+ return false;
16084
+ }
16085
+ let state;
16086
+ try {
16087
+ state = JSON.parse(raw);
16088
+ } catch {
16089
+ return false;
16090
+ }
16091
+ restoringRawRef.current = raw;
16092
+ try {
16093
+ if (state.camera) {
16094
+ await setCameraState(state.camera, false);
16095
+ }
16096
+ if (state.hidden) {
16097
+ const hiddenIds = new Set(state.hidden);
16098
+ onObjectBindingsChange(
16099
+ Object.fromEntries(
16100
+ Object.entries(objectBindings).map(([bindingKey, binding]) => [
16101
+ bindingKey,
16102
+ { ...binding, visible: !hiddenIds.has(binding.id) }
16103
+ ])
16104
+ )
16105
+ );
16106
+ }
16107
+ if (state.selected !== void 0) {
16108
+ const bindingKey = state.selected ? resolveBindingKey(objectBindings, state.selected) : null;
16109
+ setSelectedObjectBinding(
16110
+ bindingKey ? objectBindings[bindingKey] : null
16111
+ );
16112
+ }
16113
+ lastRestoredRawRef.current = raw;
16114
+ return true;
16115
+ } finally {
16116
+ if (restoringRawRef.current === raw) {
16117
+ restoringRawRef.current = null;
16118
+ }
16119
+ }
16120
+ }, [
16121
+ objectBindings,
16122
+ onObjectBindingsChange,
16123
+ paramKey,
16124
+ setCameraState,
16125
+ setSelectedObjectBinding
16126
+ ]);
16127
+ useEffect20(() => {
16128
+ const handleHashChange = () => {
16129
+ void restoreFromUrl();
16130
+ };
16131
+ window.addEventListener("hashchange", handleHashChange);
16132
+ return () => {
16133
+ window.removeEventListener("hashchange", handleHashChange);
16134
+ };
16135
+ }, [restoreFromUrl]);
16136
+ return { getShareUrl, syncUrl, restoreFromUrl };
16137
+ }
15994
16138
  export {
15995
16139
  BindingBuilder,
15996
16140
  MATERIAL_BLENDING_MODES,
@@ -16000,6 +16144,7 @@ export {
16000
16144
  useObjectBinding,
16001
16145
  useObjectBindingIds,
16002
16146
  useObjectVisibility,
16147
+ useShareableViewerState,
16003
16148
  useViewerActions,
16004
16149
  useViewerAnimations,
16005
16150
  useViewerCamera,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveroom-tech/react-immersive",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",