@pascal-app/viewer 1.0.6 → 1.1.0
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/README.md +54 -21
- package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
- package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/daylight-atmosphere.js +315 -0
- package/dist/components/viewer/drone-controller.d.ts +15 -0
- package/dist/components/viewer/drone-controller.d.ts.map +1 -0
- package/dist/components/viewer/drone-controller.js +194 -0
- package/dist/components/viewer/glb-interactive.d.ts +4 -18
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +32 -268
- package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
- package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene-motion.js +57 -0
- package/dist/components/viewer/glb-scene.d.ts +5 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +290 -49
- package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
- package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/glb-selection-callout.js +54 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
- package/dist/components/viewer/index.d.ts +5 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +38 -10
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +192 -5
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +150 -94
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- package/dist/components/viewer/scene-environment.js +4 -5
- package/dist/components/viewer/selection-callout.d.ts +51 -0
- package/dist/components/viewer/selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/selection-callout.js +197 -0
- package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
- package/dist/components/viewer/use-daylight-rooms.js +278 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +26 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -8
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/ground.d.ts +21 -0
- package/dist/lib/daylight/ground.d.ts.map +1 -0
- package/dist/lib/daylight/ground.js +53 -0
- package/dist/lib/daylight/location.d.ts +32 -0
- package/dist/lib/daylight/location.d.ts.map +1 -0
- package/dist/lib/daylight/location.js +74 -0
- package/dist/lib/daylight/look.d.ts +33 -0
- package/dist/lib/daylight/look.d.ts.map +1 -0
- package/dist/lib/daylight/look.js +13 -0
- package/dist/lib/daylight/material-hooks.d.ts +2 -0
- package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
- package/dist/lib/daylight/material-hooks.js +150 -0
- package/dist/lib/daylight/room-context.d.ts +12 -0
- package/dist/lib/daylight/room-context.d.ts.map +1 -0
- package/dist/lib/daylight/room-context.js +54 -0
- package/dist/lib/daylight/room-map.d.ts +124 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +630 -0
- package/dist/lib/daylight/room-scene.d.ts +56 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +391 -0
- package/dist/lib/daylight/rooms.d.ts +292 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1268 -0
- package/dist/lib/daylight/runtime.d.ts +33 -0
- package/dist/lib/daylight/runtime.d.ts.map +1 -0
- package/dist/lib/daylight/runtime.js +42 -0
- package/dist/lib/daylight/sky.d.ts +26 -0
- package/dist/lib/daylight/sky.d.ts.map +1 -0
- package/dist/lib/daylight/sky.js +217 -0
- package/dist/lib/daylight/solar.d.ts +46 -0
- package/dist/lib/daylight/solar.d.ts.map +1 -0
- package/dist/lib/daylight/solar.js +156 -0
- package/dist/lib/device-tier.d.ts +29 -0
- package/dist/lib/device-tier.d.ts.map +1 -0
- package/dist/lib/device-tier.js +71 -0
- package/dist/lib/dispose-object3d.d.ts.map +1 -1
- package/dist/lib/dispose-object3d.js +4 -2
- package/dist/lib/drone-flight.d.ts +44 -0
- package/dist/lib/drone-flight.d.ts.map +1 -0
- package/dist/lib/drone-flight.js +121 -0
- package/dist/lib/editor-highlight.d.ts +16 -0
- package/dist/lib/editor-highlight.d.ts.map +1 -0
- package/dist/lib/editor-highlight.js +33 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -1
- package/dist/lib/foundation-materials.js +2 -4
- package/dist/lib/glb-merge.d.ts +23 -0
- package/dist/lib/glb-merge.d.ts.map +1 -0
- package/dist/lib/glb-merge.js +184 -0
- package/dist/lib/hardware-geometry.d.ts +21 -0
- package/dist/lib/hardware-geometry.d.ts.map +1 -0
- package/dist/lib/hardware-geometry.js +283 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- package/dist/lib/layer-pass.d.ts +2 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layer-pass.js +20 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +50 -23
- package/dist/lib/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +5 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +8 -0
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -1
- package/dist/lib/occlusion-culler.d.ts +34 -0
- package/dist/lib/occlusion-culler.d.ts.map +1 -0
- package/dist/lib/occlusion-culler.js +698 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- package/dist/lib/post-processing-resources.d.ts +4 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/post-processing-resources.js +5 -0
- package/dist/lib/readback-rows.d.ts +14 -0
- package/dist/lib/readback-rows.d.ts.map +1 -0
- package/dist/lib/readback-rows.js +31 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -1
- package/dist/lib/reveal-pose.js +3 -0
- package/dist/lib/scene-matrices.d.ts +12 -0
- package/dist/lib/scene-matrices.d.ts.map +1 -0
- package/dist/lib/scene-matrices.js +287 -0
- package/dist/lib/scene-pass-mrt.d.ts +5 -2
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
- package/dist/lib/scene-pass-mrt.js +7 -2
- package/dist/lib/scene-themes.d.ts +0 -2
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +0 -2
- package/dist/lib/scene-visibility.d.ts +2 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +12 -3
- package/dist/lib/site-surfaces.d.ts +1 -8
- package/dist/lib/site-surfaces.d.ts.map +1 -1
- package/dist/lib/site-surfaces.js +0 -19
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +24 -40
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +13 -10
- package/dist/lib/underground-ambiance.d.ts +0 -2
- package/dist/lib/underground-ambiance.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.js +0 -2
- package/dist/lib/viewer-config.d.ts +21 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +21 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/store/use-item-light-pool.d.ts +3 -6
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +12 -0
- package/dist/store/use-viewer.d.ts +62 -59
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +106 -19
- package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +11 -1
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +501 -145
- package/dist/systems/door/leaf-profile.d.ts +11 -0
- package/dist/systems/door/leaf-profile.d.ts.map +1 -0
- package/dist/systems/door/leaf-profile.js +140 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/interactive/callout-actions.d.ts +72 -0
- package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
- package/dist/systems/interactive/callout-actions.js +212 -0
- package/dist/systems/interactive/interactive-system.d.ts +5 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +44 -121
- package/dist/systems/interactive/opening-settled.d.ts +16 -0
- package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
- package/dist/systems/interactive/opening-settled.js +53 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +22 -214
- package/dist/systems/item-light/pooled-lights.d.ts +70 -0
- package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
- package/dist/systems/item-light/pooled-lights.js +449 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -1
- package/dist/systems/roof/roof-material-slots.d.ts +14 -0
- package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
- package/dist/systems/roof/roof-material-slots.js +13 -0
- package/dist/systems/roof/roof-materials.d.ts +25 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +85 -26
- package/dist/systems/roof/roof-system.d.ts +34 -1
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +582 -168
- package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
- package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-casing-geometry.js +102 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
- package/dist/systems/wall/opening-cutout-geometry.js +7 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +28 -35
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +86 -25
- package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
- package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
- package/dist/systems/wall/wall-t-junctions.js +149 -0
- package/dist/systems/window/window-animation-system.d.ts +3 -12
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +37 -122
- package/dist/systems/window/window-pose.d.ts +31 -0
- package/dist/systems/window/window-pose.d.ts.map +1 -0
- package/dist/systems/window/window-pose.js +152 -0
- package/dist/systems/window/window-roller-shutter.d.ts +8 -0
- package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
- package/dist/systems/window/window-roller-shutter.js +64 -0
- package/dist/systems/window/window-system.d.ts +2 -11
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +470 -303
- package/package.json +6 -6
- package/dist/systems/interactive/control-widget.d.ts +0 -10
- package/dist/systems/interactive/control-widget.d.ts.map +0 -1
- package/dist/systems/interactive/control-widget.js +0 -35
- package/dist/systems/interactive/procedural-controls.d.ts +0 -13
- package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
- package/dist/systems/interactive/procedural-controls.js +0 -17
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { emitter, usePascalI18n, } from '@pascal-app/core';
|
|
4
|
+
import { Html } from '@react-three/drei';
|
|
5
|
+
import { useFrame, useThree } from '@react-three/fiber';
|
|
6
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
7
|
+
import { Box3, Vector3 } from 'three';
|
|
8
|
+
function resolve(i18n, text) {
|
|
9
|
+
return typeof text === 'string' ? text : i18n._(text);
|
|
10
|
+
}
|
|
11
|
+
const opens = (icon) => icon === 'door' || icon === 'window' || icon === 'shutters';
|
|
12
|
+
const STEM_MIN = 64;
|
|
13
|
+
const STEM_MAX = 104;
|
|
14
|
+
const EDGE = 16;
|
|
15
|
+
const _box = new Box3();
|
|
16
|
+
const _world = new Vector3();
|
|
17
|
+
const _ndc = new Vector3();
|
|
18
|
+
/**
|
|
19
|
+
* The selected element's label: a dot on it, a hairline rising from the dot,
|
|
20
|
+
* then its name, a detail line, its controls and a close button. It follows
|
|
21
|
+
* the element's world point each frame rather than living inside its object:
|
|
22
|
+
* kinds that rebuild their children (a window opening) would drop it.
|
|
23
|
+
*/
|
|
24
|
+
export function SelectionCallout({ object, anchor: anchorProp, title, subtitle, actions, onDismiss, }) {
|
|
25
|
+
const camera = useThree((state) => state.camera);
|
|
26
|
+
const size = useThree((state) => state.size);
|
|
27
|
+
const rootRef = useRef(null);
|
|
28
|
+
const copyRef = useRef(null);
|
|
29
|
+
const [visible, setVisible] = useState(false);
|
|
30
|
+
const i18n = usePascalI18n();
|
|
31
|
+
const deselect = i18n._({ id: 'viewer.callout.deselect', message: 'Deselect' });
|
|
32
|
+
// The dot sits at the centre of the element's bounds, in its own space.
|
|
33
|
+
const anchor = useMemo(() => {
|
|
34
|
+
if (anchorProp)
|
|
35
|
+
return anchorProp;
|
|
36
|
+
object.updateWorldMatrix(true, true);
|
|
37
|
+
_box.setFromObject(object);
|
|
38
|
+
if (_box.isEmpty())
|
|
39
|
+
return new Vector3();
|
|
40
|
+
return object.worldToLocal(_box.getCenter(new Vector3()));
|
|
41
|
+
}, [object, anchorProp]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const frame = requestAnimationFrame(() => setVisible(true));
|
|
44
|
+
return () => cancelAnimationFrame(frame);
|
|
45
|
+
}, []);
|
|
46
|
+
// Escape deselects, as the close button does.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const onKey = (event) => {
|
|
49
|
+
if (event.key === 'Escape')
|
|
50
|
+
onDismiss();
|
|
51
|
+
};
|
|
52
|
+
window.addEventListener('keydown', onKey);
|
|
53
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
54
|
+
}, [onDismiss]);
|
|
55
|
+
const copyHeight = useRef(0);
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
const copy = copyRef.current;
|
|
58
|
+
if (!copy)
|
|
59
|
+
return;
|
|
60
|
+
const observer = new ResizeObserver(() => {
|
|
61
|
+
copyHeight.current = copy.offsetHeight;
|
|
62
|
+
});
|
|
63
|
+
observer.observe(copy);
|
|
64
|
+
copyHeight.current = copy.offsetHeight;
|
|
65
|
+
return () => observer.disconnect();
|
|
66
|
+
}, []);
|
|
67
|
+
// Stem height and side follow the canvas: the copy flips left near the right
|
|
68
|
+
// edge and hangs below the dot near the top.
|
|
69
|
+
useFrame(() => {
|
|
70
|
+
const root = rootRef.current;
|
|
71
|
+
if (!root)
|
|
72
|
+
return;
|
|
73
|
+
const stem = Math.max(copyHeight.current + 12, Math.min(STEM_MAX, Math.max(STEM_MIN, size.height * 0.12)));
|
|
74
|
+
_ndc.copy(object.localToWorld(_world.copy(anchor))).project(camera);
|
|
75
|
+
const x = ((_ndc.x + 1) / 2) * size.width;
|
|
76
|
+
const y = ((1 - _ndc.y) / 2) * size.height;
|
|
77
|
+
const width = copyRef.current?.offsetWidth ?? 0;
|
|
78
|
+
root.style.setProperty('--callout-stem', `${stem}px`);
|
|
79
|
+
root.dataset.left = String(x + 12 + width > size.width - EDGE);
|
|
80
|
+
root.dataset.below = String(y - stem < EDGE);
|
|
81
|
+
});
|
|
82
|
+
// Drei places the label from its own group: move that group onto the
|
|
83
|
+
// element's anchor first, so projection and the behind-camera test agree.
|
|
84
|
+
const calculatePosition = useCallback((el, view, viewport) => {
|
|
85
|
+
object.localToWorld(_world.copy(anchor));
|
|
86
|
+
el.position.copy(el.parent ? el.parent.worldToLocal(_world) : _world);
|
|
87
|
+
el.updateWorldMatrix(false, false);
|
|
88
|
+
_ndc.setFromMatrixPosition(el.matrixWorld).project(view);
|
|
89
|
+
return [((_ndc.x + 1) / 2) * viewport.width, ((1 - _ndc.y) / 2) * viewport.height];
|
|
90
|
+
}, [object, anchor]);
|
|
91
|
+
return (_jsxs(Html, { calculatePosition: calculatePosition, pointerEvents: "none", zIndexRange: [20, 0], children: [_jsx("style", { children: CALLOUT_CSS }), _jsxs("div", { className: "pascal-callout", "data-visible": visible, ref: rootRef, children: [_jsx("span", { "aria-hidden": "true", className: "pascal-callout-pin" }), _jsx("span", { "aria-hidden": "true", className: "pascal-callout-leader" }), _jsxs("div", { className: "pascal-callout-copy", onClick: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), onPointerUp: (event) => event.stopPropagation(), ref: copyRef, children: [_jsx("strong", { children: title }), subtitle ? (_jsx("span", { className: "pascal-callout-detail", children: resolve(i18n, subtitle) })) : null, _jsxs("div", { className: "pascal-callout-actions", children: [actions.map((action) => action.kind === 'toggle' ? (_jsx(ToggleAction, { action: action }, action.key)) : (_jsx(RangeAction, { action: action }, action.key))), _jsx("button", { "aria-label": deselect, className: "pascal-callout-dismiss", onClick: onDismiss, title: deselect, type: "button", children: _jsx(CalloutGlyph, { icon: "close", on: false }) })] })] })] })] }));
|
|
92
|
+
}
|
|
93
|
+
function ToggleAction({ action }) {
|
|
94
|
+
const i18n = usePascalI18n();
|
|
95
|
+
const text = action.text
|
|
96
|
+
? resolve(i18n, action.text)
|
|
97
|
+
: i18n._({
|
|
98
|
+
id: 'viewer.callout.state',
|
|
99
|
+
message: '{state, select, open {Open} closed {Closed} on {On} other {Off}}',
|
|
100
|
+
values: {
|
|
101
|
+
state: opens(action.icon) ? (action.on ? 'open' : 'closed') : action.on ? 'on' : 'off',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const label = action.icon === 'shutters'
|
|
105
|
+
? i18n._({
|
|
106
|
+
id: 'viewer.callout.shuttersAction',
|
|
107
|
+
message: '{action, select, close {Close the shutters of {name}} other {Open the shutters of {name}}}',
|
|
108
|
+
values: { action: action.on ? 'close' : 'open', name: action.name },
|
|
109
|
+
})
|
|
110
|
+
: i18n._({
|
|
111
|
+
id: 'viewer.callout.action',
|
|
112
|
+
message: '{action, select, open {Open {name}} close {Close {name}} on {Turn on {name}} other {Turn off {name}}}',
|
|
113
|
+
values: {
|
|
114
|
+
action: opens(action.icon) ? (action.on ? 'close' : 'open') : action.on ? 'off' : 'on',
|
|
115
|
+
name: action.text ? resolve(i18n, action.text) : action.name,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return (_jsxs("button", { "aria-label": label, "aria-pressed": action.on, className: "pascal-callout-action", "data-control": action.text ? 'named' : opens(action.icon) ? 'opening' : 'toggle', onClick: () => {
|
|
119
|
+
if (!action.on)
|
|
120
|
+
emitter.emit('interaction:sound', action.sound.on);
|
|
121
|
+
const settled = action.toggle();
|
|
122
|
+
if (action.on)
|
|
123
|
+
playClosing(action, settled);
|
|
124
|
+
}, title: label, type: "button", children: [_jsx(CalloutGlyph, { icon: action.icon, on: action.on }), _jsx("span", { children: text })] }));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A door, window or cabinet door swings shut, then latches as it lands
|
|
128
|
+
* (shutters and drawers just land); anything without a known end sounds at once.
|
|
129
|
+
*/
|
|
130
|
+
function playClosing(action, settled) {
|
|
131
|
+
// A garage door's motor runs the whole way, so it starts with the press.
|
|
132
|
+
if (!settled || action.sound.off === 'garage') {
|
|
133
|
+
emitter.emit('interaction:sound', action.sound.off);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (action.sound.swing)
|
|
137
|
+
emitter.emit('interaction:sound', action.sound.swing);
|
|
138
|
+
void settled.then(() => emitter.emit('interaction:sound', action.sound.off));
|
|
139
|
+
}
|
|
140
|
+
function RangeAction({ action }) {
|
|
141
|
+
return (_jsxs("label", { className: "pascal-callout-action pascal-callout-range", children: [_jsxs("span", { children: [action.label, " ", action.value, action.unit ?? ''] }), _jsx("input", { "aria-label": action.label, max: action.max, min: action.min, onChange: (event) => action.onChange(Number(event.target.value)), step: action.step, type: "range", value: action.value })] }));
|
|
142
|
+
}
|
|
143
|
+
function Svg({ children }) {
|
|
144
|
+
return (_jsx("svg", { "aria-hidden": "true", fill: "none", focusable: "false", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, viewBox: "0 0 24 24", children: children }));
|
|
145
|
+
}
|
|
146
|
+
function CalloutGlyph({ icon, on }) {
|
|
147
|
+
switch (icon) {
|
|
148
|
+
case 'light':
|
|
149
|
+
return (_jsxs(Svg, { children: [_jsx("path", { d: "M9 18h6m-5 3h4M8 13a6 6 0 1 1 8 0c-1.2 1-1 2-1 2H9s.2-1-1-2Z" }), on ? (_jsx("path", { d: "M12 1v1M3 4l1 1m16-1-1 1M1 10h1m20 0h-1" })) : (_jsx("path", { d: "m3 3 18 18", opacity: ".6" }))] }));
|
|
150
|
+
case 'fan':
|
|
151
|
+
return (_jsxs(Svg, { children: [_jsx("circle", { cx: "12", cy: "12", r: "2" }), _jsx("path", { d: "M10.5 10.5C6 5 10 1 13 3c2 2 0 5-1 7M14 11c6-2 9 3 6 5-2 2-5-1-7-3M11.5 14c-1 6-7 6-8 3-1-3 4-4 7-4" }), on ? null : _jsx("path", { d: "m3 3 18 18", opacity: ".6" })] }));
|
|
152
|
+
case 'window':
|
|
153
|
+
return (_jsx(Svg, { children: _jsx("path", { d: on
|
|
154
|
+
? 'M3 3h18v18H3ZM3 12h10M13 3v18m0-14 6-2v14l-6-2'
|
|
155
|
+
: 'M3 3h18v18H3ZM12 3v18M3 12h18' }) }));
|
|
156
|
+
case 'door':
|
|
157
|
+
return (_jsx(Svg, { children: _jsx("path", { d: on ? 'M3 21h18M5 21V3h14v18M5 3l9 3v15M11 12v2' : 'M3 21h18M5 21V3h14v18M15 12h1' }) }));
|
|
158
|
+
case 'shutters':
|
|
159
|
+
return (_jsx(Svg, { children: on ? (_jsxs(_Fragment, { children: [_jsx("rect", { height: "16", rx: "1", width: "8", x: "8", y: "4" }), _jsx("path", { d: "M8 4 3 6v12l5 2M16 4l5 2v12l-5 2" })] })) : (_jsxs(_Fragment, { children: [_jsx("rect", { height: "18", rx: "1", width: "16", x: "4", y: "3" }), _jsx("path", { d: "M12 3v18M7 8h2M7 12h2M7 16h2M15 8h2M15 12h2M15 16h2" })] })) }));
|
|
160
|
+
case 'close':
|
|
161
|
+
return (_jsx(Svg, { children: _jsx("path", { d: "m6 6 12 12M6 18 18 6" }) }));
|
|
162
|
+
default:
|
|
163
|
+
return (_jsx(Svg, { children: _jsx("path", { d: "M12 2v9m-5-7a9 9 0 1 0 10 0" }) }));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// The marketing house's callout at dusk: light ink on a dark halo reads over
|
|
167
|
+
// any scene; only the controls get a surface.
|
|
168
|
+
const CALLOUT_CSS = `
|
|
169
|
+
.pascal-callout { --callout-ink:#f1f2f4; --callout-secondary:#e0e3e8; --callout-halo:#000000e0; --callout-halo-soft:#00000080; --callout-control:#24272d70; --callout-active:#e7e9ee2b; position:absolute; width:0; height:0; pointer-events:none; user-select:none; color:var(--callout-ink); text-shadow:0 0 2px var(--callout-halo), 0 1px 6px var(--callout-halo-soft); opacity:0; transition:opacity 70ms ease; }
|
|
170
|
+
.pascal-callout[data-visible="true"] { opacity:1; transition-duration:0ms; }
|
|
171
|
+
.pascal-callout-pin { position:absolute; left:-2px; top:-2px; width:4px; height:4px; border-radius:50%; background:currentColor; box-shadow:0 0 2px var(--callout-halo); opacity:.8; }
|
|
172
|
+
.pascal-callout-leader { position:absolute; left:0; bottom:0; width:1px; height:var(--callout-stem, 100px); background:currentColor; box-shadow:0 0 2px var(--callout-halo); opacity:.48; transform:scaleY(0); transform-origin:bottom; transition:transform 100ms cubic-bezier(.22,1,.36,1); }
|
|
173
|
+
.pascal-callout[data-visible="true"] .pascal-callout-leader { transform:scaleY(1); }
|
|
174
|
+
.pascal-callout-copy { position:absolute; left:12px; bottom:var(--callout-stem, 100px); width:max-content; transform:translateY(100%); pointer-events:auto; }
|
|
175
|
+
.pascal-callout-copy strong { display:block; max-width:min(220px, calc(100vw - 32px)); font-size:15px; line-height:1.3; font-weight:550; letter-spacing:-.015em; }
|
|
176
|
+
.pascal-callout-detail { display:block; max-width:min(220px, calc(100vw - 32px)); margin-top:4px; font-size:11px; line-height:1.3; letter-spacing:.025em; font-variant-numeric:tabular-nums; color:var(--callout-secondary); }
|
|
177
|
+
.pascal-callout[data-left="true"] .pascal-callout-copy { left:auto; right:12px; text-align:right; }
|
|
178
|
+
.pascal-callout[data-left="true"] .pascal-callout-copy strong, .pascal-callout[data-left="true"] .pascal-callout-detail { margin-left:auto; }
|
|
179
|
+
.pascal-callout[data-left="true"] .pascal-callout-actions { justify-content:flex-end; }
|
|
180
|
+
.pascal-callout[data-below="true"] .pascal-callout-leader { bottom:auto; top:0; transform-origin:top; }
|
|
181
|
+
.pascal-callout[data-below="true"] .pascal-callout-copy { bottom:auto; top:var(--callout-stem, 100px); transform:translateY(-100%); }
|
|
182
|
+
.pascal-callout-actions { display:flex; flex-wrap:nowrap; gap:6px; margin-top:9px; align-items:center; }
|
|
183
|
+
.pascal-callout-actions button, .pascal-callout-range { min-width:44px; height:44px; padding:0 10px; display:inline-flex; align-items:center; justify-content:center; gap:6px; color:var(--callout-ink); font:inherit; font-size:10px; border:1px solid color-mix(in srgb, currentColor 20%, transparent); border-radius:22px; background:var(--callout-control); -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px); text-shadow:0 0 2px var(--callout-halo), 0 1px 6px var(--callout-halo-soft); cursor:pointer; touch-action:manipulation; white-space:nowrap; }
|
|
184
|
+
.pascal-callout-actions button[aria-pressed="true"] { background:var(--callout-active); }
|
|
185
|
+
.pascal-callout-actions button:active { transform:scale(.96); }
|
|
186
|
+
.pascal-callout-actions button:focus-visible { outline:2px solid var(--callout-ink); outline-offset:3px; }
|
|
187
|
+
.pascal-callout-actions svg { width:18px; height:18px; flex:none; filter:drop-shadow(0 0 2px var(--callout-halo)); }
|
|
188
|
+
.pascal-callout-action > span { display:inline-block; text-align:left; }
|
|
189
|
+
.pascal-callout-action[data-control="toggle"] > span { width:1.7em; }
|
|
190
|
+
.pascal-callout-action[data-control="opening"] > span { width:3.4em; }
|
|
191
|
+
.pascal-callout-actions .pascal-callout-dismiss { padding:0; background:transparent; border-color:transparent; -webkit-backdrop-filter:none; backdrop-filter:none; }
|
|
192
|
+
.pascal-callout-range { cursor:default; }
|
|
193
|
+
.pascal-callout-range > span { font-variant-numeric:tabular-nums; }
|
|
194
|
+
.pascal-callout-range input { width:96px; accent-color:var(--callout-ink); }
|
|
195
|
+
@media (max-width:760px) { .pascal-callout-copy strong, .pascal-callout-detail { max-width:160px; } .pascal-callout-copy strong { font-size:14px; } }
|
|
196
|
+
@media (prefers-reduced-motion: reduce) { .pascal-callout, .pascal-callout-leader { transition:none; } }
|
|
197
|
+
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unsupported-gpu-fallback.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/unsupported-gpu-fallback.tsx"],"names":[],"mappings":"AAAA,wBAAgB,4BAA4B,
|
|
1
|
+
{"version":3,"file":"unsupported-gpu-fallback.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/unsupported-gpu-fallback.tsx"],"names":[],"mappings":"AAAA,wBAAgB,4BAA4B,gCAsC3C"}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
export function UnsupportedGpuViewerFallback() {
|
|
3
|
-
return (_jsx("div", {
|
|
3
|
+
return (_jsx("div", { style: {
|
|
4
|
+
display: 'flex',
|
|
5
|
+
height: '100%',
|
|
6
|
+
minHeight: 256,
|
|
7
|
+
width: '100%',
|
|
8
|
+
alignItems: 'center',
|
|
9
|
+
justifyContent: 'center',
|
|
10
|
+
background: '#fafafa',
|
|
11
|
+
padding: 24,
|
|
12
|
+
textAlign: 'center',
|
|
13
|
+
color: '#171717',
|
|
14
|
+
boxSizing: 'border-box',
|
|
15
|
+
}, children: _jsxs("div", { style: {
|
|
16
|
+
maxWidth: 448,
|
|
17
|
+
boxSizing: 'border-box',
|
|
18
|
+
borderRadius: 16,
|
|
19
|
+
border: '1px solid #e5e5e5',
|
|
20
|
+
background: 'white',
|
|
21
|
+
padding: 24,
|
|
22
|
+
boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
|
23
|
+
}, children: [_jsx("h2", { style: { margin: 0, fontWeight: 600, fontSize: 18, lineHeight: '28px' }, children: "3D viewer unavailable" }), _jsx("p", { style: { margin: '8px 0 0', color: '#525252', fontSize: 14, lineHeight: '20px' }, children: "This browser or environment could not initialize WebGPU or WebGL, so Pascal cannot render the 3D scene here. Try opening the editor in a browser with hardware acceleration enabled." })] }) }));
|
|
4
24
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the daylight room system in step with the scene: rebuilds rooms, portals and lamps
|
|
3
|
+
* after edits (debounced; uniforms and textures only, no recompiles), and every frame feeds it
|
|
4
|
+
* the live level heights, the camera, lamp switches and door openness.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useDaylightRooms(): void;
|
|
7
|
+
/** Whether a scene edit touched anything the rooms are built from (anything but furniture). Exported for tests. */
|
|
8
|
+
export declare function roomInputsChanged(next: Readonly<Record<string, {
|
|
9
|
+
type: string;
|
|
10
|
+
} | undefined>>, previous: Readonly<Record<string, {
|
|
11
|
+
type: string;
|
|
12
|
+
} | undefined>>): boolean;
|
|
13
|
+
//# sourceMappingURL=use-daylight-rooms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-daylight-rooms.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/use-daylight-rooms.ts"],"names":[],"mappings":"AAyCA;;;;GAIG;AACH,wBAAgB,gBAAgB,SAiI/B;AAuCD,mHAAmH;AACnH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CAAC,EAC5D,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CAAC,GAC/D,OAAO,CAcT"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { sceneRegistry, useInteractive, useScene } from '@pascal-app/core';
|
|
3
|
+
import { useFrame, useThree } from '@react-three/fiber';
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
|
+
import { Box3, Vector3 } from 'three';
|
|
6
|
+
import { DEFAULT_TEXTURE_LIMIT } from '../../lib/daylight/room-map';
|
|
7
|
+
import { bakedLevelObjects, bakedRoomScene, feedLamps, lampFeeds, liveDoorOpenness, liveLevelObjects, liveRoomScene, syncLampEmitters, } from '../../lib/daylight/room-scene';
|
|
8
|
+
import { daylightRooms } from '../../lib/daylight/rooms';
|
|
9
|
+
import { heldReasons } from '../../lib/scene-visibility';
|
|
10
|
+
import useViewer from '../../store/use-viewer';
|
|
11
|
+
/** Seconds between scene-change checks, and the quiet time before a rebuild. */
|
|
12
|
+
const CHECK_INTERVAL = 0.5;
|
|
13
|
+
const REBUILD_DELAY = 0.4;
|
|
14
|
+
/** Longest wait for the scene to settle before building anyway (seconds). */
|
|
15
|
+
const SETTLE_WAIT = 10;
|
|
16
|
+
/**
|
|
17
|
+
* Furniture: what the rooms are built from (walls, slabs, ceilings, roofs, doors, windows,
|
|
18
|
+
* zones) never includes it, so moving or placing it doesn't rebuild them (a rebuild casts
|
|
19
|
+
* thousands of rays). A lamp among it is followed by its light's position instead.
|
|
20
|
+
*/
|
|
21
|
+
const FURNITURE_TYPES = new Set([
|
|
22
|
+
'item',
|
|
23
|
+
'procedural-item',
|
|
24
|
+
'imported-mesh',
|
|
25
|
+
'cabinet',
|
|
26
|
+
'cabinet-module',
|
|
27
|
+
'shelf',
|
|
28
|
+
]);
|
|
29
|
+
const LAMP_POSITION_STEP = 0.05;
|
|
30
|
+
/**
|
|
31
|
+
* Keeps the daylight room system in step with the scene: rebuilds rooms, portals and lamps
|
|
32
|
+
* after edits (debounced; uniforms and textures only, no recompiles), and every frame feeds it
|
|
33
|
+
* the live level heights, the camera, lamp switches and door openness.
|
|
34
|
+
*/
|
|
35
|
+
export function useDaylightRooms() {
|
|
36
|
+
const scene = useThree((s) => s.scene);
|
|
37
|
+
const gl = useThree((s) => s.gl);
|
|
38
|
+
const nodesVersion = useRef(0);
|
|
39
|
+
const state = useRef({
|
|
40
|
+
signature: '',
|
|
41
|
+
pendingSince: -1,
|
|
42
|
+
sinceCheck: CHECK_INTERVAL,
|
|
43
|
+
/** Times the scene finished rebuilding geometry (no dirty nodes left), and whether it is now. */
|
|
44
|
+
settles: 0,
|
|
45
|
+
dirty: false,
|
|
46
|
+
dirtyRooms: false,
|
|
47
|
+
/** Dirty nodes left when rooms were last built mid-load (they rebuild as the count falls). */
|
|
48
|
+
builtDirty: Number.POSITIVE_INFINITY,
|
|
49
|
+
/** Level offsets last frame: rays are cast at stacked heights, so no build while levels move. */
|
|
50
|
+
offsets: [],
|
|
51
|
+
levelsMoved: false,
|
|
52
|
+
live: true,
|
|
53
|
+
bakedRoot: null,
|
|
54
|
+
levelObjects: [],
|
|
55
|
+
});
|
|
56
|
+
useEffect(() => useScene.subscribe((next, previous) => {
|
|
57
|
+
if (next.nodes !== previous.nodes && roomInputsChanged(next.nodes, previous.nodes))
|
|
58
|
+
nodesVersion.current++;
|
|
59
|
+
}), []);
|
|
60
|
+
useFrame(({ camera }, delta) => {
|
|
61
|
+
const s = state.current;
|
|
62
|
+
const lampDefault = useInteractive.getState().lampDefault;
|
|
63
|
+
s.sinceCheck += delta;
|
|
64
|
+
// Rooms are built from finished walls, windows and covers: wait for the scene to settle, and
|
|
65
|
+
// build again each time it does (walls and openings finish without a geometry revision).
|
|
66
|
+
const { dirtyNodes, nodes } = useScene.getState();
|
|
67
|
+
const dirty = dirtyNodes.size > 0;
|
|
68
|
+
if (dirty && !s.dirtyRooms)
|
|
69
|
+
for (const id of dirtyNodes) {
|
|
70
|
+
const type = nodes[id]?.type;
|
|
71
|
+
if (type && !FURNITURE_TYPES.has(type)) {
|
|
72
|
+
s.dirtyRooms = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (s.dirty && !dirty && s.dirtyRooms) {
|
|
77
|
+
s.settles++;
|
|
78
|
+
s.dirtyRooms = false;
|
|
79
|
+
}
|
|
80
|
+
s.dirty = dirty;
|
|
81
|
+
if (s.sinceCheck >= CHECK_INTERVAL) {
|
|
82
|
+
s.sinceCheck = 0;
|
|
83
|
+
// Items can swap their materials (edits, loads): keep the lamps' own emitters installed.
|
|
84
|
+
syncLampEmitters();
|
|
85
|
+
const live = (sceneRegistry.byType.zone?.size ?? 0) > 0;
|
|
86
|
+
const baked = live ? null : findBakedRoot(scene);
|
|
87
|
+
const feedKeys = feedSignature();
|
|
88
|
+
const signature = live
|
|
89
|
+
? `live|${roomObjectsSignature()}|${nodesVersion.current}|${useViewer.getState().geometryRevision}|${s.settles}|${feedKeys}`
|
|
90
|
+
: `baked|${baked?.uuid ?? ''}|${baked?.children.length ?? 0}|${feedKeys}`;
|
|
91
|
+
if (signature !== s.signature) {
|
|
92
|
+
s.signature = signature;
|
|
93
|
+
s.pendingSince = 0;
|
|
94
|
+
s.live = live;
|
|
95
|
+
s.bakedRoot = baked;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (s.pendingSince >= 0) {
|
|
99
|
+
s.pendingSince += delta;
|
|
100
|
+
const exploded = useViewer.getState().levelMode === 'exploded' || s.levelsMoved;
|
|
101
|
+
if (s.pendingSince >= REBUILD_DELAY &&
|
|
102
|
+
!exploded &&
|
|
103
|
+
(!(s.live && dirty) || s.pendingSince > SETTLE_WAIT)) {
|
|
104
|
+
s.pendingSince = -1;
|
|
105
|
+
const input = s.live
|
|
106
|
+
? liveRoomScene(lampDefault)
|
|
107
|
+
: s.bakedRoot
|
|
108
|
+
? bakedRoomScene(s.bakedRoot, lampDefault)
|
|
109
|
+
: null;
|
|
110
|
+
daylightRooms.map.textureLimit = textureLimit(gl);
|
|
111
|
+
daylightRooms.build(input ?? {
|
|
112
|
+
bounds: emptyBounds,
|
|
113
|
+
levels: [],
|
|
114
|
+
zones: [],
|
|
115
|
+
covers: [],
|
|
116
|
+
walls: [],
|
|
117
|
+
openings: [],
|
|
118
|
+
lamps: feedLamps(),
|
|
119
|
+
virtualLampsOn: lampDefault,
|
|
120
|
+
});
|
|
121
|
+
s.levelObjects = s.live ? liveLevelObjects() : bakedLevelObjects(s.bakedRoot);
|
|
122
|
+
// Built before the scene settled (a large scene still building walls and windows): build
|
|
123
|
+
// again after the next wait while geometry keeps arriving, so the rooms catch up.
|
|
124
|
+
if (s.live && dirty && dirtyNodes.size < s.builtDirty) {
|
|
125
|
+
s.builtDirty = dirtyNodes.size;
|
|
126
|
+
s.pendingSince = 0;
|
|
127
|
+
}
|
|
128
|
+
else if (!dirty)
|
|
129
|
+
s.builtDirty = Number.POSITIVE_INFINITY;
|
|
130
|
+
flagGlassMaterials(scene);
|
|
131
|
+
syncLampEmitters();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Live level heights (stacked / exploded motion), what is shown, and the camera.
|
|
135
|
+
const levels = daylightRooms.levels;
|
|
136
|
+
const offsets = s.levelObjects.map((object, index) => object ? object.getWorldPosition(scratch).y - (levels[index]?.y ?? 0) : 0);
|
|
137
|
+
s.levelsMoved = offsets.some((offset, index) => Math.abs(offset - (s.offsets[index] ?? offset)) > 1e-4);
|
|
138
|
+
s.offsets = offsets;
|
|
139
|
+
const shown = s.levelObjects.map((object) => !!object && presented(object));
|
|
140
|
+
const focus = selectedFocusLevel();
|
|
141
|
+
const selected = s.levelObjects.map((object) => !!object && object === focus);
|
|
142
|
+
daylightRooms.setLevelPresentation(offsets, shown, selected);
|
|
143
|
+
daylightRooms.cameraY.value = camera.position.y;
|
|
144
|
+
// Lamp switches (fed lamps by key; virtual lamps follow the time-of-day default) and doors.
|
|
145
|
+
const lit = litFeedKeys();
|
|
146
|
+
if (daylightRooms.setLampStates((lamp) => (lamp.itemId ? lit.has(lamp.itemId) : lampDefault)))
|
|
147
|
+
syncLampEmitters();
|
|
148
|
+
if (s.live)
|
|
149
|
+
daylightRooms.updateDoors(liveDoorOpenness);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const emptyBounds = new Box3();
|
|
153
|
+
const scratch = new Vector3();
|
|
154
|
+
/**
|
|
155
|
+
* Glass is a slot on the mesh, but shaders compile per material (shared by every pane): mark
|
|
156
|
+
* the material itself so the tuned glass applies whichever pane compiles it first.
|
|
157
|
+
*/
|
|
158
|
+
function flagGlassMaterials(scene) {
|
|
159
|
+
scene.traverse((object) => {
|
|
160
|
+
const mesh = object;
|
|
161
|
+
if (!mesh.isMesh || mesh.userData.slotId !== 'glass')
|
|
162
|
+
return;
|
|
163
|
+
for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
|
|
164
|
+
if (!material || material.userData.isGlass)
|
|
165
|
+
continue;
|
|
166
|
+
material.userData.isGlass = true;
|
|
167
|
+
material.needsUpdate = true;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const feedPosition = new Vector3();
|
|
172
|
+
/** The lamps fed to the rooms and where they stand (a moved lamp, or its host, rebuilds). */
|
|
173
|
+
function feedSignature() {
|
|
174
|
+
let keys = '';
|
|
175
|
+
for (const feed of lampFeeds) {
|
|
176
|
+
for (const light of feed.lights()) {
|
|
177
|
+
keys += `${light.key}`;
|
|
178
|
+
if (light.getWorldPosition(feedPosition)) {
|
|
179
|
+
const q = (v) => Math.round(v / LAMP_POSITION_STEP);
|
|
180
|
+
keys += `@${q(feedPosition.x)},${q(feedPosition.y)},${q(feedPosition.z)}`;
|
|
181
|
+
}
|
|
182
|
+
keys += ';';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return keys;
|
|
186
|
+
}
|
|
187
|
+
/** Whether a scene edit touched anything the rooms are built from (anything but furniture). Exported for tests. */
|
|
188
|
+
export function roomInputsChanged(next, previous) {
|
|
189
|
+
for (const id in next) {
|
|
190
|
+
const node = next[id];
|
|
191
|
+
const before = previous[id];
|
|
192
|
+
if (node === before || FURNITURE_TYPES.has((node ?? before)?.type ?? ''))
|
|
193
|
+
continue;
|
|
194
|
+
// A level (or any container) whose child list alone changed: what joined or left it shows up
|
|
195
|
+
// as its own change, so furniture re-parenting under it doesn't rebuild the rooms.
|
|
196
|
+
if (node && before && onlyChildrenDiffer(node, before))
|
|
197
|
+
continue;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
for (const id in previous) {
|
|
201
|
+
if (!(id in next) && !FURNITURE_TYPES.has(previous[id]?.type ?? ''))
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
function onlyChildrenDiffer(a, b) {
|
|
207
|
+
const x = a;
|
|
208
|
+
const y = b;
|
|
209
|
+
for (const key in x)
|
|
210
|
+
if (key !== 'children' && x[key] !== y[key])
|
|
211
|
+
return false;
|
|
212
|
+
for (const key in y)
|
|
213
|
+
if (key !== 'children' && !(key in x))
|
|
214
|
+
return false;
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
/** The registered objects the rooms are built from: changes when one mounts, unmounts or is replaced. */
|
|
218
|
+
function roomObjectsSignature() {
|
|
219
|
+
let count = 0;
|
|
220
|
+
let ids = 0;
|
|
221
|
+
for (const [type, set] of Object.entries(sceneRegistry.byType)) {
|
|
222
|
+
if (FURNITURE_TYPES.has(type))
|
|
223
|
+
continue;
|
|
224
|
+
for (const id of set) {
|
|
225
|
+
const object = sceneRegistry.nodes.get(id);
|
|
226
|
+
count++;
|
|
227
|
+
if (object)
|
|
228
|
+
ids += object.id;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return `${count}:${ids}`;
|
|
232
|
+
}
|
|
233
|
+
function litFeedKeys() {
|
|
234
|
+
const lit = new Set();
|
|
235
|
+
for (const feed of lampFeeds)
|
|
236
|
+
for (const light of feed.lights())
|
|
237
|
+
if (feed.intensity(light) > 0)
|
|
238
|
+
lit.add(light.key);
|
|
239
|
+
return lit;
|
|
240
|
+
}
|
|
241
|
+
/** The baked artifact's root (the first object carrying export identities), if any. */
|
|
242
|
+
function findBakedRoot(scene) {
|
|
243
|
+
let found = null;
|
|
244
|
+
scene.traverse((object) => {
|
|
245
|
+
if (found)
|
|
246
|
+
return;
|
|
247
|
+
const kind = object.userData.kind;
|
|
248
|
+
if (kind === 'site' || kind === 'building' || kind === 'level')
|
|
249
|
+
found = object.parent ?? object;
|
|
250
|
+
});
|
|
251
|
+
return found;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* On screen: visible through every ancestor (hidden levels are switched off on their group) and
|
|
255
|
+
* not drawn for shadows only (levels hidden above the selection keep shadowing it).
|
|
256
|
+
*/
|
|
257
|
+
function presented(object) {
|
|
258
|
+
for (let x = object; x; x = x.parent)
|
|
259
|
+
if (!x.visible)
|
|
260
|
+
return false;
|
|
261
|
+
return !heldReasons(object)?.has('shadow-only');
|
|
262
|
+
}
|
|
263
|
+
/** The selected level when the levels over it are hidden (solo, or hide-above), else null. */
|
|
264
|
+
function selectedFocusLevel() {
|
|
265
|
+
const viewer = useViewer.getState();
|
|
266
|
+
const hidesAbove = viewer.levelMode === 'solo' || viewer.hideLevelsAboveSelection;
|
|
267
|
+
const levelId = viewer.selection.levelId;
|
|
268
|
+
if (!hidesAbove || !levelId)
|
|
269
|
+
return null;
|
|
270
|
+
return sceneRegistry.nodes.get(levelId) ?? null;
|
|
271
|
+
}
|
|
272
|
+
/** The device's largest 2D texture side (WebGPU device limits, or the WebGL context's). */
|
|
273
|
+
function textureLimit(gl) {
|
|
274
|
+
const backend = gl.backend;
|
|
275
|
+
const limit = backend?.device?.limits?.maxTextureDimension2D ??
|
|
276
|
+
backend?.gl?.getParameter(backend.gl.MAX_TEXTURE_SIZE);
|
|
277
|
+
return limit && limit > 0 ? limit : DEFAULT_TEXTURE_LIMIT;
|
|
278
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewer-camera.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/viewer-camera.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"viewer-camera.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/viewer-camera.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAYnC,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,cAWxF;AAcD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,OAAO;;;EAExD;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,sBAAsB,IAAI,IAAI,CAAA;CAAE,EACrE,WAAW,EAAE,OAAO,QAMrB;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB,IAAI,IAAI,CAAA;CAC/B,cASA;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,WAEnF;AAED,eAAO,MAAM,YAAY,oBAA6B;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,gCA4B9E,CAAA"}
|
|
@@ -2,9 +2,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { OrthographicCamera, PerspectiveCamera } from '@react-three/drei';
|
|
3
3
|
import { useThree } from '@react-three/fiber';
|
|
4
4
|
import { useEffect } from 'react';
|
|
5
|
+
import { isDaylightAtmosphere } from '../../lib/daylight/look';
|
|
5
6
|
import { GRID_LAYER, OVERLAY_LAYER, ZONE_LAYER } from '../../lib/layers';
|
|
6
7
|
import useViewer from '../../store/use-viewer';
|
|
8
|
+
import { useSceneAtmosphere } from './scene-atmosphere';
|
|
7
9
|
import { useSceneGroundReplacement } from './scene-ground-replacement';
|
|
10
|
+
/** Far plane of the daylight look (its ground fades into the sky by 2.6 km). */
|
|
11
|
+
const DAYLIGHT_FAR = 6000;
|
|
8
12
|
const IMMERSIVE_XR_VISIBLE_LAYERS = [OVERLAY_LAYER, ZONE_LAYER, GRID_LAYER];
|
|
9
13
|
export function enableImmersiveXRViewLayers(cameraLayers, raycasterLayers) {
|
|
10
14
|
const cameraMask = cameraLayers.mask;
|
|
@@ -53,6 +57,13 @@ export function viewerUsesPerspectiveCamera(cameraMode, immersiveXR) {
|
|
|
53
57
|
export const ViewerCamera = ({ immersiveXR = false }) => {
|
|
54
58
|
const cameraMode = useViewer((state) => state.cameraMode);
|
|
55
59
|
const clipping = viewerCameraClipping(immersiveXR);
|
|
56
|
-
const
|
|
60
|
+
const groundReplaced = useSceneGroundReplacement();
|
|
61
|
+
// The daylight look's ground runs to the horizon: see it out to where it meets the sky.
|
|
62
|
+
const daylight = isDaylightAtmosphere(useSceneAtmosphere());
|
|
63
|
+
const far = groundReplaced
|
|
64
|
+
? 20_000
|
|
65
|
+
: daylight
|
|
66
|
+
? Math.max(clipping.far, DAYLIGHT_FAR)
|
|
67
|
+
: clipping.far;
|
|
57
68
|
return (_jsxs(_Fragment, { children: [viewerUsesPerspectiveCamera(cameraMode, immersiveXR) ? (_jsx(PerspectiveCamera, { far: far, fov: 50, makeDefault: true, near: clipping.near, position: [10, 10, 10] })) : (_jsx(OrthographicCamera, { far: 1000, makeDefault: true, near: -1000, position: [10, 10, 10], zoom: 20 })), _jsx(ImmersiveXRViewLayers, { enabled: immersiveXR })] }));
|
|
58
69
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-gltf-ktx2.d.ts","sourceRoot":"","sources":["../../src/hooks/use-gltf-ktx2.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"use-gltf-ktx2.d.ts","sourceRoot":"","sources":["../../src/hooks/use-gltf-ktx2.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAO3C,QAAA,MAAM,WAAW,SAAU,MAAM,KAAG,UAAU,CAAC,OAAO,OAAO,CAQ5D,CAAA;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -3,9 +3,10 @@ import { useThree } from '@react-three/fiber';
|
|
|
3
3
|
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
|
4
4
|
import { preserveGltfUnlit } from '../lib/gltf-unlit';
|
|
5
5
|
import { configureKtx2Support } from '../lib/ktx2-loader';
|
|
6
|
+
import { getViewerConfig } from '../lib/viewer-config';
|
|
6
7
|
const useGLTFKTX2 = (path) => {
|
|
7
8
|
const gl = useThree((state) => state.gl);
|
|
8
|
-
return useGLTF(path,
|
|
9
|
+
return useGLTF(path, getViewerConfig().decoders.dracoPath, true, (loader) => {
|
|
9
10
|
loader.register(preserveGltfUnlit);
|
|
10
11
|
configureKtx2Support(loader, gl);
|
|
11
12
|
loader.setMeshoptDecoder(MeshoptDecoder);
|