@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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/node-renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"node-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/node-renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,EAIZ,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,KAAK,aAAa,EAAqD,MAAM,OAAO,CAAA;AAO7F,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,GAC9B,aAAa,CAAC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,IAAI,CASzC;AAED,eAAO,MAAM,YAAY,GAAI,YAAY;IAAE,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,uCA+BjE,CAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { isNodeKindEnabled, nodeRegistry, useScene, } from '@pascal-app/core';
|
|
4
|
-
import { lazy, Suspense } from 'react';
|
|
3
|
+
import { isNodeKindEnabled, nodeRegistry, onRegistryChange, useScene, } from '@pascal-app/core';
|
|
4
|
+
import { lazy, Suspense, useCallback, useSyncExternalStore } from 'react';
|
|
5
5
|
import { ParametricNodeRenderer } from './parametric-node-renderer';
|
|
6
6
|
// Cache lazy components by their RendererSource so React.lazy isn't re-invoked
|
|
7
7
|
// on every render — that would create a new Suspense boundary each time.
|
|
@@ -21,11 +21,16 @@ export function getRegistryRenderer(source) {
|
|
|
21
21
|
export const NodeRenderer = ({ nodeId }) => {
|
|
22
22
|
const node = useScene((state) => state.nodes[nodeId]);
|
|
23
23
|
const installedPlugins = useScene((state) => state.installedPlugins);
|
|
24
|
+
// Plugins register after the first mount (async discovery). Subscribe to this
|
|
25
|
+
// node's own kind only: a registration re-renders the nodes of that kind and
|
|
26
|
+
// leaves every other mounted node alone.
|
|
27
|
+
const kind = node?.type;
|
|
28
|
+
const readDefinition = useCallback(() => (kind ? nodeRegistry.get(kind) : undefined), [kind]);
|
|
29
|
+
const def = useSyncExternalStore(onRegistryChange, readDefinition, readDefinition);
|
|
24
30
|
if (!node)
|
|
25
31
|
return null;
|
|
26
32
|
if (!isNodeKindEnabled(node.type, installedPlugins))
|
|
27
33
|
return null;
|
|
28
|
-
const def = nodeRegistry.get(node.type);
|
|
29
34
|
if (!def)
|
|
30
35
|
return null;
|
|
31
36
|
// Two-checkbox dispatch (see wiki/architecture/node-definitions.md):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frame-limiter.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/frame-limiter.tsx"],"names":[],"mappings":"AAKA,KAAK,iBAAiB,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"frame-limiter.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/frame-limiter.tsx"],"names":[],"mappings":"AAKA,KAAK,iBAAiB,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACxC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IACjE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,SAAI,GAAG,UAAU,CAwB5D;AAgBD,QAAA,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmG7C,CAAA;AAED,eAAe,YAAY,CAAA"}
|
|
@@ -40,8 +40,10 @@ const DRAW_DISABLED = typeof window !== 'undefined' &&
|
|
|
40
40
|
new Set((new URLSearchParams(window.location.search).get('disable') ?? '')
|
|
41
41
|
.split(',')
|
|
42
42
|
.map((s) => s.trim())).has('draw');
|
|
43
|
-
const FrameLimiter = ({ fps = 50, paused = false }) => {
|
|
44
|
-
const { advance, set, frameloop: initFrameloop } = useThree();
|
|
43
|
+
const FrameLimiter = ({ fps = 50, paused = false, onFrameError }) => {
|
|
44
|
+
const { advance: advanceFrame, set, frameloop: initFrameloop } = useThree();
|
|
45
|
+
const onFrameErrorRef = useRef(onFrameError);
|
|
46
|
+
onFrameErrorRef.current = onFrameError;
|
|
45
47
|
const nextFrameTimeRef = useRef(0);
|
|
46
48
|
const renderer = useThree((state) => state.gl);
|
|
47
49
|
const size = useThree((state) => state.size);
|
|
@@ -55,6 +57,23 @@ const FrameLimiter = ({ fps = 50, paused = false }) => {
|
|
|
55
57
|
let raf = null;
|
|
56
58
|
let timer = null;
|
|
57
59
|
let sizeSynced = false;
|
|
60
|
+
// A system that throws inside useFrame runs outside React's error
|
|
61
|
+
// boundaries: report the first failure to the host and keep the loop alive.
|
|
62
|
+
let frameErrorReported = false;
|
|
63
|
+
function advance(frameTime) {
|
|
64
|
+
try {
|
|
65
|
+
advanceFrame(frameTime);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (frameErrorReported)
|
|
69
|
+
return;
|
|
70
|
+
frameErrorReported = true;
|
|
71
|
+
if (onFrameErrorRef.current)
|
|
72
|
+
onFrameErrorRef.current(error);
|
|
73
|
+
else
|
|
74
|
+
console.error('[viewer] a frame callback threw', error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
58
77
|
const effectiveFps = Number.isFinite(fps) && fps > 0 ? fps : 50;
|
|
59
78
|
const interval = 1000 / effectiveFps;
|
|
60
79
|
function syncSize() {
|
|
@@ -115,7 +134,7 @@ const FrameLimiter = ({ fps = 50, paused = false }) => {
|
|
|
115
134
|
set({ frameloop: initFrameloop });
|
|
116
135
|
};
|
|
117
136
|
}, [
|
|
118
|
-
|
|
137
|
+
advanceFrame,
|
|
119
138
|
dpr,
|
|
120
139
|
fps,
|
|
121
140
|
initFrameloop,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AnyNodeId, type Interactive, type SceneGraph } from '@pascal-app/core';
|
|
2
|
-
import { type
|
|
2
|
+
import { type EvaluatedLight, type ProceduralItemNode } from '@pascal-app/core/procedural-items';
|
|
3
|
+
import { type AnimationAction, type Object3D, Vector3 } from 'three';
|
|
3
4
|
/** An interactive item recovered from the scene graph so the baked GLB can be
|
|
4
5
|
* re-lit / re-animated by joining on `pascalId`. The GLB carries the geometry
|
|
5
6
|
* + identity; the effects + controls live in the DB scene graph (no sidecar). */
|
|
@@ -9,17 +10,29 @@ export type GlbInteractiveItem = {
|
|
|
9
10
|
/** Item height (world units) for placing the controls overlay above it. */
|
|
10
11
|
height: number;
|
|
11
12
|
interactive: Interactive;
|
|
13
|
+
/** Built from a script: its clips keep the script's names (`<pascalId>: <name>`
|
|
14
|
+
* in the GLB) and its toggles start at their own defaults. */
|
|
15
|
+
scripted?: boolean;
|
|
16
|
+
localScale?: [number, number, number];
|
|
17
|
+
procedural?: {
|
|
18
|
+
lights: EvaluatedLight[];
|
|
19
|
+
parts: ProceduralItemNode['recipe']['parts'];
|
|
20
|
+
/** v2: the recipe's parts and joints, so non-fixed joints get controls too. */
|
|
21
|
+
recipe?: Pick<ProceduralItemNode['recipe'], 'parts' | 'joints'>;
|
|
22
|
+
};
|
|
12
23
|
};
|
|
13
24
|
/** A baked zone's identity node + its local floor polygon (from `extras`). */
|
|
14
25
|
export type GlbZoneRef = {
|
|
15
26
|
id: string;
|
|
16
27
|
node: Object3D;
|
|
17
28
|
polygon: [number, number][];
|
|
29
|
+
holes?: [number, number][][];
|
|
18
30
|
};
|
|
19
31
|
/** Pull the interactive items out of a scene graph. Only items that actually
|
|
20
32
|
* carry effects (light / animation) are returned — everything else baked
|
|
21
33
|
* faithfully and needs no runtime help. */
|
|
22
34
|
export declare function buildGlbInteractiveItems(sceneGraph: SceneGraph | null | undefined): GlbInteractiveItem[];
|
|
35
|
+
export declare function buildGlbLightRegs(items: GlbInteractiveItem[], identity: Map<string, Object3D>): GlbLightReg[];
|
|
23
36
|
/**
|
|
24
37
|
* Re-creates the item-driven interactivity the parametric viewer has — pooled
|
|
25
38
|
* lights, ambient animation, and the controls overlay — on top of a baked GLB.
|
|
@@ -38,4 +51,15 @@ export declare function GlbInteractive({ items, identity, zones, actions, levelO
|
|
|
38
51
|
* lights when nothing is focused (mirrors the parametric level factor). */
|
|
39
52
|
levelOrder: string[];
|
|
40
53
|
}): import("react").JSX.Element;
|
|
54
|
+
export type GlbLightReg = {
|
|
55
|
+
key: string;
|
|
56
|
+
nodeId: AnyNodeId;
|
|
57
|
+
object: Object3D;
|
|
58
|
+
color: string;
|
|
59
|
+
distance: number;
|
|
60
|
+
getWorldPosition: (out: Vector3) => void;
|
|
61
|
+
getIntensity: () => number;
|
|
62
|
+
isOn: () => boolean;
|
|
63
|
+
levelId: string | null;
|
|
64
|
+
};
|
|
41
65
|
//# sourceMappingURL=glb-interactive.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glb-interactive.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-interactive.tsx"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"glb-interactive.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-interactive.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,SAAS,EAEd,KAAK,WAAW,EAGhB,KAAK,UAAU,EAOhB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,kBAAkB,EACxB,MAAM,mCAAmC,CAAA;AAI1C,OAAO,EACL,KAAK,eAAe,EAGpB,KAAK,QAAQ,EAEb,OAAO,EACR,MAAM,OAAO,CAAA;AAcd;;kFAEkF;AAClF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,SAAS,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,WAAW,CAAA;IACxB;mEAC+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,UAAU,CAAC,EAAE;QACX,MAAM,EAAE,cAAc,EAAE,CAAA;QACxB,KAAK,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAA;QAC5C,+EAA+E;QAC/E,MAAM,CAAC,EAAE,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAA;KAChE,CAAA;CACF,CAAA;AAED,8EAA8E;AAC9E,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;IAC3B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAA;CAC7B,CAAA;AAED;;4CAE4C;AAC5C,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACxC,kBAAkB,EAAE,CAiEtB;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,kBAAkB,EAAE,EAC3B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,GAC9B,WAAW,EAAE,CAiFf;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,OAAO,EACP,UAAU,GACX,EAAE;IACD,KAAK,EAAE,kBAAkB,EAAE,CAAA;IAC3B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC/B,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB;8BAC0B;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC,CAAA;IAC/C;gFAC4E;IAC5E,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB,+BA0FA;AA8CD,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,SAAS,CAAA;IACjB,MAAM,EAAE,QAAQ,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IACxC,YAAY,EAAE,MAAM,MAAM,CAAA;IAC1B,IAAI,EAAE,MAAM,OAAO,CAAA;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,CAAA"}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { containsPoint, isScriptedNode, scriptedOrigin, scriptedSize, scriptInteractive, useInteractive, } from '@pascal-app/core';
|
|
4
|
+
import { evaluateRecipe, } from '@pascal-app/core/procedural-items';
|
|
4
5
|
import { Html } from '@react-three/drei';
|
|
5
|
-
import { createPortal, useFrame } from '@react-three/fiber';
|
|
6
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { createPortal, useFrame, useThree } from '@react-three/fiber';
|
|
7
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
7
8
|
import { LoopRepeat, MathUtils, Vector3, } from 'three';
|
|
8
9
|
import { useShallow } from 'zustand/react/shallow';
|
|
10
|
+
import { SCENE_LAYER } from '../../lib/layers';
|
|
11
|
+
import { decorateProceduralEmission, proceduralSlotMeshes, setProceduralEmission, } from '../../lib/procedural-emission';
|
|
12
|
+
import { useItemLightPool } from '../../store/use-item-light-pool';
|
|
9
13
|
import useViewer from '../../store/use-viewer';
|
|
10
14
|
import { ControlWidget } from '../../systems/interactive/control-widget';
|
|
15
|
+
import { proceduralControlDescriptors } from '../../systems/interactive/procedural-controls';
|
|
16
|
+
import { ScriptedClips } from '../../systems/interactive/scripted-clips';
|
|
11
17
|
/** Pull the interactive items out of a scene graph. Only items that actually
|
|
12
18
|
* carry effects (light / animation) are returned — everything else baked
|
|
13
19
|
* faithfully and needs no runtime help. */
|
|
@@ -18,6 +24,48 @@ export function buildGlbInteractiveItems(sceneGraph) {
|
|
|
18
24
|
const items = [];
|
|
19
25
|
for (const [id, raw] of Object.entries(nodes)) {
|
|
20
26
|
const node = raw;
|
|
27
|
+
if (node?.type === 'procedural-item') {
|
|
28
|
+
const procedural = raw;
|
|
29
|
+
const evaluation = evaluateRecipe(procedural.recipe, procedural.parameters);
|
|
30
|
+
if (!evaluation.lights.length && !evaluation.motions.length)
|
|
31
|
+
continue;
|
|
32
|
+
items.push({
|
|
33
|
+
pascalId: id,
|
|
34
|
+
label: procedural.name ?? id,
|
|
35
|
+
height: evaluation.max[1],
|
|
36
|
+
interactive: { controls: [], effects: [] },
|
|
37
|
+
procedural: {
|
|
38
|
+
lights: evaluation.lights,
|
|
39
|
+
parts: procedural.recipe.parts,
|
|
40
|
+
recipe: procedural.recipe,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// A node built from a script: its controls come from what the script
|
|
46
|
+
// emitted, as in the editor.
|
|
47
|
+
if (isScriptedNode(raw)) {
|
|
48
|
+
const scripted = raw;
|
|
49
|
+
const interactive = scriptInteractive(scripted.source.manifest, scriptedOrigin(scripted));
|
|
50
|
+
if (!interactive)
|
|
51
|
+
continue;
|
|
52
|
+
const [, height] = scriptedSize(scripted.source.manifest);
|
|
53
|
+
items.push({
|
|
54
|
+
pascalId: id,
|
|
55
|
+
label: scripted.name ??
|
|
56
|
+
(scripted.type === 'item'
|
|
57
|
+
? scripted.asset.name
|
|
58
|
+
: `${scripted.type[0].toUpperCase()}${scripted.type.slice(1)}`),
|
|
59
|
+
// A window or door is placed by its centre, everything else by its base.
|
|
60
|
+
height: scripted.type === 'window' || scripted.type === 'door'
|
|
61
|
+
? height / 2
|
|
62
|
+
: height * (scripted.type === 'item' ? (scripted.scale?.[1] ?? 1) : 1),
|
|
63
|
+
interactive,
|
|
64
|
+
scripted: true,
|
|
65
|
+
localScale: scripted.type === 'item' ? scripted.scale : [1, 1, 1],
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
21
69
|
if (node?.type !== 'item')
|
|
22
70
|
continue;
|
|
23
71
|
const interactive = node.asset?.interactive;
|
|
@@ -34,6 +82,86 @@ export function buildGlbInteractiveItems(sceneGraph) {
|
|
|
34
82
|
}
|
|
35
83
|
return items;
|
|
36
84
|
}
|
|
85
|
+
export function buildGlbLightRegs(items, identity) {
|
|
86
|
+
const regs = [];
|
|
87
|
+
for (const item of items) {
|
|
88
|
+
const object = identity.get(item.pascalId);
|
|
89
|
+
if (!object)
|
|
90
|
+
continue;
|
|
91
|
+
if (item.procedural) {
|
|
92
|
+
const groups = new Map();
|
|
93
|
+
object.traverse((child) => {
|
|
94
|
+
const motion = child.userData.proceduralMotion;
|
|
95
|
+
if (motion?.groupId)
|
|
96
|
+
groups.set(motion.groupId, child);
|
|
97
|
+
});
|
|
98
|
+
for (const light of item.procedural.lights) {
|
|
99
|
+
const motion = light.motionGroup ? groups.get(light.motionGroup) : undefined;
|
|
100
|
+
const local = new Vector3(...light.position);
|
|
101
|
+
// A nested group's position is relative to its parent group: the design-space pivot
|
|
102
|
+
// is the sum along its chain of motion groups.
|
|
103
|
+
for (let group = motion; group && group !== object; group = group.parent ?? undefined)
|
|
104
|
+
if (group.userData.proceduralMotion)
|
|
105
|
+
local.sub(group.position);
|
|
106
|
+
regs.push({
|
|
107
|
+
key: `${item.pascalId}:procedural:${light.id}`,
|
|
108
|
+
nodeId: item.pascalId,
|
|
109
|
+
object,
|
|
110
|
+
color: light.color,
|
|
111
|
+
distance: light.distance,
|
|
112
|
+
getWorldPosition: (out) => {
|
|
113
|
+
const anchor = motion ?? object;
|
|
114
|
+
anchor.updateWorldMatrix(true, false);
|
|
115
|
+
out.copy(local).applyMatrix4(anchor.matrixWorld);
|
|
116
|
+
},
|
|
117
|
+
getIntensity: () => light.intensity,
|
|
118
|
+
isOn: () => useInteractive.getState().procedural[item.pascalId]?.lightsOn ??
|
|
119
|
+
useInteractive.getState().lampDefault,
|
|
120
|
+
levelId: findLevelId(object),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const controls = item.interactive.controls;
|
|
125
|
+
const toggleIndex = controls.findIndex((control) => control.kind === 'toggle');
|
|
126
|
+
const sliderIndex = controls.findIndex((control) => control.kind === 'slider');
|
|
127
|
+
const slider = sliderIndex >= 0 ? controls[sliderIndex] : null;
|
|
128
|
+
item.interactive.effects.forEach((effect, index) => {
|
|
129
|
+
if (effect.kind !== 'light')
|
|
130
|
+
return;
|
|
131
|
+
regs.push({
|
|
132
|
+
key: `${item.pascalId}:${index}`,
|
|
133
|
+
nodeId: item.pascalId,
|
|
134
|
+
object,
|
|
135
|
+
color: effect.color,
|
|
136
|
+
distance: effect.distance ?? 0,
|
|
137
|
+
getWorldPosition: (out) => {
|
|
138
|
+
object.updateWorldMatrix(true, false);
|
|
139
|
+
if (item.scripted) {
|
|
140
|
+
const scale = item.localScale ?? [1, 1, 1];
|
|
141
|
+
out
|
|
142
|
+
.set(effect.offset[0] * scale[0], effect.offset[1] * scale[1], effect.offset[2] * scale[2])
|
|
143
|
+
.applyMatrix4(object.matrixWorld);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
object.getWorldPosition(out);
|
|
147
|
+
out.set(out.x + effect.offset[0], out.y + effect.offset[1], out.z + effect.offset[2]);
|
|
148
|
+
},
|
|
149
|
+
getIntensity: () => {
|
|
150
|
+
const values = useInteractive.getState().items[item.pascalId]?.controlValues;
|
|
151
|
+
const raw = slider ? (values?.[sliderIndex] ?? slider.min) : 1;
|
|
152
|
+
const fraction = slider && slider.max > slider.min ? (raw - slider.min) / (slider.max - slider.min) : 1;
|
|
153
|
+
return MathUtils.lerp(effect.intensityRange[0], effect.intensityRange[1], fraction);
|
|
154
|
+
},
|
|
155
|
+
isOn: () => {
|
|
156
|
+
const values = useInteractive.getState().items[item.pascalId]?.controlValues;
|
|
157
|
+
return toggleIndex < 0 || Boolean(values?.[toggleIndex]);
|
|
158
|
+
},
|
|
159
|
+
levelId: findLevelId(object),
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return regs;
|
|
164
|
+
}
|
|
37
165
|
const _itemPos = new Vector3();
|
|
38
166
|
/**
|
|
39
167
|
* Re-creates the item-driven interactivity the parametric viewer has — pooled
|
|
@@ -43,18 +171,26 @@ const _itemPos = new Vector3();
|
|
|
43
171
|
* the GLB itself, so the artifact stays integrator-clean.
|
|
44
172
|
*/
|
|
45
173
|
export function GlbInteractive({ items, identity, zones, actions, levelOrder, }) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
174
|
+
const scene = useThree((state) => state.scene);
|
|
175
|
+
useLayoutEffect(() => {
|
|
176
|
+
useItemLightPool.getState().setBakedCanvas(scene, true);
|
|
177
|
+
return () => useItemLightPool.getState().setBakedCanvas(scene, false);
|
|
178
|
+
}, [scene]);
|
|
179
|
+
// Baked catalog animation toggles start on; light toggles follow the current
|
|
180
|
+
// theme; an authored object's toggles keep their own defaults, as in the
|
|
181
|
+
// editor. Clear per-item state on unmount so it cannot carry into another scene.
|
|
51
182
|
useEffect(() => {
|
|
52
183
|
const store = useInteractive.getState();
|
|
53
184
|
for (const item of items) {
|
|
54
|
-
store.initItem(item.pascalId, item.interactive);
|
|
55
|
-
item.
|
|
56
|
-
|
|
57
|
-
|
|
185
|
+
store.initItem(item.pascalId, item.interactive, true);
|
|
186
|
+
if (item.scripted)
|
|
187
|
+
continue;
|
|
188
|
+
const lampIndex = item.interactive.effects.some((effect) => effect.kind === 'light')
|
|
189
|
+
? item.interactive.controls.findIndex((control) => control.kind === 'toggle')
|
|
190
|
+
: -1;
|
|
191
|
+
item.interactive.controls.forEach((control, index) => {
|
|
192
|
+
if (control.kind === 'toggle' && index !== lampIndex)
|
|
193
|
+
store.setControlValue(item.pascalId, index, true);
|
|
58
194
|
});
|
|
59
195
|
}
|
|
60
196
|
return () => {
|
|
@@ -64,40 +200,12 @@ export function GlbInteractive({ items, identity, zones, actions, levelOrder, })
|
|
|
64
200
|
};
|
|
65
201
|
}, [items]);
|
|
66
202
|
const animationItems = useMemo(() => items.filter((item) => item.interactive.effects.some((e) => e.kind === 'animation')), [items]);
|
|
67
|
-
|
|
68
|
-
// node. Fed to a fixed pool (below) rather than mounting a light per item.
|
|
69
|
-
const lightRegs = useMemo(() => {
|
|
70
|
-
const regs = [];
|
|
71
|
-
for (const item of items) {
|
|
72
|
-
const effect = item.interactive.effects.find((e) => e.kind === 'light');
|
|
73
|
-
if (!effect)
|
|
74
|
-
continue;
|
|
75
|
-
const object = identity.get(item.pascalId);
|
|
76
|
-
if (!object)
|
|
77
|
-
continue;
|
|
78
|
-
const controls = item.interactive.controls;
|
|
79
|
-
const toggleIndex = controls.findIndex((c) => c.kind === 'toggle');
|
|
80
|
-
const sliderIndex = controls.findIndex((c) => c.kind === 'slider');
|
|
81
|
-
const slider = sliderIndex >= 0 ? controls[sliderIndex] : null;
|
|
82
|
-
regs.push({
|
|
83
|
-
key: item.pascalId,
|
|
84
|
-
object,
|
|
85
|
-
effect,
|
|
86
|
-
toggleIndex,
|
|
87
|
-
sliderIndex,
|
|
88
|
-
hasSlider: !!slider,
|
|
89
|
-
sliderMin: slider?.min ?? 0,
|
|
90
|
-
sliderMax: slider?.max ?? 1,
|
|
91
|
-
levelId: findLevelId(object),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return regs;
|
|
95
|
-
}, [items, identity]);
|
|
203
|
+
const lightRegs = useMemo(() => buildGlbLightRegs(items, identity), [items, identity]);
|
|
96
204
|
const levelIndexById = useMemo(() => new Map(levelOrder.map((id, i) => [id, i])), [levelOrder]);
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// item's world position can be point-tested regardless of level stacking.
|
|
205
|
+
// Project the zone's baked-local polygon into world space so focused zone
|
|
206
|
+
// membership still works after level stacking moves its parent.
|
|
100
207
|
const focusedZoneId = useViewer((s) => s.selection.zoneId);
|
|
208
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds);
|
|
101
209
|
const worldPolygon = useMemo(() => {
|
|
102
210
|
if (!focusedZoneId)
|
|
103
211
|
return null;
|
|
@@ -105,16 +213,51 @@ export function GlbInteractive({ items, identity, zones, actions, levelOrder, })
|
|
|
105
213
|
if (!zone)
|
|
106
214
|
return null;
|
|
107
215
|
zone.node.updateWorldMatrix(true, false);
|
|
108
|
-
|
|
216
|
+
const project = (ring) => ring.map(([x, z]) => {
|
|
109
217
|
const v = new Vector3(x, 0, z).applyMatrix4(zone.node.matrixWorld);
|
|
110
218
|
return [v.x, v.z];
|
|
111
219
|
});
|
|
220
|
+
return { outer: project(zone.polygon), holes: (zone.holes ?? []).map(project) };
|
|
112
221
|
}, [focusedZoneId, zones]);
|
|
113
|
-
return (_jsxs(_Fragment, { children: [_jsx(GlbItemLights, { levelIndexById: levelIndexById, regs: lightRegs }), animationItems.map((item) => (_jsx(
|
|
222
|
+
return (_jsxs(_Fragment, { children: [_jsx(GlbItemLights, { levelIndexById: levelIndexById, regs: lightRegs }), animationItems.map((item) => item.scripted ? (_jsx(GlbScriptedClips, { actions: actions, item: item }, item.pascalId)) : (_jsx(GlbItemAnimation, { actions: actions, item: item }, item.pascalId))), items
|
|
223
|
+
.filter((item) => item.procedural?.lights.length)
|
|
224
|
+
.map((item) => {
|
|
225
|
+
const object = identity.get(item.pascalId);
|
|
226
|
+
return object ? (_jsx(GlbProceduralEmission, { item: item, object: object }, item.pascalId)) : null;
|
|
227
|
+
}), items
|
|
228
|
+
.filter((item) => worldPolygon || selectedIds.includes(item.pascalId))
|
|
229
|
+
.map((item) => {
|
|
114
230
|
const object = identity.get(item.pascalId);
|
|
115
|
-
return object ? (_jsx(GlbItemControls, { item: item, object: object, worldPolygon: worldPolygon }, item.pascalId)) : null;
|
|
231
|
+
return object ? (_jsx(GlbItemControls, { isSelected: selectedIds.includes(item.pascalId), item: item, object: object, worldPolygon: worldPolygon }, item.pascalId)) : null;
|
|
116
232
|
})] }));
|
|
117
233
|
}
|
|
234
|
+
function GlbProceduralEmission({ item, object }) {
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
const lights = item.procedural?.lights ?? [];
|
|
237
|
+
const restore = decorateProceduralEmission(object, lights, true);
|
|
238
|
+
const update = () => {
|
|
239
|
+
const on = useInteractive.getState().procedural[item.pascalId]?.lightsOn ??
|
|
240
|
+
useInteractive.getState().lampDefault;
|
|
241
|
+
const slots = new Set(lights.map((light) => light.emissiveSlot).filter(Boolean));
|
|
242
|
+
for (const mesh of proceduralSlotMeshes(object, slots)) {
|
|
243
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
244
|
+
for (const material of materials)
|
|
245
|
+
setProceduralEmission(material, on);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
update();
|
|
249
|
+
const unsubscribe = useInteractive.subscribe((state, previous) => {
|
|
250
|
+
if (state.procedural[item.pascalId] !== previous.procedural[item.pascalId] ||
|
|
251
|
+
state.lampDefault !== previous.lampDefault)
|
|
252
|
+
update();
|
|
253
|
+
});
|
|
254
|
+
return () => {
|
|
255
|
+
unsubscribe();
|
|
256
|
+
restore();
|
|
257
|
+
};
|
|
258
|
+
}, [item, object]);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
118
261
|
// ── Pooled item lights ──────────────────────────────────────────────────────
|
|
119
262
|
//
|
|
120
263
|
// Mirrors the parametric `ItemLightSystem`: a fixed pool of point lights is
|
|
@@ -142,15 +285,21 @@ function findLevelId(object) {
|
|
|
142
285
|
}
|
|
143
286
|
return null;
|
|
144
287
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
288
|
+
function isRendered(object) {
|
|
289
|
+
let current = object;
|
|
290
|
+
while (current) {
|
|
291
|
+
if (!current.visible || !current.layers.isEnabled(SCENE_LAYER))
|
|
292
|
+
return false;
|
|
293
|
+
current = current.parent;
|
|
149
294
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
function scoreReg(reg, selectedLevelId, levelMode, levelIndexById) {
|
|
298
|
+
if (!reg.isOn() || !isRendered(reg.object))
|
|
299
|
+
return Number.POSITIVE_INFINITY;
|
|
300
|
+
if (selectedLevelId && reg.levelId !== selectedLevelId && levelMode === 'solo')
|
|
301
|
+
return Number.POSITIVE_INFINITY;
|
|
302
|
+
reg.getWorldPosition(_lightWorld);
|
|
154
303
|
_dir.copy(_lightWorld).sub(_camPos).normalize();
|
|
155
304
|
const angular = 1 - _camFwd.dot(_dir);
|
|
156
305
|
const dist = _camPos.distanceTo(_lightWorld) / 200;
|
|
@@ -173,7 +322,6 @@ function GlbItemLights({ regs, levelIndexById, }) {
|
|
|
173
322
|
const regByKey = useMemo(() => new Map(regs.map((r) => [r.key, r])), [regs]);
|
|
174
323
|
useFrame(({ camera }, delta) => {
|
|
175
324
|
const dt = Math.min(delta, 0.1);
|
|
176
|
-
const interactiveState = useInteractive.getState();
|
|
177
325
|
camera.getWorldPosition(_camPos);
|
|
178
326
|
camera.getWorldDirection(_camFwd);
|
|
179
327
|
const camMoved = _camPos.distanceTo(prevCamPos.current) > CAM_MOVE_DIST ||
|
|
@@ -188,7 +336,7 @@ function GlbItemLights({ regs, levelIndexById, }) {
|
|
|
188
336
|
const levelMode = viewer.levelMode;
|
|
189
337
|
const scored = regs.map((reg) => ({
|
|
190
338
|
key: reg.key,
|
|
191
|
-
score: scoreReg(reg, selectedLevelId, levelMode, levelIndexById
|
|
339
|
+
score: scoreReg(reg, selectedLevelId, levelMode, levelIndexById),
|
|
192
340
|
}));
|
|
193
341
|
scored.sort((a, b) => a.score - b.score);
|
|
194
342
|
const scoreByKey = new Map(scored.map((s) => [s.key, s.score]));
|
|
@@ -244,8 +392,8 @@ function GlbItemLights({ regs, levelIndexById, }) {
|
|
|
244
392
|
const light = lightRefs.current[freeSlot];
|
|
245
393
|
const reg = regByKey.get(key);
|
|
246
394
|
if (light && reg) {
|
|
247
|
-
light.color.set(reg.
|
|
248
|
-
light.distance = reg.
|
|
395
|
+
light.color.set(reg.color);
|
|
396
|
+
light.distance = reg.distance;
|
|
249
397
|
}
|
|
250
398
|
}
|
|
251
399
|
}
|
|
@@ -283,8 +431,8 @@ function GlbItemLights({ regs, levelIndexById, }) {
|
|
|
283
431
|
if (slot.key) {
|
|
284
432
|
const reg = regByKey.get(slot.key);
|
|
285
433
|
if (reg) {
|
|
286
|
-
light.color.set(reg.
|
|
287
|
-
light.distance = reg.
|
|
434
|
+
light.color.set(reg.color);
|
|
435
|
+
light.distance = reg.distance;
|
|
288
436
|
}
|
|
289
437
|
}
|
|
290
438
|
}
|
|
@@ -299,24 +447,12 @@ function GlbItemLights({ regs, levelIndexById, }) {
|
|
|
299
447
|
slot.key = null;
|
|
300
448
|
continue;
|
|
301
449
|
}
|
|
302
|
-
reg.
|
|
303
|
-
light.position.
|
|
304
|
-
const
|
|
305
|
-
const isOn = reg.toggleIndex >= 0 ? Boolean(values?.[reg.toggleIndex]) : true;
|
|
306
|
-
let t = 1;
|
|
307
|
-
if (reg.hasSlider) {
|
|
308
|
-
const raw = values?.[reg.sliderIndex] ?? reg.sliderMin;
|
|
309
|
-
t =
|
|
310
|
-
reg.sliderMax > reg.sliderMin
|
|
311
|
-
? (raw - reg.sliderMin) / (reg.sliderMax - reg.sliderMin)
|
|
312
|
-
: 1;
|
|
313
|
-
}
|
|
314
|
-
const targetIntensity = isOn
|
|
315
|
-
? MathUtils.lerp(reg.effect.intensityRange[0], reg.effect.intensityRange[1], t)
|
|
316
|
-
: reg.effect.intensityRange[0];
|
|
450
|
+
reg.getWorldPosition(_lightWorld);
|
|
451
|
+
light.position.copy(_lightWorld);
|
|
452
|
+
const targetIntensity = reg.isOn() && isRendered(reg.object) ? reg.getIntensity() : 0;
|
|
317
453
|
light.intensity = MathUtils.lerp(light.intensity, targetIntensity, dt * 12);
|
|
318
454
|
}
|
|
319
|
-
});
|
|
455
|
+
}, 6);
|
|
320
456
|
return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, ref: (el) => {
|
|
321
457
|
lightRefs.current[i] = el;
|
|
322
458
|
} }, i))) }));
|
|
@@ -345,16 +481,47 @@ function GlbItemAnimation({ item, actions, }) {
|
|
|
345
481
|
}, [actions, item.pascalId, isOn]);
|
|
346
482
|
return null;
|
|
347
483
|
}
|
|
484
|
+
/** Plays an authored object's baked clips through the shared `ScriptedClips`,
|
|
485
|
+
* addressing each by the name the script gave it. */
|
|
486
|
+
function GlbScriptedClips({ item, actions, }) {
|
|
487
|
+
// drei creates each action on first read, once the scene root is mounted, so
|
|
488
|
+
// forward reads instead of copying the actions during render.
|
|
489
|
+
const scriptActions = useMemo(() => {
|
|
490
|
+
const prefix = `${item.pascalId}: `;
|
|
491
|
+
const forwarded = {};
|
|
492
|
+
for (const name of Object.keys(actions)) {
|
|
493
|
+
if (!name.startsWith(prefix))
|
|
494
|
+
continue;
|
|
495
|
+
Object.defineProperty(forwarded, name.slice(prefix.length), {
|
|
496
|
+
enumerable: true,
|
|
497
|
+
get: () => actions[name],
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return forwarded;
|
|
501
|
+
}, [actions, item.pascalId]);
|
|
502
|
+
return (_jsx(ScriptedClips, { actions: scriptActions, interactive: item.interactive, nodeId: item.pascalId }));
|
|
503
|
+
}
|
|
348
504
|
const FADE_MS = 300;
|
|
349
|
-
/** Controls overlay for
|
|
350
|
-
|
|
351
|
-
function GlbItemControls({ item, object, worldPolygon, }) {
|
|
505
|
+
/** Controls overlay for a selected item or one inside the focused zone. */
|
|
506
|
+
function GlbItemControls({ item, object, worldPolygon, isSelected, }) {
|
|
352
507
|
const controlValues = useInteractive(useShallow((s) => s.items[item.pascalId]?.controlValues));
|
|
508
|
+
const proceduralState = useInteractive((s) => s.procedural[item.pascalId]);
|
|
509
|
+
const lampDefault = useInteractive((s) => s.lampDefault);
|
|
353
510
|
const setControlValue = useInteractive((s) => s.setControlValue);
|
|
354
|
-
|
|
355
|
-
|
|
511
|
+
const togglePart = useInteractive((s) => s.toggleProceduralPart);
|
|
512
|
+
const toggleLights = useInteractive((s) => s.toggleProceduralLights);
|
|
513
|
+
const descriptors = item.procedural
|
|
514
|
+
? proceduralControlDescriptors(item.procedural.recipe ?? { parts: item.procedural.parts }, proceduralState, (partId) => togglePart(item.pascalId, partId), () => toggleLights(item.pascalId), lampDefault)
|
|
515
|
+
: item.interactive.controls.map((control, index) => ({
|
|
516
|
+
key: String(index),
|
|
517
|
+
control,
|
|
518
|
+
value: controlValues?.[index] ?? false,
|
|
519
|
+
onChange: (value) => setControlValue(item.pascalId, index, value),
|
|
520
|
+
}));
|
|
521
|
+
let visible = isSelected;
|
|
522
|
+
if (worldPolygon) {
|
|
356
523
|
object.getWorldPosition(_itemPos);
|
|
357
|
-
visible =
|
|
524
|
+
visible = visible || containsPoint([worldPolygon], [_itemPos.x, _itemPos.z]);
|
|
358
525
|
}
|
|
359
526
|
// Fade in on mount and fade out before unmounting the <Html>.
|
|
360
527
|
const [mounted, setMounted] = useState(false);
|
|
@@ -375,7 +542,7 @@ function GlbItemControls({ item, object, worldPolygon, }) {
|
|
|
375
542
|
const timeout = setTimeout(() => setMounted(false), FADE_MS);
|
|
376
543
|
return () => clearTimeout(timeout);
|
|
377
544
|
}, [visible]);
|
|
378
|
-
if (!(mounted &&
|
|
545
|
+
if (!(mounted && descriptors.length))
|
|
379
546
|
return null;
|
|
380
547
|
return createPortal(_jsx(Html, { center: true, distanceFactor: 8, eps: -1, position: [0, item.height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { onClick: (e) => e.stopPropagation(), onPointerDown: (e) => e.stopPropagation(), onPointerUp: (e) => e.stopPropagation(), style: {
|
|
381
548
|
display: 'flex',
|
|
@@ -390,5 +557,5 @@ function GlbItemControls({ item, object, worldPolygon, }) {
|
|
|
390
557
|
userSelect: 'none',
|
|
391
558
|
opacity: shown ? 1 : 0,
|
|
392
559
|
transition: `opacity ${FADE_MS}ms ease`,
|
|
393
|
-
}, children:
|
|
560
|
+
}, children: descriptors.map((descriptor) => (_jsx(ControlWidget, { control: descriptor.control, onChange: descriptor.onChange, value: descriptor.value }, descriptor.key))) }) }), object);
|
|
394
561
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Object3D } from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Baked nodes whose loop clips no other controller plays (a plugin kind's
|
|
4
|
+
* mechanism, such as an articulated asset's joints), read from the GLB alone:
|
|
5
|
+
* `pascalId` → its `: loop` clip names. `owned` holds the nodes procedural
|
|
6
|
+
* playback or the scene graph's interactive items already drive; openables
|
|
7
|
+
* keep their open/close path.
|
|
8
|
+
*/
|
|
9
|
+
export declare function bakedLoopMechanisms(identity: ReadonlyMap<string, Object3D>, owned: ReadonlySet<string>): Map<string, string[]>;
|
|
10
|
+
//# sourceMappingURL=glb-mechanisms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glb-mechanisms.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-mechanisms.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACvC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GACzB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAWvB"}
|