@pascal-app/viewer 1.0.3 → 1.0.5
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/capture/capture-runtime.js +1 -1
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.d.ts +1 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +12 -4
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
- 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 +253 -86
- 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-reference-nodes.js +1 -1
- package/dist/components/viewer/glb-replace-instances.js +1 -1
- 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 +421 -508
- 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 +13 -2
- package/dist/components/viewer/index.d.ts +24 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +111 -37
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +113 -22
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +6 -0
- package/dist/components/viewer/perf-panel.d.ts.map +1 -1
- package/dist/components/viewer/perf-panel.js +1 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +104 -36
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +17 -3
- 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-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +1 -1
- package/dist/components/viewer/scene-environment.d.ts.map +1 -1
- package/dist/components/viewer/scene-environment.js +31 -11
- 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 +25 -7
- package/dist/components/viewer/underground-ambiance.d.ts +9 -0
- package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
- package/dist/components/viewer/underground-ambiance.js +105 -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 +12 -2
- package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -0
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/index.d.ts +36 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -19
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -1
- package/dist/lib/backdrop.d.ts +17 -0
- package/dist/lib/backdrop.d.ts.map +1 -1
- package/dist/lib/backdrop.js +18 -1
- package/dist/lib/construction-dust.d.ts +57 -0
- package/dist/lib/construction-dust.d.ts.map +1 -0
- package/dist/lib/construction-dust.js +187 -0
- 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/foundation-materials.d.ts +26 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -0
- package/dist/lib/foundation-materials.js +65 -0
- package/dist/lib/gltf-unlit.d.ts +10 -0
- package/dist/lib/gltf-unlit.d.ts.map +1 -0
- package/dist/lib/gltf-unlit.js +23 -0
- package/dist/lib/ground-window.d.ts +33 -0
- package/dist/lib/ground-window.d.ts.map +1 -0
- package/dist/lib/ground-window.js +114 -0
- package/dist/lib/hero-pose.d.ts +13 -3
- 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 +6 -3
- 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/ktx2-loader.d.ts +7 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +35 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layers.d.ts +11 -7
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +16 -6
- 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 +19 -7
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +65 -9
- package/dist/lib/merged-outline-node.d.ts +5 -0
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +80 -2
- 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 +26 -2
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +99 -12
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- 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/reveal-candidates.d.ts +80 -0
- package/dist/lib/reveal-candidates.d.ts.map +1 -0
- package/dist/lib/reveal-candidates.js +524 -0
- package/dist/lib/reveal-ghost.d.ts +9 -0
- package/dist/lib/reveal-ghost.d.ts.map +1 -0
- package/dist/lib/reveal-ghost.js +62 -0
- package/dist/lib/reveal-pending.d.ts +12 -0
- package/dist/lib/reveal-pending.d.ts.map +1 -0
- package/dist/lib/reveal-pending.js +12 -0
- package/dist/lib/reveal-pose.d.ts +70 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -0
- package/dist/lib/reveal-pose.js +248 -0
- package/dist/lib/reveal-schedule.d.ts +208 -0
- package/dist/lib/reveal-schedule.d.ts.map +1 -0
- package/dist/lib/reveal-schedule.js +410 -0
- package/dist/lib/reveal-texture-scale.d.ts +5 -0
- package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
- package/dist/lib/reveal-texture-scale.js +113 -0
- package/dist/lib/reveal-windows.d.ts +23 -0
- package/dist/lib/reveal-windows.d.ts.map +1 -0
- package/dist/lib/reveal-windows.js +31 -0
- package/dist/lib/reveal-wipe.d.ts +71 -0
- package/dist/lib/reveal-wipe.d.ts.map +1 -0
- package/dist/lib/reveal-wipe.js +105 -0
- package/dist/lib/scene-pass-mrt.d.ts +12 -0
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
- package/dist/lib/scene-pass-mrt.js +23 -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-themes.d.ts +30 -8
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +36 -0
- package/dist/lib/scene-visibility.d.ts +17 -1
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +53 -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/site-surfaces.d.ts +32 -0
- package/dist/lib/site-surfaces.d.ts.map +1 -0
- package/dist/lib/site-surfaces.js +70 -0
- package/dist/lib/snapshot-pipeline.d.ts +34 -3
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +247 -117
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
- package/dist/lib/terrain-perimeter-fill.js +22 -14
- package/dist/lib/tsl-compat.d.ts +2 -2
- package/dist/lib/tsl-compat.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.d.ts +168 -0
- package/dist/lib/underground-ambiance.d.ts.map +1 -0
- package/dist/lib/underground-ambiance.js +334 -0
- 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 +194 -26
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +164 -21
- 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 +88 -6
- package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
- package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
- package/dist/systems/construction-reveal/construction-dust.js +92 -0
- package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
- package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
- package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
- package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
- package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
- package/dist/systems/construction-reveal/dust-material.js +17 -0
- package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
- package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
- package/dist/systems/construction-reveal/ghost-dress.js +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +55 -17
- package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +18 -1
- 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/control-widget.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +52 -17
- 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 +48 -53
- package/dist/systems/level/level-hold.d.ts +4 -0
- package/dist/systems/level/level-hold.d.ts.map +1 -0
- package/dist/systems/level/level-hold.js +14 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -6
- package/dist/systems/level/level-utils.d.ts +19 -6
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +68 -9
- 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/__fixtures__/roof-plane-cases.d.ts +103 -0
- package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
- package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
- package/dist/systems/roof/roof-materials.d.ts +16 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +64 -31
- package/dist/systems/roof/roof-system.d.ts +21 -4
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +374 -49
- 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 +39 -3
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +141 -19
- 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-material-variant.d.ts.map +1 -1
- 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 +135 -153
- package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.d.ts +33 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +621 -159
- package/dist/systems/window/corner-frame.d.ts +10 -0
- package/dist/systems/window/corner-frame.d.ts.map +1 -0
- package/dist/systems/window/corner-frame.js +17 -0
- 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 +71 -27
- package/package.json +10 -10
- 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,27 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
3
|
-
import { pointInPolygon, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { isScriptedNode, pointInPolygon, sceneRegistry, scriptedOrigin, scriptedSize, scriptInteractive, 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,18 @@ 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
|
|
32
|
+
.filter((n) => (isScriptedNode(n) && scriptInteractive(n.source.manifest) != null) ||
|
|
33
|
+
(n.type === 'item' && n.asset.interactive != null) ||
|
|
34
|
+
(n.type === 'procedural-item' &&
|
|
35
|
+
(operableParts(n.recipe).length > 0 || n.recipe.parts.some((part) => part.light))))
|
|
33
36
|
.map((n) => n.id)));
|
|
34
|
-
return (_jsx(_Fragment, { children: interactiveNodeIds
|
|
37
|
+
return (_jsx(_Fragment, { children: interactiveNodeIds
|
|
38
|
+
.filter((id) => zonePolygon?.length || selectedIds.includes(id))
|
|
39
|
+
.map((id) => (_jsx(ItemControlsOverlay, { isSelected: selectedIds.includes(id), nodeId: id, zonePolygon: zonePolygon }, id))) }));
|
|
35
40
|
};
|
|
36
41
|
// ---- Child: polls sceneRegistry then portals controls into the item group ----
|
|
37
42
|
const FADE_MS = 300;
|
|
38
|
-
const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
43
|
+
const ItemControlsOverlay = ({ nodeId, zonePolygon, isSelected, }) => {
|
|
39
44
|
const node = useScene((state) => state.nodes[nodeId]);
|
|
40
45
|
const [itemObj, setItemObj] = useState(null);
|
|
41
46
|
useFrame(() => {
|
|
@@ -46,11 +51,43 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
|
46
51
|
setItemObj(obj);
|
|
47
52
|
});
|
|
48
53
|
const controlValues = useInteractive(useShallow((state) => state.items[nodeId]?.controlValues));
|
|
54
|
+
const proceduralState = useInteractive((state) => state.procedural[nodeId]);
|
|
55
|
+
const lampDefault = useInteractive((state) => state.lampDefault);
|
|
49
56
|
const setControlValue = useInteractive((state) => state.setControlValue);
|
|
50
|
-
|
|
57
|
+
const togglePart = useInteractive((state) => state.toggleProceduralPart);
|
|
58
|
+
const toggleLights = useInteractive((state) => state.toggleProceduralLights);
|
|
59
|
+
const proceduralHeight = useMemo(() => node?.type === 'procedural-item' ? evaluateRecipe(node.recipe, node.parameters).max[1] : 0, [
|
|
60
|
+
node?.type === 'procedural-item' ? node.recipe : null,
|
|
61
|
+
node?.type === 'procedural-item' ? node.parameters : null,
|
|
62
|
+
]);
|
|
63
|
+
let descriptors = [];
|
|
64
|
+
let height = 0;
|
|
65
|
+
const interactive = isScriptedNode(node)
|
|
66
|
+
? scriptInteractive(node.source.manifest, scriptedOrigin(node))
|
|
67
|
+
: node?.type === 'item'
|
|
68
|
+
? node.asset.interactive
|
|
69
|
+
: undefined;
|
|
70
|
+
if (interactive && controlValues) {
|
|
71
|
+
descriptors = interactive.controls.map((control, i) => ({
|
|
72
|
+
key: String(i),
|
|
73
|
+
control,
|
|
74
|
+
value: controlValues[i] ?? false,
|
|
75
|
+
onChange: (value) => setControlValue(nodeId, i, value),
|
|
76
|
+
}));
|
|
77
|
+
height = isScriptedNode(node)
|
|
78
|
+
? scriptedSize(node.source.manifest)[1] + scriptedOrigin(node)[1]
|
|
79
|
+
: node?.type === 'item'
|
|
80
|
+
? node.asset.dimensions[1]
|
|
81
|
+
: 0;
|
|
82
|
+
}
|
|
83
|
+
else if (node?.type === 'procedural-item') {
|
|
84
|
+
descriptors = proceduralControlDescriptors(node.recipe, proceduralState, (partId) => togglePart(nodeId, partId), () => toggleLights(nodeId), lampDefault);
|
|
85
|
+
height = proceduralHeight;
|
|
86
|
+
}
|
|
87
|
+
let visible = isSelected;
|
|
51
88
|
if (itemObj && zonePolygon?.length) {
|
|
52
89
|
itemObj.getWorldPosition(_tempVec);
|
|
53
|
-
visible = pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
|
|
90
|
+
visible = visible || pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
|
|
54
91
|
}
|
|
55
92
|
// Fade in on mount and fade out before unmounting the <Html>.
|
|
56
93
|
const [mounted, setMounted] = useState(false);
|
|
@@ -73,10 +110,8 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
|
73
110
|
const timeout = setTimeout(() => setMounted(false), FADE_MS);
|
|
74
111
|
return () => clearTimeout(timeout);
|
|
75
112
|
}, [visible]);
|
|
76
|
-
if (!(mounted && itemObj &&
|
|
113
|
+
if (!(mounted && itemObj && descriptors.length))
|
|
77
114
|
return null;
|
|
78
|
-
const { controls } = node.asset.interactive;
|
|
79
|
-
const [, height] = node.asset.dimensions;
|
|
80
115
|
return createPortal(_jsx(Html, { center: true, distanceFactor: 8, eps: -1, position: [0, height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { style: {
|
|
81
116
|
display: 'flex',
|
|
82
117
|
flexDirection: 'column',
|
|
@@ -90,5 +125,5 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
|
90
125
|
userSelect: 'none',
|
|
91
126
|
opacity: shown ? 1 : 0,
|
|
92
127
|
transition: `opacity ${FADE_MS}ms ease`,
|
|
93
|
-
}, children:
|
|
128
|
+
}, children: descriptors.map((descriptor) => (_jsx(ControlWidget, { control: descriptor.control, onChange: descriptor.onChange, value: descriptor.value }, descriptor.key))) }) }), itemObj);
|
|
94
129
|
};
|
|
@@ -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 { Fragment as _Fragment, jsx as _jsx } 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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { findLevelAncestorId, sceneRegistry,
|
|
3
|
-
import { useFrame } from '@react-three/fiber';
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { findLevelAncestorId, sceneRegistry, useScene } from '@pascal-app/core';
|
|
3
|
+
import { useFrame, useThree } from '@react-three/fiber';
|
|
4
4
|
import { useRef } from 'react';
|
|
5
5
|
import { MathUtils, Vector3 } from 'three';
|
|
6
|
+
import { SCENE_LAYER } from '../../lib/layers';
|
|
6
7
|
import { useItemLightPool } from '../../store/use-item-light-pool';
|
|
7
8
|
import useViewer from '../../store/use-viewer';
|
|
8
9
|
const POOL_SIZE = 12;
|
|
@@ -19,22 +20,30 @@ const _dir = new Vector3();
|
|
|
19
20
|
const _camPos = new Vector3();
|
|
20
21
|
const _camFwd = new Vector3();
|
|
21
22
|
const _itemPos = new Vector3();
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (!
|
|
23
|
+
function isRendered(nodeId) {
|
|
24
|
+
let object = sceneRegistry.nodes.get(nodeId);
|
|
25
|
+
if (!object?.layers.isEnabled(SCENE_LAYER))
|
|
26
|
+
return false;
|
|
27
|
+
while (object) {
|
|
28
|
+
if (!object.visible)
|
|
29
|
+
return false;
|
|
30
|
+
object = object.parent;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function scoreRegistration(reg, nodes, selectedLevelId, levelMode, hideAbove) {
|
|
35
|
+
if (!reg.isEligible() || !isRendered(reg.nodeId) || !reg.getWorldPosition(_itemPos))
|
|
36
|
+
return Number.POSITIVE_INFINITY;
|
|
37
|
+
const { nodeId } = reg;
|
|
38
|
+
let current = nodes[nodeId];
|
|
39
|
+
while (current) {
|
|
40
|
+
if (current.type !== 'site' && current.visible === false)
|
|
28
41
|
return Number.POSITIVE_INFINITY;
|
|
42
|
+
current = current.parentId ? nodes[current.parentId] : undefined;
|
|
29
43
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (!obj)
|
|
44
|
+
const itemLevelId = findLevelAncestorId(nodeId, nodes);
|
|
45
|
+
if (selectedLevelId && itemLevelId !== selectedLevelId && levelMode === 'solo')
|
|
33
46
|
return Number.POSITIVE_INFINITY;
|
|
34
|
-
obj.getWorldPosition(_itemPos);
|
|
35
|
-
_itemPos.x += effect.offset[0];
|
|
36
|
-
_itemPos.y += effect.offset[1];
|
|
37
|
-
_itemPos.z += effect.offset[2];
|
|
38
47
|
_dir.copy(_itemPos).sub(_camPos).normalize();
|
|
39
48
|
const dot = _camFwd.dot(_dir); // 1 = ahead, -1 = behind
|
|
40
49
|
// Angular component (0 = dead ahead, 2 = directly behind)
|
|
@@ -42,12 +51,15 @@ function scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveSt
|
|
|
42
51
|
// Normalised distance component (assumes scenes < 200 units)
|
|
43
52
|
const dist = _camPos.distanceTo(_itemPos) / 200;
|
|
44
53
|
// ── Level factor ──────────────────────────────────────────────────────────
|
|
45
|
-
const itemLevelId = findLevelAncestorId(nodeId, nodes);
|
|
46
54
|
let levelPenalty = 0;
|
|
47
55
|
if (selectedLevelId) {
|
|
48
56
|
if (itemLevelId !== selectedLevelId) {
|
|
49
|
-
//
|
|
50
|
-
|
|
57
|
+
// Items on hidden levels (every other one in solo, those above the
|
|
58
|
+
// selected one while editing) are invisible — deprioritize strongly
|
|
59
|
+
const itemIndex = nodes[itemLevelId]?.level ?? 0;
|
|
60
|
+
const selectedIndex = nodes[selectedLevelId]?.level ?? 0;
|
|
61
|
+
const hidden = levelMode === 'solo' || (hideAbove && itemIndex > selectedIndex);
|
|
62
|
+
levelPenalty = hidden ? 100 : 0.8;
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
else if (itemLevelId) {
|
|
@@ -60,6 +72,8 @@ function scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveSt
|
|
|
60
72
|
return angular * 0.7 + dist * 0.3 + levelPenalty;
|
|
61
73
|
}
|
|
62
74
|
export function ItemLightSystem() {
|
|
75
|
+
const scene = useThree((state) => state.scene);
|
|
76
|
+
const bakedOwner = useItemLightPool((state) => state.bakedCanvases.has(scene));
|
|
63
77
|
const lightRefs = useRef(Array.from({ length: POOL_SIZE }, () => null));
|
|
64
78
|
const slots = useRef(Array.from({ length: POOL_SIZE }, () => ({ key: null, pendingKey: null, isFadingOut: false })));
|
|
65
79
|
const reassignTimer = useRef(0);
|
|
@@ -67,9 +81,10 @@ export function ItemLightSystem() {
|
|
|
67
81
|
const prevReassignCamPos = useRef(new Vector3());
|
|
68
82
|
const prevReassignCamFwd = useRef(new Vector3(0, 0, -1));
|
|
69
83
|
useFrame(({ camera }, delta) => {
|
|
84
|
+
if (bakedOwner)
|
|
85
|
+
return;
|
|
70
86
|
const dt = Math.min(delta, 0.1);
|
|
71
87
|
const { registrations } = useItemLightPool.getState();
|
|
72
|
-
const interactiveState = useInteractive.getState();
|
|
73
88
|
// ── 1. Throttled priority reassignment ──────────────────────────────────
|
|
74
89
|
camera.getWorldPosition(_camPos);
|
|
75
90
|
camera.getWorldDirection(_camFwd);
|
|
@@ -86,12 +101,13 @@ export function ItemLightSystem() {
|
|
|
86
101
|
const viewerState = useViewer.getState();
|
|
87
102
|
const selectedLevelId = viewerState.selection.levelId;
|
|
88
103
|
const levelMode = viewerState.levelMode;
|
|
104
|
+
const hideAbove = viewerState.hideLevelsAboveSelection;
|
|
89
105
|
// Score every registration
|
|
90
106
|
const scored = [];
|
|
91
107
|
for (const [key, reg] of registrations) {
|
|
92
108
|
scored.push({
|
|
93
109
|
key,
|
|
94
|
-
score: scoreRegistration(reg, nodes, selectedLevelId, levelMode,
|
|
110
|
+
score: scoreRegistration(reg, nodes, selectedLevelId, levelMode, hideAbove),
|
|
95
111
|
});
|
|
96
112
|
}
|
|
97
113
|
scored.sort((a, b) => a.score - b.score);
|
|
@@ -155,8 +171,8 @@ export function ItemLightSystem() {
|
|
|
155
171
|
const light = lightRefs.current[freeSlot];
|
|
156
172
|
const reg = registrations.get(key);
|
|
157
173
|
if (light && reg) {
|
|
158
|
-
light.color.set(reg.
|
|
159
|
-
light.distance = reg.
|
|
174
|
+
light.color.set(reg.color);
|
|
175
|
+
light.distance = reg.distance;
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
178
|
}
|
|
@@ -183,19 +199,17 @@ export function ItemLightSystem() {
|
|
|
183
199
|
continue;
|
|
184
200
|
// Fade-out phase: lerp intensity → 0, then complete the transition
|
|
185
201
|
if (slot.isFadingOut) {
|
|
186
|
-
light.visible = true;
|
|
187
202
|
light.intensity = MathUtils.lerp(light.intensity, 0, dt * 12);
|
|
188
203
|
if (light.intensity < 0.01) {
|
|
189
204
|
light.intensity = 0;
|
|
190
|
-
light.visible = false;
|
|
191
205
|
slot.isFadingOut = false;
|
|
192
206
|
slot.key = slot.pendingKey;
|
|
193
207
|
slot.pendingKey = null;
|
|
194
208
|
if (slot.key) {
|
|
195
209
|
const reg = registrations.get(slot.key);
|
|
196
210
|
if (reg) {
|
|
197
|
-
light.color.set(reg.
|
|
198
|
-
light.distance = reg.
|
|
211
|
+
light.color.set(reg.color);
|
|
212
|
+
light.distance = reg.distance;
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
}
|
|
@@ -204,45 +218,26 @@ export function ItemLightSystem() {
|
|
|
204
218
|
if (!slot.key) {
|
|
205
219
|
// Idle slot — keep dark
|
|
206
220
|
light.intensity = 0;
|
|
207
|
-
light.visible = false;
|
|
208
221
|
continue;
|
|
209
222
|
}
|
|
210
223
|
const reg = registrations.get(slot.key);
|
|
211
224
|
if (!reg) {
|
|
212
225
|
slot.key = null;
|
|
213
226
|
light.intensity = 0;
|
|
214
|
-
light.visible = false;
|
|
215
227
|
continue;
|
|
216
228
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
obj.getWorldPosition(_itemPos);
|
|
221
|
-
const [ox, oy, oz] = reg.effect.offset;
|
|
222
|
-
light.position.set(_itemPos.x + ox, _itemPos.y + oy, _itemPos.z + oz);
|
|
223
|
-
}
|
|
224
|
-
// Compute target intensity
|
|
225
|
-
const values = interactiveState.items[reg.nodeId]?.controlValues;
|
|
226
|
-
const isOn = reg.toggleIndex >= 0 ? Boolean(values?.[reg.toggleIndex]) : true;
|
|
227
|
-
let t = 1;
|
|
228
|
-
if (reg.hasSlider) {
|
|
229
|
-
const raw = values?.[reg.sliderIndex] ?? reg.sliderMin;
|
|
230
|
-
t = (raw - reg.sliderMin) / (reg.sliderMax - reg.sliderMin);
|
|
231
|
-
}
|
|
232
|
-
const targetIntensity = isOn
|
|
233
|
-
? MathUtils.lerp(reg.effect.intensityRange[0], reg.effect.intensityRange[1], t)
|
|
234
|
-
: reg.effect.intensityRange[0];
|
|
235
|
-
if (targetIntensity > 0) {
|
|
236
|
-
light.visible = true;
|
|
237
|
-
}
|
|
229
|
+
if (reg.getWorldPosition(_itemPos))
|
|
230
|
+
light.position.copy(_itemPos);
|
|
231
|
+
const targetIntensity = reg.isEligible() && isRendered(reg.nodeId) ? reg.getIntensity() : 0;
|
|
238
232
|
light.intensity = MathUtils.lerp(light.intensity, targetIntensity, dt * 12);
|
|
239
233
|
if (targetIntensity <= 0 && light.intensity < 0.01) {
|
|
240
234
|
light.intensity = 0;
|
|
241
|
-
light.visible = false;
|
|
242
235
|
}
|
|
243
236
|
}
|
|
244
|
-
});
|
|
245
|
-
|
|
237
|
+
}, 6);
|
|
238
|
+
if (bakedOwner)
|
|
239
|
+
return null;
|
|
240
|
+
return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, visible: true, ref: (el) => {
|
|
246
241
|
lightRefs.current[i] = el;
|
|
247
242
|
} }, i))) }));
|
|
248
243
|
}
|