@pascal-app/viewer 0.4.0 → 0.5.1
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.map +1 -1
- package/dist/components/renderers/building/building-renderer.js +1 -1
- package/dist/components/renderers/fence/fence-renderer.d.ts +5 -0
- package/dist/components/renderers/fence/fence-renderer.d.ts.map +1 -0
- package/dist/components/renderers/fence/fence-renderer.js +15 -0
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +2 -1
- package/dist/components/renderers/stair/stair-renderer.d.ts.map +1 -1
- package/dist/components/renderers/stair/stair-renderer.js +468 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +2 -2
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +50 -3
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +9 -1
- package/dist/hooks/use-node-events.d.ts +5 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/systems/guide/guide-system.d.ts.map +1 -1
- package/dist/systems/guide/guide-system.js +26 -1
- package/dist/systems/scan/scan-system.d.ts.map +1 -1
- package/dist/systems/scan/scan-system.js +26 -1
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +36 -2
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"building-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/building/building-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,kBAAkB,CAAA;AAMjE,eAAO,MAAM,gBAAgB,GAAI,UAAU;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"building-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/building/building-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,kBAAkB,CAAA;AAMjE,eAAO,MAAM,gBAAgB,GAAI,UAAU;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,4CAkBhE,CAAA"}
|
|
@@ -7,5 +7,5 @@ export const BuildingRenderer = ({ node }) => {
|
|
|
7
7
|
const ref = useRef(null);
|
|
8
8
|
useRegistry(node.id, node.type, ref);
|
|
9
9
|
const handlers = useNodeEvents(node, 'building');
|
|
10
|
-
return (_jsx("group", { ref: ref, ...handlers, children: node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) }));
|
|
10
|
+
return (_jsx("group", { position: node.position, ref: ref, rotation: [node.rotation[0], node.rotation[1], node.rotation[2]], ...handlers, children: node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) }));
|
|
11
11
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fence-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/fence/fence-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAyB,MAAM,kBAAkB,CAAA;AAMxE,eAAO,MAAM,aAAa,GAAI,UAAU;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,4CAe1D,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRegistry, useScene } from '@pascal-app/core';
|
|
3
|
+
import { useLayoutEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import { useNodeEvents } from '../../../hooks/use-node-events';
|
|
5
|
+
import { DEFAULT_STAIR_MATERIAL } from '../../../lib/materials';
|
|
6
|
+
export const FenceRenderer = ({ node }) => {
|
|
7
|
+
const ref = useRef(null);
|
|
8
|
+
const handlers = useNodeEvents(node, 'fence');
|
|
9
|
+
const material = useMemo(() => DEFAULT_STAIR_MATERIAL, []);
|
|
10
|
+
useRegistry(node.id, 'fence', ref);
|
|
11
|
+
useLayoutEffect(() => {
|
|
12
|
+
useScene.getState().markDirty(node.id);
|
|
13
|
+
}, [node.id]);
|
|
14
|
+
return (_jsx("mesh", { castShadow: true, material: material, receiveShadow: true, ref: ref, visible: node.visible, ...handlers, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }));
|
|
15
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/node-renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAY,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"node-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/node-renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAY,MAAM,kBAAkB,CAAA;AAmBzD,eAAO,MAAM,YAAY,GAAI,YAAY;IAAE,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,mDA0BjE,CAAA"}
|
|
@@ -4,6 +4,7 @@ import { useScene } from '@pascal-app/core';
|
|
|
4
4
|
import { BuildingRenderer } from './building/building-renderer';
|
|
5
5
|
import { CeilingRenderer } from './ceiling/ceiling-renderer';
|
|
6
6
|
import { DoorRenderer } from './door/door-renderer';
|
|
7
|
+
import { FenceRenderer } from './fence/fence-renderer';
|
|
7
8
|
import { GuideRenderer } from './guide/guide-renderer';
|
|
8
9
|
import { ItemRenderer } from './item/item-renderer';
|
|
9
10
|
import { LevelRenderer } from './level/level-renderer';
|
|
@@ -21,5 +22,5 @@ export const NodeRenderer = ({ nodeId }) => {
|
|
|
21
22
|
const node = useScene((state) => state.nodes[nodeId]);
|
|
22
23
|
if (!node)
|
|
23
24
|
return null;
|
|
24
|
-
return (_jsxs(_Fragment, { children: [node.type === 'site' && _jsx(SiteRenderer, { node: node }), node.type === 'building' && _jsx(BuildingRenderer, { node: node }), node.type === 'ceiling' && _jsx(CeilingRenderer, { node: node }), node.type === 'level' && _jsx(LevelRenderer, { node: node }), node.type === 'item' && _jsx(ItemRenderer, { node: node }), node.type === 'slab' && _jsx(SlabRenderer, { node: node }), node.type === 'wall' && _jsx(WallRenderer, { node: node }), node.type === 'door' && _jsx(DoorRenderer, { node: node }), node.type === 'window' && _jsx(WindowRenderer, { node: node }), node.type === 'zone' && _jsx(ZoneRenderer, { node: node }), node.type === 'roof' && _jsx(RoofRenderer, { node: node }), node.type === 'roof-segment' && _jsx(RoofSegmentRenderer, { node: node }), node.type === 'stair' && _jsx(StairRenderer, { node: node }), node.type === 'stair-segment' && _jsx(StairSegmentRenderer, { node: node }), node.type === 'scan' && _jsx(ScanRenderer, { node: node }), node.type === 'guide' && _jsx(GuideRenderer, { node: node })] }));
|
|
25
|
+
return (_jsxs(_Fragment, { children: [node.type === 'site' && _jsx(SiteRenderer, { node: node }), node.type === 'building' && _jsx(BuildingRenderer, { node: node }), node.type === 'ceiling' && _jsx(CeilingRenderer, { node: node }), node.type === 'level' && _jsx(LevelRenderer, { node: node }), node.type === 'item' && _jsx(ItemRenderer, { node: node }), node.type === 'slab' && _jsx(SlabRenderer, { node: node }), node.type === 'wall' && _jsx(WallRenderer, { node: node }), node.type === 'fence' && _jsx(FenceRenderer, { node: node }), node.type === 'door' && _jsx(DoorRenderer, { node: node }), node.type === 'window' && _jsx(WindowRenderer, { node: node }), node.type === 'zone' && _jsx(ZoneRenderer, { node: node }), node.type === 'roof' && _jsx(RoofRenderer, { node: node }), node.type === 'roof-segment' && _jsx(RoofSegmentRenderer, { node: node }), node.type === 'stair' && _jsx(StairRenderer, { node: node }), node.type === 'stair-segment' && _jsx(StairSegmentRenderer, { node: node }), node.type === 'scan' && _jsx(ScanRenderer, { node: node }), node.type === 'guide' && _jsx(GuideRenderer, { node: node })] }));
|
|
25
26
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stair-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/stair/stair-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"stair-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/stair/stair-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,SAAS,EAAgD,MAAM,kBAAkB,CAAA;AAqC/G,eAAO,MAAM,aAAa,GAAI,UAAU;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,4CAwC1D,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useRegistry, useScene } from '@pascal-app/core';
|
|
3
3
|
import { useLayoutEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import * as THREE from 'three';
|
|
4
5
|
import { useNodeEvents } from '../../../hooks/use-node-events';
|
|
5
6
|
import { createMaterial, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials';
|
|
6
7
|
import { NodeRenderer } from '../node-renderer';
|
|
@@ -17,5 +18,471 @@ export const StairRenderer = ({ node }) => {
|
|
|
17
18
|
return DEFAULT_STAIR_MATERIAL;
|
|
18
19
|
return createMaterial(mat);
|
|
19
20
|
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
|
|
20
|
-
return (_jsxs("group", { "position-x": node.position[0], "position-z": node.position[2], ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers, children: [_jsx("mesh", { castShadow: true, material: material, name: "merged-stair", receiveShadow: true, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), _jsx("group", { name: "segments-wrapper", visible: false, children: (node.children ?? []).map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) })] }));
|
|
21
|
+
return (_jsxs("group", { "position-x": node.position[0], "position-z": node.position[2], ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers, children: [_jsx("mesh", { castShadow: true, material: material, name: "merged-stair", receiveShadow: true, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), node.stairType === 'curved' || node.stairType === 'spiral' ? (_jsx(CurvedStairBody, { material: material, stair: node })) : null, _jsx(StairRailings, { material: material, stair: node }), _jsx("group", { name: "segments-wrapper", visible: false, children: (node.children ?? []).map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) })] }));
|
|
21
22
|
};
|
|
23
|
+
function StairRailings({ stair, material }) {
|
|
24
|
+
const nodes = useScene((state) => state.nodes);
|
|
25
|
+
const segments = useMemo(() => (stair.children ?? [])
|
|
26
|
+
.map((childId) => nodes[childId])
|
|
27
|
+
.filter((node) => node?.type === 'stair-segment' && node.visible !== false), [nodes, stair.children]);
|
|
28
|
+
const railPaths = useMemo(() => buildStairRailPaths(segments, stair.railingMode ?? 'none'), [segments, stair.railingMode]);
|
|
29
|
+
const railHeight = stair.railingHeight ?? 0.92;
|
|
30
|
+
const midRailHeight = Math.max(railHeight * 0.45, 0.35);
|
|
31
|
+
const railRadius = 0.022;
|
|
32
|
+
const balusterRadius = 0.018;
|
|
33
|
+
if ((stair.railingMode ?? 'none') === 'none') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (stair.stairType === 'curved' || stair.stairType === 'spiral') {
|
|
37
|
+
const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
|
|
38
|
+
const sweepAngle = stair.sweepAngle ?? (stair.stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2);
|
|
39
|
+
const stepSweep = sweepAngle / stepCount;
|
|
40
|
+
const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount;
|
|
41
|
+
const innerRadius = Math.max(stair.stairType === 'spiral' ? 0.05 : 0.2, stair.innerRadius ?? 0.9);
|
|
42
|
+
const outerRadius = innerRadius + Math.max(stair.width ?? 1, 0.4);
|
|
43
|
+
const leftRadius = sweepAngle >= 0 ? innerRadius + 0.04 : outerRadius - 0.04;
|
|
44
|
+
const rightRadius = sweepAngle >= 0 ? outerRadius - 0.04 : innerRadius + 0.04;
|
|
45
|
+
const radii = stair.railingMode === 'both'
|
|
46
|
+
? [leftRadius, rightRadius]
|
|
47
|
+
: stair.railingMode === 'left'
|
|
48
|
+
? [leftRadius]
|
|
49
|
+
: stair.railingMode === 'right'
|
|
50
|
+
? [rightRadius]
|
|
51
|
+
: [];
|
|
52
|
+
return (_jsx("group", { name: "stair-railing", children: radii.map((radius, sideIndex) => {
|
|
53
|
+
const sidePoints = Array.from({ length: stepCount }).map((_, index) => {
|
|
54
|
+
const angle = -sweepAngle / 2 + stepSweep * index + stepSweep / 2;
|
|
55
|
+
return [
|
|
56
|
+
Math.cos(angle) * radius,
|
|
57
|
+
stair.stairType === 'spiral'
|
|
58
|
+
? stepHeight * index + Math.max(stair.thickness ?? 0.25, 0.02)
|
|
59
|
+
: stepHeight * (index + 1),
|
|
60
|
+
Math.sin(angle) * radius,
|
|
61
|
+
];
|
|
62
|
+
});
|
|
63
|
+
return (_jsxs("group", { children: [sidePoints.map((point, pointIndex) => (_jsx("mesh", { castShadow: true, geometry: BALUSTER_GEOMETRY, material: material, position: [point[0], point[1] + railHeight / 2, point[2]], receiveShadow: true, scale: [balusterRadius, railHeight, balusterRadius] }, `${stair.id}-curved-baluster-${sideIndex}-${pointIndex}`))), sidePoints.slice(0, -1).map((point, pointIndex) => {
|
|
64
|
+
const nextPoint = sidePoints[pointIndex + 1];
|
|
65
|
+
if (!nextPoint)
|
|
66
|
+
return null;
|
|
67
|
+
return (_jsxs("group", { children: [_jsx(RailSegment, { end: [nextPoint[0], nextPoint[1] + railHeight, nextPoint[2]], material: material, radius: railRadius, start: [point[0], point[1] + railHeight, point[2]] }), _jsx(RailSegment, { end: [nextPoint[0], nextPoint[1] + midRailHeight, nextPoint[2]], material: material, radius: railRadius * 0.8, start: [point[0], point[1] + midRailHeight, point[2]] })] }, `${stair.id}-curved-rail-${sideIndex}-${pointIndex}`));
|
|
68
|
+
})] }, `${stair.id}-curved-railing-${sideIndex}`));
|
|
69
|
+
}) }));
|
|
70
|
+
}
|
|
71
|
+
if (railPaths.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return (_jsxs("group", { name: "stair-railing", children: [railPaths.map((segmentPath, index) => (_jsx("group", { position: [segmentPath.layout.center[0], segmentPath.layout.elevation, segmentPath.layout.center[1]], "rotation-y": segmentPath.layout.rotation, children: segmentPath.sidePaths.map((sidePath, sideIndex) => (_jsxs("group", { children: [sidePath.points.map((point, pointIndex) => (_jsx("mesh", { castShadow: true, geometry: BALUSTER_GEOMETRY, material: material, position: [point[2], point[1] + railHeight / 2, point[0]], receiveShadow: true, scale: [balusterRadius, railHeight, balusterRadius] }, `${segmentPath.layout.segment.id}-${sidePath.side}-baluster-${pointIndex}`))), sidePath.points.slice(0, -1).map((point, pointIndex) => {
|
|
75
|
+
const nextPoint = sidePath.points[pointIndex + 1];
|
|
76
|
+
if (!nextPoint)
|
|
77
|
+
return null;
|
|
78
|
+
return (_jsxs("group", { children: [_jsx(RailSegment, { end: [nextPoint[2], nextPoint[1] + railHeight, nextPoint[0]], material: material, radius: railRadius, start: [point[2], point[1] + railHeight, point[0]] }), _jsx(RailSegment, { end: [nextPoint[2], nextPoint[1] + midRailHeight, nextPoint[0]], material: material, radius: railRadius * 0.8, start: [point[2], point[1] + midRailHeight, point[0]] })] }, `${segmentPath.layout.segment.id}-${sidePath.side}-rail-${pointIndex}`));
|
|
79
|
+
})] }, `${segmentPath.layout.segment.id}-${sidePath.side}-${sideIndex}`))) }, `${segmentPath.layout.segment.id}-railing`))), railPaths.slice(1).map((segmentPath, index) => {
|
|
80
|
+
const previousPath = railPaths[index];
|
|
81
|
+
if (!previousPath || !segmentPath.connectFromPrevious)
|
|
82
|
+
return null;
|
|
83
|
+
if (previousPath.layout.segment.segmentType === 'landing')
|
|
84
|
+
return null;
|
|
85
|
+
if (segmentPath.layout.segment.segmentType === 'landing')
|
|
86
|
+
return null;
|
|
87
|
+
return segmentPath.sidePaths.map((sidePath, sideIndex) => {
|
|
88
|
+
const currentPoint = sidePath.points[0];
|
|
89
|
+
if (!currentPoint)
|
|
90
|
+
return null;
|
|
91
|
+
const currentWorldPoint = toWorldRailPoint(segmentPath.layout, currentPoint);
|
|
92
|
+
const previousSidePath = [...previousPath.sidePaths]
|
|
93
|
+
.map((entry) => {
|
|
94
|
+
const lastPoint = entry.points[entry.points.length - 1];
|
|
95
|
+
return {
|
|
96
|
+
entry,
|
|
97
|
+
distance: lastPoint ? distance3(toWorldRailPoint(previousPath.layout, lastPoint), currentWorldPoint) : Number.POSITIVE_INFINITY,
|
|
98
|
+
};
|
|
99
|
+
})
|
|
100
|
+
.sort((left, right) => left.distance - right.distance)[0]?.entry;
|
|
101
|
+
const previousPoint = previousSidePath && previousSidePath.points.length
|
|
102
|
+
? previousSidePath.points[previousSidePath.points.length - 1]
|
|
103
|
+
: null;
|
|
104
|
+
if (!(previousPoint && currentPoint)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const previousWorldPoint = toWorldRailPoint(previousPath.layout, previousPoint);
|
|
108
|
+
return (_jsxs("group", { children: [_jsx(RailSegment, { end: [currentWorldPoint[0], currentWorldPoint[1] + railHeight, currentWorldPoint[2]], material: material, radius: railRadius, start: [previousWorldPoint[0], previousWorldPoint[1] + railHeight, previousWorldPoint[2]] }), _jsx(RailSegment, { end: [currentWorldPoint[0], currentWorldPoint[1] + midRailHeight, currentWorldPoint[2]], material: material, radius: railRadius * 0.8, start: [previousWorldPoint[0], previousWorldPoint[1] + midRailHeight, previousWorldPoint[2]] })] }, `${previousPath.layout.segment.id}-${segmentPath.layout.segment.id}-${sideIndex}`));
|
|
109
|
+
});
|
|
110
|
+
})] }));
|
|
111
|
+
}
|
|
112
|
+
const BALUSTER_GEOMETRY = new THREE.CylinderGeometry(1, 1, 1, 8);
|
|
113
|
+
const RAIL_GEOMETRY = new THREE.CylinderGeometry(1, 1, 1, 8);
|
|
114
|
+
function RailSegment({ start, end, radius, material, }) {
|
|
115
|
+
const startVector = useMemo(() => new THREE.Vector3(...start), [start]);
|
|
116
|
+
const endVector = useMemo(() => new THREE.Vector3(...end), [end]);
|
|
117
|
+
const direction = useMemo(() => endVector.clone().sub(startVector), [endVector, startVector]);
|
|
118
|
+
const length = Math.max(direction.length(), 0.01);
|
|
119
|
+
const quaternion = useMemo(() => new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize()), [direction]);
|
|
120
|
+
const midpoint = useMemo(() => startVector.clone().add(endVector).multiplyScalar(0.5), [endVector, startVector]);
|
|
121
|
+
return (_jsx("mesh", { castShadow: true, geometry: RAIL_GEOMETRY, material: material, position: [midpoint.x, midpoint.y, midpoint.z], quaternion: quaternion, receiveShadow: true, scale: [Math.max(radius, 0.01), length, Math.max(radius, 0.01)] }));
|
|
122
|
+
}
|
|
123
|
+
function CurvedStairBody({ stair, material }) {
|
|
124
|
+
const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
|
|
125
|
+
const totalRise = Math.max(stair.totalRise ?? 2.5, 0.1);
|
|
126
|
+
const stepHeight = totalRise / stepCount;
|
|
127
|
+
const isSpiral = stair.stairType === 'spiral';
|
|
128
|
+
const innerRadius = Math.max(isSpiral ? 0.05 : 0.2, stair.innerRadius ?? 0.9);
|
|
129
|
+
const outerRadius = innerRadius + Math.max(stair.width ?? 1, 0.4);
|
|
130
|
+
const sweepAngle = stair.sweepAngle ?? (isSpiral ? Math.PI * 2 : Math.PI / 2);
|
|
131
|
+
const stepSweep = sweepAngle / stepCount;
|
|
132
|
+
const thickness = Math.max(stair.thickness ?? 0.25, 0.02);
|
|
133
|
+
const fillToFloor = stair.fillToFloor ?? true;
|
|
134
|
+
const spiralColumnRadius = Math.max(0.05, Math.min(innerRadius * 0.72, innerRadius - 0.03));
|
|
135
|
+
const spiralColumnHeight = totalRise + thickness;
|
|
136
|
+
const spiralLandingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max((stair.width ?? 1) * 0.9, 0.8));
|
|
137
|
+
const spiralLandingSweep = isSpiral && (stair.topLandingMode ?? 'none') === 'integrated'
|
|
138
|
+
? Math.min(Math.PI * 0.75, spiralLandingDepth / Math.max(innerRadius + (stair.width ?? 1) / 2, 0.1)) *
|
|
139
|
+
Math.sign(sweepAngle || 1)
|
|
140
|
+
: 0;
|
|
141
|
+
const spiralLastStepTop = stepHeight * Math.max(stepCount - 1, 0) + thickness;
|
|
142
|
+
const spiralLandingThickness = isSpiral && (stair.topLandingMode ?? 'none') === 'integrated'
|
|
143
|
+
? Math.max(0.02, totalRise - spiralLastStepTop)
|
|
144
|
+
: 0;
|
|
145
|
+
return (_jsxs("group", { name: isSpiral ? 'spiral-stair' : 'curved-stair', children: [isSpiral && (stair.showCenterColumn ?? true) ? (_jsx("mesh", { castShadow: true, receiveShadow: true, material: material, position: [0, spiralColumnHeight / 2, 0], children: _jsx("cylinderGeometry", { args: [spiralColumnRadius, spiralColumnRadius, spiralColumnHeight, 10] }) })) : null, Array.from({ length: stepCount }).map((_, index) => {
|
|
146
|
+
const currentHeight = stepHeight * (index + 1);
|
|
147
|
+
const actualStepHeight = isSpiral ? thickness : fillToFloor ? Math.max(currentHeight, thickness) : thickness;
|
|
148
|
+
const startAngle = -sweepAngle / 2 + stepSweep * index;
|
|
149
|
+
const endAngle = startAngle + stepSweep;
|
|
150
|
+
const stepY = isSpiral ? stepHeight * index : fillToFloor ? 0 : Math.max(currentHeight - thickness, 0);
|
|
151
|
+
const midAngle = startAngle + stepSweep / 2;
|
|
152
|
+
return (_jsxs("group", { "position-y": stepY, children: [isSpiral && (stair.showStepSupports ?? true) ? (_jsx("mesh", { castShadow: true, material: material, position: [
|
|
153
|
+
Math.cos(midAngle) * (spiralColumnRadius + Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 - 0.02),
|
|
154
|
+
Math.max(thickness * 0.55, 0.025) / 2,
|
|
155
|
+
Math.sin(midAngle) * (spiralColumnRadius + Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 - 0.02),
|
|
156
|
+
], receiveShadow: true, "rotation-y": -midAngle, children: _jsx("boxGeometry", { args: [
|
|
157
|
+
Math.max(0.04, innerRadius - spiralColumnRadius + 0.04),
|
|
158
|
+
Math.max(thickness * 0.55, 0.025),
|
|
159
|
+
Math.max(0.04, Math.min(0.12, Math.max(thickness * 0.55, 0.025) * 1.5)),
|
|
160
|
+
] }) })) : null, _jsx(CurvedStepMesh, { endAngle: endAngle, innerRadius: innerRadius, material: material, outerRadius: outerRadius, positionY: 0, startAngle: startAngle, stepHeight: actualStepHeight, thickness: thickness })] }, `${stair.id}-${isSpiral ? 'spiral' : 'curved'}-step-${index}`));
|
|
161
|
+
}), isSpiral && (stair.topLandingMode ?? 'none') === 'integrated' ? (_jsx(CurvedStepMesh, { endAngle: sweepAngle / 2 + spiralLandingSweep, innerRadius: innerRadius, material: material, outerRadius: outerRadius, positionY: spiralLastStepTop, startAngle: sweepAngle / 2, stepHeight: spiralLandingThickness, thickness: spiralLandingThickness })) : null] }));
|
|
162
|
+
}
|
|
163
|
+
function CurvedStepMesh({ innerRadius, outerRadius, startAngle, endAngle, stepHeight, thickness, positionY, material, }) {
|
|
164
|
+
const geometry = useMemo(() => buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle, Math.max(stepHeight, thickness)), [endAngle, innerRadius, outerRadius, startAngle, stepHeight, thickness]);
|
|
165
|
+
return _jsx("mesh", { castShadow: true, geometry: geometry, material: material, "position-y": positionY, receiveShadow: true });
|
|
166
|
+
}
|
|
167
|
+
function buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle, height) {
|
|
168
|
+
const clampedHeight = Math.max(height, 0.02);
|
|
169
|
+
const y0 = 0;
|
|
170
|
+
const y1 = clampedHeight;
|
|
171
|
+
const sweepAngle = endAngle - startAngle;
|
|
172
|
+
const sweepDirection = Math.sign(sweepAngle) || 1;
|
|
173
|
+
const segmentCount = Math.max(4, Math.min(24, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 18) + Math.max(0, (outerRadius - innerRadius) * 3))));
|
|
174
|
+
const positions = [];
|
|
175
|
+
const normals = [];
|
|
176
|
+
const pointOnArc = (radius, angle, y) => new THREE.Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius);
|
|
177
|
+
const pushTriangle = (a, b, c, normal) => {
|
|
178
|
+
const edgeAB = b.clone().sub(a);
|
|
179
|
+
const edgeAC = c.clone().sub(a);
|
|
180
|
+
const faceNormal = edgeAB.cross(edgeAC);
|
|
181
|
+
const ordered = faceNormal.dot(normal) >= 0 ? [a, b, c] : [a, c, b];
|
|
182
|
+
for (const point of ordered) {
|
|
183
|
+
positions.push(point.x, point.y, point.z);
|
|
184
|
+
normals.push(normal.x, normal.y, normal.z);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const pushQuad = (a, b, c, d, normal) => {
|
|
188
|
+
pushTriangle(a, b, c, normal);
|
|
189
|
+
pushTriangle(a, c, d, normal);
|
|
190
|
+
};
|
|
191
|
+
const upNormal = new THREE.Vector3(0, 1, 0);
|
|
192
|
+
const downNormal = new THREE.Vector3(0, -1, 0);
|
|
193
|
+
for (let index = 0; index < segmentCount; index++) {
|
|
194
|
+
const t0 = index / segmentCount;
|
|
195
|
+
const t1 = (index + 1) / segmentCount;
|
|
196
|
+
const segStart = startAngle + sweepAngle * t0;
|
|
197
|
+
const segEnd = startAngle + sweepAngle * t1;
|
|
198
|
+
const midAngle = (segStart + segEnd) / 2;
|
|
199
|
+
const innerStartBottom = pointOnArc(innerRadius, segStart, y0);
|
|
200
|
+
const innerEndBottom = pointOnArc(innerRadius, segEnd, y0);
|
|
201
|
+
const outerStartBottom = pointOnArc(outerRadius, segStart, y0);
|
|
202
|
+
const outerEndBottom = pointOnArc(outerRadius, segEnd, y0);
|
|
203
|
+
const innerStartTop = pointOnArc(innerRadius, segStart, y1);
|
|
204
|
+
const innerEndTop = pointOnArc(innerRadius, segEnd, y1);
|
|
205
|
+
const outerStartTop = pointOnArc(outerRadius, segStart, y1);
|
|
206
|
+
const outerEndTop = pointOnArc(outerRadius, segEnd, y1);
|
|
207
|
+
const outerNormal = new THREE.Vector3(Math.cos(midAngle), 0, Math.sin(midAngle)).normalize();
|
|
208
|
+
const innerNormal = new THREE.Vector3(-Math.cos(midAngle), 0, -Math.sin(midAngle)).normalize();
|
|
209
|
+
pushQuad(innerStartTop, outerStartTop, outerEndTop, innerEndTop, upNormal);
|
|
210
|
+
pushQuad(innerStartBottom, innerEndBottom, outerEndBottom, outerStartBottom, downNormal);
|
|
211
|
+
pushQuad(innerStartBottom, innerStartTop, innerEndTop, innerEndBottom, innerNormal);
|
|
212
|
+
pushQuad(outerStartBottom, outerEndBottom, outerEndTop, outerStartTop, outerNormal);
|
|
213
|
+
}
|
|
214
|
+
const startInnerBottom = pointOnArc(innerRadius, startAngle, y0);
|
|
215
|
+
const startOuterBottom = pointOnArc(outerRadius, startAngle, y0);
|
|
216
|
+
const startInnerTop = pointOnArc(innerRadius, startAngle, y1);
|
|
217
|
+
const startOuterTop = pointOnArc(outerRadius, startAngle, y1);
|
|
218
|
+
const endInnerBottom = pointOnArc(innerRadius, endAngle, y0);
|
|
219
|
+
const endOuterBottom = pointOnArc(outerRadius, endAngle, y0);
|
|
220
|
+
const endInnerTop = pointOnArc(innerRadius, endAngle, y1);
|
|
221
|
+
const endOuterTop = pointOnArc(outerRadius, endAngle, y1);
|
|
222
|
+
const startNormal = new THREE.Vector3(sweepDirection * Math.sin(startAngle), 0, -sweepDirection * Math.cos(startAngle)).normalize();
|
|
223
|
+
const endNormal = new THREE.Vector3(-sweepDirection * Math.sin(endAngle), 0, sweepDirection * Math.cos(endAngle)).normalize();
|
|
224
|
+
pushQuad(startInnerBottom, startOuterBottom, startOuterTop, startInnerTop, startNormal);
|
|
225
|
+
pushQuad(endInnerBottom, endInnerTop, endOuterTop, endOuterBottom, endNormal);
|
|
226
|
+
const geometry = new THREE.BufferGeometry();
|
|
227
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
228
|
+
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
|
|
229
|
+
geometry.computeVertexNormals();
|
|
230
|
+
return geometry;
|
|
231
|
+
}
|
|
232
|
+
function buildStairRailPaths(segments, railingMode) {
|
|
233
|
+
if (!segments.length || railingMode === 'none')
|
|
234
|
+
return [];
|
|
235
|
+
const layouts = computeStairRailLayouts(segments);
|
|
236
|
+
const landingInset = 0.08;
|
|
237
|
+
if (railingMode === 'both') {
|
|
238
|
+
const isStraightLineDoubleLandingLayout = layouts.length === 4 &&
|
|
239
|
+
layouts[0]?.segment.segmentType === 'stair' &&
|
|
240
|
+
layouts[1]?.segment.segmentType === 'landing' &&
|
|
241
|
+
layouts[2]?.segment.segmentType === 'stair' &&
|
|
242
|
+
layouts[2]?.segment.attachmentSide === 'front' &&
|
|
243
|
+
layouts[3]?.segment.segmentType === 'landing' &&
|
|
244
|
+
layouts[3]?.segment.attachmentSide === 'front';
|
|
245
|
+
return layouts.map((layout, index) => {
|
|
246
|
+
const previousLayout = index > 0 ? layouts[index - 1] : undefined;
|
|
247
|
+
const nextLayout = layouts[index + 1];
|
|
248
|
+
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index);
|
|
249
|
+
const hideLandingRailing = layout.segment.segmentType === 'landing' &&
|
|
250
|
+
previousLayout?.segment.segmentType === 'stair' &&
|
|
251
|
+
nextLayout?.segment.segmentType === 'stair';
|
|
252
|
+
const visualTurnSide = isTerminalLandingBeforeStair && nextStairLayout?.segment.attachmentSide
|
|
253
|
+
? nextStairLayout.segment.attachmentSide
|
|
254
|
+
: nextLayout?.segment.attachmentSide;
|
|
255
|
+
const sideCandidates = isTerminalLandingBeforeStair && layout.segment.segmentType === 'landing'
|
|
256
|
+
? visualTurnSide === 'left'
|
|
257
|
+
? ['front', 'right']
|
|
258
|
+
: visualTurnSide === 'right'
|
|
259
|
+
? ['front', 'left']
|
|
260
|
+
: ['left', 'right']
|
|
261
|
+
: hideLandingRailing
|
|
262
|
+
? visualTurnSide === 'left'
|
|
263
|
+
? ['front', 'right']
|
|
264
|
+
: visualTurnSide === 'right'
|
|
265
|
+
? ['front', 'left']
|
|
266
|
+
: ['left', 'right']
|
|
267
|
+
: layout.segment.segmentType === 'landing'
|
|
268
|
+
? nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'left'
|
|
269
|
+
? ['front', 'right']
|
|
270
|
+
: nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'right'
|
|
271
|
+
? ['front', 'left']
|
|
272
|
+
: visualTurnSide === 'left'
|
|
273
|
+
? ['right']
|
|
274
|
+
: visualTurnSide === 'right'
|
|
275
|
+
? ['left']
|
|
276
|
+
: ['left', 'right']
|
|
277
|
+
: ['left', 'right'];
|
|
278
|
+
return {
|
|
279
|
+
layout,
|
|
280
|
+
sidePaths: isStraightLineDoubleLandingLayout && index === 1
|
|
281
|
+
? ['left', 'right'].map((side) => buildSegmentRailPath(layouts, index, side, landingInset))
|
|
282
|
+
: sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
|
|
283
|
+
connectFromPrevious: index > 0 &&
|
|
284
|
+
!(previousLayout?.segment.segmentType === 'landing' && layout.segment.segmentType === 'landing'),
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const isStraightLineDoubleLandingLayout = layouts.length === 4 &&
|
|
289
|
+
layouts[0]?.segment.segmentType === 'stair' &&
|
|
290
|
+
layouts[1]?.segment.segmentType === 'landing' &&
|
|
291
|
+
layouts[2]?.segment.segmentType === 'stair' &&
|
|
292
|
+
layouts[2]?.segment.attachmentSide === 'front' &&
|
|
293
|
+
layouts[3]?.segment.segmentType === 'landing' &&
|
|
294
|
+
layouts[3]?.segment.attachmentSide === 'front';
|
|
295
|
+
return layouts.map((layout, index) => {
|
|
296
|
+
const previousLayout = index > 0 ? layouts[index - 1] : undefined;
|
|
297
|
+
const nextLayout = layouts[index + 1];
|
|
298
|
+
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index);
|
|
299
|
+
const isMiddleLandingBetweenFlights = layout.segment.segmentType === 'landing' &&
|
|
300
|
+
previousLayout?.segment.segmentType === 'stair' &&
|
|
301
|
+
nextLayout?.segment.segmentType === 'stair';
|
|
302
|
+
const nextAttachmentSide = nextLayout?.segment.attachmentSide;
|
|
303
|
+
const terminalNextAttachmentSide = nextStairLayout?.segment.attachmentSide;
|
|
304
|
+
const suppressMiddleLandingOnPreferredTurnSide = isMiddleLandingBetweenFlights &&
|
|
305
|
+
nextAttachmentSide != null &&
|
|
306
|
+
nextAttachmentSide !== 'front' &&
|
|
307
|
+
nextAttachmentSide === railingMode;
|
|
308
|
+
const suppressLandingRailing = (layout.segment.segmentType === 'landing' &&
|
|
309
|
+
nextLayout?.segment.segmentType === 'landing' &&
|
|
310
|
+
nextAttachmentSide === railingMode) ||
|
|
311
|
+
suppressMiddleLandingOnPreferredTurnSide;
|
|
312
|
+
const landingContinuesOnPreferredSide = layout.segment.segmentType === 'landing'
|
|
313
|
+
? nextAttachmentSide == null || nextAttachmentSide === 'front' || nextAttachmentSide === railingMode
|
|
314
|
+
: true;
|
|
315
|
+
const sideCandidates = suppressLandingRailing
|
|
316
|
+
? []
|
|
317
|
+
: layout.segment.segmentType !== 'landing'
|
|
318
|
+
? [railingMode]
|
|
319
|
+
: isTerminalLandingBeforeStair
|
|
320
|
+
? railingMode === 'left'
|
|
321
|
+
? terminalNextAttachmentSide === 'right'
|
|
322
|
+
? ['front', 'left']
|
|
323
|
+
: terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
|
|
324
|
+
? ['left']
|
|
325
|
+
: []
|
|
326
|
+
: railingMode === 'right'
|
|
327
|
+
? terminalNextAttachmentSide === 'left'
|
|
328
|
+
? ['front', 'right']
|
|
329
|
+
: terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
|
|
330
|
+
? ['right']
|
|
331
|
+
: []
|
|
332
|
+
: [railingMode]
|
|
333
|
+
: isStraightLineDoubleLandingLayout
|
|
334
|
+
? [railingMode]
|
|
335
|
+
: isMiddleLandingBetweenFlights && railingMode === 'left'
|
|
336
|
+
? nextAttachmentSide === 'right'
|
|
337
|
+
? ['front', 'left']
|
|
338
|
+
: ['left']
|
|
339
|
+
: isMiddleLandingBetweenFlights && railingMode === 'right'
|
|
340
|
+
? nextAttachmentSide === 'left'
|
|
341
|
+
? ['front', 'right']
|
|
342
|
+
: ['right']
|
|
343
|
+
: nextLayout?.segment.segmentType === 'landing' &&
|
|
344
|
+
nextAttachmentSide != null &&
|
|
345
|
+
nextAttachmentSide !== 'front' &&
|
|
346
|
+
nextAttachmentSide !== railingMode
|
|
347
|
+
? ['front', railingMode]
|
|
348
|
+
: [railingMode];
|
|
349
|
+
return {
|
|
350
|
+
layout,
|
|
351
|
+
sidePaths: sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
|
|
352
|
+
connectFromPrevious: index > 0 &&
|
|
353
|
+
!suppressLandingRailing &&
|
|
354
|
+
sideCandidates.length > 0 &&
|
|
355
|
+
(layout.segment.segmentType === 'landing' ? landingContinuesOnPreferredSide : true),
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function resolveLandingChainNextStair(layouts, index) {
|
|
360
|
+
const layout = layouts[index];
|
|
361
|
+
if (!layout || layout.segment.segmentType !== 'landing') {
|
|
362
|
+
return { isTerminalLandingBeforeStair: false };
|
|
363
|
+
}
|
|
364
|
+
let cursor = index;
|
|
365
|
+
while (cursor + 1 < layouts.length && layouts[cursor + 1]?.segment.segmentType === 'landing') {
|
|
366
|
+
cursor += 1;
|
|
367
|
+
}
|
|
368
|
+
const nextStairLayout = cursor + 1 < layouts.length && layouts[cursor + 1]?.segment.segmentType === 'stair'
|
|
369
|
+
? layouts[cursor + 1]
|
|
370
|
+
: undefined;
|
|
371
|
+
return {
|
|
372
|
+
nextStairLayout,
|
|
373
|
+
isTerminalLandingBeforeStair: Boolean(nextStairLayout) && cursor === index,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function computeStairRailLayouts(segments) {
|
|
377
|
+
const transforms = computeSegmentTransforms(segments);
|
|
378
|
+
return segments.map((segment, index) => {
|
|
379
|
+
const transform = transforms[index];
|
|
380
|
+
const [centerOffsetX, centerOffsetZ] = rotateXZ(0, segment.length / 2, transform.rotation);
|
|
381
|
+
return {
|
|
382
|
+
center: [transform.position[0] + centerOffsetX, transform.position[2] + centerOffsetZ],
|
|
383
|
+
elevation: transform.position[1],
|
|
384
|
+
rotation: transform.rotation,
|
|
385
|
+
segment,
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function buildSegmentRailPath(layouts, layoutIndex, side, landingInset) {
|
|
390
|
+
const layout = layouts[layoutIndex];
|
|
391
|
+
const segment = layout.segment;
|
|
392
|
+
const previousLayout = layoutIndex > 0 ? layouts[layoutIndex - 1] : undefined;
|
|
393
|
+
const nextLayout = layoutIndex >= 0 ? layouts[layoutIndex + 1] : undefined;
|
|
394
|
+
const steps = Math.max(1, segment.segmentType === 'landing' ? 1 : segment.stepCount);
|
|
395
|
+
const stepDepth = segment.length / steps;
|
|
396
|
+
const stepHeight = segment.segmentType === 'landing' ? 0 : segment.height / steps;
|
|
397
|
+
const flightSideOffset = side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045;
|
|
398
|
+
const flightStartX = previousLayout?.segment.segmentType === 'landing' ? -segment.length / 2 + landingInset : -segment.length / 2;
|
|
399
|
+
const flightEndX = nextLayout?.segment.segmentType === 'landing' ? segment.length / 2 - landingInset : segment.length / 2;
|
|
400
|
+
const landingFrontX = previousLayout?.segment.segmentType === 'stair' &&
|
|
401
|
+
segment.attachmentSide &&
|
|
402
|
+
segment.attachmentSide !== 'front'
|
|
403
|
+
? -segment.length / 2 + landingInset
|
|
404
|
+
: segment.length / 2 - landingInset;
|
|
405
|
+
if (segment.segmentType === 'landing') {
|
|
406
|
+
const backX = -segment.length / 2 + landingInset;
|
|
407
|
+
const frontX = segment.length / 2 - landingInset;
|
|
408
|
+
const leftZ = segment.width / 2 - landingInset;
|
|
409
|
+
const rightZ = -segment.width / 2 + landingInset;
|
|
410
|
+
return {
|
|
411
|
+
side,
|
|
412
|
+
points: side === 'left'
|
|
413
|
+
? [
|
|
414
|
+
[backX, 0, leftZ],
|
|
415
|
+
[frontX, 0, leftZ],
|
|
416
|
+
]
|
|
417
|
+
: side === 'right'
|
|
418
|
+
? [
|
|
419
|
+
[backX, 0, rightZ],
|
|
420
|
+
[frontX, 0, rightZ],
|
|
421
|
+
]
|
|
422
|
+
: [
|
|
423
|
+
[landingFrontX, 0, leftZ],
|
|
424
|
+
[landingFrontX, 0, rightZ],
|
|
425
|
+
],
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
side,
|
|
430
|
+
points: [
|
|
431
|
+
...(previousLayout?.segment.segmentType === 'landing' ? [] : [[flightStartX, stepHeight > 0 ? stepHeight : 0, flightSideOffset]]),
|
|
432
|
+
...Array.from({ length: steps }).map((_, index) => [
|
|
433
|
+
-segment.length / 2 + stepDepth * index + stepDepth / 2,
|
|
434
|
+
stepHeight * (index + 1),
|
|
435
|
+
flightSideOffset,
|
|
436
|
+
]),
|
|
437
|
+
...(nextLayout?.segment.segmentType === 'landing'
|
|
438
|
+
? []
|
|
439
|
+
: [[flightEndX, segment.height, flightSideOffset]]),
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function toWorldRailPoint(layout, point) {
|
|
444
|
+
const [localX, localY, localZ] = point;
|
|
445
|
+
const [offsetX, offsetZ] = rotateXZ(localZ, localX, layout.rotation);
|
|
446
|
+
return [layout.center[0] + offsetX, layout.elevation + localY, layout.center[1] + offsetZ];
|
|
447
|
+
}
|
|
448
|
+
function computeSegmentTransforms(segments) {
|
|
449
|
+
const transforms = [];
|
|
450
|
+
let currentPos = new THREE.Vector3(0, 0, 0);
|
|
451
|
+
let currentRot = 0;
|
|
452
|
+
for (let i = 0; i < segments.length; i++) {
|
|
453
|
+
const segment = segments[i];
|
|
454
|
+
if (i === 0) {
|
|
455
|
+
transforms.push({ position: [currentPos.x, currentPos.y, currentPos.z], rotation: currentRot });
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const prev = segments[i - 1];
|
|
459
|
+
const localAttachPos = new THREE.Vector3();
|
|
460
|
+
let rotChange = 0;
|
|
461
|
+
switch (segment.attachmentSide) {
|
|
462
|
+
case 'front':
|
|
463
|
+
localAttachPos.set(0, prev.height, prev.length);
|
|
464
|
+
break;
|
|
465
|
+
case 'left':
|
|
466
|
+
localAttachPos.set(prev.width / 2, prev.height, prev.length / 2);
|
|
467
|
+
rotChange = Math.PI / 2;
|
|
468
|
+
break;
|
|
469
|
+
case 'right':
|
|
470
|
+
localAttachPos.set(-prev.width / 2, prev.height, prev.length / 2);
|
|
471
|
+
rotChange = -Math.PI / 2;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
localAttachPos.applyAxisAngle(new THREE.Vector3(0, 1, 0), currentRot);
|
|
475
|
+
currentPos = currentPos.clone().add(localAttachPos);
|
|
476
|
+
currentRot += rotChange;
|
|
477
|
+
transforms.push({ position: [currentPos.x, currentPos.y, currentPos.z], rotation: currentRot });
|
|
478
|
+
}
|
|
479
|
+
return transforms;
|
|
480
|
+
}
|
|
481
|
+
function rotateXZ(x, z, angle) {
|
|
482
|
+
const cos = Math.cos(angle);
|
|
483
|
+
const sin = Math.sin(angle);
|
|
484
|
+
return [x * cos + z * sin, -x * sin + z * cos];
|
|
485
|
+
}
|
|
486
|
+
function distance3(a, b) {
|
|
487
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
488
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAkB,KAAK,kBAAkB,EAAsB,MAAM,oBAAoB,CAAA;AAEhG,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AA0CrC,OAAO,QAAQ,oBAAoB,CAAC;IAClC,UAAU,aAAc,SAAQ,kBAAkB,CAAC,OAAO,KAAK,CAAC;KAAG;CACpE;AA+BD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,QAAA,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CA8DjC,CAAA;AASD,eAAe,MAAM,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { CeilingSystem, DoorSystem, ItemSystem, RoofSystem, SlabSystem, StairSystem, WallSystem, WindowSystem, } from '@pascal-app/core';
|
|
3
|
+
import { CeilingSystem, DoorSystem, FenceSystem, ItemSystem, RoofSystem, SlabSystem, StairSystem, WallSystem, WindowSystem, } from '@pascal-app/core';
|
|
4
4
|
import { Bvh } from '@react-three/drei';
|
|
5
5
|
import { Canvas, extend, useFrame, useThree } from '@react-three/fiber';
|
|
6
6
|
import { useEffect, useMemo, useRef } from 'react';
|
|
@@ -74,7 +74,7 @@ const Viewer = ({ children, selectionManager = 'default', perf = false, }) => {
|
|
|
74
74
|
}, shadows: {
|
|
75
75
|
type: THREE.PCFShadowMap,
|
|
76
76
|
enabled: true,
|
|
77
|
-
}, children: [_jsx(ViewerCamera, {}), _jsx(Lights, {}), _jsx(Bvh, { children: _jsx(SceneRenderer, {}) }), _jsx(LevelSystem, {}), _jsx(GuideSystem, {}), _jsx(ScanSystem, {}), _jsx(WallCutout, {}), _jsx(CeilingSystem, {}), _jsx(DoorSystem, {}), _jsx(ItemSystem, {}), _jsx(RoofSystem, {}), _jsx(SlabSystem, {}), _jsx(StairSystem, {}), _jsx(WallSystem, {}), _jsx(WindowSystem, {}), _jsx(ZoneSystem, {}), _jsx(PostProcessing, {}), _jsx(GPUDeviceWatcher, {}), _jsx(ItemLightSystem, {}), selectionManager === 'default' && _jsx(SelectionManager, {}), perf && _jsx(PerfMonitor, {}), children] }));
|
|
77
|
+
}, children: [_jsx(ViewerCamera, {}), _jsx(Lights, {}), _jsx(Bvh, { children: _jsx(SceneRenderer, {}) }), _jsx(LevelSystem, {}), _jsx(GuideSystem, {}), _jsx(ScanSystem, {}), _jsx(WallCutout, {}), _jsx(CeilingSystem, {}), _jsx(DoorSystem, {}), _jsx(FenceSystem, {}), _jsx(ItemSystem, {}), _jsx(RoofSystem, {}), _jsx(SlabSystem, {}), _jsx(StairSystem, {}), _jsx(WallSystem, {}), _jsx(WindowSystem, {}), _jsx(ZoneSystem, {}), _jsx(PostProcessing, {}), _jsx(GPUDeviceWatcher, {}), _jsx(ItemLightSystem, {}), selectionManager === 'default' && _jsx(SelectionManager, {}), perf && _jsx(PerfMonitor, {}), children] }));
|
|
78
78
|
};
|
|
79
79
|
const DebugRenderer = () => {
|
|
80
80
|
useFrame(({ gl, scene, camera }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA4BA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AAQD,QAAA,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA4BA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AAQD,QAAA,MAAM,oBAAoB,YAmTzB,CAAA;AAED,eAAe,oBAAoB,CAAA"}
|
|
@@ -32,6 +32,7 @@ const PostProcessingPasses = () => {
|
|
|
32
32
|
const renderPipelineRef = useRef(null);
|
|
33
33
|
const hasPipelineErrorRef = useRef(false);
|
|
34
34
|
const retryCountRef = useRef(0);
|
|
35
|
+
const rebuildTimeoutRef = useRef(null);
|
|
35
36
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
36
37
|
// Background color uniform — updated every frame via lerp, read by the TSL pipeline.
|
|
37
38
|
// Initialised from the current theme so there's no flash on first render.
|
|
@@ -51,6 +52,10 @@ const PostProcessingPasses = () => {
|
|
|
51
52
|
// Bump this to force a pipeline rebuild (used by retry logic)
|
|
52
53
|
const [pipelineVersion, setPipelineVersion] = useState(0);
|
|
53
54
|
const requestPipelineRebuild = useCallback(() => {
|
|
55
|
+
if (rebuildTimeoutRef.current !== null) {
|
|
56
|
+
clearTimeout(rebuildTimeoutRef.current);
|
|
57
|
+
rebuildTimeoutRef.current = null;
|
|
58
|
+
}
|
|
54
59
|
setPipelineVersion((v) => v + 1);
|
|
55
60
|
}, []);
|
|
56
61
|
// Renderer initialization
|
|
@@ -77,12 +82,30 @@ const PostProcessingPasses = () => {
|
|
|
77
82
|
mounted = false;
|
|
78
83
|
};
|
|
79
84
|
}, [renderer]);
|
|
80
|
-
// Reset retry
|
|
85
|
+
// Reset retry state when project changes
|
|
81
86
|
useEffect(() => {
|
|
87
|
+
// Intentionally touch projectId so the effect reruns on project switches.
|
|
88
|
+
void projectId;
|
|
82
89
|
retryCountRef.current = 0;
|
|
90
|
+
if (rebuildTimeoutRef.current !== null) {
|
|
91
|
+
clearTimeout(rebuildTimeoutRef.current);
|
|
92
|
+
rebuildTimeoutRef.current = null;
|
|
93
|
+
}
|
|
94
|
+
}, [projectId]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
return () => {
|
|
97
|
+
if (rebuildTimeoutRef.current !== null) {
|
|
98
|
+
clearTimeout(rebuildTimeoutRef.current);
|
|
99
|
+
rebuildTimeoutRef.current = null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
83
102
|
}, []);
|
|
84
103
|
// Build / rebuild the post-processing pipeline
|
|
85
104
|
useEffect(() => {
|
|
105
|
+
// Intentionally touch these so React/biome treat project switches and retry bumps
|
|
106
|
+
// as explicit rebuild triggers instead of accidental extra dependencies.
|
|
107
|
+
void projectId;
|
|
108
|
+
void pipelineVersion;
|
|
86
109
|
if (!(renderer && scene && camera && isInitialized)) {
|
|
87
110
|
return;
|
|
88
111
|
}
|
|
@@ -179,6 +202,7 @@ const PostProcessingPasses = () => {
|
|
|
179
202
|
const renderPipeline = new RenderPipeline(renderer);
|
|
180
203
|
renderPipeline.outputNode = finalOutput;
|
|
181
204
|
renderPipelineRef.current = renderPipeline;
|
|
205
|
+
retryCountRef.current = 0;
|
|
182
206
|
}
|
|
183
207
|
catch (error) {
|
|
184
208
|
hasPipelineErrorRef.current = true;
|
|
@@ -194,13 +218,33 @@ const PostProcessingPasses = () => {
|
|
|
194
218
|
}
|
|
195
219
|
renderPipelineRef.current = null;
|
|
196
220
|
};
|
|
197
|
-
}, [
|
|
221
|
+
}, [
|
|
222
|
+
renderer,
|
|
223
|
+
scene,
|
|
224
|
+
camera,
|
|
225
|
+
hoverHighlightMode,
|
|
226
|
+
isInitialized,
|
|
227
|
+
zoneLayers,
|
|
228
|
+
projectId,
|
|
229
|
+
pipelineVersion,
|
|
230
|
+
]);
|
|
198
231
|
useFrame((_, delta) => {
|
|
199
232
|
// Animate background colour toward the current theme target (same lerp as AnimatedBackground)
|
|
200
233
|
bgTarget.current.set(useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG);
|
|
201
234
|
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4);
|
|
202
235
|
bgUniform.current.value.copy(bgCurrent.current);
|
|
203
236
|
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
|
|
237
|
+
try {
|
|
238
|
+
if (renderer.setClearAlpha) {
|
|
239
|
+
;
|
|
240
|
+
renderer.setClearAlpha(1);
|
|
241
|
+
}
|
|
242
|
+
;
|
|
243
|
+
renderer.render(scene, camera);
|
|
244
|
+
}
|
|
245
|
+
catch (fallbackError) {
|
|
246
|
+
console.error('[viewer] Fallback render failed.', fallbackError);
|
|
247
|
+
}
|
|
204
248
|
return;
|
|
205
249
|
}
|
|
206
250
|
try {
|
|
@@ -221,7 +265,10 @@ const PostProcessingPasses = () => {
|
|
|
221
265
|
// Auto-retry: schedule a pipeline rebuild if we haven't exceeded the retry limit
|
|
222
266
|
retryCountRef.current++;
|
|
223
267
|
console.warn(`[viewer] Scheduling post-processing rebuild (attempt ${retryCountRef.current}/${MAX_PIPELINE_RETRIES})`);
|
|
224
|
-
|
|
268
|
+
if (rebuildTimeoutRef.current !== null) {
|
|
269
|
+
clearTimeout(rebuildTimeoutRef.current);
|
|
270
|
+
}
|
|
271
|
+
rebuildTimeoutRef.current = setTimeout(requestPipelineRebuild, RETRY_DELAY_MS);
|
|
225
272
|
}
|
|
226
273
|
else {
|
|
227
274
|
console.error('[viewer] Post-processing retries exhausted. Rendering without post FX for this session.');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"AAiRA,eAAO,MAAM,gBAAgB,+CAuE5B,CAAA"}
|
|
@@ -88,6 +88,12 @@ const isNodeInZone = (node, levelId, zoneId) => {
|
|
|
88
88
|
const endIn = pointInPolygonWithTolerance(wall.end[0], wall.end[1], zone.polygon);
|
|
89
89
|
return startIn || endIn;
|
|
90
90
|
}
|
|
91
|
+
if (node.type === 'fence') {
|
|
92
|
+
const fence = node;
|
|
93
|
+
const startIn = pointInPolygonWithTolerance(fence.start[0], fence.start[1], zone.polygon);
|
|
94
|
+
const endIn = pointInPolygonWithTolerance(fence.end[0], fence.end[1], zone.polygon);
|
|
95
|
+
return startIn || endIn;
|
|
96
|
+
}
|
|
91
97
|
if (node.type === 'slab' || node.type === 'ceiling') {
|
|
92
98
|
const poly = node.polygon;
|
|
93
99
|
if (!poly?.length)
|
|
@@ -164,7 +170,7 @@ const getStrategy = () => {
|
|
|
164
170
|
}
|
|
165
171
|
// Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows, doors)
|
|
166
172
|
return {
|
|
167
|
-
types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'roof-segment', 'window', 'door'],
|
|
173
|
+
types: ['wall', 'fence', 'item', 'slab', 'ceiling', 'roof', 'roof-segment', 'window', 'door'],
|
|
168
174
|
handleClick: (node, nativeEvent) => {
|
|
169
175
|
let nodeToSelect = node;
|
|
170
176
|
if (node.type === 'roof-segment' && node.parentId) {
|
|
@@ -191,6 +197,7 @@ const getStrategy = () => {
|
|
|
191
197
|
isValid: (node) => {
|
|
192
198
|
const validTypes = [
|
|
193
199
|
'wall',
|
|
200
|
+
'fence',
|
|
194
201
|
'item',
|
|
195
202
|
'slab',
|
|
196
203
|
'ceiling',
|
|
@@ -245,6 +252,7 @@ export const SelectionManager = () => {
|
|
|
245
252
|
'level',
|
|
246
253
|
'zone',
|
|
247
254
|
'wall',
|
|
255
|
+
'fence',
|
|
248
256
|
'item',
|
|
249
257
|
'slab',
|
|
250
258
|
'ceiling',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BuildingEvent, type BuildingNode, type CeilingEvent, type CeilingNode, type DoorEvent, type DoorNode, type ItemEvent, type ItemNode, type LevelEvent, type LevelNode, type RoofEvent, type RoofNode, type RoofSegmentEvent, type RoofSegmentNode, type SiteEvent, type SiteNode, type SlabEvent, type SlabNode, type StairEvent, type StairNode, type StairSegmentEvent, type StairSegmentNode, type WallEvent, type WallNode, type WindowEvent, type WindowNode, type ZoneEvent, type ZoneNode } from '@pascal-app/core';
|
|
1
|
+
import { type BuildingEvent, type BuildingNode, type CeilingEvent, type CeilingNode, type DoorEvent, type DoorNode, type FenceEvent, type FenceNode, type ItemEvent, type ItemNode, type LevelEvent, type LevelNode, type RoofEvent, type RoofNode, type RoofSegmentEvent, type RoofSegmentNode, type SiteEvent, type SiteNode, type SlabEvent, type SlabNode, type StairEvent, type StairNode, type StairSegmentEvent, type StairSegmentNode, type WallEvent, type WallNode, type WindowEvent, type WindowNode, type ZoneEvent, type ZoneNode } from '@pascal-app/core';
|
|
2
2
|
import type { ThreeEvent } from '@react-three/fiber';
|
|
3
3
|
type NodeConfig = {
|
|
4
4
|
site: {
|
|
@@ -13,6 +13,10 @@ type NodeConfig = {
|
|
|
13
13
|
node: WallNode;
|
|
14
14
|
event: WallEvent;
|
|
15
15
|
};
|
|
16
|
+
fence: {
|
|
17
|
+
node: FenceNode;
|
|
18
|
+
event: FenceEvent;
|
|
19
|
+
};
|
|
16
20
|
building: {
|
|
17
21
|
node: BuildingNode;
|
|
18
22
|
event: BuildingEvent;
|
|
@@ -1 +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,EAChB,KAAK,SAAS,EACd,KAAK,QAAQ,EAGb,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAGpD,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,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;IAC1C,cAAc,EAAE;QAAE,IAAI,EAAE,eAAe,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,CAAA;IAClE,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,UAAU,CAAA;KAAE,CAAA;IAC7C,eAAe,EAAE;QAAE,IAAI,EAAE,gBAAgB,CAAC;QAAC,KAAK,EAAE,iBAAiB,CAAA;KAAE,CAAA;IACrE,MAAM,EAAE;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE,CAAA;IAChD,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;qBAK1B,UAAU,CAAC,YAAY,CAAC;iBAQ5B,UAAU,CAAC,YAAY,CAAC;wBAIjB,UAAU,CAAC,YAAY,CAAC;wBAIxB,UAAU,CAAC,YAAY,CAAC;uBAIzB,UAAU,CAAC,YAAY,CAAC;uBAIxB,UAAU,CAAC,YAAY,CAAC;uBAIxB,UAAU,CAAC,YAAY,CAAC;EAK9C"}
|
|
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,EAChB,KAAK,SAAS,EACd,KAAK,QAAQ,EAGb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAGpD,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,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,UAAU,CAAA;KAAE,CAAA;IAC7C,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;IAC1C,cAAc,EAAE;QAAE,IAAI,EAAE,eAAe,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,CAAA;IAClE,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,UAAU,CAAA;KAAE,CAAA;IAC7C,eAAe,EAAE;QAAE,IAAI,EAAE,gBAAgB,CAAC;QAAC,KAAK,EAAE,iBAAiB,CAAA;KAAE,CAAA;IACrE,MAAM,EAAE;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE,CAAA;IAChD,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;qBAK1B,UAAU,CAAC,YAAY,CAAC;iBAQ5B,UAAU,CAAC,YAAY,CAAC;wBAIjB,UAAU,CAAC,YAAY,CAAC;wBAIxB,UAAU,CAAC,YAAY,CAAC;uBAIzB,UAAU,CAAC,YAAY,CAAC;uBAIxB,UAAU,CAAC,YAAY,CAAC;uBAIxB,UAAU,CAAC,YAAY,CAAC;EAK9C"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as Viewer } from './components/viewer';
|
|
2
|
+
export { SSGI_PARAMS } from './components/viewer/post-processing';
|
|
2
3
|
export { WalkthroughControls } from './components/viewer/walkthrough-controls';
|
|
3
4
|
export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url';
|
|
4
5
|
export { SCENE_LAYER, ZONE_LAYER } from './lib/layers';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,EACd,wBAAwB,EACxB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,eAAe,GAChB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAA;AAC5E,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,EACd,wBAAwB,EACxB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,eAAe,GAChB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAA;AAC5E,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as Viewer } from './components/viewer';
|
|
2
|
+
export { SSGI_PARAMS } from './components/viewer/post-processing';
|
|
2
3
|
export { WalkthroughControls } from './components/viewer/walkthrough-controls';
|
|
3
4
|
export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url';
|
|
4
5
|
export { SCENE_LAYER, ZONE_LAYER } from './lib/layers';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guide-system.d.ts","sourceRoot":"","sources":["../../../src/systems/guide/guide-system.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"guide-system.d.ts","sourceRoot":"","sources":["../../../src/systems/guide/guide-system.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW,YAsCvB,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sceneRegistry } from '@pascal-app/core';
|
|
1
|
+
import { emitter, sceneRegistry } from '@pascal-app/core';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
import useViewer from '../../store/use-viewer';
|
|
4
4
|
export const GuideSystem = () => {
|
|
@@ -12,5 +12,30 @@ export const GuideSystem = () => {
|
|
|
12
12
|
}
|
|
13
13
|
});
|
|
14
14
|
}, [showGuides]);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const hideForCapture = () => {
|
|
17
|
+
const guides = sceneRegistry.byType.guide || new Set();
|
|
18
|
+
guides.forEach((guideId) => {
|
|
19
|
+
const node = sceneRegistry.nodes.get(guideId);
|
|
20
|
+
if (node)
|
|
21
|
+
node.visible = false;
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const restoreAfterCapture = () => {
|
|
25
|
+
const showGuidesNow = useViewer.getState().showGuides;
|
|
26
|
+
const guides = sceneRegistry.byType.guide || new Set();
|
|
27
|
+
guides.forEach((guideId) => {
|
|
28
|
+
const node = sceneRegistry.nodes.get(guideId);
|
|
29
|
+
if (node)
|
|
30
|
+
node.visible = showGuidesNow;
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
emitter.on('thumbnail:before-capture', hideForCapture);
|
|
34
|
+
emitter.on('thumbnail:after-capture', restoreAfterCapture);
|
|
35
|
+
return () => {
|
|
36
|
+
emitter.off('thumbnail:before-capture', hideForCapture);
|
|
37
|
+
emitter.off('thumbnail:after-capture', restoreAfterCapture);
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
15
40
|
return null;
|
|
16
41
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan-system.d.ts","sourceRoot":"","sources":["../../../src/systems/scan/scan-system.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"scan-system.d.ts","sourceRoot":"","sources":["../../../src/systems/scan/scan-system.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,YAsCtB,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sceneRegistry } from '@pascal-app/core';
|
|
1
|
+
import { emitter, sceneRegistry } from '@pascal-app/core';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
import useViewer from '../../store/use-viewer';
|
|
4
4
|
export const ScanSystem = () => {
|
|
@@ -12,5 +12,30 @@ export const ScanSystem = () => {
|
|
|
12
12
|
}
|
|
13
13
|
});
|
|
14
14
|
}, [showScans]);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const hideForCapture = () => {
|
|
17
|
+
const scans = sceneRegistry.byType.scan || new Set();
|
|
18
|
+
scans.forEach((scanId) => {
|
|
19
|
+
const node = sceneRegistry.nodes.get(scanId);
|
|
20
|
+
if (node)
|
|
21
|
+
node.visible = false;
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const restoreAfterCapture = () => {
|
|
25
|
+
const showScansNow = useViewer.getState().showScans;
|
|
26
|
+
const scans = sceneRegistry.byType.scan || new Set();
|
|
27
|
+
scans.forEach((scanId) => {
|
|
28
|
+
const node = sceneRegistry.nodes.get(scanId);
|
|
29
|
+
if (node)
|
|
30
|
+
node.visible = showScansNow;
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
emitter.on('thumbnail:before-capture', hideForCapture);
|
|
34
|
+
emitter.on('thumbnail:after-capture', restoreAfterCapture);
|
|
35
|
+
return () => {
|
|
36
|
+
emitter.off('thumbnail:before-capture', hideForCapture);
|
|
37
|
+
emitter.off('thumbnail:after-capture', restoreAfterCapture);
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
15
40
|
return null;
|
|
16
41
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAsMA,eAAO,MAAM,UAAU,YAoHtB,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { baseMaterial, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { baseMaterial, emitter, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
|
-
import { useRef } from 'react';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
4
|
import { Color } from 'three';
|
|
5
5
|
import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, vec2 } from 'three/tsl';
|
|
6
6
|
import { MeshStandardNodeMaterial, Vector3 } from 'three/webgpu';
|
|
@@ -216,5 +216,39 @@ export const WallCutout = () => {
|
|
|
216
216
|
lastHighlightKey.current = highlightKey;
|
|
217
217
|
}
|
|
218
218
|
});
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const snapshot = new Map();
|
|
221
|
+
const restoreForCapture = () => {
|
|
222
|
+
sceneRegistry.byType.wall.forEach((wallId) => {
|
|
223
|
+
const wallMesh = sceneRegistry.nodes.get(wallId);
|
|
224
|
+
if (!wallMesh)
|
|
225
|
+
return;
|
|
226
|
+
const wallNode = useScene.getState().nodes[wallId];
|
|
227
|
+
if (!wallNode || wallNode.type !== 'wall')
|
|
228
|
+
return;
|
|
229
|
+
const mats = getMaterialsForWall(wallNode);
|
|
230
|
+
const current = wallMesh.material;
|
|
231
|
+
snapshot.set(wallMesh, current);
|
|
232
|
+
if (current === mats.highlightedVisible || current === mats.deleteVisible) {
|
|
233
|
+
wallMesh.material = mats.visible;
|
|
234
|
+
}
|
|
235
|
+
else if (current === mats.highlightedInvisible || current === mats.deleteInvisible) {
|
|
236
|
+
wallMesh.material = mats.invisible;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
const reapplyAfterCapture = () => {
|
|
241
|
+
snapshot.forEach((mat, mesh) => {
|
|
242
|
+
mesh.material = mat;
|
|
243
|
+
});
|
|
244
|
+
snapshot.clear();
|
|
245
|
+
};
|
|
246
|
+
emitter.on('thumbnail:before-capture', restoreForCapture);
|
|
247
|
+
emitter.on('thumbnail:after-capture', reapplyAfterCapture);
|
|
248
|
+
return () => {
|
|
249
|
+
emitter.off('thumbnail:before-capture', restoreForCapture);
|
|
250
|
+
emitter.off('thumbnail:after-capture', reapplyAfterCapture);
|
|
251
|
+
};
|
|
252
|
+
}, []);
|
|
219
253
|
return null;
|
|
220
254
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "3D viewer component for Pascal building editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"prepublishOnly": "npm run build"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@pascal-app/core": "^0.1
|
|
25
|
+
"@pascal-app/core": "^0.5.1",
|
|
26
26
|
"@react-three/drei": "^10",
|
|
27
27
|
"@react-three/fiber": "^9",
|
|
28
28
|
"react": "^18 || ^19",
|