@pascal-app/viewer 0.8.0 → 0.9.2
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/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +6 -2
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +41 -22
- package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
- package/dist/components/renderers/parametric-node-renderer.js +37 -0
- package/dist/components/renderers/scene-renderer.d.ts +1 -1
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +39 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
- package/dist/components/viewer/index.d.ts +56 -3
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +280 -65
- package/dist/components/viewer/lights.d.ts +1 -1
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +201 -43
- package/dist/components/viewer/perf-monitor.d.ts +1 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +115 -12
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +395 -63
- package/dist/components/viewer/registered-systems.d.ts +12 -0
- package/dist/components/viewer/registered-systems.d.ts.map +1 -0
- package/dist/components/viewer/registered-systems.js +49 -0
- package/dist/components/viewer/scene-bvh.d.ts +18 -0
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.js +97 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts +1 -1
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +37 -14
- package/dist/components/viewer/viewer-camera.d.ts +1 -1
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.d.ts +2 -1
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.js +20 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +3 -22
- package/dist/hooks/use-node-events.d.ts +6 -74
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +28 -8
- package/dist/index.d.ts +48 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -4
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +10 -0
- package/dist/lib/csg-utils.d.ts.map +1 -0
- package/dist/lib/csg-utils.js +93 -0
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/drawable-geometry.d.ts +21 -0
- package/dist/lib/drawable-geometry.d.ts.map +1 -0
- package/dist/lib/drawable-geometry.js +22 -0
- package/dist/lib/edge-style.d.ts +4 -0
- package/dist/lib/edge-style.d.ts.map +1 -0
- package/dist/lib/edge-style.js +31 -0
- package/dist/lib/gpu-perf.d.ts +4 -0
- package/dist/lib/gpu-perf.d.ts.map +1 -0
- package/dist/lib/gpu-perf.js +23 -0
- package/dist/lib/ink-edges.d.ts +9 -0
- package/dist/lib/ink-edges.d.ts.map +1 -0
- package/dist/lib/ink-edges.js +49 -0
- package/dist/lib/isolation.d.ts +39 -0
- package/dist/lib/isolation.d.ts.map +1 -0
- package/dist/lib/isolation.js +103 -0
- package/dist/lib/ktx2-loader.d.ts +43 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +136 -0
- package/dist/lib/layers.d.ts +28 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +28 -0
- package/dist/lib/materials.d.ts +38 -15
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +364 -72
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -0
- package/dist/lib/polygon-union.d.ts +4 -0
- package/dist/lib/polygon-union.d.ts.map +1 -0
- package/dist/lib/polygon-union.js +289 -0
- package/dist/lib/polygon-union.test.d.ts +2 -0
- package/dist/lib/polygon-union.test.d.ts.map +1 -0
- package/dist/lib/polygon-union.test.js +105 -0
- package/dist/lib/scene-themes.d.ts +30 -0
- package/dist/lib/scene-themes.d.ts.map +1 -0
- package/dist/lib/scene-themes.js +202 -0
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +92 -12
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +201 -6
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +42 -0
- package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +66 -13
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +864 -180
- package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-interaction-system.js +62 -0
- package/dist/systems/fence/fence-system.d.ts +20 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +229 -57
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
- package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
- package/dist/systems/geometry/geometry-system.d.ts +41 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.js +348 -0
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts +1 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts +7 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +28 -17
- 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 +18 -5
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +28 -4
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +2 -41
- package/dist/systems/roof/roof-materials.d.ts +3 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +59 -14
- package/dist/systems/roof/roof-system.d.ts +46 -6
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1365 -170
- package/dist/systems/slab/slab-system.d.ts +5 -3
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +204 -68
- package/dist/systems/slab/slab-system.test.d.ts +2 -0
- package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
- package/dist/systems/slab/slab-system.test.js +63 -0
- package/dist/systems/stair/stair-materials.d.ts +4 -3
- package/dist/systems/stair/stair-materials.d.ts.map +1 -1
- package/dist/systems/stair/stair-materials.js +30 -15
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +55 -63
- package/dist/systems/surface-hole-geometry.d.ts +3 -0
- package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
- package/dist/systems/surface-hole-geometry.js +4 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +62 -17
- package/dist/systems/wall/wall-materials.d.ts +12 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +279 -46
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +48 -0
- package/dist/systems/wall/wall-system.d.ts +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +493 -193
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +308 -18
- package/package.json +5 -6
- package/dist/components/renderers/building/building-renderer.d.ts +0 -5
- package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
- package/dist/components/renderers/building/building-renderer.js +0 -11
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
- package/dist/components/renderers/column/column-renderer.d.ts +0 -5
- package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
- package/dist/components/renderers/column/column-renderer.js +0 -597
- package/dist/components/renderers/door/door-renderer.d.ts +0 -5
- package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
- package/dist/components/renderers/door/door-renderer.js +0 -16
- package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
- package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
- package/dist/components/renderers/fence/fence-renderer.js +0 -15
- package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
- package/dist/components/renderers/guide/guide-renderer.js +0 -38
- package/dist/components/renderers/item/item-renderer.d.ts +0 -5
- package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
- package/dist/components/renderers/item/item-renderer.js +0 -172
- package/dist/components/renderers/level/level-renderer.d.ts +0 -5
- package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
- package/dist/components/renderers/level/level-renderer.js +0 -11
- package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
- package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-materials.js +0 -16
- package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-renderer.js +0 -32
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
- package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
- package/dist/components/renderers/scan/scan-renderer.js +0 -54
- package/dist/components/renderers/site/site-renderer.d.ts +0 -5
- package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
- package/dist/components/renderers/site/site-renderer.js +0 -106
- package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
- package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
- package/dist/components/renderers/slab/slab-renderer.js +0 -58
- package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
- package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
- package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
- package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
- package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair/stair-renderer.js +0 -603
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
- package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
- package/dist/components/renderers/wall/wall-renderer.js +0 -16
- package/dist/components/renderers/window/window-renderer.d.ts +0 -5
- package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
- package/dist/components/renderers/window/window-renderer.js +0 -21
- package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
- package/dist/components/renderers/zone/zone-renderer.js +0 -181
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- package/dist/components/viewer/ground-occluder.js +0 -79
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { getEffectiveNode, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
3
|
+
import { useFrame } from '@react-three/fiber';
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
|
+
import { FrontSide } from 'three';
|
|
6
|
+
import { createSurfaceRoleMaterial, } from '../../lib/materials';
|
|
7
|
+
import useViewer from '../../store/use-viewer';
|
|
8
|
+
/**
|
|
9
|
+
* Generic geometry system.
|
|
10
|
+
*
|
|
11
|
+
* For every node in `dirtyNodes` whose definition exposes `def.geometry`,
|
|
12
|
+
* this system:
|
|
13
|
+
* 1. Looks up the registered `Group` from `sceneRegistry` (mounted by the
|
|
14
|
+
* framework's `<ParametricNodeRenderer>`, or a custom renderer that
|
|
15
|
+
* opts into the same mount contract).
|
|
16
|
+
* 2. Builds a `GeometryContext` from the current scene snapshot.
|
|
17
|
+
* 3. Calls `def.geometry(node, ctx)` to get the new `Object3D`.
|
|
18
|
+
* 4. Disposes the registered group's existing children + their geometries
|
|
19
|
+
* and materials.
|
|
20
|
+
* 5. Reparents the returned object's children onto the registered group.
|
|
21
|
+
* 6. Clears the dirty flag.
|
|
22
|
+
*
|
|
23
|
+
* This is the "no per-kind system needed" path documented in
|
|
24
|
+
* `wiki/architecture/node-definitions.md`. A kind that only rebuilds on
|
|
25
|
+
* dirty (shelf, item, fence segment, etc.) ships nothing more than a pure
|
|
26
|
+
* `geometry` function — no `renderer.tsx`, no `system.tsx`.
|
|
27
|
+
*
|
|
28
|
+
* Kinds with `def.system` declared run their own systems *in addition* to
|
|
29
|
+
* this one — animation + cascade + named-mesh material poking stay
|
|
30
|
+
* kind-specific.
|
|
31
|
+
*
|
|
32
|
+
* Frame priority 2 mirrors the per-kind shelf system it replaces. Door
|
|
33
|
+
* animation systems run at priority 2 today too, marking dirty so the
|
|
34
|
+
* geometry rebuild lands at priority 3-4 next frame. Door/window/wall
|
|
35
|
+
* still have their own systems (they need cross-cutting work this system
|
|
36
|
+
* doesn't cover) — they coexist; this system only acts on kinds that
|
|
37
|
+
* declare `def.geometry`.
|
|
38
|
+
*/
|
|
39
|
+
export const GeometrySystem = () => {
|
|
40
|
+
const dirtyNodes = useScene((s) => s.dirtyNodes);
|
|
41
|
+
const clearDirty = useScene((s) => s.clearDirty);
|
|
42
|
+
const shading = useViewer((s) => s.shading);
|
|
43
|
+
const textures = useViewer((s) => s.textures);
|
|
44
|
+
const colorPreset = useViewer((s) => s.colorPreset);
|
|
45
|
+
const sceneTheme = useViewer((s) => s.sceneTheme);
|
|
46
|
+
const bumpGeometryRevision = useViewer((s) => s.bumpGeometryRevision);
|
|
47
|
+
// The shared scene-material library, threaded into each builder's ctx so
|
|
48
|
+
// pure geometry builders can resolve `scene:<id>` slot refs without
|
|
49
|
+
// importing `useScene`.
|
|
50
|
+
const sceneMaterials = useScene((s) => s.materials);
|
|
51
|
+
// Per-node cache of the last-built geometry key (for kinds that declare
|
|
52
|
+
// `def.geometryKey`). Lets us skip a dispose+rebuild when a node is dirty
|
|
53
|
+
// but its geometry inputs are unchanged — e.g. an item reparenting onto a
|
|
54
|
+
// shelf dirties the shelf without altering its boards.
|
|
55
|
+
const builtGeometryKeyRef = useRef(new Map());
|
|
56
|
+
// Re-mark every geometry-backed node dirty whenever a viewer appearance
|
|
57
|
+
// value changes, so `def.geometry` builders re-run and pick up the new
|
|
58
|
+
// shading / texture / preset / theme. These four are deliberate re-run
|
|
59
|
+
// TRIGGERS, not values read in the body — the effect re-fires on any
|
|
60
|
+
// change. They're primitives (stable by value), so listing them is safe;
|
|
61
|
+
// biome flags them as "unnecessary" because the body doesn't reference
|
|
62
|
+
// them, but dropping them silently breaks appearance-mode switching.
|
|
63
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: shading/textures/colorPreset/sceneTheme are intentional re-run triggers; removing them stops geometry from rebuilding on appearance change.
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const nodes = useScene.getState().nodes;
|
|
66
|
+
for (const node of Object.values(nodes)) {
|
|
67
|
+
const def = nodeRegistry.get(node.type);
|
|
68
|
+
if (def?.geometry) {
|
|
69
|
+
useScene.getState().markDirty(node.id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, [shading, textures, colorPreset, sceneTheme]);
|
|
73
|
+
// Editing a scene material must re-colour every geometry node that
|
|
74
|
+
// references it through a `scene:<id>` slot ref. Such a node's
|
|
75
|
+
// `geometryKey` is unchanged (only the referenced material's contents
|
|
76
|
+
// moved), so clear its cached key to defeat the skip in the rebuild loop,
|
|
77
|
+
// then mark it dirty. Scoped to nodes carrying a `scene:` ref so an
|
|
78
|
+
// unrelated material edit doesn't churn the whole scene.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
void sceneMaterials;
|
|
81
|
+
const nodes = useScene.getState().nodes;
|
|
82
|
+
for (const node of Object.values(nodes)) {
|
|
83
|
+
const def = nodeRegistry.get(node.type);
|
|
84
|
+
if (!def?.geometry)
|
|
85
|
+
continue;
|
|
86
|
+
if (!nodeReferencesSceneMaterial(node))
|
|
87
|
+
continue;
|
|
88
|
+
builtGeometryKeyRef.current.delete(node.id);
|
|
89
|
+
useScene.getState().markDirty(node.id);
|
|
90
|
+
}
|
|
91
|
+
}, [sceneMaterials]);
|
|
92
|
+
useFrame(() => {
|
|
93
|
+
if (dirtyNodes.size === 0)
|
|
94
|
+
return;
|
|
95
|
+
const nodes = useScene.getState().nodes;
|
|
96
|
+
let rebuiltGeometry = false;
|
|
97
|
+
const batches = new Map();
|
|
98
|
+
const dirtyIds = [];
|
|
99
|
+
dirtyNodes.forEach((id) => {
|
|
100
|
+
const node = nodes[id];
|
|
101
|
+
if (!node)
|
|
102
|
+
return;
|
|
103
|
+
const def = nodeRegistry.get(node.type);
|
|
104
|
+
if (!def?.geometry)
|
|
105
|
+
return;
|
|
106
|
+
dirtyIds.push(id);
|
|
107
|
+
const parentId = (node.parentId ?? null);
|
|
108
|
+
const key = `${node.type}::${parentId ?? ''}`;
|
|
109
|
+
const existing = batches.get(key);
|
|
110
|
+
if (existing)
|
|
111
|
+
existing.ids.push(id);
|
|
112
|
+
else
|
|
113
|
+
batches.set(key, { kind: node.type, parentId, ids: [id] });
|
|
114
|
+
});
|
|
115
|
+
// Phase 2 — for each batch whose kind declares `computeLevelData`,
|
|
116
|
+
// collect every sibling in the level + run the precompute once.
|
|
117
|
+
const levelDataByBatch = new Map();
|
|
118
|
+
for (const [key, batch] of batches) {
|
|
119
|
+
const def = nodeRegistry.get(batch.kind);
|
|
120
|
+
if (!def?.computeLevelData)
|
|
121
|
+
continue;
|
|
122
|
+
const siblings = [];
|
|
123
|
+
if (batch.parentId) {
|
|
124
|
+
const parent = nodes[batch.parentId];
|
|
125
|
+
const childIds = parent?.children;
|
|
126
|
+
if (Array.isArray(childIds)) {
|
|
127
|
+
for (const cid of childIds) {
|
|
128
|
+
const child = nodes[cid];
|
|
129
|
+
if (child?.type === batch.kind)
|
|
130
|
+
siblings.push(child);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
for (const node of Object.values(nodes)) {
|
|
136
|
+
if (node?.type === batch.kind && !node.parentId)
|
|
137
|
+
siblings.push(node);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
levelDataByBatch.set(key, def.computeLevelData(siblings));
|
|
141
|
+
}
|
|
142
|
+
// Phase 3 — per-node rebuild. Each node receives its batch's
|
|
143
|
+
// precomputed `levelData` in ctx.
|
|
144
|
+
for (const id of dirtyIds) {
|
|
145
|
+
const node = nodes[id];
|
|
146
|
+
if (!node)
|
|
147
|
+
continue;
|
|
148
|
+
const def = nodeRegistry.get(node.type);
|
|
149
|
+
const builder = def?.geometry;
|
|
150
|
+
if (!builder)
|
|
151
|
+
continue;
|
|
152
|
+
const group = sceneRegistry.nodes.get(id);
|
|
153
|
+
if (!group)
|
|
154
|
+
continue; // mount hasn't run — keep dirty for next frame
|
|
155
|
+
// Merge any live drag override into the node so resize arrows /
|
|
156
|
+
// 2D affordances see their in-flight patch on every pointer move
|
|
157
|
+
// — zustand only learns about the final value on commit. Mirrors
|
|
158
|
+
// the WallSystem / DoorSystem / WindowSystem hook-up; any kind
|
|
159
|
+
// that drives its mesh through `def.geometry` (fence, shelf, item)
|
|
160
|
+
// now smooths drags through this single line.
|
|
161
|
+
const effectiveNode = getEffectiveNode(node);
|
|
162
|
+
// Skip the rebuild when the geometry inputs are unchanged (kinds that
|
|
163
|
+
// opt in via `def.geometryKey`). Fold in the global rendering inputs so
|
|
164
|
+
// a theme / shading change — which re-dirties every geometry node — is
|
|
165
|
+
// never skipped. This kills the board remount + pointer enter/leave
|
|
166
|
+
// churn when an item reparents onto a shelf.
|
|
167
|
+
if (def.geometryKey) {
|
|
168
|
+
const childLiveOverrideKey = liveChildOverrideKey(node);
|
|
169
|
+
const builtKey = `${shading}|${textures}|${colorPreset}|${sceneTheme}|${def.geometryKey(effectiveNode)}|${childLiveOverrideKey}`;
|
|
170
|
+
if (shouldReuseGeometryBuild(builtGeometryKeyRef.current, id, group, builtKey)) {
|
|
171
|
+
clearDirty(id);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const parentId = (node.parentId ?? null);
|
|
176
|
+
const key = `${node.type}::${parentId ?? ''}`;
|
|
177
|
+
const levelData = levelDataByBatch.get(key);
|
|
178
|
+
const ctx = buildGeometryContext(effectiveNode, nodes, levelData, sceneMaterials);
|
|
179
|
+
// The builder is typed against the kind's specific node — at the
|
|
180
|
+
// generic system level we lose that refinement, so the cast lands
|
|
181
|
+
// here. Builders are responsible for trusting their schema.
|
|
182
|
+
const built = builder(effectiveNode, ctx, shading, textures, colorPreset, sceneTheme);
|
|
183
|
+
if (!textures && def.surfaceRole) {
|
|
184
|
+
applyDefaultSurfaceRole(built, def.surfaceRole, colorPreset, sceneTheme);
|
|
185
|
+
}
|
|
186
|
+
disposeChildren(group);
|
|
187
|
+
for (const child of [...built.children]) {
|
|
188
|
+
markGeometryBuildOutput(child);
|
|
189
|
+
group.add(child);
|
|
190
|
+
}
|
|
191
|
+
rebuiltGeometry = true;
|
|
192
|
+
// NOTE: we intentionally do NOT reset `group.position` / `group.rotation`
|
|
193
|
+
// here. The `ParametricNodeRenderer` binds them via JSX (`position={...}`
|
|
194
|
+
// / `rotation={...}`) driven by `useLiveTransforms` during drag and
|
|
195
|
+
// `node.position` / `node.rotation` after commit. Zeroing them out
|
|
196
|
+
// during a rebuild would clobber the React-applied transform — and
|
|
197
|
+
// because the renderer doesn't necessarily re-render on the rebuild
|
|
198
|
+
// tick, R3F wouldn't re-apply the props, leaving the group stuck at
|
|
199
|
+
// origin. Geometry builders are expected to emit local-space children.
|
|
200
|
+
clearDirty(id);
|
|
201
|
+
}
|
|
202
|
+
if (rebuiltGeometry)
|
|
203
|
+
bumpGeometryRevision();
|
|
204
|
+
}, 2);
|
|
205
|
+
return null;
|
|
206
|
+
};
|
|
207
|
+
function liveChildOverrideKey(node) {
|
|
208
|
+
const childIds = node.children;
|
|
209
|
+
if (!Array.isArray(childIds) || childIds.length === 0)
|
|
210
|
+
return '';
|
|
211
|
+
const overrides = useLiveNodeOverrides.getState().overrides;
|
|
212
|
+
const entries = [];
|
|
213
|
+
for (const childId of childIds) {
|
|
214
|
+
const override = overrides.get(childId);
|
|
215
|
+
if (override)
|
|
216
|
+
entries.push([childId, override]);
|
|
217
|
+
}
|
|
218
|
+
return entries.length === 0 ? '' : JSON.stringify(entries);
|
|
219
|
+
}
|
|
220
|
+
function nodeReferencesSceneMaterial(node) {
|
|
221
|
+
const slots = node.slots;
|
|
222
|
+
if (!slots)
|
|
223
|
+
return false;
|
|
224
|
+
for (const ref of Object.values(slots)) {
|
|
225
|
+
if (typeof ref === 'string' && ref.startsWith('scene:'))
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
function buildGeometryContext(node, nodes, levelData, materials) {
|
|
231
|
+
const resolve = (id) => {
|
|
232
|
+
const resolved = nodes[id];
|
|
233
|
+
return resolved ? getEffectiveNode(resolved) : undefined;
|
|
234
|
+
};
|
|
235
|
+
const childIds = node.children;
|
|
236
|
+
const children = Array.isArray(childIds)
|
|
237
|
+
? childIds.map((cid) => resolve(cid)).filter((n) => n !== undefined)
|
|
238
|
+
: [];
|
|
239
|
+
const parentId = node.parentId;
|
|
240
|
+
const parent = parentId ? (resolve(parentId) ?? null) : null;
|
|
241
|
+
// Siblings = same kind, same parent, excluding self. Walks the parent's
|
|
242
|
+
// children array; falls back to scanning the whole scene if the parent
|
|
243
|
+
// doesn't carry a `children` list (rare — most parents do).
|
|
244
|
+
let siblings = [];
|
|
245
|
+
if (parent) {
|
|
246
|
+
const parentChildIds = parent.children;
|
|
247
|
+
if (Array.isArray(parentChildIds)) {
|
|
248
|
+
for (const sid of parentChildIds) {
|
|
249
|
+
if (sid === node.id)
|
|
250
|
+
continue;
|
|
251
|
+
const s = resolve(sid);
|
|
252
|
+
if (s && s.type === node.type)
|
|
253
|
+
siblings.push(s);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
siblings = Object.values(nodes)
|
|
258
|
+
.filter((n) => n !== node && n.type === node.type && n.parentId === parentId)
|
|
259
|
+
.map((n) => getEffectiveNode(n));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { resolve, children, siblings, parent, levelData, materials };
|
|
263
|
+
}
|
|
264
|
+
function disposeChildren(group) {
|
|
265
|
+
// Only dispose meshes the geometry builder produced on the previous
|
|
266
|
+
// rebuild (marked via `userData.__fromGeometry`). React-managed
|
|
267
|
+
// children (hosted node renderers) get left in place — they have
|
|
268
|
+
// their own React-driven lifecycle and would lose their meshes /
|
|
269
|
+
// materials if we disposed them here.
|
|
270
|
+
for (const child of [...group.children]) {
|
|
271
|
+
const fromGeometry = child.userData
|
|
272
|
+
?.__fromGeometry;
|
|
273
|
+
if (!fromGeometry)
|
|
274
|
+
continue;
|
|
275
|
+
group.remove(child);
|
|
276
|
+
const mesh = child;
|
|
277
|
+
if (mesh.geometry?.dispose)
|
|
278
|
+
mesh.geometry.dispose();
|
|
279
|
+
if ('material' in mesh) {
|
|
280
|
+
const m = mesh.material;
|
|
281
|
+
if (Array.isArray(m)) {
|
|
282
|
+
for (const mat of m) {
|
|
283
|
+
if (isCachedMaterial(mat))
|
|
284
|
+
continue;
|
|
285
|
+
if (mat && typeof mat.dispose === 'function') {
|
|
286
|
+
;
|
|
287
|
+
mat.dispose();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (isCachedMaterial(m)) {
|
|
292
|
+
}
|
|
293
|
+
else if (m && typeof m.dispose === 'function') {
|
|
294
|
+
;
|
|
295
|
+
m.dispose();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function applyDefaultSurfaceRole(root, defaultRole, colorPreset, sceneTheme) {
|
|
301
|
+
root.traverse((child) => {
|
|
302
|
+
const mesh = child;
|
|
303
|
+
if (!('material' in mesh) || !mesh.material)
|
|
304
|
+
return;
|
|
305
|
+
const role = getMeshSurfaceRole(mesh.userData.surfaceRole, defaultRole);
|
|
306
|
+
mesh.userData.surfaceRole = role;
|
|
307
|
+
mesh.material = createSurfaceRoleMaterial(role, colorPreset, getMaterialSide(mesh.material), sceneTheme);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function getMeshSurfaceRole(value, fallback) {
|
|
311
|
+
return typeof value === 'string' && isSurfaceRole(value) ? value : fallback;
|
|
312
|
+
}
|
|
313
|
+
function isSurfaceRole(value) {
|
|
314
|
+
return (value === 'wall' ||
|
|
315
|
+
value === 'floor' ||
|
|
316
|
+
value === 'ceiling' ||
|
|
317
|
+
value === 'roof' ||
|
|
318
|
+
value === 'joinery' ||
|
|
319
|
+
value === 'glazing' ||
|
|
320
|
+
value === 'furnishing');
|
|
321
|
+
}
|
|
322
|
+
function getMaterialSide(material) {
|
|
323
|
+
const source = Array.isArray(material) ? material[0] : material;
|
|
324
|
+
return source?.side ?? FrontSide;
|
|
325
|
+
}
|
|
326
|
+
function isCachedMaterial(value) {
|
|
327
|
+
return Boolean(value?.userData
|
|
328
|
+
?.__pascalCachedMaterial);
|
|
329
|
+
}
|
|
330
|
+
export default GeometrySystem;
|
|
331
|
+
export function markGeometryBuildOutput(child) {
|
|
332
|
+
// Tag the builder subtree so paint previews can reach nested meshes (for
|
|
333
|
+
// example a cabinet sink faucet handle), while disposal still removes only
|
|
334
|
+
// top-level builder children from the registered group.
|
|
335
|
+
child.traverse((object) => {
|
|
336
|
+
object.userData = {
|
|
337
|
+
...object.userData,
|
|
338
|
+
__fromGeometry: true,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
export function shouldReuseGeometryBuild(cache, id, group, key) {
|
|
343
|
+
const cached = cache.get(id);
|
|
344
|
+
if (cached?.group === group && cached.key === key)
|
|
345
|
+
return true;
|
|
346
|
+
cache.set(id, { group, key });
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry-system.test.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
|
|
2
|
+
// depend on @types/bun so the import type is unresolved at compile time.
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
import { BoxGeometry, Group, Mesh, MeshBasicMaterial } from 'three';
|
|
5
|
+
import { markGeometryBuildOutput, shouldReuseGeometryBuild, } from './geometry-system';
|
|
6
|
+
describe('markGeometryBuildOutput', () => {
|
|
7
|
+
test('marks nested builder meshes for slot paint preview', () => {
|
|
8
|
+
const root = new Group();
|
|
9
|
+
const nested = new Group();
|
|
10
|
+
const mesh = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial());
|
|
11
|
+
mesh.userData.slotId = 'appliance';
|
|
12
|
+
nested.add(mesh);
|
|
13
|
+
root.add(nested);
|
|
14
|
+
markGeometryBuildOutput(root);
|
|
15
|
+
expect(root.userData.__fromGeometry).toBe(true);
|
|
16
|
+
expect(nested.userData.__fromGeometry).toBe(true);
|
|
17
|
+
expect(mesh.userData.__fromGeometry).toBe(true);
|
|
18
|
+
expect(mesh.userData.slotId).toBe('appliance');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('shouldReuseGeometryBuild', () => {
|
|
22
|
+
test('rebuilds when the same node id remounts into a new group with the same key', () => {
|
|
23
|
+
const cache = new Map();
|
|
24
|
+
const firstGroup = new Group();
|
|
25
|
+
const remountedGroup = new Group();
|
|
26
|
+
expect(shouldReuseGeometryBuild(cache, 'duct_1', firstGroup, 'same-key')).toBe(false);
|
|
27
|
+
expect(shouldReuseGeometryBuild(cache, 'duct_1', firstGroup, 'same-key')).toBe(true);
|
|
28
|
+
expect(shouldReuseGeometryBuild(cache, 'duct_1', remountedGroup, 'same-key')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Control, ControlValue } from '@pascal-app/core';
|
|
2
|
+
/** One interactive control (toggle / slider / temperature) rendered inside the
|
|
3
|
+
* item controls overlay. Shared by the parametric `InteractiveSystem` and the
|
|
4
|
+
* baked-GLB `GlbInteractive` overlay so both look and behave identically. */
|
|
5
|
+
export declare const ControlWidget: ({ control, value, onChange, }: {
|
|
6
|
+
control: Control;
|
|
7
|
+
value: ControlValue;
|
|
8
|
+
onChange: (v: ControlValue) => void;
|
|
9
|
+
}) => import("react").JSX.Element | null;
|
|
10
|
+
//# sourceMappingURL=control-widget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-widget.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/control-widget.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE7D;;8EAE8E;AAC9E,eAAO,MAAM,aAAa,GAAI,+BAI3B;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;CACpC,uCAuEA,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/** One interactive control (toggle / slider / temperature) rendered inside the
|
|
4
|
+
* item controls overlay. Shared by the parametric `InteractiveSystem` and the
|
|
5
|
+
* baked-GLB `GlbInteractive` overlay so both look and behave identically. */
|
|
6
|
+
export const ControlWidget = ({ control, value, onChange, }) => {
|
|
7
|
+
const labelStyle = {
|
|
8
|
+
color: 'white',
|
|
9
|
+
fontSize: 11,
|
|
10
|
+
fontFamily: 'monospace',
|
|
11
|
+
display: 'flex',
|
|
12
|
+
flexDirection: 'column',
|
|
13
|
+
gap: 2,
|
|
14
|
+
};
|
|
15
|
+
if (control.kind === 'toggle') {
|
|
16
|
+
return (_jsx("button", { onClick: () => onChange(!value), style: {
|
|
17
|
+
background: value ? '#4ade80' : '#374151',
|
|
18
|
+
color: 'white',
|
|
19
|
+
border: 'none',
|
|
20
|
+
borderRadius: 4,
|
|
21
|
+
padding: '4px 8px',
|
|
22
|
+
cursor: 'pointer',
|
|
23
|
+
fontSize: 12,
|
|
24
|
+
fontFamily: 'monospace',
|
|
25
|
+
transition: 'background 0.2s',
|
|
26
|
+
}, children: control.label ?? (value ? 'On' : 'Off') }));
|
|
27
|
+
}
|
|
28
|
+
if (control.kind === 'slider') {
|
|
29
|
+
return (_jsxs("label", { style: labelStyle, children: [_jsxs("span", { children: [control.label, ": ", value, control.unit ? ` ${control.unit}` : ''] }), _jsx("input", { max: control.max, min: control.min, onChange: (e) => onChange(Number(e.target.value)), onPointerDown: (e) => e.stopPropagation(), step: control.step, type: "range", value: value })] }));
|
|
30
|
+
}
|
|
31
|
+
if (control.kind === 'temperature') {
|
|
32
|
+
return (_jsxs("label", { style: labelStyle, children: [_jsxs("span", { children: [control.label, ": ", value, "\u00B0", control.unit] }), _jsx("input", { max: control.max, min: control.min, onChange: (e) => onChange(Number(e.target.value)), onPointerDown: (e) => e.stopPropagation(), step: 1, type: "range", value: value })] }));
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const InteractiveSystem: () => import("react
|
|
1
|
+
export declare const InteractiveSystem: () => import("react").JSX.Element;
|
|
2
2
|
//# sourceMappingURL=interactive-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interactive-system.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/interactive-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"interactive-system.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/interactive-system.tsx"],"names":[],"mappings":"AAiCA,eAAO,MAAM,iBAAiB,mCAsB7B,CAAA"}
|
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, Fragment as _Fragment
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { pointInPolygon, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
|
|
4
4
|
import { Html } from '@react-three/drei';
|
|
5
5
|
import { createPortal, useFrame } from '@react-three/fiber';
|
|
6
|
-
import { useState } from 'react';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
7
|
import { Vector3 } from 'three';
|
|
8
8
|
import { useShallow } from 'zustand/react/shallow';
|
|
9
9
|
import useViewer from '../../store/use-viewer';
|
|
10
|
+
import { ControlWidget } from './control-widget';
|
|
10
11
|
const _tempVec = new Vector3();
|
|
11
|
-
// ---- Parent: one overlay per interactive item ----
|
|
12
|
+
// ---- Parent: one overlay per interactive item inside the selected zone ----
|
|
13
|
+
//
|
|
14
|
+
// The <Html> overlays only exist while a zone is selected and the item sits
|
|
15
|
+
// inside it. Mounting them unconditionally is not an option: each drei <Html>
|
|
16
|
+
// repositions and re-sorts its DOM element on every camera-move frame, and
|
|
17
|
+
// with `occlude` it also raycasts the entire scene per overlay per frame. On
|
|
18
|
+
// large scenes (hundreds of interactive items) that starves the frame budget
|
|
19
|
+
// and the display/z-index churn makes the whole DOM UI flicker.
|
|
20
|
+
//
|
|
21
|
+
// The child components stay rendered (returning null) so an overlay can fade
|
|
22
|
+
// out before its <Html> unmounts.
|
|
12
23
|
export const InteractiveSystem = () => {
|
|
24
|
+
const zoneId = useViewer((s) => s.selection.zoneId);
|
|
25
|
+
const zonePolygon = useScene((s) => {
|
|
26
|
+
if (!zoneId)
|
|
27
|
+
return null;
|
|
28
|
+
const z = s.nodes[zoneId];
|
|
29
|
+
return z?.polygon ?? null;
|
|
30
|
+
});
|
|
13
31
|
const interactiveNodeIds = useScene(useShallow((state) => Object.values(state.nodes)
|
|
14
32
|
.filter((n) => n.type === 'item' && n.asset.interactive != null)
|
|
15
33
|
.map((n) => n.id)));
|
|
16
|
-
return (_jsx(_Fragment, { children: interactiveNodeIds.map((id) => (_jsx(ItemControlsOverlay, { nodeId: id }, id))) }));
|
|
34
|
+
return (_jsx(_Fragment, { children: interactiveNodeIds.map((id) => (_jsx(ItemControlsOverlay, { nodeId: id, zonePolygon: zonePolygon }, id))) }));
|
|
17
35
|
};
|
|
18
36
|
// ---- Child: polls sceneRegistry then portals controls into the item group ----
|
|
19
|
-
const
|
|
37
|
+
const FADE_MS = 300;
|
|
38
|
+
const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
|
|
20
39
|
const node = useScene((state) => state.nodes[nodeId]);
|
|
21
40
|
const [itemObj, setItemObj] = useState(null);
|
|
22
41
|
useFrame(() => {
|
|
@@ -28,26 +47,37 @@ const ItemControlsOverlay = ({ nodeId }) => {
|
|
|
28
47
|
});
|
|
29
48
|
const controlValues = useInteractive(useShallow((state) => state.items[nodeId]?.controlValues));
|
|
30
49
|
const setControlValue = useInteractive((state) => state.setControlValue);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
let visible = false;
|
|
51
|
+
if (itemObj && zonePolygon?.length) {
|
|
52
|
+
itemObj.getWorldPosition(_tempVec);
|
|
53
|
+
visible = pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
|
|
54
|
+
}
|
|
55
|
+
// Fade in on mount and fade out before unmounting the <Html>.
|
|
56
|
+
const [mounted, setMounted] = useState(false);
|
|
57
|
+
const [shown, setShown] = useState(false);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (visible) {
|
|
60
|
+
setMounted(true);
|
|
61
|
+
// Double rAF: the overlay has to paint once at opacity 0 before the
|
|
62
|
+
// opacity-1 style lands, otherwise the fade-in transition is skipped.
|
|
63
|
+
let raf2 = 0;
|
|
64
|
+
const raf1 = requestAnimationFrame(() => {
|
|
65
|
+
raf2 = requestAnimationFrame(() => setShown(true));
|
|
66
|
+
});
|
|
67
|
+
return () => {
|
|
68
|
+
cancelAnimationFrame(raf1);
|
|
69
|
+
cancelAnimationFrame(raf2);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
setShown(false);
|
|
73
|
+
const timeout = setTimeout(() => setMounted(false), FADE_MS);
|
|
74
|
+
return () => clearTimeout(timeout);
|
|
75
|
+
}, [visible]);
|
|
76
|
+
if (!(mounted && itemObj && controlValues && node?.asset.interactive))
|
|
39
77
|
return null;
|
|
40
78
|
const { controls } = node.asset.interactive;
|
|
41
79
|
const [, height] = node.asset.dimensions;
|
|
42
|
-
|
|
43
|
-
let pointerEvents = 'none';
|
|
44
|
-
if (zoneId && zonePolygon?.length) {
|
|
45
|
-
itemObj.getWorldPosition(_tempVec);
|
|
46
|
-
const inside = pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
|
|
47
|
-
opacity = inside ? 1 : 0.1;
|
|
48
|
-
pointerEvents = inside ? 'auto' : 'none';
|
|
49
|
-
}
|
|
50
|
-
return createPortal(_jsx(Html, { center: true, distanceFactor: 8, occlude: true, position: [0, height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { style: {
|
|
80
|
+
return createPortal(_jsx(Html, { center: true, distanceFactor: 8, eps: -1, position: [0, height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { style: {
|
|
51
81
|
display: 'flex',
|
|
52
82
|
flexDirection: 'column',
|
|
53
83
|
gap: 6,
|
|
@@ -56,40 +86,9 @@ const ItemControlsOverlay = ({ nodeId }) => {
|
|
|
56
86
|
borderRadius: 8,
|
|
57
87
|
padding: '8px 12px',
|
|
58
88
|
minWidth: 120,
|
|
59
|
-
pointerEvents,
|
|
89
|
+
pointerEvents: visible ? 'auto' : 'none',
|
|
60
90
|
userSelect: 'none',
|
|
61
|
-
opacity,
|
|
62
|
-
transition:
|
|
91
|
+
opacity: shown ? 1 : 0,
|
|
92
|
+
transition: `opacity ${FADE_MS}ms ease`,
|
|
63
93
|
}, children: controls.map((control, i) => (_jsx(ControlWidget, { control: control, onChange: (v) => setControlValue(nodeId, i, v), value: controlValues[i] ?? false }, i))) }) }), itemObj);
|
|
64
94
|
};
|
|
65
|
-
// ---- Control widgets ----
|
|
66
|
-
const ControlWidget = ({ control, value, onChange, }) => {
|
|
67
|
-
const labelStyle = {
|
|
68
|
-
color: 'white',
|
|
69
|
-
fontSize: 11,
|
|
70
|
-
fontFamily: 'monospace',
|
|
71
|
-
display: 'flex',
|
|
72
|
-
flexDirection: 'column',
|
|
73
|
-
gap: 2,
|
|
74
|
-
};
|
|
75
|
-
if (control.kind === 'toggle') {
|
|
76
|
-
return (_jsx("button", { onClick: () => onChange(!value), style: {
|
|
77
|
-
background: value ? '#4ade80' : '#374151',
|
|
78
|
-
color: 'white',
|
|
79
|
-
border: 'none',
|
|
80
|
-
borderRadius: 4,
|
|
81
|
-
padding: '4px 8px',
|
|
82
|
-
cursor: 'pointer',
|
|
83
|
-
fontSize: 12,
|
|
84
|
-
fontFamily: 'monospace',
|
|
85
|
-
transition: 'background 0.2s',
|
|
86
|
-
}, children: control.label ?? (value ? 'On' : 'Off') }));
|
|
87
|
-
}
|
|
88
|
-
if (control.kind === 'slider') {
|
|
89
|
-
return (_jsxs("label", { style: labelStyle, children: [_jsxs("span", { children: [control.label, ": ", value, control.unit ? ` ${control.unit}` : ''] }), _jsx("input", { max: control.max, min: control.min, onChange: (e) => onChange(Number(e.target.value)), onPointerDown: (e) => e.stopPropagation(), step: control.step, type: "range", value: value })] }));
|
|
90
|
-
}
|
|
91
|
-
if (control.kind === 'temperature') {
|
|
92
|
-
return (_jsxs("label", { style: labelStyle, children: [_jsxs("span", { children: [control.label, ": ", value, "\u00B0", control.unit] }), _jsx("input", { max: control.max, min: control.min, onChange: (e) => onChange(Number(e.target.value)), onPointerDown: (e) => e.stopPropagation(), step: 1, type: "range", value: value })] }));
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
};
|
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-frame wall-side offset for items mounted to wall faces. The slab-
|
|
3
|
+
* elevation lift for floor items lives in the generic
|
|
4
|
+
* `<FloorElevationSystem>` and runs at priority 1 — it has already
|
|
5
|
+
* landed `mesh.position.y` by the time this system clears the dirty
|
|
6
|
+
* mark at priority 2.
|
|
7
|
+
*/
|
|
1
8
|
export declare const ItemSystem: () => null;
|
|
2
9
|
//# sourceMappingURL=item-system.d.ts.map
|
|
@@ -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":"AAcA;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,YA8CtB,CAAA"}
|