@3driseai/3drise-viewer 0.1.25 → 0.1.26

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.
Files changed (116) hide show
  1. package/dist/animationsV2/ClipPlayer.d.ts +13 -0
  2. package/dist/animationsV2/ClipPlayer.d.ts.map +1 -0
  3. package/dist/animationsV2/ClipPlayer.js +140 -0
  4. package/dist/animationsV2/ClipsRuntime.d.ts +111 -0
  5. package/dist/animationsV2/ClipsRuntime.d.ts.map +1 -0
  6. package/dist/animationsV2/ClipsRuntime.js +476 -0
  7. package/dist/animationsV2/appliers.d.ts +16 -0
  8. package/dist/animationsV2/appliers.d.ts.map +1 -0
  9. package/dist/animationsV2/appliers.js +204 -0
  10. package/dist/animationsV2/clipUtils.d.ts +33 -0
  11. package/dist/animationsV2/clipUtils.d.ts.map +1 -0
  12. package/dist/animationsV2/clipUtils.js +164 -0
  13. package/dist/animationsV2/elementSettings.d.ts +15 -0
  14. package/dist/animationsV2/elementSettings.d.ts.map +1 -0
  15. package/dist/animationsV2/elementSettings.js +16 -0
  16. package/dist/animationsV2/index.d.ts +16 -0
  17. package/dist/animationsV2/index.d.ts.map +1 -0
  18. package/dist/animationsV2/index.js +12 -0
  19. package/dist/animationsV2/nodeTracks.d.ts +13 -0
  20. package/dist/animationsV2/nodeTracks.d.ts.map +1 -0
  21. package/dist/animationsV2/nodeTracks.js +165 -0
  22. package/dist/animationsV2/normalize.d.ts +4 -0
  23. package/dist/animationsV2/normalize.d.ts.map +1 -0
  24. package/dist/animationsV2/normalize.js +49 -0
  25. package/dist/animationsV2/sequenceUtils.d.ts +57 -0
  26. package/dist/animationsV2/sequenceUtils.d.ts.map +1 -0
  27. package/dist/animationsV2/sequenceUtils.js +120 -0
  28. package/dist/animationsV2/types.d.ts +95 -0
  29. package/dist/animationsV2/types.d.ts.map +1 -0
  30. package/dist/animationsV2/types.js +8 -0
  31. package/dist/api/sceneHandle.d.ts +56 -17
  32. package/dist/api/sceneHandle.d.ts.map +1 -1
  33. package/dist/api/sceneHandle.js +154 -77
  34. package/dist/components/CameraController.d.ts.map +1 -1
  35. package/dist/components/CameraController.js +0 -44
  36. package/dist/components/Clouds.d.ts.map +1 -1
  37. package/dist/components/Clouds.js +0 -34
  38. package/dist/components/CreatedObject.d.ts.map +1 -1
  39. package/dist/components/CreatedObject.js +19 -1
  40. package/dist/components/EnvironmentManager.js +1 -1
  41. package/dist/components/ObjectsRenderer.d.ts +4 -1
  42. package/dist/components/ObjectsRenderer.d.ts.map +1 -1
  43. package/dist/components/ObjectsRenderer.js +23 -3
  44. package/dist/components/Ocean.d.ts.map +1 -1
  45. package/dist/components/Ocean.js +0 -63
  46. package/dist/components/SceneBuilder.d.ts.map +1 -1
  47. package/dist/components/SceneBuilder.js +19 -4
  48. package/dist/components/SkyController.d.ts.map +1 -1
  49. package/dist/components/SkyController.js +0 -39
  50. package/dist/components/Stars.d.ts.map +1 -1
  51. package/dist/components/Stars.js +3 -47
  52. package/dist/components/terrain/TerrainSurface.d.ts.map +1 -1
  53. package/dist/components/terrain/TerrainSurface.js +0 -5
  54. package/dist/components/volumetricClouds/VolumetricCloudsPass.d.ts.map +1 -1
  55. package/dist/components/volumetricClouds/VolumetricCloudsPass.js +0 -6
  56. package/dist/hooks/index.d.ts +0 -2
  57. package/dist/hooks/index.d.ts.map +1 -1
  58. package/dist/hooks/index.js +0 -2
  59. package/dist/hooks/useInteraction.d.ts +6 -7
  60. package/dist/hooks/useInteraction.d.ts.map +1 -1
  61. package/dist/hooks/useInteraction.js +87 -268
  62. package/dist/hooks/useObjectAnimation.d.ts.map +1 -1
  63. package/dist/hooks/useObjectAnimation.js +0 -2
  64. package/dist/hooks/useOceanMaterial.d.ts.map +1 -1
  65. package/dist/hooks/useOceanMaterial.js +0 -5
  66. package/dist/hooks/useRuntimeSettings.d.ts +1 -1
  67. package/dist/hooks/useRuntimeSettings.js +1 -1
  68. package/dist/index.d.ts +5 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +3 -0
  71. package/dist/interactions/index.d.ts +3 -0
  72. package/dist/interactions/index.d.ts.map +1 -0
  73. package/dist/interactions/index.js +1 -0
  74. package/dist/interactions/runner.d.ts +9 -0
  75. package/dist/interactions/runner.d.ts.map +1 -0
  76. package/dist/interactions/runner.js +23 -0
  77. package/dist/operations/catalogue/displayImage.d.ts +3 -0
  78. package/dist/operations/catalogue/displayImage.d.ts.map +1 -0
  79. package/dist/operations/catalogue/displayImage.js +39 -0
  80. package/dist/operations/catalogue/fade.d.ts +3 -0
  81. package/dist/operations/catalogue/fade.d.ts.map +1 -0
  82. package/dist/operations/catalogue/fade.js +44 -0
  83. package/dist/operations/catalogue/focus.d.ts +3 -0
  84. package/dist/operations/catalogue/focus.d.ts.map +1 -0
  85. package/dist/operations/catalogue/focus.js +22 -0
  86. package/dist/operations/catalogue/glow.d.ts +3 -0
  87. package/dist/operations/catalogue/glow.d.ts.map +1 -0
  88. package/dist/operations/catalogue/glow.js +49 -0
  89. package/dist/operations/catalogue/materialFx.d.ts +6 -0
  90. package/dist/operations/catalogue/materialFx.d.ts.map +1 -0
  91. package/dist/operations/catalogue/materialFx.js +13 -0
  92. package/dist/operations/catalogue/moveToFront.d.ts +3 -0
  93. package/dist/operations/catalogue/moveToFront.d.ts.map +1 -0
  94. package/dist/operations/catalogue/moveToFront.js +25 -0
  95. package/dist/operations/catalogue/visibility.d.ts +3 -0
  96. package/dist/operations/catalogue/visibility.d.ts.map +1 -0
  97. package/dist/operations/catalogue/visibility.js +24 -0
  98. package/dist/operations/catalogue/zoom.d.ts +3 -0
  99. package/dist/operations/catalogue/zoom.d.ts.map +1 -0
  100. package/dist/operations/catalogue/zoom.js +32 -0
  101. package/dist/operations/index.d.ts +5 -0
  102. package/dist/operations/index.d.ts.map +1 -0
  103. package/dist/operations/index.js +4 -0
  104. package/dist/operations/registry.d.ts +11 -0
  105. package/dist/operations/registry.d.ts.map +1 -0
  106. package/dist/operations/registry.js +21 -0
  107. package/dist/operations/runtime.d.ts +22 -0
  108. package/dist/operations/runtime.d.ts.map +1 -0
  109. package/dist/operations/runtime.js +161 -0
  110. package/dist/operations/types.d.ts +96 -0
  111. package/dist/operations/types.d.ts.map +1 -0
  112. package/dist/operations/types.js +3 -0
  113. package/dist/operations/useOperationsRuntime.d.ts +3 -0
  114. package/dist/operations/useOperationsRuntime.d.ts.map +1 -0
  115. package/dist/operations/useOperationsRuntime.js +31 -0
  116. package/package.json +2 -2
