@pascal-app/viewer 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/renderers/building/building-renderer.d.ts +5 -0
- package/dist/components/renderers/building/building-renderer.d.ts.map +1 -0
- package/dist/components/renderers/building/building-renderer.js +11 -0
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +5 -0
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -0
- package/dist/components/renderers/ceiling/ceiling-renderer.js +24 -0
- package/dist/components/renderers/guide/guide-renderer.d.ts +5 -0
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +1 -0
- package/dist/components/renderers/guide/guide-renderer.js +36 -0
- package/dist/components/renderers/item/item-renderer.d.ts +5 -0
- package/dist/components/renderers/item/item-renderer.d.ts.map +1 -0
- package/dist/components/renderers/item/item-renderer.js +70 -0
- package/dist/components/renderers/level/level-renderer.d.ts +5 -0
- package/dist/components/renderers/level/level-renderer.d.ts.map +1 -0
- package/dist/components/renderers/level/level-renderer.js +11 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -0
- package/dist/components/renderers/node-renderer.d.ts.map +1 -0
- package/dist/components/renderers/node-renderer.js +19 -0
- package/dist/components/renderers/roof/roof-renderer.d.ts +5 -0
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -0
- package/dist/components/renderers/roof/roof-renderer.js +10 -0
- package/dist/components/renderers/scan/scan-renderer.d.ts +5 -0
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +1 -0
- package/dist/components/renderers/scan/scan-renderer.js +50 -0
- package/dist/components/renderers/scene-renderer.d.ts +2 -0
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -0
- package/dist/components/renderers/scene-renderer.js +8 -0
- package/dist/components/renderers/slab/slab-renderer.d.ts +5 -0
- package/dist/components/renderers/slab/slab-renderer.d.ts.map +1 -0
- package/dist/components/renderers/slab/slab-renderer.js +10 -0
- package/dist/components/renderers/wall/wall-renderer.d.ts +5 -0
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +1 -0
- package/dist/components/renderers/wall/wall-renderer.js +11 -0
- package/dist/components/renderers/zone/zone-renderer.d.ts +5 -0
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +1 -0
- package/dist/components/renderers/zone/zone-renderer.js +154 -0
- package/dist/components/viewer/index.d.ts +13 -0
- package/dist/components/viewer/index.d.ts.map +1 -0
- package/dist/components/viewer/index.js +29 -0
- package/dist/components/viewer/lights.d.ts +2 -0
- package/dist/components/viewer/lights.d.ts.map +1 -0
- package/dist/components/viewer/lights.js +10 -0
- package/dist/components/viewer/post-processing.d.ts +17 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -0
- package/dist/components/viewer/post-processing.js +139 -0
- package/dist/components/viewer/selection-manager.d.ts +2 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -0
- package/dist/components/viewer/selection-manager.js +279 -0
- package/dist/components/viewer/viewer-camera.d.ts +2 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -0
- package/dist/components/viewer/viewer-camera.js +7 -0
- package/dist/hooks/use-asset-url.d.ts +6 -0
- package/dist/hooks/use-asset-url.d.ts.map +1 -0
- package/dist/hooks/use-asset-url.js +21 -0
- package/dist/hooks/use-gltf-ktx2.d.ts +4 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -0
- package/dist/hooks/use-gltf-ktx2.js +16 -0
- package/dist/hooks/use-grid-events.d.ts +12 -0
- package/dist/hooks/use-grid-events.d.ts.map +1 -0
- package/dist/hooks/use-grid-events.js +33 -0
- package/dist/hooks/use-node-events.d.ts +49 -0
- package/dist/hooks/use-node-events.d.ts.map +1 -0
- package/dist/hooks/use-node-events.js +38 -0
- package/dist/index.d.ts +4 -81
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -19168
- package/dist/store/use-viewer.d.ts +35 -0
- package/dist/store/use-viewer.d.ts.map +1 -0
- package/dist/store/use-viewer.js +46 -0
- package/dist/systems/guide/guide-system.d.ts +2 -0
- package/dist/systems/guide/guide-system.d.ts.map +1 -0
- package/dist/systems/guide/guide-system.js +16 -0
- package/dist/systems/level/level-system.d.ts +2 -0
- package/dist/systems/level/level-system.d.ts.map +1 -0
- package/dist/systems/level/level-system.js +23 -0
- package/dist/systems/scan/scan-system.d.ts +2 -0
- package/dist/systems/scan/scan-system.d.ts.map +1 -0
- package/dist/systems/scan/scan-system.js +16 -0
- package/dist/systems/wall/wall-cutout.d.ts +2 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -0
- package/dist/systems/wall/wall-cutout.js +103 -0
- package/package.json +36 -32
- package/dist/index.js.map +0 -1
- package/types.d.ts +0 -81
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const SSGI_PARAMS: {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
sliceCount: number;
|
|
4
|
+
stepCount: number;
|
|
5
|
+
radius: number;
|
|
6
|
+
expFactor: number;
|
|
7
|
+
thickness: number;
|
|
8
|
+
backfaceLighting: number;
|
|
9
|
+
aoIntensity: number;
|
|
10
|
+
giIntensity: number;
|
|
11
|
+
useLinearThickness: boolean;
|
|
12
|
+
useScreenSpaceSampling: boolean;
|
|
13
|
+
useTemporalFiltering: boolean;
|
|
14
|
+
};
|
|
15
|
+
declare const PostProcessingPasses: () => null;
|
|
16
|
+
export default PostProcessingPasses;
|
|
17
|
+
//# sourceMappingURL=post-processing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA0BA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AAED,QAAA,MAAM,oBAAoB,YA8IzB,CAAA;AAED,eAAe,oBAAoB,CAAA"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useFrame, useThree } from '@react-three/fiber';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { Color, UnsignedByteType } from 'three';
|
|
4
|
+
import { outline } from 'three/addons/tsl/display/OutlineNode.js';
|
|
5
|
+
import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
|
|
6
|
+
import { traa } from 'three/addons/tsl/display/TRAANode.js';
|
|
7
|
+
import { add, colorToDirection, diffuseColor, directionToColor, mrt, normalView, oscSine, output, pass, sample, time, uniform, vec4, velocity, } from 'three/tsl';
|
|
8
|
+
import { PostProcessing } from 'three/webgpu';
|
|
9
|
+
import useViewer from '../../store/use-viewer';
|
|
10
|
+
// SSGI Parameters - adjust these to fine-tune global illumination and ambient occlusion
|
|
11
|
+
export const SSGI_PARAMS = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
sliceCount: 2,
|
|
14
|
+
stepCount: 8,
|
|
15
|
+
radius: 2,
|
|
16
|
+
expFactor: 2,
|
|
17
|
+
thickness: 0.5,
|
|
18
|
+
backfaceLighting: 0.5,
|
|
19
|
+
aoIntensity: 1.5,
|
|
20
|
+
giIntensity: 0.5,
|
|
21
|
+
useLinearThickness: false,
|
|
22
|
+
useScreenSpaceSampling: true,
|
|
23
|
+
useTemporalFiltering: true,
|
|
24
|
+
};
|
|
25
|
+
const PostProcessingPasses = () => {
|
|
26
|
+
const { gl: renderer, scene, camera } = useThree();
|
|
27
|
+
const postProcessingRef = useRef(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!renderer || !scene || !camera) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Scene pass with MRT for SSGI
|
|
33
|
+
const scenePass = pass(scene, camera);
|
|
34
|
+
scenePass.setMRT(mrt({
|
|
35
|
+
output: output,
|
|
36
|
+
diffuseColor: diffuseColor,
|
|
37
|
+
normal: directionToColor(normalView),
|
|
38
|
+
velocity: velocity,
|
|
39
|
+
}));
|
|
40
|
+
// Get texture outputs
|
|
41
|
+
const scenePassColor = scenePass.getTextureNode('output');
|
|
42
|
+
const scenePassDiffuse = scenePass.getTextureNode('diffuseColor');
|
|
43
|
+
const scenePassDepth = scenePass.getTextureNode('depth');
|
|
44
|
+
const scenePassNormal = scenePass.getTextureNode('normal');
|
|
45
|
+
const scenePassVelocity = scenePass.getTextureNode('velocity');
|
|
46
|
+
// Optimize texture bandwidth
|
|
47
|
+
const diffuseTexture = scenePass.getTexture('diffuseColor');
|
|
48
|
+
diffuseTexture.type = UnsignedByteType;
|
|
49
|
+
const normalTexture = scenePass.getTexture('normal');
|
|
50
|
+
normalTexture.type = UnsignedByteType;
|
|
51
|
+
// Extract normal from color-encoded texture
|
|
52
|
+
const sceneNormal = sample((uv) => {
|
|
53
|
+
return colorToDirection(scenePassNormal.sample(uv));
|
|
54
|
+
});
|
|
55
|
+
// SSGI Pass (cast to PerspectiveCamera for SSGI)
|
|
56
|
+
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera);
|
|
57
|
+
giPass.sliceCount.value = SSGI_PARAMS.sliceCount;
|
|
58
|
+
giPass.stepCount.value = SSGI_PARAMS.stepCount;
|
|
59
|
+
giPass.radius.value = SSGI_PARAMS.radius;
|
|
60
|
+
giPass.expFactor.value = SSGI_PARAMS.expFactor;
|
|
61
|
+
giPass.thickness.value = SSGI_PARAMS.thickness;
|
|
62
|
+
giPass.backfaceLighting.value = SSGI_PARAMS.backfaceLighting;
|
|
63
|
+
giPass.aoIntensity.value = SSGI_PARAMS.aoIntensity;
|
|
64
|
+
giPass.giIntensity.value = SSGI_PARAMS.giIntensity;
|
|
65
|
+
giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness;
|
|
66
|
+
giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling;
|
|
67
|
+
giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering;
|
|
68
|
+
// Extract GI and AO from SSGI pass
|
|
69
|
+
const gi = giPass.rgb;
|
|
70
|
+
const ao = giPass.a;
|
|
71
|
+
// Composite: scene * AO + diffuse * GI
|
|
72
|
+
const compositePass = vec4(add(scenePassColor.rgb.mul(ao), scenePassDiffuse.rgb.mul(gi)), scenePassColor.a);
|
|
73
|
+
// TRAA (Temporal Reprojection Anti-Aliasing)
|
|
74
|
+
const traaPass = traa(compositePass, scenePassDepth, scenePassVelocity, camera);
|
|
75
|
+
function generateSelectedOutlinePass() {
|
|
76
|
+
const edgeStrength = uniform(3);
|
|
77
|
+
const edgeGlow = uniform(0);
|
|
78
|
+
const edgeThickness = uniform(1);
|
|
79
|
+
const visibleEdgeColor = uniform(new Color(0xffffff));
|
|
80
|
+
const hiddenEdgeColor = uniform(new Color(0xf3ff47));
|
|
81
|
+
const outlinePass = outline(scene, camera, {
|
|
82
|
+
selectedObjects: useViewer.getState().outliner.selectedObjects,
|
|
83
|
+
edgeGlow,
|
|
84
|
+
edgeThickness,
|
|
85
|
+
});
|
|
86
|
+
const { visibleEdge, hiddenEdge } = outlinePass;
|
|
87
|
+
const outlineColor = visibleEdge
|
|
88
|
+
.mul(visibleEdgeColor)
|
|
89
|
+
.add(hiddenEdge.mul(hiddenEdgeColor))
|
|
90
|
+
.mul(edgeStrength);
|
|
91
|
+
return outlineColor;
|
|
92
|
+
}
|
|
93
|
+
function generateHoverOutlinePass() {
|
|
94
|
+
const edgeStrength = uniform(5);
|
|
95
|
+
const edgeGlow = uniform(0.5);
|
|
96
|
+
const edgeThickness = uniform(1.5);
|
|
97
|
+
const pulsePeriod = uniform(3);
|
|
98
|
+
const visibleEdgeColor = uniform(new Color(0x00aaff));
|
|
99
|
+
const hiddenEdgeColor = uniform(new Color(0xf3ff47));
|
|
100
|
+
const outlinePass = outline(scene, camera, {
|
|
101
|
+
selectedObjects: useViewer.getState().outliner.hoveredObjects,
|
|
102
|
+
edgeGlow,
|
|
103
|
+
edgeThickness,
|
|
104
|
+
});
|
|
105
|
+
const { visibleEdge, hiddenEdge } = outlinePass;
|
|
106
|
+
const period = time.div(pulsePeriod).mul(2);
|
|
107
|
+
const osc = oscSine(period).mul(0.5).add(0.5); // osc [ 0.5, 1.0 ]
|
|
108
|
+
const outlineColor = visibleEdge
|
|
109
|
+
.mul(visibleEdgeColor)
|
|
110
|
+
.add(hiddenEdge.mul(hiddenEdgeColor))
|
|
111
|
+
.mul(edgeStrength);
|
|
112
|
+
const outlinePulse = pulsePeriod.greaterThan(0).select(outlineColor.mul(osc), outlineColor);
|
|
113
|
+
return outlinePulse;
|
|
114
|
+
}
|
|
115
|
+
// Setup post-processing
|
|
116
|
+
const postProcessing = new PostProcessing(renderer);
|
|
117
|
+
const selectedOutlinePass = generateSelectedOutlinePass();
|
|
118
|
+
const hoverOutlinePass = generateHoverOutlinePass();
|
|
119
|
+
// Combine SSGI output with outlines
|
|
120
|
+
const finalOutput = SSGI_PARAMS.enabled
|
|
121
|
+
? selectedOutlinePass.add(hoverOutlinePass).add(traaPass)
|
|
122
|
+
: selectedOutlinePass.add(hoverOutlinePass).add(scenePassColor);
|
|
123
|
+
postProcessing.outputNode = finalOutput;
|
|
124
|
+
postProcessingRef.current = postProcessing;
|
|
125
|
+
return () => {
|
|
126
|
+
if (postProcessingRef.current) {
|
|
127
|
+
postProcessingRef.current.dispose();
|
|
128
|
+
}
|
|
129
|
+
postProcessingRef.current = null;
|
|
130
|
+
};
|
|
131
|
+
}, [renderer, scene, camera]);
|
|
132
|
+
useFrame(() => {
|
|
133
|
+
if (postProcessingRef.current) {
|
|
134
|
+
postProcessingRef.current.render();
|
|
135
|
+
}
|
|
136
|
+
}, 1);
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
export default PostProcessingPasses;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"AAkNA,eAAO,MAAM,gBAAgB,+CA0D5B,CAAA"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { emitter, pointInPolygon, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
4
|
+
import { useThree } from '@react-three/fiber';
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import { Vector3 } from 'three';
|
|
7
|
+
import useViewer from '../../store/use-viewer';
|
|
8
|
+
const tempWorldPos = new Vector3();
|
|
9
|
+
// Tolerance for edge detection (in meters)
|
|
10
|
+
const EDGE_TOLERANCE = 0.5;
|
|
11
|
+
// Expand polygon outward by a small amount to include items on edges
|
|
12
|
+
const expandPolygon = (polygon, tolerance) => {
|
|
13
|
+
if (polygon.length < 3)
|
|
14
|
+
return polygon;
|
|
15
|
+
// Calculate centroid
|
|
16
|
+
let cx = 0, cz = 0;
|
|
17
|
+
for (const [x, z] of polygon) {
|
|
18
|
+
cx += x;
|
|
19
|
+
cz += z;
|
|
20
|
+
}
|
|
21
|
+
cx /= polygon.length;
|
|
22
|
+
cz /= polygon.length;
|
|
23
|
+
// Expand each point outward from centroid
|
|
24
|
+
return polygon.map(([x, z]) => {
|
|
25
|
+
const dx = x - cx;
|
|
26
|
+
const dz = z - cz;
|
|
27
|
+
const len = Math.sqrt(dx * dx + dz * dz);
|
|
28
|
+
if (len === 0)
|
|
29
|
+
return [x, z];
|
|
30
|
+
const scale = (len + tolerance) / len;
|
|
31
|
+
return [cx + dx * scale, cz + dz * scale];
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
// Check if point is in polygon with tolerance for edges
|
|
35
|
+
const pointInPolygonWithTolerance = (x, z, polygon) => {
|
|
36
|
+
// First try exact check
|
|
37
|
+
if (pointInPolygon(x, z, polygon))
|
|
38
|
+
return true;
|
|
39
|
+
// Then try with expanded polygon for edge tolerance
|
|
40
|
+
const expanded = expandPolygon(polygon, EDGE_TOLERANCE);
|
|
41
|
+
return pointInPolygon(x, z, expanded);
|
|
42
|
+
};
|
|
43
|
+
// Check if a node belongs to the selected level (directly or via wall parent)
|
|
44
|
+
const isNodeOnLevel = (node, levelId) => {
|
|
45
|
+
const nodes = useScene.getState().nodes;
|
|
46
|
+
// Direct child of level
|
|
47
|
+
if (node.parentId === levelId)
|
|
48
|
+
return true;
|
|
49
|
+
// Wall-attached items (windows/doors): check if parent wall is on the level
|
|
50
|
+
if (node.type === 'item' && node.parentId) {
|
|
51
|
+
const parentNode = nodes[node.parentId];
|
|
52
|
+
if (parentNode?.type === 'wall' && parentNode.parentId === levelId) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
};
|
|
58
|
+
// Check if a node is on the selected level and within the selected zone's polygon
|
|
59
|
+
const isNodeInZone = (node, levelId, zoneId) => {
|
|
60
|
+
const nodes = useScene.getState().nodes;
|
|
61
|
+
const zone = nodes[zoneId];
|
|
62
|
+
if (!zone?.polygon?.length)
|
|
63
|
+
return false;
|
|
64
|
+
// First check: node must be on the same level (directly or via wall)
|
|
65
|
+
if (!isNodeOnLevel(node, levelId))
|
|
66
|
+
return false;
|
|
67
|
+
// Use world position from scene registry for accurate polygon check
|
|
68
|
+
const object3D = sceneRegistry.nodes.get(node.id);
|
|
69
|
+
if (object3D) {
|
|
70
|
+
object3D.getWorldPosition(tempWorldPos);
|
|
71
|
+
return pointInPolygonWithTolerance(tempWorldPos.x, tempWorldPos.z, zone.polygon);
|
|
72
|
+
}
|
|
73
|
+
// Fallback to node data if 3D object not available
|
|
74
|
+
if (node.type === 'item') {
|
|
75
|
+
const item = node;
|
|
76
|
+
return pointInPolygonWithTolerance(item.position[0], item.position[2], zone.polygon);
|
|
77
|
+
}
|
|
78
|
+
if (node.type === 'wall') {
|
|
79
|
+
const wall = node;
|
|
80
|
+
const startIn = pointInPolygonWithTolerance(wall.start[0], wall.start[1], zone.polygon);
|
|
81
|
+
const endIn = pointInPolygonWithTolerance(wall.end[0], wall.end[1], zone.polygon);
|
|
82
|
+
return startIn || endIn;
|
|
83
|
+
}
|
|
84
|
+
if (node.type === 'slab' || node.type === 'ceiling') {
|
|
85
|
+
const poly = node.polygon;
|
|
86
|
+
if (!poly?.length)
|
|
87
|
+
return false;
|
|
88
|
+
// Check if any point of the node's polygon is in the zone (with tolerance)
|
|
89
|
+
for (const [px, pz] of poly) {
|
|
90
|
+
if (pointInPolygonWithTolerance(px, pz, zone.polygon))
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
// Check if any point of the zone is in the node's polygon
|
|
94
|
+
for (const [zx, zz] of zone.polygon) {
|
|
95
|
+
if (pointInPolygon(zx, zz, poly))
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (node.type === 'roof') {
|
|
101
|
+
// Roofs on the same level are valid when zone is selected
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
};
|
|
106
|
+
const getStrategy = () => {
|
|
107
|
+
const { buildingId, levelId, zoneId } = useViewer.getState().selection;
|
|
108
|
+
// No building selected -> can select buildings
|
|
109
|
+
if (!buildingId) {
|
|
110
|
+
return {
|
|
111
|
+
types: ['building'],
|
|
112
|
+
handleClick: (node) => {
|
|
113
|
+
useViewer.getState().setSelection({ buildingId: node.id });
|
|
114
|
+
},
|
|
115
|
+
handleDeselect: () => {
|
|
116
|
+
// Nothing to deselect at root level
|
|
117
|
+
},
|
|
118
|
+
isValid: (node) => node.type === 'building',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Building selected, no level -> can select levels
|
|
122
|
+
if (!levelId) {
|
|
123
|
+
return {
|
|
124
|
+
types: ['level'],
|
|
125
|
+
handleClick: (node) => {
|
|
126
|
+
useViewer.getState().setSelection({ levelId: node.id });
|
|
127
|
+
},
|
|
128
|
+
handleDeselect: () => {
|
|
129
|
+
useViewer.getState().setSelection({ buildingId: null });
|
|
130
|
+
},
|
|
131
|
+
isValid: (node) => node.type === 'level',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Level selected, no zone -> can select zones (only zones on the selected level)
|
|
135
|
+
if (!zoneId) {
|
|
136
|
+
return {
|
|
137
|
+
types: ['zone'],
|
|
138
|
+
handleClick: (node) => {
|
|
139
|
+
useViewer.getState().setSelection({ zoneId: node.id });
|
|
140
|
+
},
|
|
141
|
+
handleDeselect: () => {
|
|
142
|
+
useViewer.getState().setSelection({ levelId: null });
|
|
143
|
+
},
|
|
144
|
+
isValid: (node) => node.type === 'zone' && node.parentId === levelId,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs)
|
|
148
|
+
return {
|
|
149
|
+
types: ['wall', 'item', 'slab', 'ceiling', 'roof'],
|
|
150
|
+
handleClick: (node) => {
|
|
151
|
+
const { selectedIds } = useViewer.getState().selection;
|
|
152
|
+
// Toggle selection - if already selected, deselect; otherwise select
|
|
153
|
+
if (selectedIds.includes(node.id)) {
|
|
154
|
+
useViewer.getState().setSelection({ selectedIds: selectedIds.filter((id) => id !== node.id) });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
useViewer.getState().setSelection({ selectedIds: [node.id] });
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
handleDeselect: () => {
|
|
161
|
+
const { selectedIds } = useViewer.getState().selection;
|
|
162
|
+
// If items are selected, deselect them first; otherwise go back to level
|
|
163
|
+
if (selectedIds.length > 0) {
|
|
164
|
+
useViewer.getState().setSelection({ selectedIds: [] });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
useViewer.getState().setSelection({ zoneId: null });
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
isValid: (node) => {
|
|
171
|
+
const validTypes = ['wall', 'item', 'slab', 'ceiling', 'roof'];
|
|
172
|
+
if (!validTypes.includes(node.type))
|
|
173
|
+
return false;
|
|
174
|
+
return isNodeInZone(node, levelId, zoneId);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
export const SelectionManager = () => {
|
|
179
|
+
const selection = useViewer((s) => s.selection);
|
|
180
|
+
const clickHandledRef = useRef(false);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const onEnter = (event) => {
|
|
183
|
+
const strategy = getStrategy();
|
|
184
|
+
if (!strategy)
|
|
185
|
+
return;
|
|
186
|
+
if (strategy.isValid(event.node)) {
|
|
187
|
+
event.stopPropagation();
|
|
188
|
+
useViewer.setState({ hoveredId: event.node.id });
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const onLeave = (event) => {
|
|
192
|
+
const strategy = getStrategy();
|
|
193
|
+
if (!strategy)
|
|
194
|
+
return;
|
|
195
|
+
if (strategy.isValid(event.node)) {
|
|
196
|
+
event.stopPropagation();
|
|
197
|
+
useViewer.setState({ hoveredId: null });
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const onClick = (event) => {
|
|
201
|
+
const strategy = getStrategy();
|
|
202
|
+
if (!strategy)
|
|
203
|
+
return;
|
|
204
|
+
if (!strategy.isValid(event.node))
|
|
205
|
+
return;
|
|
206
|
+
event.stopPropagation();
|
|
207
|
+
clickHandledRef.current = true;
|
|
208
|
+
strategy.handleClick(event.node);
|
|
209
|
+
// Clear hover immediately after clicking on building/level/zone
|
|
210
|
+
useViewer.setState({ hoveredId: null });
|
|
211
|
+
};
|
|
212
|
+
// Subscribe to all node types
|
|
213
|
+
const allTypes = ['building', 'level', 'zone', 'wall', 'item', 'slab', 'ceiling', 'roof'];
|
|
214
|
+
for (const type of allTypes) {
|
|
215
|
+
emitter.on(`${type}:enter`, onEnter);
|
|
216
|
+
emitter.on(`${type}:leave`, onLeave);
|
|
217
|
+
emitter.on(`${type}:click`, onClick);
|
|
218
|
+
}
|
|
219
|
+
return () => {
|
|
220
|
+
for (const type of allTypes) {
|
|
221
|
+
emitter.off(`${type}:enter`, onEnter);
|
|
222
|
+
emitter.off(`${type}:leave`, onLeave);
|
|
223
|
+
emitter.off(`${type}:click`, onClick);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}, [selection]);
|
|
227
|
+
return (_jsxs(_Fragment, { children: [_jsx(PointerMissedHandler, { clickHandledRef: clickHandledRef }), _jsx(OutlinerSync, {})] }));
|
|
228
|
+
};
|
|
229
|
+
const PointerMissedHandler = ({ clickHandledRef }) => {
|
|
230
|
+
const gl = useThree((s) => s.gl);
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
const handleClick = (event) => {
|
|
233
|
+
// Only handle left clicks
|
|
234
|
+
if (event.button !== 0)
|
|
235
|
+
return;
|
|
236
|
+
// Use requestAnimationFrame to check after R3F event handlers
|
|
237
|
+
requestAnimationFrame(() => {
|
|
238
|
+
if (clickHandledRef.current) {
|
|
239
|
+
clickHandledRef.current = false;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// Click was not handled by any 3D object -> deselect
|
|
243
|
+
const strategy = getStrategy();
|
|
244
|
+
if (strategy) {
|
|
245
|
+
strategy.handleDeselect();
|
|
246
|
+
useViewer.setState({ hoveredId: null });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
const canvas = gl.domElement;
|
|
251
|
+
canvas.addEventListener('click', handleClick);
|
|
252
|
+
return () => {
|
|
253
|
+
canvas.removeEventListener('click', handleClick);
|
|
254
|
+
};
|
|
255
|
+
}, [gl, clickHandledRef]);
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
258
|
+
const OutlinerSync = () => {
|
|
259
|
+
const selection = useViewer((s) => s.selection);
|
|
260
|
+
const hoveredId = useViewer((s) => s.hoveredId);
|
|
261
|
+
const outliner = useViewer((s) => s.outliner);
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
// Sync selected objects
|
|
264
|
+
outliner.selectedObjects.length = 0;
|
|
265
|
+
for (const id of selection.selectedIds) {
|
|
266
|
+
const obj = sceneRegistry.nodes.get(id);
|
|
267
|
+
if (obj)
|
|
268
|
+
outliner.selectedObjects.push(obj);
|
|
269
|
+
}
|
|
270
|
+
// Sync hovered objects
|
|
271
|
+
outliner.hoveredObjects.length = 0;
|
|
272
|
+
if (hoveredId) {
|
|
273
|
+
const obj = sceneRegistry.nodes.get(hoveredId);
|
|
274
|
+
if (obj)
|
|
275
|
+
outliner.hoveredObjects.push(obj);
|
|
276
|
+
}
|
|
277
|
+
}, [selection, hoveredId, outliner]);
|
|
278
|
+
return null;
|
|
279
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewer-camera.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/viewer-camera.tsx"],"names":[],"mappings":"AAGA,eAAO,MAAM,YAAY,+CAOxB,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { OrthographicCamera, PerspectiveCamera } from "@react-three/drei";
|
|
3
|
+
import useViewer from "../../store/use-viewer";
|
|
4
|
+
export const ViewerCamera = () => {
|
|
5
|
+
const cameraMode = useViewer((state) => state.cameraMode);
|
|
6
|
+
return cameraMode === 'perspective' ? (_jsx(PerspectiveCamera, { far: 1000, fov: 50, makeDefault: true, near: 0.1, position: [10, 10, 10] })) : (_jsx(OrthographicCamera, { far: 1000, makeDefault: true, near: -1000, position: [10, 10, 10], zoom: 20 }));
|
|
7
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-asset-url.d.ts","sourceRoot":"","sources":["../../src/hooks/use-asset-url.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAetD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadAssetUrl } from '@pascal-app/core';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves an asset:// URL to a blob URL for use with Three.js loaders.
|
|
5
|
+
* Returns null while loading or if resolution fails.
|
|
6
|
+
*/
|
|
7
|
+
export function useAssetUrl(url) {
|
|
8
|
+
const [resolved, setResolved] = useState(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
setResolved(null);
|
|
12
|
+
loadAssetUrl(url).then((result) => {
|
|
13
|
+
if (!cancelled)
|
|
14
|
+
setResolved(result);
|
|
15
|
+
});
|
|
16
|
+
return () => {
|
|
17
|
+
cancelled = true;
|
|
18
|
+
};
|
|
19
|
+
}, [url]);
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
@@ -0,0 +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;AAQ3C,QAAA,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,UAAU,CAAC,OAAO,OAAO,CAS5D,CAAA;AACD,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useGLTF } from "@react-three/drei";
|
|
2
|
+
import { useThree } from "@react-three/fiber";
|
|
3
|
+
import { KTX2Loader } from "three/examples/jsm/Addons.js";
|
|
4
|
+
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
5
|
+
const ktx2LoaderInstance = new KTX2Loader();
|
|
6
|
+
ktx2LoaderInstance.setTranscoderPath('https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/basis/');
|
|
7
|
+
const useGLTFKTX2 = (path) => {
|
|
8
|
+
const gl = useThree((state) => state.gl);
|
|
9
|
+
return useGLTF(path, true, true, (loader) => {
|
|
10
|
+
ktx2LoaderInstance.detectSupport(gl);
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
loader.setKTX2Loader(ktx2LoaderInstance);
|
|
13
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
export { useGLTFKTX2 };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ThreeEvent } from "@react-three/fiber";
|
|
2
|
+
export declare function useGridEvents(): {
|
|
3
|
+
onPointerDown: (e: ThreeEvent<PointerEvent>) => void;
|
|
4
|
+
onPointerUp: (e: ThreeEvent<PointerEvent>) => void;
|
|
5
|
+
onClick: (e: ThreeEvent<PointerEvent>) => void;
|
|
6
|
+
onPointerEnter: (e: ThreeEvent<PointerEvent>) => void;
|
|
7
|
+
onPointerLeave: (e: ThreeEvent<PointerEvent>) => void;
|
|
8
|
+
onPointerMove: (e: ThreeEvent<PointerEvent>) => void;
|
|
9
|
+
onDoubleClick: (e: ThreeEvent<PointerEvent>) => void;
|
|
10
|
+
onContextMenu: (e: ThreeEvent<PointerEvent>) => void;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=use-grid-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-grid-events.d.ts","sourceRoot":"","sources":["../../src/hooks/use-grid-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,wBAAgB,aAAa;uBAYN,UAAU,CAAC,YAAY,CAAC;qBAI1B,UAAU,CAAC,YAAY,CAAC;iBAI5B,UAAU,CAAC,YAAY,CAAC;wBAIjB,UAAU,CAAC,YAAY,CAAC;wBACxB,UAAU,CAAC,YAAY,CAAC;uBACzB,UAAU,CAAC,YAAY,CAAC;uBACxB,UAAU,CAAC,YAAY,CAAC;uBACxB,UAAU,CAAC,YAAY,CAAC;EAE9C"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { emitter } from "@pascal-app/core";
|
|
2
|
+
export function useGridEvents() {
|
|
3
|
+
const emit = (suffix, e) => {
|
|
4
|
+
const eventKey = `grid:${suffix}`;
|
|
5
|
+
const payload = {
|
|
6
|
+
position: [e.point.x, e.point.y, e.point.z],
|
|
7
|
+
nativeEvent: e,
|
|
8
|
+
};
|
|
9
|
+
emitter.emit(eventKey, payload);
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
onPointerDown: (e) => {
|
|
13
|
+
if (e.button !== 0)
|
|
14
|
+
return;
|
|
15
|
+
emit("pointerdown", e);
|
|
16
|
+
},
|
|
17
|
+
onPointerUp: (e) => {
|
|
18
|
+
if (e.button !== 0)
|
|
19
|
+
return;
|
|
20
|
+
emit("pointerup", e);
|
|
21
|
+
},
|
|
22
|
+
onClick: (e) => {
|
|
23
|
+
if (e.button !== 0)
|
|
24
|
+
return;
|
|
25
|
+
emit("click", e);
|
|
26
|
+
},
|
|
27
|
+
onPointerEnter: (e) => emit("enter", e),
|
|
28
|
+
onPointerLeave: (e) => emit("leave", e),
|
|
29
|
+
onPointerMove: (e) => emit("move", e),
|
|
30
|
+
onDoubleClick: (e) => emit("double-click", e),
|
|
31
|
+
onContextMenu: (e) => emit("context-menu", e),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type BuildingEvent, type BuildingNode, type CeilingEvent, type CeilingNode, type ItemEvent, type ItemNode, type LevelEvent, type LevelNode, type RoofEvent, type RoofNode, type SlabEvent, type SlabNode, type WallEvent, type WallNode, type ZoneEvent, type ZoneNode } from '@pascal-app/core';
|
|
2
|
+
import type { ThreeEvent } from '@react-three/fiber';
|
|
3
|
+
type NodeConfig = {
|
|
4
|
+
item: {
|
|
5
|
+
node: ItemNode;
|
|
6
|
+
event: ItemEvent;
|
|
7
|
+
};
|
|
8
|
+
wall: {
|
|
9
|
+
node: WallNode;
|
|
10
|
+
event: WallEvent;
|
|
11
|
+
};
|
|
12
|
+
building: {
|
|
13
|
+
node: BuildingNode;
|
|
14
|
+
event: BuildingEvent;
|
|
15
|
+
};
|
|
16
|
+
level: {
|
|
17
|
+
node: LevelNode;
|
|
18
|
+
event: LevelEvent;
|
|
19
|
+
};
|
|
20
|
+
zone: {
|
|
21
|
+
node: ZoneNode;
|
|
22
|
+
event: ZoneEvent;
|
|
23
|
+
};
|
|
24
|
+
slab: {
|
|
25
|
+
node: SlabNode;
|
|
26
|
+
event: SlabEvent;
|
|
27
|
+
};
|
|
28
|
+
ceiling: {
|
|
29
|
+
node: CeilingNode;
|
|
30
|
+
event: CeilingEvent;
|
|
31
|
+
};
|
|
32
|
+
roof: {
|
|
33
|
+
node: RoofNode;
|
|
34
|
+
event: RoofEvent;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
type NodeType = keyof NodeConfig;
|
|
38
|
+
export declare function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T): {
|
|
39
|
+
onPointerDown: (e: ThreeEvent<PointerEvent>) => void;
|
|
40
|
+
onPointerUp: (e: ThreeEvent<PointerEvent>) => void;
|
|
41
|
+
onClick: (e: ThreeEvent<PointerEvent>) => void;
|
|
42
|
+
onPointerEnter: (e: ThreeEvent<PointerEvent>) => void;
|
|
43
|
+
onPointerLeave: (e: ThreeEvent<PointerEvent>) => void;
|
|
44
|
+
onPointerMove: (e: ThreeEvent<PointerEvent>) => void;
|
|
45
|
+
onDoubleClick: (e: ThreeEvent<PointerEvent>) => void;
|
|
46
|
+
onContextMenu: (e: ThreeEvent<PointerEvent>) => void;
|
|
47
|
+
};
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=use-node-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-node-events.d.ts","sourceRoot":"","sources":["../../src/hooks/use-node-events.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,WAAW,EAGhB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,QAAQ,EAAE;QAAE,IAAI,EAAE,YAAY,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAA;IACtD,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,UAAU,CAAA;KAAE,CAAA;IAC7C,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,OAAO,EAAE;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,YAAY,CAAA;KAAE,CAAA;IACnD,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;CAC3C,CAAA;AAED,KAAK,QAAQ,GAAG,MAAM,UAAU,CAAA;AAEhC,wBAAgB,aAAa,CAAC,CAAC,SAAS,QAAQ,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;uBAiB/D,UAAU,CAAC,YAAY,CAAC;qBAI1B,UAAU,CAAC,YAAY,CAAC;iBAI5B,UAAU,CAAC,YAAY,CAAC;wBAIjB,UAAU,CAAC,YAAY,CAAC;wBACxB,UAAU,CAAC,YAAY,CAAC;uBACzB,UAAU,CAAC,YAAY,CAAC;uBACxB,UAAU,CAAC,YAAY,CAAC;uBACxB,UAAU,CAAC,YAAY,CAAC;EAE9C"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { emitter, } from '@pascal-app/core';
|
|
2
|
+
export function useNodeEvents(node, type) {
|
|
3
|
+
const emit = (suffix, e) => {
|
|
4
|
+
const eventKey = `${type}:${suffix}`;
|
|
5
|
+
const localPoint = e.object.worldToLocal(e.point.clone());
|
|
6
|
+
const payload = {
|
|
7
|
+
node,
|
|
8
|
+
position: [e.point.x, e.point.y, e.point.z],
|
|
9
|
+
localPosition: [localPoint.x, localPoint.y, localPoint.z],
|
|
10
|
+
normal: e.face ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] : undefined,
|
|
11
|
+
stopPropagation: () => e.stopPropagation(),
|
|
12
|
+
nativeEvent: e,
|
|
13
|
+
};
|
|
14
|
+
emitter.emit(eventKey, payload);
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
onPointerDown: (e) => {
|
|
18
|
+
if (e.button !== 0)
|
|
19
|
+
return;
|
|
20
|
+
emit('pointerdown', e);
|
|
21
|
+
},
|
|
22
|
+
onPointerUp: (e) => {
|
|
23
|
+
if (e.button !== 0)
|
|
24
|
+
return;
|
|
25
|
+
emit('pointerup', e);
|
|
26
|
+
},
|
|
27
|
+
onClick: (e) => {
|
|
28
|
+
if (e.button !== 0)
|
|
29
|
+
return;
|
|
30
|
+
emit('click', e);
|
|
31
|
+
},
|
|
32
|
+
onPointerEnter: (e) => emit('enter', e),
|
|
33
|
+
onPointerLeave: (e) => emit('leave', e),
|
|
34
|
+
onPointerMove: (e) => emit('move', e),
|
|
35
|
+
onDoubleClick: (e) => emit('double-click', e),
|
|
36
|
+
onContextMenu: (e) => emit('context-menu', e),
|
|
37
|
+
};
|
|
38
|
+
}
|