@pascal-app/viewer 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/components/renderers/building/building-renderer.d.ts +5 -0
  2. package/dist/components/renderers/building/building-renderer.d.ts.map +1 -0
  3. package/dist/components/renderers/building/building-renderer.js +11 -0
  4. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +5 -0
  5. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -0
  6. package/dist/components/renderers/ceiling/ceiling-renderer.js +24 -0
  7. package/dist/components/renderers/guide/guide-renderer.d.ts +5 -0
  8. package/dist/components/renderers/guide/guide-renderer.d.ts.map +1 -0
  9. package/dist/components/renderers/guide/guide-renderer.js +36 -0
  10. package/dist/components/renderers/item/item-renderer.d.ts +5 -0
  11. package/dist/components/renderers/item/item-renderer.d.ts.map +1 -0
  12. package/dist/components/renderers/item/item-renderer.js +71 -0
  13. package/dist/components/renderers/level/level-renderer.d.ts +5 -0
  14. package/dist/components/renderers/level/level-renderer.d.ts.map +1 -0
  15. package/dist/components/renderers/level/level-renderer.js +11 -0
  16. package/dist/components/renderers/node-renderer.d.ts +5 -0
  17. package/dist/components/renderers/node-renderer.d.ts.map +1 -0
  18. package/dist/components/renderers/node-renderer.js +19 -0
  19. package/dist/components/renderers/roof/roof-renderer.d.ts +5 -0
  20. package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -0
  21. package/dist/components/renderers/roof/roof-renderer.js +10 -0
  22. package/dist/components/renderers/scan/scan-renderer.d.ts +5 -0
  23. package/dist/components/renderers/scan/scan-renderer.d.ts.map +1 -0
  24. package/dist/components/renderers/scan/scan-renderer.js +50 -0
  25. package/dist/components/renderers/scene-renderer.d.ts +2 -0
  26. package/dist/components/renderers/scene-renderer.d.ts.map +1 -0
  27. package/dist/components/renderers/scene-renderer.js +8 -0
  28. package/dist/components/renderers/slab/slab-renderer.d.ts +5 -0
  29. package/dist/components/renderers/slab/slab-renderer.d.ts.map +1 -0
  30. package/dist/components/renderers/slab/slab-renderer.js +10 -0
  31. package/dist/components/renderers/wall/wall-renderer.d.ts +5 -0
  32. package/dist/components/renderers/wall/wall-renderer.d.ts.map +1 -0
  33. package/dist/components/renderers/wall/wall-renderer.js +11 -0
  34. package/dist/components/renderers/zone/zone-renderer.d.ts +5 -0
  35. package/dist/components/renderers/zone/zone-renderer.d.ts.map +1 -0
  36. package/dist/components/renderers/zone/zone-renderer.js +154 -0
  37. package/dist/components/viewer/index.d.ts +13 -0
  38. package/dist/components/viewer/index.d.ts.map +1 -0
  39. package/dist/components/viewer/index.js +29 -0
  40. package/dist/components/viewer/lights.d.ts +2 -0
  41. package/dist/components/viewer/lights.d.ts.map +1 -0
  42. package/dist/components/viewer/lights.js +10 -0
  43. package/dist/components/viewer/post-processing.d.ts +17 -0
  44. package/dist/components/viewer/post-processing.d.ts.map +1 -0
  45. package/dist/components/viewer/post-processing.js +139 -0
  46. package/dist/components/viewer/selection-manager.d.ts +2 -0
  47. package/dist/components/viewer/selection-manager.d.ts.map +1 -0
  48. package/dist/components/viewer/selection-manager.js +279 -0
  49. package/dist/components/viewer/viewer-camera.d.ts +2 -0
  50. package/dist/components/viewer/viewer-camera.d.ts.map +1 -0
  51. package/dist/components/viewer/viewer-camera.js +7 -0
  52. package/dist/hooks/use-asset-url.d.ts +6 -0
  53. package/dist/hooks/use-asset-url.d.ts.map +1 -0
  54. package/dist/hooks/use-asset-url.js +21 -0
  55. package/dist/hooks/use-gltf-ktx2.d.ts +4 -0
  56. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -0
  57. package/dist/hooks/use-gltf-ktx2.js +16 -0
  58. package/dist/hooks/use-grid-events.d.ts +12 -0
  59. package/dist/hooks/use-grid-events.d.ts.map +1 -0
  60. package/dist/hooks/use-grid-events.js +33 -0
  61. package/dist/hooks/use-node-events.d.ts +49 -0
  62. package/dist/hooks/use-node-events.d.ts.map +1 -0
  63. package/dist/hooks/use-node-events.js +38 -0
  64. package/dist/index.d.ts +5 -81
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +4 -46669
  67. package/dist/lib/asset-url.d.ts +15 -0
  68. package/dist/lib/asset-url.d.ts.map +1 -0
  69. package/dist/lib/asset-url.js +44 -0
  70. package/dist/store/use-viewer.d.ts +35 -0
  71. package/dist/store/use-viewer.d.ts.map +1 -0
  72. package/dist/store/use-viewer.js +46 -0
  73. package/dist/systems/guide/guide-system.d.ts +2 -0
  74. package/dist/systems/guide/guide-system.d.ts.map +1 -0
  75. package/dist/systems/guide/guide-system.js +16 -0
  76. package/dist/systems/level/level-system.d.ts +2 -0
  77. package/dist/systems/level/level-system.d.ts.map +1 -0
  78. package/dist/systems/level/level-system.js +23 -0
  79. package/dist/systems/scan/scan-system.d.ts +2 -0
  80. package/dist/systems/scan/scan-system.d.ts.map +1 -0
  81. package/dist/systems/scan/scan-system.js +16 -0
  82. package/dist/systems/wall/wall-cutout.d.ts +2 -0
  83. package/dist/systems/wall/wall-cutout.d.ts.map +1 -0
  84. package/dist/systems/wall/wall-cutout.js +103 -0
  85. package/package.json +49 -34
  86. package/dist/index.js.map +0 -1
  87. 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,2 @@
1
+ export declare const SelectionManager: () => import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=selection-manager.d.ts.map
@@ -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,2 @@
1
+ export declare const ViewerCamera: () => import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=viewer-camera.d.ts.map
@@ -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,6 @@
1
+ /**
2
+ * Resolves an asset:// URL to a blob URL for use with Three.js loaders.
3
+ * Returns null while loading or if resolution fails.
4
+ */
5
+ export declare function useAssetUrl(url: string): string | null;
6
+ //# sourceMappingURL=use-asset-url.d.ts.map
@@ -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,4 @@
1
+ import { useGLTF } from "@react-three/drei";
2
+ declare const useGLTFKTX2: (path: string) => ReturnType<typeof useGLTF>;
3
+ export { useGLTFKTX2 };
4
+ //# sourceMappingURL=use-gltf-ktx2.d.ts.map
@@ -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
+ }