@pascal-app/viewer 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +8 -3
- package/dist/components/viewer/frame-limiter.d.ts +1 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +22 -3
- package/dist/components/viewer/glb-interactive.d.ts +25 -1
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +252 -85
- package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
- package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
- package/dist/components/viewer/glb-mechanisms.js +19 -0
- package/dist/components/viewer/glb-scene.d.ts +45 -27
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +420 -506
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +12 -1
- package/dist/components/viewer/index.d.ts +26 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +108 -29
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +6 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +6 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +8 -3
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +16 -2
- package/dist/components/viewer/render-error.d.ts +18 -0
- package/dist/components/viewer/render-error.d.ts.map +1 -0
- package/dist/components/viewer/render-error.js +29 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
- package/dist/components/viewer/scene-ground-replacement.js +15 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +10 -0
- package/dist/components/viewer/viewer-camera.d.ts +19 -1
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +53 -5
- package/dist/index.d.ts +28 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -18
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -1
- package/dist/lib/csg-utils.d.ts +1 -1
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +1 -1
- package/dist/lib/display-state.d.ts +25 -0
- package/dist/lib/display-state.d.ts.map +1 -0
- package/dist/lib/display-state.js +1 -0
- package/dist/lib/hero-pose.d.ts +12 -2
- package/dist/lib/hero-pose.d.ts.map +1 -1
- package/dist/lib/hero-pose.js +72 -5
- package/dist/lib/ink-edges.d.ts +4 -2
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +17 -7
- package/dist/lib/key-light-override.d.ts +11 -0
- package/dist/lib/key-light-override.d.ts.map +1 -0
- package/dist/lib/key-light-override.js +14 -0
- package/dist/lib/live-frame-hold.d.ts +14 -0
- package/dist/lib/live-frame-hold.d.ts.map +1 -0
- package/dist/lib/live-frame-hold.js +28 -0
- package/dist/lib/materials.d.ts +14 -7
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +31 -5
- package/dist/lib/node-top-surface-height.d.ts +6 -0
- package/dist/lib/node-top-surface-height.d.ts.map +1 -0
- package/dist/lib/node-top-surface-height.js +74 -0
- package/dist/lib/perf-probe-state.d.ts +39 -0
- package/dist/lib/perf-probe-state.d.ts.map +1 -0
- package/dist/lib/perf-probe-state.js +17 -0
- package/dist/lib/pointer-events.d.ts +8 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +27 -4
- package/dist/lib/procedural-emission.d.ts +7 -0
- package/dist/lib/procedural-emission.d.ts.map +1 -0
- package/dist/lib/procedural-emission.js +54 -0
- package/dist/lib/renderer-capability.d.ts +2 -1
- package/dist/lib/renderer-capability.d.ts.map +1 -1
- package/dist/lib/renderer-capability.js +2 -2
- package/dist/lib/scene-support-height.d.ts +4 -0
- package/dist/lib/scene-support-height.d.ts.map +1 -0
- package/dist/lib/scene-support-height.js +34 -0
- package/dist/lib/scene-visibility.d.ts +6 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +15 -3
- package/dist/lib/scripted-opening.d.ts +11 -0
- package/dist/lib/scripted-opening.d.ts.map +1 -0
- package/dist/lib/scripted-opening.js +20 -0
- package/dist/lib/snapshot-pipeline.d.ts +32 -2
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +215 -105
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
- package/dist/lib/terrain-perimeter-fill.js +22 -14
- package/dist/lib/zone-geometry.d.ts +11 -0
- package/dist/lib/zone-geometry.d.ts.map +1 -0
- package/dist/lib/zone-geometry.js +46 -0
- package/dist/store/use-item-light-pool.d.ts +14 -10
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +52 -26
- package/dist/store/use-viewer.d.ts +11 -0
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +13 -6
- package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +86 -6
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +73 -22
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +148 -5
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +71 -11
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +40 -15
- package/dist/systems/interactive/procedural-controls.d.ts +13 -0
- package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
- package/dist/systems/interactive/procedural-controls.js +17 -0
- package/dist/systems/interactive/scripted-clips.d.ts +19 -0
- package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
- package/dist/systems/interactive/scripted-clips.js +134 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +9 -3
- package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
- package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
- package/dist/systems/item/object-cut-invalidation.js +68 -0
- package/dist/systems/item-light/item-light-system.d.ts +1 -1
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +47 -52
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +17 -19
- package/dist/systems/level/level-utils.d.ts +28 -1
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +36 -4
- package/dist/systems/level/shadow-only-levels.d.ts +9 -0
- package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
- package/dist/systems/level/shadow-only-levels.js +7 -0
- package/dist/systems/roof/roof-materials.d.ts +14 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +54 -15
- package/dist/systems/roof/roof-system.d.ts +13 -3
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +188 -13
- package/dist/systems/slab/slab-system.d.ts +1 -1
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +49 -19
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +133 -354
- package/dist/systems/wall/wall-cutout-cache.d.ts +11 -1
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +58 -20
- package/dist/systems/wall/wall-cutout.d.ts +9 -1
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +46 -32
- package/dist/systems/wall/wall-finish-data.d.ts +22 -0
- package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
- package/dist/systems/wall/wall-finish-data.js +17 -0
- package/dist/systems/wall/wall-materials.d.ts +17 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +109 -156
- package/dist/systems/wall/wall-system.d.ts +43 -4
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +601 -163
- package/dist/systems/window/window-system.d.ts +1 -1
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +44 -24
- package/dist/xr/presentation-context.d.ts +7 -0
- package/dist/xr/presentation-context.d.ts.map +1 -0
- package/dist/xr/presentation-context.js +10 -0
- package/package.json +6 -6
- package/dist/systems/fence/fence-system.d.ts +0 -22
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -357
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getEffectiveNode, getFloorStackedPosition, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
|
+
import { useEffect, useMemo } from 'react';
|
|
4
|
+
import { Euler, Matrix4, Quaternion, Vector3 } from 'three';
|
|
3
5
|
function withLiveTransform(node, liveTransform) {
|
|
4
6
|
if (!liveTransform)
|
|
5
7
|
return node;
|
|
@@ -15,6 +17,16 @@ function withLiveTransform(node, liveTransform) {
|
|
|
15
17
|
...(rotation !== undefined ? { rotation } : {}),
|
|
16
18
|
};
|
|
17
19
|
}
|
|
20
|
+
function restoreMountedExitPose(saved) {
|
|
21
|
+
saved.mesh.matrixAutoUpdate = saved.autoUpdate;
|
|
22
|
+
saved.mesh.scale.copy(saved.scale);
|
|
23
|
+
if (!saved.mesh.parent || saved.mesh.parent === saved.parent) {
|
|
24
|
+
saved.mesh.position.copy(saved.position);
|
|
25
|
+
saved.mesh.quaternion.copy(saved.quaternion);
|
|
26
|
+
}
|
|
27
|
+
saved.mesh.updateMatrix();
|
|
28
|
+
saved.mesh.updateMatrixWorld(true);
|
|
29
|
+
}
|
|
18
30
|
/**
|
|
19
31
|
* Generic floor-elevation system.
|
|
20
32
|
*
|
|
@@ -31,11 +43,67 @@ function withLiveTransform(node, liveTransform) {
|
|
|
31
43
|
* Runs at priority 1 — before the priority-2 systems (`GeometrySystem`,
|
|
32
44
|
* `ItemSystem`) so the dirty mark survives long enough for those to do
|
|
33
45
|
* their own work. Kinds with no geometry/system have no downstream dirty
|
|
34
|
-
* consumer, so this system clears their dirty mark after applying the lift
|
|
46
|
+
* consumer, so this system clears their dirty mark after applying the lift,
|
|
47
|
+
* at priority 2 like every other consumer: the node batch snapshots marks at
|
|
48
|
+
* priority 1, and a mark cleared there could run before that snapshot.
|
|
35
49
|
*/
|
|
36
50
|
export const FloorElevationSystem = () => {
|
|
37
51
|
const dirtyNodes = useScene((s) => s.dirtyNodes);
|
|
38
52
|
const clearDirty = useScene((s) => s.clearDirty);
|
|
53
|
+
const consumed = useMemo(() => new Set(), []);
|
|
54
|
+
// Meshes already lifted once. A mesh mounts at its base Y, and a remount (entering Preview
|
|
55
|
+
// builds a fresh viewer tree) raises no dirty mark, so the lift would never be applied.
|
|
56
|
+
const mounted = useMemo(() => ({ revision: -1, meshes: new WeakSet() }), []);
|
|
57
|
+
const preview = useMemo(() => ({
|
|
58
|
+
local: new Matrix4(),
|
|
59
|
+
target: new Matrix4(),
|
|
60
|
+
position: new Vector3(),
|
|
61
|
+
rotation: new Euler(),
|
|
62
|
+
quaternion: new Quaternion(),
|
|
63
|
+
unit: new Vector3(1, 1, 1),
|
|
64
|
+
saved: new Map(),
|
|
65
|
+
}), []);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const restore = (id) => {
|
|
68
|
+
const saved = preview.saved.get(id);
|
|
69
|
+
if (!saved)
|
|
70
|
+
return;
|
|
71
|
+
restoreMountedExitPose(saved);
|
|
72
|
+
preview.saved.delete(id);
|
|
73
|
+
};
|
|
74
|
+
const sync = () => {
|
|
75
|
+
const { nodes } = useScene.getState();
|
|
76
|
+
const overrides = useLiveNodeOverrides.getState().overrides;
|
|
77
|
+
for (const id of preview.saved.keys()) {
|
|
78
|
+
const node = nodes[id];
|
|
79
|
+
const patch = overrides.get(id);
|
|
80
|
+
if (!node || !patch?.parentId || patch.parentId === node.parentId)
|
|
81
|
+
restore(id);
|
|
82
|
+
}
|
|
83
|
+
for (const [id, patch] of overrides) {
|
|
84
|
+
const node = nodes[id];
|
|
85
|
+
if (!node || !patch.parentId || patch.parentId === node.parentId || preview.saved.has(id))
|
|
86
|
+
continue;
|
|
87
|
+
const mesh = sceneRegistry.nodes.get(id);
|
|
88
|
+
if (!mesh?.parent)
|
|
89
|
+
continue;
|
|
90
|
+
preview.saved.set(id, {
|
|
91
|
+
mesh,
|
|
92
|
+
parent: mesh.parent,
|
|
93
|
+
position: mesh.position.clone(),
|
|
94
|
+
quaternion: mesh.quaternion.clone(),
|
|
95
|
+
scale: mesh.scale.clone(),
|
|
96
|
+
autoUpdate: mesh.matrixAutoUpdate,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const unsubscribe = useLiveNodeOverrides.subscribe(sync);
|
|
101
|
+
return () => {
|
|
102
|
+
unsubscribe();
|
|
103
|
+
for (const id of preview.saved.keys())
|
|
104
|
+
restore(id);
|
|
105
|
+
};
|
|
106
|
+
}, [preview]);
|
|
39
107
|
useFrame(() => {
|
|
40
108
|
// Nodes with a live preview (override / transform) are reapplied EVERY
|
|
41
109
|
// frame, not only while dirty: the React commit that rebinds the group's
|
|
@@ -45,7 +113,8 @@ export const FloorElevationSystem = () => {
|
|
|
45
113
|
// during group drags over elevated slabs).
|
|
46
114
|
const overrides = useLiveNodeOverrides.getState().overrides;
|
|
47
115
|
const transforms = useLiveTransforms.getState().transforms;
|
|
48
|
-
|
|
116
|
+
const registryChanged = sceneRegistry.revision !== mounted.revision;
|
|
117
|
+
if (dirtyNodes.size === 0 && overrides.size === 0 && transforms.size === 0 && !registryChanged)
|
|
49
118
|
return;
|
|
50
119
|
const nodes = useScene.getState().nodes;
|
|
51
120
|
const applyLift = (id) => {
|
|
@@ -64,9 +133,13 @@ export const FloorElevationSystem = () => {
|
|
|
64
133
|
const position = effectiveNode.position;
|
|
65
134
|
if (!position)
|
|
66
135
|
return;
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
136
|
+
if (effectiveNode.parentId !== node.parentId)
|
|
137
|
+
return;
|
|
138
|
+
// A mounted object still loading (a scripted column's artifact) keeps its mark, so
|
|
139
|
+
// scene-ready and bakes wait for it; settling marks the node dirty again.
|
|
140
|
+
const settled = def.capabilities?.batchable?.settled?.(mesh.userData) ?? true;
|
|
141
|
+
if (!(def.geometry || def.system) && dirtyNodes.has(id) && settled)
|
|
142
|
+
consumed.add(id);
|
|
70
143
|
// `applies === false` means the kind opts OUT of floor stacking for this
|
|
71
144
|
// node: its Y belongs to a host frame (a wall/ceiling-mounted item, a
|
|
72
145
|
// cabinet module inside a run, a wall duct terminal). `getFloorPlacedElevation`
|
|
@@ -98,6 +171,20 @@ export const FloorElevationSystem = () => {
|
|
|
98
171
|
});
|
|
99
172
|
mesh.position.y = visualPosition[1];
|
|
100
173
|
};
|
|
174
|
+
if (registryChanged) {
|
|
175
|
+
mounted.revision = sceneRegistry.revision;
|
|
176
|
+
for (const [kind, ids] of Object.entries(sceneRegistry.byType)) {
|
|
177
|
+
if (!nodeRegistry.get(kind)?.capabilities?.floorPlaced)
|
|
178
|
+
continue;
|
|
179
|
+
for (const id of ids) {
|
|
180
|
+
const mesh = sceneRegistry.nodes.get(id);
|
|
181
|
+
if (!mesh || mounted.meshes.has(mesh))
|
|
182
|
+
continue;
|
|
183
|
+
mounted.meshes.add(mesh);
|
|
184
|
+
applyLift(id);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
101
188
|
dirtyNodes.forEach((id) => {
|
|
102
189
|
applyLift(id);
|
|
103
190
|
});
|
|
@@ -110,5 +197,61 @@ export const FloorElevationSystem = () => {
|
|
|
110
197
|
applyLift(id);
|
|
111
198
|
});
|
|
112
199
|
}, 1);
|
|
200
|
+
useFrame(() => {
|
|
201
|
+
for (const id of consumed)
|
|
202
|
+
clearDirty(id);
|
|
203
|
+
consumed.clear();
|
|
204
|
+
}, 2);
|
|
205
|
+
// PostProcessing draws at priority 1 after this system; later callbacks would show one wrong frame per move.
|
|
206
|
+
useFrame(() => {
|
|
207
|
+
const nodes = useScene.getState().nodes;
|
|
208
|
+
for (const [id, patch] of useLiveNodeOverrides.getState().overrides) {
|
|
209
|
+
const node = nodes[id];
|
|
210
|
+
if (!node || !patch.parentId || patch.parentId === node.parentId)
|
|
211
|
+
continue;
|
|
212
|
+
const effective = getEffectiveNode(node);
|
|
213
|
+
if (effective.parentId === node.parentId || !effective.position)
|
|
214
|
+
continue;
|
|
215
|
+
if (!effective.parentId || nodes[effective.parentId]?.type !== 'level')
|
|
216
|
+
continue;
|
|
217
|
+
const mesh = sceneRegistry.nodes.get(id);
|
|
218
|
+
const level = sceneRegistry.nodes.get(effective.parentId);
|
|
219
|
+
if (!mesh?.parent || !level)
|
|
220
|
+
continue;
|
|
221
|
+
const previous = preview.saved.get(id);
|
|
222
|
+
if (previous?.mesh !== mesh) {
|
|
223
|
+
if (previous)
|
|
224
|
+
restoreMountedExitPose(previous);
|
|
225
|
+
preview.saved.set(id, {
|
|
226
|
+
mesh,
|
|
227
|
+
parent: mesh.parent,
|
|
228
|
+
position: mesh.position.clone(),
|
|
229
|
+
quaternion: mesh.quaternion.clone(),
|
|
230
|
+
scale: mesh.scale.clone(),
|
|
231
|
+
autoUpdate: mesh.matrixAutoUpdate,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const position = getFloorStackedPosition({
|
|
235
|
+
node: effective,
|
|
236
|
+
nodes,
|
|
237
|
+
position: effective.position,
|
|
238
|
+
});
|
|
239
|
+
const rotation = typeof effective.rotation === 'number'
|
|
240
|
+
? [0, effective.rotation, 0]
|
|
241
|
+
: (effective.rotation ?? [0, 0, 0]);
|
|
242
|
+
level.updateWorldMatrix(true, false);
|
|
243
|
+
mesh.parent.updateWorldMatrix(true, false);
|
|
244
|
+
// The override's parent is logical; the mesh still inherits the mounted host and surface wrapper.
|
|
245
|
+
preview.local
|
|
246
|
+
.copy(mesh.parent.matrixWorld)
|
|
247
|
+
.invert()
|
|
248
|
+
.multiply(level.matrixWorld)
|
|
249
|
+
.multiply(preview.target.compose(preview.position.fromArray(position), preview.quaternion.setFromEuler(preview.rotation.set(rotation[0], rotation[1], rotation[2])), preview.unit));
|
|
250
|
+
// Keep the full matrix: a rotated child beneath nonuniform scale can require shear.
|
|
251
|
+
mesh.matrixAutoUpdate = false;
|
|
252
|
+
mesh.matrix.copy(preview.local);
|
|
253
|
+
mesh.updateMatrixWorld(true);
|
|
254
|
+
}
|
|
255
|
+
}, 1);
|
|
113
256
|
return null;
|
|
114
257
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAa,KAAK,KAAK,EAA4B,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAa,KAAK,KAAK,EAA4B,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAatF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YA8N1B,CAAA;AA+KD,eAAe,cAAc,CAAA;AAE7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAY7D;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAC3C,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAKT"}
|
|
@@ -4,8 +4,10 @@ import { useFrame } from '@react-three/fiber';
|
|
|
4
4
|
import { useEffect, useRef } from 'react';
|
|
5
5
|
import { FrontSide } from 'three';
|
|
6
6
|
import { disposeObject3DResources } from '../../lib/dispose-object3d';
|
|
7
|
-
import { createSurfaceRoleMaterial, } from '../../lib/materials';
|
|
7
|
+
import { createSurfaceRoleMaterial, materialCastsShadow, } from '../../lib/materials';
|
|
8
|
+
import { createNodeTopSurfaceHeightSampler } from '../../lib/node-top-surface-height';
|
|
8
9
|
import { timeSpan } from '../../lib/perf-tracks';
|
|
10
|
+
import { createSceneSupportHeightSampler } from '../../lib/scene-support-height';
|
|
9
11
|
import useViewer from '../../store/use-viewer';
|
|
10
12
|
/**
|
|
11
13
|
* Generic geometry system.
|
|
@@ -55,6 +57,22 @@ export const GeometrySystem = () => {
|
|
|
55
57
|
// but its geometry inputs are unchanged — e.g. an item reparenting onto a
|
|
56
58
|
// shelf dirties the shelf without altering its boards.
|
|
57
59
|
const builtGeometryKeyRef = useRef(new Map());
|
|
60
|
+
const liveBuildIds = useRef(new Set());
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const unsubscribe = useLiveNodeOverrides.subscribe((state, previous) => {
|
|
63
|
+
for (const id of new Set([...state.overrides.keys(), ...previous.overrides.keys()])) {
|
|
64
|
+
if (state.overrides.get(id) === previous.overrides.get(id))
|
|
65
|
+
continue;
|
|
66
|
+
const node = useScene.getState().nodes[id];
|
|
67
|
+
if (node && nodeRegistry.get(node.type)?.geometry)
|
|
68
|
+
liveBuildIds.current.add(id);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
unsubscribe();
|
|
73
|
+
liveBuildIds.current.clear();
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
58
76
|
// Re-mark every geometry-backed node dirty whenever a viewer appearance
|
|
59
77
|
// value changes, so `def.geometry` builders re-run and pick up the new
|
|
60
78
|
// shading / texture / preset / theme. These four are deliberate re-run
|
|
@@ -92,19 +110,23 @@ export const GeometrySystem = () => {
|
|
|
92
110
|
}
|
|
93
111
|
}, [sceneMaterials]);
|
|
94
112
|
useFrame(() => {
|
|
95
|
-
if (dirtyNodes.size === 0)
|
|
113
|
+
if (dirtyNodes.size === 0 && liveBuildIds.current.size === 0)
|
|
96
114
|
return;
|
|
97
115
|
const nodes = useScene.getState().nodes;
|
|
98
116
|
let rebuiltGeometry = false;
|
|
99
117
|
const batches = new Map();
|
|
100
118
|
const dirtyIds = [];
|
|
101
|
-
dirtyNodes.
|
|
119
|
+
for (const id of new Set([...dirtyNodes, ...liveBuildIds.current])) {
|
|
102
120
|
const node = nodes[id];
|
|
103
|
-
if (!node)
|
|
104
|
-
|
|
121
|
+
if (!node) {
|
|
122
|
+
liveBuildIds.current.delete(id);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
105
125
|
const def = nodeRegistry.get(node.type);
|
|
106
|
-
if (!def?.geometry)
|
|
107
|
-
|
|
126
|
+
if (!def?.geometry) {
|
|
127
|
+
liveBuildIds.current.delete(id);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
108
130
|
dirtyIds.push(id);
|
|
109
131
|
const parentId = (node.parentId ?? null);
|
|
110
132
|
const key = `${node.type}::${parentId ?? ''}`;
|
|
@@ -113,7 +135,7 @@ export const GeometrySystem = () => {
|
|
|
113
135
|
existing.ids.push(id);
|
|
114
136
|
else
|
|
115
137
|
batches.set(key, { kind: node.type, parentId, ids: [id] });
|
|
116
|
-
}
|
|
138
|
+
}
|
|
117
139
|
// Phase 2 — for each batch whose kind declares `computeLevelData`,
|
|
118
140
|
// collect every sibling in the level + run the precompute once.
|
|
119
141
|
const levelDataByBatch = new Map();
|
|
@@ -170,7 +192,9 @@ export const GeometrySystem = () => {
|
|
|
170
192
|
const childLiveOverrideKey = liveChildOverrideKey(node);
|
|
171
193
|
const builtKey = `${shading}|${textures}|${colorPreset}|${sceneTheme}|${def.geometryKey(effectiveNode)}|${childLiveOverrideKey}`;
|
|
172
194
|
if (shouldReuseGeometryBuild(builtGeometryKeyRef.current, id, group, builtKey)) {
|
|
173
|
-
|
|
195
|
+
liveBuildIds.current.delete(id);
|
|
196
|
+
if (dirtyNodes.has(id))
|
|
197
|
+
clearDirty(id);
|
|
174
198
|
continue;
|
|
175
199
|
}
|
|
176
200
|
}
|
|
@@ -199,7 +223,9 @@ export const GeometrySystem = () => {
|
|
|
199
223
|
// because the renderer doesn't necessarily re-render on the rebuild
|
|
200
224
|
// tick, R3F wouldn't re-apply the props, leaving the group stuck at
|
|
201
225
|
// origin. Geometry builders are expected to emit local-space children.
|
|
202
|
-
|
|
226
|
+
liveBuildIds.current.delete(id);
|
|
227
|
+
if (dirtyNodes.has(id))
|
|
228
|
+
clearDirty(id);
|
|
203
229
|
}
|
|
204
230
|
if (rebuiltGeometry)
|
|
205
231
|
bumpGeometryRevision();
|
|
@@ -281,7 +307,38 @@ function buildGeometryContext(node, nodes, levelData, materials) {
|
|
|
281
307
|
noteLevelBaseConsumer(node.type);
|
|
282
308
|
return levelId ? levelBaseElevationAt(nodes, levelId, x, z) : 0;
|
|
283
309
|
};
|
|
284
|
-
|
|
310
|
+
const surfaceSamplers = new Map();
|
|
311
|
+
const surfaceHeightAt = (hostId, x, z) => {
|
|
312
|
+
if (!levelId)
|
|
313
|
+
return null;
|
|
314
|
+
if (!surfaceSamplers.has(hostId)) {
|
|
315
|
+
surfaceSamplers.set(hostId, createNodeTopSurfaceHeightSampler(hostId, levelId));
|
|
316
|
+
}
|
|
317
|
+
return surfaceSamplers.get(hostId)?.(x, z) ?? null;
|
|
318
|
+
};
|
|
319
|
+
const supportSamplers = new Map();
|
|
320
|
+
const supportHeightAt = (x, z, selectedHostId) => {
|
|
321
|
+
if (!levelId)
|
|
322
|
+
return levelBaseAt(x, z);
|
|
323
|
+
const key = selectedHostId ?? 'auto';
|
|
324
|
+
let sampler = supportSamplers.get(key);
|
|
325
|
+
if (!sampler) {
|
|
326
|
+
sampler = createSceneSupportHeightSampler(nodes, levelId, selectedHostId);
|
|
327
|
+
supportSamplers.set(key, sampler);
|
|
328
|
+
}
|
|
329
|
+
return sampler(x, z);
|
|
330
|
+
};
|
|
331
|
+
return {
|
|
332
|
+
resolve,
|
|
333
|
+
children,
|
|
334
|
+
siblings,
|
|
335
|
+
parent,
|
|
336
|
+
levelBaseAt,
|
|
337
|
+
surfaceHeightAt,
|
|
338
|
+
supportHeightAt,
|
|
339
|
+
levelData,
|
|
340
|
+
materials,
|
|
341
|
+
};
|
|
285
342
|
}
|
|
286
343
|
function disposeChildren(group) {
|
|
287
344
|
// Only dispose meshes the geometry builder produced on the previous
|
|
@@ -330,6 +387,9 @@ export function markGeometryBuildOutput(child) {
|
|
|
330
387
|
// example a cabinet sink faucet handle), while disposal still removes only
|
|
331
388
|
// top-level builder children from the registered group.
|
|
332
389
|
child.traverse((object) => {
|
|
390
|
+
const mesh = object;
|
|
391
|
+
if (mesh.isMesh)
|
|
392
|
+
mesh.castShadow = mesh.castShadow && materialCastsShadow(mesh.material);
|
|
333
393
|
object.userData = {
|
|
334
394
|
...object.userData,
|
|
335
395
|
__fromGeometry: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interactive-system.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/interactive-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"interactive-system.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/interactive-system.tsx"],"names":[],"mappings":"AAmCA,eAAO,MAAM,iBAAiB,mCAmC7B,CAAA"}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { pointInPolygon, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
|
|
4
|
+
import { evaluateRecipe, operableParts, } from '@pascal-app/core/procedural-items';
|
|
4
5
|
import { Html } from '@react-three/drei';
|
|
5
6
|
import { createPortal, useFrame } from '@react-three/fiber';
|
|
6
|
-
import { useEffect, useState } from 'react';
|
|
7
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
7
8
|
import { Vector3 } from 'three';
|
|
8
9
|
import { useShallow } from 'zustand/react/shallow';
|
|
9
10
|
import useViewer from '../../store/use-viewer';
|
|
10
11
|
import { ControlWidget } from './control-widget';
|
|
12
|
+
import { proceduralControlDescriptors } from './procedural-controls';
|
|
11
13
|
const _tempVec = new Vector3();
|
|
12
|
-
// ---- Parent:
|
|
14
|
+
// ---- Parent: overlays for selected interactive items and the selected zone ----
|
|
13
15
|
//
|
|
14
|
-
//
|
|
15
|
-
// inside it. Mounting them unconditionally is not an option: each drei <Html>
|
|
16
|
+
// Mounting <Html> overlays unconditionally is not an option: each drei <Html>
|
|
16
17
|
// repositions and re-sorts its DOM element on every camera-move frame, and
|
|
17
18
|
// with `occlude` it also raycasts the entire scene per overlay per frame. On
|
|
18
19
|
// large scenes (hundreds of interactive items) that starves the frame budget
|
|
19
20
|
// and the display/z-index churn makes the whole DOM UI flicker.
|
|
20
21
|
//
|
|
21
|
-
// The child components stay rendered (returning null) so an overlay can fade
|
|
22
|
-
// out before its <Html> unmounts.
|
|
23
22
|
export const InteractiveSystem = () => {
|
|
24
23
|
const zoneId = useViewer((s) => s.selection.zoneId);
|
|
24
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds);
|
|
25
25
|
const zonePolygon = useScene((s) => {
|
|
26
26
|
if (!zoneId)
|
|
27
27
|
return null;
|
|
@@ -29,13 +29,17 @@ export const InteractiveSystem = () => {
|
|
|
29
29
|
return z?.polygon ?? null;
|
|
30
30
|
});
|
|
31
31
|
const interactiveNodeIds = useScene(useShallow((state) => Object.values(state.nodes)
|
|
32
|
-
.filter((n) => n.type === 'item' && n.asset.interactive != null)
|
|
32
|
+
.filter((n) => (n.type === 'item' && n.asset.interactive != null) ||
|
|
33
|
+
(n.type === 'procedural-item' &&
|
|
34
|
+
(operableParts(n.recipe).length > 0 || n.recipe.parts.some((part) => part.light))))
|
|
33
35
|
.map((n) => n.id)));
|
|
34
|
-
return (_jsx(_Fragment, { children: interactiveNodeIds
|
|
36
|
+
return (_jsx(_Fragment, { children: interactiveNodeIds
|
|
37
|
+
.filter((id) => zonePolygon?.length || selectedIds.includes(id))
|
|
38
|
+
.map((id) => (_jsx(ItemControlsOverlay, { isSelected: selectedIds.includes(id), nodeId: id, zonePolygon: zonePolygon }, id))) }));
|
|
35
39
|
};
|
|
36
40
|
// ---- Child: polls sceneRegistry then portals controls into the item group ----
|
|
37
41
|
const FADE_MS = 300;
|
|
38
|
-
const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
42
|
+
const ItemControlsOverlay = ({ nodeId, zonePolygon, isSelected, }) => {
|
|
39
43
|
const node = useScene((state) => state.nodes[nodeId]);
|
|
40
44
|
const [itemObj, setItemObj] = useState(null);
|
|
41
45
|
useFrame(() => {
|
|
@@ -46,11 +50,34 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
|
46
50
|
setItemObj(obj);
|
|
47
51
|
});
|
|
48
52
|
const controlValues = useInteractive(useShallow((state) => state.items[nodeId]?.controlValues));
|
|
53
|
+
const proceduralState = useInteractive((state) => state.procedural[nodeId]);
|
|
54
|
+
const lampDefault = useInteractive((state) => state.lampDefault);
|
|
49
55
|
const setControlValue = useInteractive((state) => state.setControlValue);
|
|
50
|
-
|
|
56
|
+
const togglePart = useInteractive((state) => state.toggleProceduralPart);
|
|
57
|
+
const toggleLights = useInteractive((state) => state.toggleProceduralLights);
|
|
58
|
+
const proceduralHeight = useMemo(() => node?.type === 'procedural-item' ? evaluateRecipe(node.recipe, node.parameters).max[1] : 0, [
|
|
59
|
+
node?.type === 'procedural-item' ? node.recipe : null,
|
|
60
|
+
node?.type === 'procedural-item' ? node.parameters : null,
|
|
61
|
+
]);
|
|
62
|
+
let descriptors = [];
|
|
63
|
+
let height = 0;
|
|
64
|
+
if (node?.type === 'item' && node.asset.interactive && controlValues) {
|
|
65
|
+
descriptors = node.asset.interactive.controls.map((control, i) => ({
|
|
66
|
+
key: String(i),
|
|
67
|
+
control,
|
|
68
|
+
value: controlValues[i] ?? false,
|
|
69
|
+
onChange: (value) => setControlValue(nodeId, i, value),
|
|
70
|
+
}));
|
|
71
|
+
height = node.asset.dimensions[1];
|
|
72
|
+
}
|
|
73
|
+
else if (node?.type === 'procedural-item') {
|
|
74
|
+
descriptors = proceduralControlDescriptors(node.recipe, proceduralState, (partId) => togglePart(nodeId, partId), () => toggleLights(nodeId), lampDefault);
|
|
75
|
+
height = proceduralHeight;
|
|
76
|
+
}
|
|
77
|
+
let visible = isSelected;
|
|
51
78
|
if (itemObj && zonePolygon?.length) {
|
|
52
79
|
itemObj.getWorldPosition(_tempVec);
|
|
53
|
-
visible = pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
|
|
80
|
+
visible = visible || pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
|
|
54
81
|
}
|
|
55
82
|
// Fade in on mount and fade out before unmounting the <Html>.
|
|
56
83
|
const [mounted, setMounted] = useState(false);
|
|
@@ -73,10 +100,8 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
|
73
100
|
const timeout = setTimeout(() => setMounted(false), FADE_MS);
|
|
74
101
|
return () => clearTimeout(timeout);
|
|
75
102
|
}, [visible]);
|
|
76
|
-
if (!(mounted && itemObj &&
|
|
103
|
+
if (!(mounted && itemObj && descriptors.length))
|
|
77
104
|
return null;
|
|
78
|
-
const { controls } = node.asset.interactive;
|
|
79
|
-
const [, height] = node.asset.dimensions;
|
|
80
105
|
return createPortal(_jsx(Html, { center: true, distanceFactor: 8, eps: -1, position: [0, height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { style: {
|
|
81
106
|
display: 'flex',
|
|
82
107
|
flexDirection: 'column',
|
|
@@ -90,5 +115,5 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
|
90
115
|
userSelect: 'none',
|
|
91
116
|
opacity: shown ? 1 : 0,
|
|
92
117
|
transition: `opacity ${FADE_MS}ms ease`,
|
|
93
|
-
}, children:
|
|
118
|
+
}, children: descriptors.map((descriptor) => (_jsx(ControlWidget, { control: descriptor.control, onChange: descriptor.onChange, value: descriptor.value }, descriptor.key))) }) }), itemObj);
|
|
94
119
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Control, ControlValue } from '@pascal-app/core';
|
|
2
|
+
import { type ProceduralItemNode } from '@pascal-app/core/procedural-items';
|
|
3
|
+
export type ControlDescriptor = {
|
|
4
|
+
key: string;
|
|
5
|
+
control: Control;
|
|
6
|
+
value: ControlValue;
|
|
7
|
+
onChange: (value: ControlValue) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function proceduralControlDescriptors(recipe: Pick<ProceduralItemNode['recipe'], 'parts' | 'joints'>, state: {
|
|
10
|
+
parts: Record<string, boolean>;
|
|
11
|
+
lightsOn: boolean;
|
|
12
|
+
} | undefined, togglePart: (partId: string) => void, toggleLights: () => void, lampDefault?: boolean): ControlDescriptor[];
|
|
13
|
+
//# sourceMappingURL=procedural-controls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"procedural-controls.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/procedural-controls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAE1F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;CACxC,CAAA;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,EAC9D,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EACxE,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,EACpC,YAAY,EAAE,MAAM,IAAI,EACxB,WAAW,UAAQ,GAClB,iBAAiB,EAAE,CAerB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { operableParts } from '@pascal-app/core/procedural-items';
|
|
2
|
+
export function proceduralControlDescriptors(recipe, state, togglePart, toggleLights, lampDefault = false) {
|
|
3
|
+
const controls = operableParts(recipe).map((part) => ({
|
|
4
|
+
key: part.id,
|
|
5
|
+
control: { kind: 'toggle', label: part.label },
|
|
6
|
+
value: state?.parts[part.id] ?? part.kind === 'spin',
|
|
7
|
+
onChange: () => togglePart(part.id),
|
|
8
|
+
}));
|
|
9
|
+
if (recipe.parts.some((part) => part.light))
|
|
10
|
+
controls.push({
|
|
11
|
+
key: 'lights',
|
|
12
|
+
control: { kind: 'toggle', label: 'Lights' },
|
|
13
|
+
value: state?.lightsOn ?? lampDefault,
|
|
14
|
+
onChange: toggleLights,
|
|
15
|
+
});
|
|
16
|
+
return controls;
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AnyNodeId, type Interactive } from '@pascal-app/core';
|
|
2
|
+
import { type RefObject } from 'react';
|
|
3
|
+
import { type AnimationAction, type AnimationClip, Object3D } from 'three';
|
|
4
|
+
/** Actions keyed by the clip names the script wrote (`open`, `loop`, `Princess twirl`). */
|
|
5
|
+
export type ScriptedClipActions = Record<string, AnimationAction | null | undefined>;
|
|
6
|
+
/** Own the action cache so replacing clips cannot retain bindings to an old artifact. */
|
|
7
|
+
export declare function useClipActions(clips: AnimationClip[], root: RefObject<Object3D | null>): Record<string, AnimationAction | null>;
|
|
8
|
+
/**
|
|
9
|
+
* An authored object's clips, each driven by its own toggle: an open-close
|
|
10
|
+
* effect plays `open` once and holds, closing plays `close` or `open` reversed;
|
|
11
|
+
* an ambient effect plays its clip while its toggle is on (always, for `loop`).
|
|
12
|
+
* Shared by the live item renderer and the baked viewer.
|
|
13
|
+
*/
|
|
14
|
+
export declare function ScriptedClips({ nodeId, interactive, actions, }: {
|
|
15
|
+
nodeId: AnyNodeId;
|
|
16
|
+
interactive: Interactive;
|
|
17
|
+
actions: ScriptedClipActions;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=scripted-clips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripted-clips.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/scripted-clips.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,WAAW,EAEjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,KAAK,SAAS,EAA+C,MAAM,OAAO,CAAA;AACnF,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,aAAa,EAIlB,QAAQ,EACT,MAAM,OAAO,CAAA;AAEd,2FAA2F;AAC3F,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;AAEpF,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC,0CAoCtF;AAQD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,WAAW,EACX,OAAO,GACR,EAAE;IACD,MAAM,EAAE,SAAS,CAAA;IACjB,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,EAAE,mBAAmB,CAAA;CAC7B,+BAoBA"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useInteractive, } from '@pascal-app/core';
|
|
4
|
+
import { useFrame } from '@react-three/fiber';
|
|
5
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
6
|
+
import { AnimationMixer, LoopOnce, LoopRepeat, Object3D, } from 'three';
|
|
7
|
+
/** Own the action cache so replacing clips cannot retain bindings to an old artifact. */
|
|
8
|
+
export function useClipActions(clips, root) {
|
|
9
|
+
const { mixer, actions, cached, lifecycle } = useMemo(() => {
|
|
10
|
+
const mixer = new AnimationMixer(new Object3D());
|
|
11
|
+
const cached = new Map();
|
|
12
|
+
const lifecycle = { active: true };
|
|
13
|
+
const actions = {};
|
|
14
|
+
for (const clip of clips) {
|
|
15
|
+
Object.defineProperty(actions, clip.name, {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
get: () => {
|
|
19
|
+
if (!lifecycle.active || !root.current)
|
|
20
|
+
return null;
|
|
21
|
+
let action = cached.get(clip.name);
|
|
22
|
+
if (!action) {
|
|
23
|
+
action = mixer.clipAction(clip, root.current);
|
|
24
|
+
cached.set(clip.name, action);
|
|
25
|
+
}
|
|
26
|
+
return action;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return { mixer, actions, cached, lifecycle };
|
|
31
|
+
}, [clips, root]);
|
|
32
|
+
useFrame((_, delta) => mixer.update(delta));
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
lifecycle.active = true;
|
|
35
|
+
return () => {
|
|
36
|
+
// React's development prop diff can read old getters after cleanup.
|
|
37
|
+
lifecycle.active = false;
|
|
38
|
+
mixer.stopAllAction();
|
|
39
|
+
const roots = new Set([...cached.values()].map((action) => action.getRoot()));
|
|
40
|
+
cached.clear();
|
|
41
|
+
for (const object of roots)
|
|
42
|
+
mixer.uncacheRoot(object);
|
|
43
|
+
};
|
|
44
|
+
}, [mixer, cached, lifecycle]);
|
|
45
|
+
return actions;
|
|
46
|
+
}
|
|
47
|
+
/** Whether an effect's toggle is on; an effect without one always runs. */
|
|
48
|
+
const useEffectControl = (nodeId, control) => useInteractive((s) => control === undefined ? true : Boolean(s.items[nodeId]?.controlValues[control]));
|
|
49
|
+
/**
|
|
50
|
+
* An authored object's clips, each driven by its own toggle: an open-close
|
|
51
|
+
* effect plays `open` once and holds, closing plays `close` or `open` reversed;
|
|
52
|
+
* an ambient effect plays its clip while its toggle is on (always, for `loop`).
|
|
53
|
+
* Shared by the live item renderer and the baked viewer.
|
|
54
|
+
*/
|
|
55
|
+
export function ScriptedClips({ nodeId, interactive, actions, }) {
|
|
56
|
+
const effects = interactive.effects.filter((effect) => effect.kind === 'animation');
|
|
57
|
+
return (_jsx(_Fragment, { children: effects.map((effect) => effect.mode === 'open-close' ? (_jsx(OpenCloseClip, { actions: actions, effect: effect, nodeId: nodeId }, effect.clips.on)) : (_jsx(PlayClip, { actions: actions, effect: effect, nodeId: nodeId }, effect.clips.on ?? effect.clips.loop))) }));
|
|
58
|
+
}
|
|
59
|
+
const PlayClip = ({ nodeId, effect, actions, }) => {
|
|
60
|
+
const on = useEffectControl(nodeId, effect.control);
|
|
61
|
+
const name = effect.clips.on ?? effect.clips.loop;
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const action = name ? actions[name] : undefined;
|
|
64
|
+
return () => {
|
|
65
|
+
action?.stop();
|
|
66
|
+
};
|
|
67
|
+
}, [actions, name]);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const action = name ? actions[name] : undefined;
|
|
70
|
+
if (!action)
|
|
71
|
+
return;
|
|
72
|
+
if (on) {
|
|
73
|
+
action.setLoop(LoopRepeat, Number.POSITIVE_INFINITY);
|
|
74
|
+
action.clampWhenFinished = false;
|
|
75
|
+
action.paused = false;
|
|
76
|
+
action.play();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Hold the pose where it was, like pausing a music box.
|
|
80
|
+
action.paused = true;
|
|
81
|
+
}
|
|
82
|
+
}, [actions, name, on]);
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
const OpenCloseClip = ({ nodeId, effect, actions, }) => {
|
|
86
|
+
const isOpen = useEffectControl(nodeId, effect.control);
|
|
87
|
+
const mounted = useRef(false);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const open = effect.clips.on ? actions[effect.clips.on] : undefined;
|
|
90
|
+
const close = effect.clips.off ? actions[effect.clips.off] : undefined;
|
|
91
|
+
return () => {
|
|
92
|
+
open?.stop();
|
|
93
|
+
close?.stop();
|
|
94
|
+
mounted.current = false;
|
|
95
|
+
};
|
|
96
|
+
}, [actions, effect.clips.on, effect.clips.off]);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const open = effect.clips.on ? actions[effect.clips.on] : undefined;
|
|
99
|
+
const close = effect.clips.off ? actions[effect.clips.off] : undefined;
|
|
100
|
+
if (!open)
|
|
101
|
+
return;
|
|
102
|
+
const first = !mounted.current;
|
|
103
|
+
mounted.current = true;
|
|
104
|
+
for (const action of [open, close]) {
|
|
105
|
+
if (!action)
|
|
106
|
+
continue;
|
|
107
|
+
action.setLoop(LoopOnce, 1);
|
|
108
|
+
action.clampWhenFinished = true;
|
|
109
|
+
}
|
|
110
|
+
if (isOpen) {
|
|
111
|
+
close?.stop();
|
|
112
|
+
open.paused = false;
|
|
113
|
+
open.timeScale = 1;
|
|
114
|
+
if (!open.isRunning())
|
|
115
|
+
open.reset();
|
|
116
|
+
open.play();
|
|
117
|
+
// Already open when the scene loads: hold the open pose, no swing.
|
|
118
|
+
if (first)
|
|
119
|
+
open.time = open.getClip().duration;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (first)
|
|
123
|
+
return;
|
|
124
|
+
if (close) {
|
|
125
|
+
open.stop();
|
|
126
|
+
close.reset().play();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
open.paused = false;
|
|
130
|
+
open.timeScale = -1;
|
|
131
|
+
open.play();
|
|
132
|
+
}, [actions, effect.clips.on, effect.clips.off, isOpen]);
|
|
133
|
+
return null;
|
|
134
|
+
};
|