@@ -0,0 +1,204 @@
1
+ import { findById, SceneStore, applySingleProperty, applyMaterialPropertyToObject, traverseSkippingEdgesAndEffects, EDGES_GROUP_KEY, } from '@3driseai/3drise-core';
2
+ import { CONFIG_LIVE_KEY } from '../hooks/useConfigLive.js';
3
+ import { CLOUD_LIVE_KEY } from '../hooks/useCloudLive.js';
4
+ import { OCEAN_LIVE_KEY } from '../hooks/useOceanLive.js';
5
+ import { TERRAIN_LIVE_KEY } from '../hooks/useTerrainLive.js';
6
+ import { createSunPosition } from '../utils/utils3d.js';
7
+ import { applyNodeValue, clearNodeCaches } from './nodeTracks.js';
8
+ // --- live-target caches (cleared when the scene changes under us) -------------
9
+ const objectCache = new Map();
10
+ const handleCache = new Map();
11
+ export function clearApplierCaches() {
12
+ objectCache.clear();
13
+ handleCache.clear();
14
+ clearNodeCaches();
15
+ }
16
+ function objectFor(state) {
17
+ const cached = objectCache.get(state);
18
+ if (cached && cached.parent)
19
+ return cached;
20
+ const found = findById(state);
21
+ if (found)
22
+ objectCache.set(state, found);
23
+ else
24
+ objectCache.delete(state);
25
+ return found;
26
+ }
27
+ function configHandleFor(state) {
28
+ const cached = handleCache.get(state);
29
+ if (cached && cached.alive)
30
+ return cached;
31
+ let found = null;
32
+ SceneStore.scene?.traverse((o) => {
33
+ if (found)
34
+ return;
35
+ const h = o.userData?.[CONFIG_LIVE_KEY];
36
+ if (h && h.alive && h.id === state)
37
+ found = h;
38
+ });
39
+ if (found)
40
+ handleCache.set(state, found);
41
+ else
42
+ handleCache.delete(state);
43
+ return found;
44
+ }
45
+ function edgesGroupOf(obj) {
46
+ for (const child of obj.children)
47
+ if (child.userData?.[EDGES_GROUP_KEY])
48
+ return child;
49
+ return null;
50
+ }
51
+ const asVec3 = (v) => Array.isArray(v) && v.length >= 3 ? [Number(v[0]), Number(v[1]), Number(v[2])] : null;
52
+ const applyCamera = (ctx, _state, path, value) => {
53
+ const cam = ctx.camera;
54
+ if (!cam)
55
+ return false;
56
+ if (path === 'position') {
57
+ const v = asVec3(value);
58
+ if (v)
59
+ cam.position.set(...v);
60
+ }
61
+ else if (path === 'fov' && typeof value === 'number') {
62
+ cam.fov = value;
63
+ cam.updateProjectionMatrix();
64
+ }
65
+ else if (path === 'controls.target') {
66
+ const v = asVec3(value);
67
+ if (v && ctx.controls?.target) {
68
+ ctx.controls.target.set(...v);
69
+ ctx.controls.update?.();
70
+ }
71
+ }
72
+ };
73
+ const applyTransform = (_ctx, state, path, value) => {
74
+ const obj = objectFor(state);
75
+ if (!obj)
76
+ return false;
77
+ const prop = path.slice('meshSettings.'.length);
78
+ if (prop === 'visible') {
79
+ obj.visible = !!value;
80
+ return;
81
+ }
82
+ const v = asVec3(value);
83
+ if (!v)
84
+ return false;
85
+ if (prop === 'position')
86
+ obj.position.set(...v);
87
+ else if (prop === 'rotation')
88
+ obj.rotation.set(...v);
89
+ else if (prop === 'scale')
90
+ obj.scale.set(...v);
91
+ };
92
+ const applyMaterial = (_ctx, state, path, value) => {
93
+ const obj = objectFor(state);
94
+ if (!obj)
95
+ return false;
96
+ const key = path.slice('materialSettings.'.length);
97
+ traverseSkippingEdgesAndEffects(obj, true, (node) => {
98
+ if (!node.material)
99
+ return;
100
+ const mats = Array.isArray(node.material) ? node.material : [node.material];
101
+ for (const mat of mats) {
102
+ if (key === 'opacity' && typeof value === 'number' && 'transparent' in mat)
103
+ mat.transparent = value < 1;
104
+ applySingleProperty(mat, key, value);
105
+ }
106
+ });
107
+ };
108
+ const applyEdgesMaterial = (_ctx, state, path, value) => {
109
+ const obj = objectFor(state);
110
+ const edges = obj && edgesGroupOf(obj);
111
+ if (!edges)
112
+ return false;
113
+ applyMaterialPropertyToObject(edges, path.slice('edgesSettings.materialSettings.'.length), value);
114
+ };
115
+ const applyConfig = (_ctx, state, path, value) => {
116
+ const handle = configHandleFor(state);
117
+ if (!handle)
118
+ return false;
119
+ if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {
120
+ handle.set(path.slice('config.'.length), value);
121
+ }
122
+ };
123
+ // --- environment: the same live paths the controllers' sliders use -------------------
124
+ /** Sky: the sky mesh's shader uniforms (what useSkyUniforms.updateUniforms writes). */
125
+ const SKY_UNIFORM_KEYS = new Set(['turbidity', 'rayleigh', 'mieCoefficient', 'mieDirectionalG']);
126
+ const applySky = (_ctx, _state, path, value) => {
127
+ if (typeof value !== 'number')
128
+ return;
129
+ const sky = objectFor('3drise-sky');
130
+ const uniforms = sky?.material?.uniforms;
131
+ if (!uniforms)
132
+ return false;
133
+ if (SKY_UNIFORM_KEYS.has(path)) {
134
+ if (uniforms[path])
135
+ uniforms[path].value = value;
136
+ return;
137
+ }
138
+ if ((path === 'elevation' || path === 'azimuth') && uniforms.sunPosition) {
139
+ // The other angle comes from the current sun, so the two can be keyed separately.
140
+ const { x, y, z } = uniforms.sunPosition.value;
141
+ const r = Math.sqrt(x * x + y * y + z * z) || 1;
142
+ const current = { elevation: 90 - Math.acos(y / r) * (180 / Math.PI), azimuth: Math.atan2(z, x) * (180 / Math.PI) };
143
+ const elevation = path === 'elevation' ? value : current.elevation;
144
+ const azimuth = path === 'azimuth' ? value : current.azimuth;
145
+ uniforms.sunPosition.value.copy(createSunPosition(elevation, azimuth));
146
+ }
147
+ };
148
+ /** Clouds: the layer's live handle — `config.*` is the deck, `cirrus.*` the high layer. */
149
+ const applyClouds = (ctx, _state, path, value) => {
150
+ if (typeof value !== 'number' && typeof value !== 'boolean' && typeof value !== 'string')
151
+ return;
152
+ const id = ctx.getSettings('clouds')?.id;
153
+ const handle = id ? objectFor(id)?.userData?.[CLOUD_LIVE_KEY] : undefined;
154
+ if (!handle)
155
+ return false;
156
+ if (path.startsWith('config.'))
157
+ handle.deck(path.slice('config.'.length), value);
158
+ else if (path.startsWith('cirrus.'))
159
+ handle.cirrus(path.slice('cirrus.'.length), value);
160
+ };
161
+ /** Ocean: the world ocean's live handle — `config.*`. */
162
+ const applyOcean = (ctx, _state, path, value) => {
163
+ if (typeof value !== 'number' && typeof value !== 'boolean' && typeof value !== 'string')
164
+ return;
165
+ const id = ctx.getSettings('ocean')?.id;
166
+ const handle = id ? objectFor(id)?.userData?.[OCEAN_LIVE_KEY] : undefined;
167
+ if (!handle || !path.startsWith('config.'))
168
+ return false;
169
+ handle.config(path.slice('config.'.length), value);
170
+ };
171
+ /** Procedural terrain: its live handle — `config.*`. */
172
+ const applyTerrain = (ctx, _state, path, value) => {
173
+ if (typeof value !== 'number' && typeof value !== 'boolean' && typeof value !== 'string')
174
+ return;
175
+ const id = ctx.getSettings('terrain')?.id;
176
+ const handle = id ? objectFor(id)?.userData?.[TERRAIN_LIVE_KEY] : undefined;
177
+ if (!handle || !path.startsWith('config.'))
178
+ return false;
179
+ handle.config(path.slice('config.'.length), value);
180
+ };
181
+ const APPLIERS = [
182
+ { match: (s) => s === 'camera', apply: applyCamera },
183
+ // Environment before the generic path families: the ocean's `config.*` is not an object's.
184
+ { match: (s) => s === 'sky', apply: applySky },
185
+ { match: (s) => s === 'clouds', apply: applyClouds },
186
+ { match: (s) => s === 'ocean', apply: applyOcean },
187
+ { match: (s) => s === 'terrain', apply: applyTerrain },
188
+ { match: (_s, p) => p.startsWith('meshSettings.'), apply: applyTransform },
189
+ { match: (_s, p) => p.startsWith('materialSettings.'), apply: applyMaterial },
190
+ { match: (_s, p) => p.startsWith('edgesSettings.materialSettings.'), apply: applyEdgesMaterial },
191
+ { match: (_s, p) => p.startsWith('config.'), apply: applyConfig },
192
+ ];
193
+ /** Write one value. Unknown paths are ignored (the track picker never offers them). */
194
+ /** Write one value to the live scene. False when its target is not mounted yet. */
195
+ export function applyValue(ctx, state, path, value, node) {
196
+ if (node)
197
+ return applyNodeValue(state, node, path, value);
198
+ const entry = APPLIERS.find(a => a.match(state, path));
199
+ return entry ? entry.apply(ctx, state, path, value) !== false : false;
200
+ }
201
+ /** Whether a (state, path) has an applier — used by the track picker. */
202
+ export function isApplicable(state, path) {
203
+ return APPLIERS.some(a => a.match(state, path));
204
+ }
@@ -0,0 +1,33 @@
1
+ import type { AnimationClip, EaseName, Keyframe, KeyValue, Track } from './types';
2
+ export declare const uid: (prefix: string) => string;
3
+ /** Read a dotted settings path ('materialSettings.opacity') from a settings object. */
4
+ export declare function getAtPath(obj: any, path: string): any;
5
+ export declare const secondsToFrame: (t: number, fps: number) => number;
6
+ export declare const frameToSeconds: (frame: number, fps: number) => number;
7
+ export declare const snapToFrame: (t: number, fps: number) => number;
8
+ /** 'SS:FF' timecode (minutes folded into seconds past 59 — clips are short). */
9
+ export declare function timecode(t: number, fps: number): string;
10
+ export declare function easeProgress(ease: EaseName | undefined, p: number): number;
11
+ /**
12
+ * The track's value at time t. Before the first key: the first key's value;
13
+ * after the last: the last key's value. Keys must be sorted by t.
14
+ */
15
+ export declare function sampleTrack(track: Track, t: number): KeyValue | undefined;
16
+ export declare const sortKeys: (keys: Keyframe[]) => Keyframe[];
17
+ /** Add a key at t, or replace the value of a key already on that frame. */
18
+ export declare function upsertKey(track: Track, t: number, value: KeyValue, fps: number): Track;
19
+ /** Move keys by dt seconds, clamped to [0, duration], snapped to frames unless `free`. */
20
+ export declare function moveKeys(clip: AnimationClip, selection: Set<string>, dt: number, free: boolean): AnimationClip;
21
+ export declare function deleteKeys(clip: AnimationClip, selection: Set<string>): AnimationClip;
22
+ export declare function setKeysEase(clip: AnimationClip, selection: Set<string>, ease: EaseName): AnimationClip;
23
+ export declare function setKeyValue(clip: AnimationClip, keyId: string, value: KeyValue): AnimationClip;
24
+ export declare function findKey(clip: AnimationClip, keyId: string): {
25
+ track: Track;
26
+ key: Keyframe;
27
+ } | undefined;
28
+ /** All key times of the clip, sorted and unique (for Shift+←/→). */
29
+ export declare function allKeyTimes(clip: AnimationClip): number[];
30
+ export declare function newClip(name: string, key: string): AnimationClip;
31
+ /** Deep copy with fresh ids (duplicate). */
32
+ export declare function cloneClip(clip: AnimationClip, name: string, key: string): AnimationClip;
33
+ //# sourceMappingURL=clipUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipUtils.d.ts","sourceRoot":"","sources":["../../src/animationsV2/clipUtils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAOlF,eAAO,MAAM,GAAG,GAAI,QAAQ,MAAM,WAAqF,CAAC;AAExH,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAOrD;AAMD,eAAO,MAAM,cAAc,GAAI,GAAG,MAAM,EAAE,KAAK,MAAM,WAAwB,CAAC;AAC9E,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,EAAE,KAAK,MAAM,WAAgB,CAAC;AAC1E,eAAO,MAAM,WAAW,GAAI,GAAG,MAAM,EAAE,KAAK,MAAM,WAAgD,CAAC;AAEnG,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAKvD;AAaD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAsBzE;AAMD,eAAO,MAAM,QAAQ,GAAI,MAAM,QAAQ,EAAE,eAAwC,CAAC;AAElF,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CAOtF;AAED,0FAA0F;AAC1F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,aAAa,CAc9G;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAKrF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,GAAG,aAAa,CAQtG;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,aAAa,CAQ9F;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,QAAQ,CAAA;CAAE,GAAG,SAAS,CAMvG;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,EAAE,CAIzD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAEhE;AAED,4CAA4C;AAC5C,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAQvF"}
@@ -0,0 +1,164 @@
1
+ import { applyEasing, interpolateValue } from '@3driseai/3drise-core';
2
+ import { DEFAULT_EASE } from './types.js';
3
+ // -----------------------------------------------------------------------------
4
+ // ids, paths
5
+ // -----------------------------------------------------------------------------
6
+ export const uid = (prefix) => `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
7
+ /** Read a dotted settings path ('materialSettings.opacity') from a settings object. */
8
+ export function getAtPath(obj, path) {
9
+ let cur = obj;
10
+ for (const part of path.split('.')) {
11
+ if (cur == null)
12
+ return undefined;
13
+ cur = cur[part];
14
+ }
15
+ return cur;
16
+ }
17
+ // -----------------------------------------------------------------------------
18
+ // time
19
+ // -----------------------------------------------------------------------------
20
+ export const secondsToFrame = (t, fps) => Math.round(t * fps);
21
+ export const frameToSeconds = (frame, fps) => frame / fps;
22
+ export const snapToFrame = (t, fps) => frameToSeconds(secondsToFrame(t, fps), fps);
23
+ /** 'SS:FF' timecode (minutes folded into seconds past 59 — clips are short). */
24
+ export function timecode(t, fps) {
25
+ const totalFrames = Math.max(0, secondsToFrame(t, fps));
26
+ const s = Math.floor(totalFrames / fps);
27
+ const f = totalFrames - s * fps;
28
+ return `${String(s).padStart(2, '0')}:${String(f).padStart(2, '0')}`;
29
+ }
30
+ // -----------------------------------------------------------------------------
31
+ // sampling
32
+ // -----------------------------------------------------------------------------
33
+ /** Values that do not interpolate (booleans, non-colour strings) step at the next key. */
34
+ function isSteppedValue(v) {
35
+ if (typeof v === 'boolean')
36
+ return true;
37
+ if (typeof v === 'string')
38
+ return !/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(v);
39
+ return false;
40
+ }
41
+ export function easeProgress(ease, p) {
42
+ const name = ease ?? DEFAULT_EASE;
43
+ if (name === 'step')
44
+ return 0;
45
+ if (name === 'linear')
46
+ return p;
47
+ return applyEasing(name, p);
48
+ }
49
+ /**
50
+ * The track's value at time t. Before the first key: the first key's value;
51
+ * after the last: the last key's value. Keys must be sorted by t.
52
+ */
53
+ export function sampleTrack(track, t) {
54
+ const keys = track.keys;
55
+ if (keys.length === 0)
56
+ return undefined;
57
+ if (t <= keys[0].t)
58
+ return keys[0].value;
59
+ const last = keys[keys.length - 1];
60
+ if (t >= last.t)
61
+ return last.value;
62
+ // binary search: last key with k.t <= t
63
+ let lo = 0;
64
+ let hi = keys.length - 1;
65
+ while (hi - lo > 1) {
66
+ const mid = (lo + hi) >> 1;
67
+ if (keys[mid].t <= t)
68
+ lo = mid;
69
+ else
70
+ hi = mid;
71
+ }
72
+ const a = keys[lo];
73
+ const b = keys[hi];
74
+ if (isSteppedValue(a.value) || isSteppedValue(b.value))
75
+ return a.value;
76
+ const span = b.t - a.t;
77
+ const p = span > 0 ? easeProgress(a.ease, (t - a.t) / span) : 1;
78
+ // interpolateValue is type-aware (numbers, arrays, hex colours); the property name
79
+ // only disambiguates colour keys, so pass the path's last segment.
80
+ return interpolateValue(track.path.split('.').pop() ?? track.path, a.value, b.value, p);
81
+ }
82
+ // -----------------------------------------------------------------------------
83
+ // key / track edits (pure — the slice stores the results)
84
+ // -----------------------------------------------------------------------------
85
+ export const sortKeys = (keys) => [...keys].sort((a, b) => a.t - b.t);
86
+ /** Add a key at t, or replace the value of a key already on that frame. */
87
+ export function upsertKey(track, t, value, fps) {
88
+ const frame = secondsToFrame(t, fps);
89
+ const existing = track.keys.find(k => secondsToFrame(k.t, fps) === frame);
90
+ if (existing) {
91
+ return { ...track, keys: track.keys.map(k => (k.id === existing.id ? { ...k, value } : k)) };
92
+ }
93
+ return { ...track, keys: sortKeys([...track.keys, { id: uid('k'), t: snapToFrame(t, fps), value }]) };
94
+ }
95
+ /** Move keys by dt seconds, clamped to [0, duration], snapped to frames unless `free`. */
96
+ export function moveKeys(clip, selection, dt, free) {
97
+ return {
98
+ ...clip,
99
+ tracks: clip.tracks.map(track => {
100
+ if (!track.keys.some(k => selection.has(k.id)))
101
+ return track;
102
+ const keys = track.keys.map(k => {
103
+ if (!selection.has(k.id))
104
+ return k;
105
+ let t = Math.min(clip.duration, Math.max(0, k.t + dt));
106
+ if (!free)
107
+ t = snapToFrame(t, clip.fps);
108
+ return { ...k, t };
109
+ });
110
+ return { ...track, keys: sortKeys(keys) };
111
+ }),
112
+ };
113
+ }
114
+ export function deleteKeys(clip, selection) {
115
+ return {
116
+ ...clip,
117
+ tracks: clip.tracks.map(track => ({ ...track, keys: track.keys.filter(k => !selection.has(k.id)) })),
118
+ };
119
+ }
120
+ export function setKeysEase(clip, selection, ease) {
121
+ return {
122
+ ...clip,
123
+ tracks: clip.tracks.map(track => ({
124
+ ...track,
125
+ keys: track.keys.map(k => (selection.has(k.id) ? { ...k, ease } : k)),
126
+ })),
127
+ };
128
+ }
129
+ export function setKeyValue(clip, keyId, value) {
130
+ return {
131
+ ...clip,
132
+ tracks: clip.tracks.map(track => ({
133
+ ...track,
134
+ keys: track.keys.map(k => (k.id === keyId ? { ...k, value } : k)),
135
+ })),
136
+ };
137
+ }
138
+ export function findKey(clip, keyId) {
139
+ for (const track of clip.tracks) {
140
+ const key = track.keys.find(k => k.id === keyId);
141
+ if (key)
142
+ return { track, key };
143
+ }
144
+ return undefined;
145
+ }
146
+ /** All key times of the clip, sorted and unique (for Shift+←/→). */
147
+ export function allKeyTimes(clip) {
148
+ const set = new Set();
149
+ clip.tracks.forEach(tr => tr.keys.forEach(k => set.add(k.t)));
150
+ return [...set].sort((a, b) => a - b);
151
+ }
152
+ export function newClip(name, key) {
153
+ return { id: uid('clip'), name, key, duration: 4, fps: 30, loop: false, yoyo: false, endMode: 'hold', tracks: [] };
154
+ }
155
+ /** Deep copy with fresh ids (duplicate). */
156
+ export function cloneClip(clip, name, key) {
157
+ return {
158
+ ...clip,
159
+ id: uid('clip'),
160
+ name,
161
+ key,
162
+ tracks: clip.tracks.map(tr => ({ ...tr, id: uid('tr'), keys: tr.keys.map(k => ({ ...k, id: uid('k') })) })),
163
+ };
164
+ }
@@ -0,0 +1,15 @@
1
+ export interface ElementSources {
2
+ objects: Array<{
3
+ id: string;
4
+ }>;
5
+ camera: unknown;
6
+ sky: unknown;
7
+ clouds: unknown;
8
+ ocean: unknown;
9
+ terrain: unknown;
10
+ }
11
+ export declare const ENVIRONMENT_STATES: readonly ["sky", "clouds", "ocean", "terrain"];
12
+ export type EnvironmentState = typeof ENVIRONMENT_STATES[number];
13
+ export declare const isEnvironmentState: (state: string) => state is EnvironmentState;
14
+ export declare function resolveElementSettings(state: string, src: ElementSources): any;
15
+ //# sourceMappingURL=elementSettings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elementSettings.d.ts","sourceRoot":"","sources":["../../src/animationsV2/elementSettings.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,kBAAkB,gDAAiD,CAAC;AACjF,MAAM,MAAM,gBAAgB,GAAG,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACjE,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,KAAK,IAAI,gBACD,CAAC;AAE5D,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,GAAG,CAU9E"}
@@ -0,0 +1,16 @@
1
+ // Authored settings of any animatable element, with applied action patches — the
2
+ // value a track starts from and returns to. Elements: an object id, 'camera', or an
3
+ // environment element ('sky' | 'clouds' | 'ocean' | 'terrain'; the first three are the ids actions use).
4
+ import { SceneActions, applySettingsPatch } from '@3driseai/3drise-core';
5
+ export const ENVIRONMENT_STATES = ['sky', 'clouds', 'ocean', 'terrain'];
6
+ export const isEnvironmentState = (state) => ENVIRONMENT_STATES.includes(state);
7
+ export function resolveElementSettings(state, src) {
8
+ const base = state === 'camera' ? src.camera
9
+ : state === 'sky' ? src.sky
10
+ : state === 'clouds' ? src.clouds
11
+ : state === 'ocean' ? src.ocean
12
+ : state === 'terrain' ? src.terrain
13
+ : src.objects.find(o => o.id === state);
14
+ const patch = SceneActions.getPatch(state);
15
+ return base && patch ? applySettingsPatch(base, patch) : base;
16
+ }
@@ -0,0 +1,16 @@
1
+ export { default as ClipsRuntime } from './ClipsRuntime';
2
+ export type { RuntimeEvent, RuntimeEventType, DrivenSequence } from './ClipsRuntime';
3
+ export { default as ClipPlayer } from './ClipPlayer';
4
+ export type { ClipPlayerProps } from './ClipPlayer';
5
+ export * from './clipUtils';
6
+ export * from './sequenceUtils';
7
+ export * from './normalize';
8
+ export { applyValue, clearApplierCaches } from './appliers';
9
+ export type { ApplyContext } from './appliers';
10
+ export { readNodeValue, NODE_TRANSFORM_PATHS, NODE_MATERIAL_KEYS } from './nodeTracks';
11
+ export { resolveElementSettings, isEnvironmentState, ENVIRONMENT_STATES as CLIP_ENVIRONMENT_STATES } from './elementSettings';
12
+ export type { ElementSources } from './elementSettings';
13
+ export type { AnimationClip, Track as ClipTrack, Keyframe as ClipKeyframe, KeyValue as ClipKeyValue, EaseName as ClipEaseName, SequenceStep, StepClip, AnimationSequence as ClipSequence, SequenceDriver, SequenceDriverSettings, } from './types';
14
+ export { DEFAULT_DRIVER_SETTINGS } from './types';
15
+ export { DEFAULT_EASE as CLIP_DEFAULT_EASE, FPS_PRESETS as CLIP_FPS_PRESETS } from './types';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/animationsV2/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,kBAAkB,IAAI,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC9H,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,YAAY,EACR,aAAa,EAAE,KAAK,IAAI,SAAS,EAAE,QAAQ,IAAI,YAAY,EAAE,QAAQ,IAAI,YAAY,EAAE,QAAQ,IAAI,YAAY,EAC/G,YAAY,EAAE,QAAQ,EAAE,iBAAiB,IAAI,YAAY,EAAE,cAAc,EAAE,sBAAsB,GACpG,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,YAAY,IAAI,iBAAiB,EAAE,WAAW,IAAI,gBAAgB,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,12 @@
1
+ // Animations V2 runtime — clips, sequences, and the player. One ClipsRuntime
2
+ // instance serves the studio and the published viewer.
3
+ export { default as ClipsRuntime } from './ClipsRuntime.js';
4
+ export { default as ClipPlayer } from './ClipPlayer.js';
5
+ export * from './clipUtils.js';
6
+ export * from './sequenceUtils.js';
7
+ export * from './normalize.js';
8
+ export { applyValue, clearApplierCaches } from './appliers.js';
9
+ export { readNodeValue, NODE_TRANSFORM_PATHS, NODE_MATERIAL_KEYS } from './nodeTracks.js';
10
+ export { resolveElementSettings, isEnvironmentState, ENVIRONMENT_STATES as CLIP_ENVIRONMENT_STATES } from './elementSettings.js';
11
+ export { DEFAULT_DRIVER_SETTINGS } from './types.js';
12
+ export { DEFAULT_EASE as CLIP_DEFAULT_EASE, FPS_PRESETS as CLIP_FPS_PRESETS } from './types.js';
@@ -0,0 +1,13 @@
1
+ import type { KeyValue } from './types';
2
+ type Obj = any;
3
+ export declare const NODE_TRANSFORM_PATHS: readonly ["position", "rotation", "scale", "visible"];
4
+ export declare const NODE_MATERIAL_KEYS: readonly ["color", "opacity", "roughness", "metalness", "emissive", "emissiveIntensity"];
5
+ export declare function clearNodeCaches(): void;
6
+ export declare function nodesFor(state: string, node: string): Obj[];
7
+ /** The node's current value for a node-relative path (first match), as a key value. */
8
+ export declare function readNodeValue(state: string, node: string, path: string): KeyValue | undefined;
9
+ export declare function applyNodeValue(state: string, node: string, path: string, value: KeyValue): boolean;
10
+ /** Put a node track back: its snapshot, and the node's original materials once no material track remains. */
11
+ export declare function restoreNodeTrack(state: string, node: string, path: string, stillAnimated: (path: string) => boolean): void;
12
+ export {};
13
+ //# sourceMappingURL=nodeTracks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodeTracks.d.ts","sourceRoot":"","sources":["../../src/animationsV2/nodeTracks.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,KAAK,GAAG,GAAG,GAAG,CAAC;AAEf,eAAO,MAAM,oBAAoB,uDAAwD,CAAC;AAC1F,eAAO,MAAM,kBAAkB,0FAA2F,CAAC;AAa3H,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,EAAE,CAQ3D;AAUD,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAiB7F;AAoBD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CA2BlG;AAED,6GAA6G;AAC7G,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAiC1H"}
@@ -0,0 +1,165 @@
1
+ // =============================================================================
2
+ // Model-node tracks — animate one node inside a `model` (GLB) object.
3
+ // =============================================================================
4
+ //
5
+ // A node is addressed by its ModelTree key (its name; ModelTree's selection mode is
6
+ // name-only). A name matches the same node in every layout copy, so all matches
7
+ // are written.
8
+ //
9
+ // Paths are node-relative and mirror a node override (config.nodeOverrides):
10
+ // local transform, visibility, and the material tweak fields.
11
+ //
12
+ // Restore is exact rather than "authored": before the first write, the node's
13
+ // current state is snapshotted (which already includes any saved node override,
14
+ // applied by the viewer's useNodeOverrides), and a node's material is cloned so a
15
+ // material shared across GLB meshes isn't changed for every mesh. Restore puts the
16
+ // snapshot and the original materials back.
17
+ //
18
+ import { Color } from 'three';
19
+ import { findById, findNodesByName, applySingleProperty } from '@3driseai/3drise-core';
20
+ export const NODE_TRANSFORM_PATHS = ['position', 'rotation', 'scale', 'visible'];
21
+ export const NODE_MATERIAL_KEYS = ['color', 'opacity', 'roughness', 'metalness', 'emissive', 'emissiveIntensity'];
22
+ const CLIP_MATERIAL_KEY = '__clipOwnedMaterial';
23
+ const nodeKey = (state, node) => `${state}|${node}`;
24
+ const trackKey = (state, node, path) => `${state}|${node}|${path}`;
25
+ /** original value per (state, node, path), captured before the first write */
26
+ const snapshots = new Map();
27
+ /** mesh -> its material(s) before we cloned them, per (state, node) */
28
+ const originalMaterials = new Map();
29
+ const nodeCache = new Map();
30
+ export function clearNodeCaches() {
31
+ nodeCache.clear();
32
+ }
33
+ export function nodesFor(state, node) {
34
+ const key = nodeKey(state, node);
35
+ const cached = nodeCache.get(key);
36
+ if (cached && cached.length && cached.every(n => n.parent))
37
+ return cached;
38
+ const root = findById(state);
39
+ const found = root ? findNodesByName(root, node).filter((n) => n !== root) : [];
40
+ nodeCache.set(key, found);
41
+ return found;
42
+ }
43
+ const meshesUnder = (node) => {
44
+ const out = [];
45
+ node.traverse((c) => { if (c.isMesh && c.material)
46
+ out.push(c); });
47
+ return out;
48
+ };
49
+ const toHex = (c) => (c && typeof c.getHexString === 'function' ? `#${c.getHexString()}` : undefined);
50
+ /** The node's current value for a node-relative path (first match), as a key value. */
51
+ export function readNodeValue(state, node, path) {
52
+ const n = nodesFor(state, node)[0];
53
+ if (!n)
54
+ return undefined;
55
+ if (path === 'position')
56
+ return [n.position.x, n.position.y, n.position.z];
57
+ if (path === 'rotation')
58
+ return [n.rotation.x, n.rotation.y, n.rotation.z];
59
+ if (path === 'scale')
60
+ return [n.scale.x, n.scale.y, n.scale.z];
61
+ if (path === 'visible')
62
+ return !!n.visible;
63
+ if (path.startsWith('material.')) {
64
+ const key = path.slice('material.'.length);
65
+ const mesh = meshesUnder(n)[0];
66
+ const mat = mesh && (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material);
67
+ if (!mat || !(key in mat))
68
+ return undefined;
69
+ const v = mat[key];
70
+ if (v instanceof Color || (v && v.isColor))
71
+ return toHex(v);
72
+ return typeof v === 'number' ? v : undefined;
73
+ }
74
+ return undefined;
75
+ }
76
+ /** Give each mesh under the node its own material (once), remembering the original. */
77
+ function ownMaterials(state, node, n) {
78
+ const key = nodeKey(state, node);
79
+ const saved = originalMaterials.get(key) ?? [];
80
+ for (const mesh of meshesUnder(n)) {
81
+ const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
82
+ if (mats.every((m) => m.userData?.[CLIP_MATERIAL_KEY]))
83
+ continue;
84
+ saved.push({ mesh, material: mesh.material });
85
+ const clones = mats.map((m) => {
86
+ const c = m.clone();
87
+ c.userData = { ...c.userData, [CLIP_MATERIAL_KEY]: true };
88
+ return c;
89
+ });
90
+ mesh.material = Array.isArray(mesh.material) ? clones : clones[0];
91
+ }
92
+ originalMaterials.set(key, saved);
93
+ }
94
+ export function applyNodeValue(state, node, path, value) {
95
+ const nodes = nodesFor(state, node);
96
+ if (!nodes.length)
97
+ return false;
98
+ const tk = trackKey(state, node, path);
99
+ if (!snapshots.has(tk)) {
100
+ const original = readNodeValue(state, node, path);
101
+ if (original !== undefined)
102
+ snapshots.set(tk, original);
103
+ }
104
+ for (const n of nodes) {
105
+ if (path === 'visible') {
106
+ n.visible = !!value;
107
+ continue;
108
+ }
109
+ if (path === 'position' || path === 'rotation' || path === 'scale') {
110
+ if (Array.isArray(value) && value.length >= 3)
111
+ n[path].set(Number(value[0]), Number(value[1]), Number(value[2]));
112
+ continue;
113
+ }
114
+ if (path.startsWith('material.')) {
115
+ ownMaterials(state, node, n);
116
+ const key = path.slice('material.'.length);
117
+ for (const mesh of meshesUnder(n)) {
118
+ const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
119
+ for (const mat of mats) {
120
+ if (key === 'opacity' && typeof value === 'number')
121
+ mat.transparent = value < 1;
122
+ applySingleProperty(mat, key, value);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ return true;
128
+ }
129
+ /** Put a node track back: its snapshot, and the node's original materials once no material track remains. */
130
+ export function restoreNodeTrack(state, node, path, stillAnimated) {
131
+ const tk = trackKey(state, node, path);
132
+ const snap = snapshots.get(tk);
133
+ snapshots.delete(tk);
134
+ if (path.startsWith('material.')) {
135
+ const anyMaterialLeft = NODE_MATERIAL_KEYS.some(k => stillAnimated(`material.${k}`));
136
+ if (!anyMaterialLeft) {
137
+ const key = nodeKey(state, node);
138
+ for (const { mesh, material } of originalMaterials.get(key) ?? []) {
139
+ const current = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
140
+ current.forEach((m) => m.userData?.[CLIP_MATERIAL_KEY] && m.dispose?.());
141
+ mesh.material = material;
142
+ }
143
+ originalMaterials.delete(key);
144
+ // snapshots of the other material keys of this node are now meaningless
145
+ NODE_MATERIAL_KEYS.forEach(k => snapshots.delete(trackKey(state, node, `material.${k}`)));
146
+ return;
147
+ }
148
+ }
149
+ if (snap !== undefined) {
150
+ for (const n of nodesFor(state, node)) {
151
+ if (path === 'visible')
152
+ n.visible = !!snap;
153
+ else if ((path === 'position' || path === 'rotation' || path === 'scale') && Array.isArray(snap)) {
154
+ n[path].set(Number(snap[0]), Number(snap[1]), Number(snap[2]));
155
+ }
156
+ else if (path.startsWith('material.')) {
157
+ const key = path.slice('material.'.length);
158
+ for (const mesh of meshesUnder(n)) {
159
+ const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
160
+ mats.forEach((m) => applySingleProperty(m, key, snap));
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }