@pascal-app/viewer 1.0.3 → 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 +14 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +84 -26
- 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 +4 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +11 -1
- package/dist/index.d.ts +22 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -14
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -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/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 +55 -17
- 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 +33 -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 +3 -5
- package/dist/systems/level/level-utils.d.ts +7 -4
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +17 -6
- 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 +7 -0
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +46 -17
- package/dist/systems/wall/wall-cutout.d.ts +6 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +42 -28
- 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 +9 -4
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +99 -152
- package/dist/systems/wall/wall-system.d.ts +28 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +582 -143
- 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 +41 -22
- package/package.json +5 -5
- 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
|
@@ -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
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"item-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item/item-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"item-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item/item-system.tsx"],"names":[],"mappings":"AAmBA;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,YAsDtB,CAAA"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { sceneRegistry, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { getEffectiveNode, getWallBodyCenterOffset, getWallLocalFaceZ, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { initializeObjectCutInvalidation } from './object-cut-invalidation';
|
|
3
5
|
// ============================================================================
|
|
4
6
|
// ITEM SYSTEM
|
|
5
7
|
// ============================================================================
|
|
@@ -11,6 +13,7 @@ import { useFrame } from '@react-three/fiber';
|
|
|
11
13
|
* mark at priority 2.
|
|
12
14
|
*/
|
|
13
15
|
export const ItemSystem = () => {
|
|
16
|
+
useEffect(initializeObjectCutInvalidation, []);
|
|
14
17
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
15
18
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
16
19
|
useFrame(() => {
|
|
@@ -25,6 +28,10 @@ export const ItemSystem = () => {
|
|
|
25
28
|
const mesh = sceneRegistry.nodes.get(id);
|
|
26
29
|
if (!mesh)
|
|
27
30
|
return;
|
|
31
|
+
const host = item.parentId ? nodes[item.parentId] : undefined;
|
|
32
|
+
if (item.asset.attachTo === 'wall' && host?.type === 'wall') {
|
|
33
|
+
mesh.position.z = item.position[2] + getWallBodyCenterOffset(getEffectiveNode(host));
|
|
34
|
+
}
|
|
28
35
|
if (item.asset.attachTo === 'wall-side') {
|
|
29
36
|
// Wall-attached item: offset Z by half the host wall's thickness.
|
|
30
37
|
// Roof-segment wall faces share the convention — the face frame's
|
|
@@ -37,8 +44,7 @@ export const ItemSystem = () => {
|
|
|
37
44
|
? (parent.wallThickness ?? 0.1)
|
|
38
45
|
: undefined;
|
|
39
46
|
if (thickness !== undefined) {
|
|
40
|
-
|
|
41
|
-
mesh.position.z = (thickness / 2) * side;
|
|
47
|
+
mesh.position.z = getWallLocalFaceZ(parent?.type === 'wall' ? getEffectiveNode(parent) : { thickness }, item.side === 'front' ? 'a' : 'b');
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
// Hold the mark until the model settles (loaded, terminally failed, or
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-cut-invalidation.d.ts","sourceRoot":"","sources":["../../../src/systems/item/object-cut-invalidation.ts"],"names":[],"mappings":"AAUA,6EAA6E;AAC7E,wBAAgB,+BAA+B,eA6D9C"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getEffectiveCutterNode, resolveCutterHost, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
|
|
2
|
+
/** Geometry remains derived; only dirty marks cross into the scene store. */
|
|
3
|
+
export function initializeObjectCutInvalidation() {
|
|
4
|
+
let previous = new Map();
|
|
5
|
+
let cutterIds = [];
|
|
6
|
+
const collect = (nodes) => {
|
|
7
|
+
cutterIds = [];
|
|
8
|
+
for (const node of Object.values(nodes)) {
|
|
9
|
+
if (node.type === 'item' && node.source?.manifest.cutters?.length)
|
|
10
|
+
cutterIds.push(node.id);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
// Drags fire every frame: a scene without cutters pays nothing here.
|
|
14
|
+
const refresh = () => {
|
|
15
|
+
if (!cutterIds.length && !previous.size)
|
|
16
|
+
return;
|
|
17
|
+
const state = useScene.getState();
|
|
18
|
+
const nodes = { ...state.nodes };
|
|
19
|
+
for (const id of new Set([
|
|
20
|
+
...useLiveNodeOverrides.getState().overrides.keys(),
|
|
21
|
+
...useLiveTransforms.getState().transforms.keys(),
|
|
22
|
+
])) {
|
|
23
|
+
if (nodes[id])
|
|
24
|
+
nodes[id] = getEffectiveCutterNode(nodes[id]);
|
|
25
|
+
}
|
|
26
|
+
const owners = new Map();
|
|
27
|
+
for (const id of cutterIds) {
|
|
28
|
+
const node = nodes[id];
|
|
29
|
+
if (node?.type !== 'item' || !node.source?.manifest.cutters?.length)
|
|
30
|
+
continue;
|
|
31
|
+
for (const cutter of node.source.manifest.cutters) {
|
|
32
|
+
const host = resolveCutterHost(node, cutter.host === 'mounted' ? 'cutout' : `cut:${cutter.host}`, nodes);
|
|
33
|
+
if (!host)
|
|
34
|
+
continue;
|
|
35
|
+
const entries = owners.get(host.id) ?? [];
|
|
36
|
+
entries.push([node.id, node.source.artifact, node.position, node.rotation, node.scale]);
|
|
37
|
+
owners.set(host.id, entries);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const next = new Map([...owners].map(([id, entries]) => [id, JSON.stringify(entries)]));
|
|
41
|
+
const changed = [...new Set([...previous.keys(), ...next.keys()])].filter((id) => previous.get(id) !== next.get(id));
|
|
42
|
+
previous = next;
|
|
43
|
+
for (const id of changed)
|
|
44
|
+
if (nodes[id])
|
|
45
|
+
state.markDirty(id);
|
|
46
|
+
};
|
|
47
|
+
collect(useScene.getState().nodes);
|
|
48
|
+
refresh();
|
|
49
|
+
const unsubscribeScene = useScene.subscribe((state, before) => {
|
|
50
|
+
if (state.nodes === before.nodes)
|
|
51
|
+
return;
|
|
52
|
+
collect(state.nodes);
|
|
53
|
+
refresh();
|
|
54
|
+
});
|
|
55
|
+
const unsubscribeLive = useLiveNodeOverrides.subscribe((state, before) => {
|
|
56
|
+
if (state.overrides !== before.overrides)
|
|
57
|
+
refresh();
|
|
58
|
+
});
|
|
59
|
+
const unsubscribeTransforms = useLiveTransforms.subscribe((state, before) => {
|
|
60
|
+
if (state.transforms !== before.transforms)
|
|
61
|
+
refresh();
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
unsubscribeScene();
|
|
65
|
+
unsubscribeLive();
|
|
66
|
+
unsubscribeTransforms();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function ItemLightSystem(): import("react").JSX.Element;
|
|
1
|
+
export declare function ItemLightSystem(): import("react").JSX.Element | null;
|
|
2
2
|
//# sourceMappingURL=item-light-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"item-light-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item-light/item-light-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"item-light-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item-light/item-light-system.tsx"],"names":[],"mappings":"AAgGA,wBAAgB,eAAe,uCA0M9B"}
|