@pascal-app/viewer 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/components/renderers/building/building-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -1
  3. package/dist/components/renderers/ceiling/ceiling-renderer.js +4 -3
  4. package/dist/components/renderers/door/door-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/door/door-renderer.js +5 -2
  6. package/dist/components/renderers/fence/fence-renderer.d.ts +5 -0
  7. package/dist/components/renderers/fence/fence-renderer.d.ts.map +1 -0
  8. package/dist/components/renderers/fence/fence-renderer.js +29 -0
  9. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  10. package/dist/components/renderers/node-renderer.js +2 -1
  11. package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -1
  12. package/dist/components/renderers/roof/roof-renderer.js +31 -9
  13. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +1 -1
  14. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +42 -8
  15. package/dist/components/renderers/site/site-renderer.d.ts.map +1 -1
  16. package/dist/components/renderers/site/site-renderer.js +7 -1
  17. package/dist/components/renderers/slab/slab-renderer.d.ts.map +1 -1
  18. package/dist/components/renderers/slab/slab-renderer.js +37 -8
  19. package/dist/components/renderers/stair/stair-renderer.d.ts.map +1 -1
  20. package/dist/components/renderers/stair/stair-renderer.js +605 -5
  21. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +1 -1
  22. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +37 -8
  23. package/dist/components/renderers/wall/wall-renderer.d.ts.map +1 -1
  24. package/dist/components/renderers/wall/wall-renderer.js +4 -9
  25. package/dist/components/renderers/window/window-renderer.d.ts.map +1 -1
  26. package/dist/components/renderers/window/window-renderer.js +5 -2
  27. package/dist/components/viewer/frame-limiter.d.ts +6 -0
  28. package/dist/components/viewer/frame-limiter.d.ts.map +1 -0
  29. package/dist/components/viewer/frame-limiter.js +31 -0
  30. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  31. package/dist/components/viewer/ground-occluder.js +5 -1
  32. package/dist/components/viewer/index.d.ts.map +1 -1
  33. package/dist/components/viewer/index.js +12 -12
  34. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  35. package/dist/components/viewer/post-processing.js +72 -28
  36. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  37. package/dist/components/viewer/selection-manager.js +21 -2
  38. package/dist/hooks/use-node-events.d.ts +5 -1
  39. package/dist/hooks/use-node-events.d.ts.map +1 -1
  40. package/dist/hooks/use-node-events.js +2 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -0
  44. package/dist/lib/materials.d.ts +4 -1
  45. package/dist/lib/materials.d.ts.map +1 -1
  46. package/dist/lib/materials.js +178 -2
  47. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  48. package/dist/lib/merged-outline-node.js +11 -4
  49. package/dist/store/use-viewer.d.ts +2 -2
  50. package/dist/systems/guide/guide-system.d.ts.map +1 -1
  51. package/dist/systems/guide/guide-system.js +26 -1
  52. package/dist/systems/roof/roof-materials.d.ts +5 -0
  53. package/dist/systems/roof/roof-materials.d.ts.map +1 -0
  54. package/dist/systems/roof/roof-materials.js +29 -0
  55. package/dist/systems/scan/scan-system.d.ts.map +1 -1
  56. package/dist/systems/scan/scan-system.js +26 -1
  57. package/dist/systems/stair/stair-materials.d.ts +7 -0
  58. package/dist/systems/stair/stair-materials.d.ts.map +1 -0
  59. package/dist/systems/stair/stair-materials.js +33 -0
  60. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  61. package/dist/systems/wall/wall-cutout.js +39 -124
  62. package/dist/systems/wall/wall-materials.d.ts +16 -0
  63. package/dist/systems/wall/wall-materials.d.ts.map +1 -0
  64. package/dist/systems/wall/wall-materials.js +148 -0
  65. package/package.json +4 -4
@@ -1,21 +1,621 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRegistry, useScene } from '@pascal-app/core';
3
- import { useLayoutEffect, useMemo, useRef } from 'react';
2
+ import { useRegistry, useScene, } from '@pascal-app/core';
3
+ import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
4
+ import * as THREE from 'three';
4
5
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
- import { createMaterial, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials';
6
+ import { createMaterial, createMaterialFromPresetRef, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials';
7
+ import { getStairRailingMaterial, getStairBodyMaterials, } from '../../../systems/stair/stair-materials';
6
8
  import { NodeRenderer } from '../node-renderer';
7
9
  export const StairRenderer = ({ node }) => {
8
10
  const ref = useRef(null);
11
+ const isSegmentBasedStair = node.stairType === 'straight';
9
12
  useRegistry(node.id, 'stair', ref);
10
13
  useLayoutEffect(() => {
11
14
  useScene.getState().markDirty(node.id);
12
15
  }, [node.id]);
13
16
  const handlers = useNodeEvents(node, 'stair');
14
17
  const material = useMemo(() => {
18
+ const presetMaterial = createMaterialFromPresetRef(node.materialPreset);
19
+ if (presetMaterial)
20
+ return presetMaterial;
15
21
  const mat = node.material;
16
22
  if (!mat)
17
23
  return DEFAULT_STAIR_MATERIAL;
18
24
  return createMaterial(mat);
19
- }, [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))) })] }));
25
+ }, [
26
+ node.materialPreset,
27
+ node.material,
28
+ node.material?.preset,
29
+ node.material?.properties,
30
+ node.material?.texture,
31
+ ]);
32
+ const straightBodyMaterials = useMemo(() => getStairBodyMaterials(node), [
33
+ node.material,
34
+ node.materialPreset,
35
+ node.railingMaterial,
36
+ node.railingMaterialPreset,
37
+ node.sideMaterial,
38
+ node.sideMaterialPreset,
39
+ node.treadMaterial,
40
+ node.treadMaterialPreset,
41
+ ]);
42
+ const railingMaterial = useMemo(() => getStairRailingMaterial(node), [
43
+ node.material,
44
+ node.materialPreset,
45
+ node.railingMaterial,
46
+ node.railingMaterialPreset,
47
+ node.sideMaterial,
48
+ node.sideMaterialPreset,
49
+ node.treadMaterial,
50
+ node.treadMaterialPreset,
51
+ ]);
52
+ const straightPlaceholderGeometry = useMemo(() => {
53
+ const geometry = new THREE.BufferGeometry();
54
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
55
+ geometry.addGroup(0, 0, 0);
56
+ geometry.addGroup(0, 0, 1);
57
+ return geometry;
58
+ }, []);
59
+ useEffect(() => {
60
+ return () => {
61
+ straightPlaceholderGeometry.dispose();
62
+ };
63
+ }, [straightPlaceholderGeometry]);
64
+ return (_jsxs("group", { "position-x": node.position[0], "position-z": node.position[2], ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers, children: [isSegmentBasedStair ? (_jsx("mesh", { castShadow: true, geometry: straightPlaceholderGeometry, material: straightBodyMaterials, name: "merged-stair", receiveShadow: true })) : null, !isSegmentBasedStair ? _jsx(CurvedStairBody, { bodyMaterials: straightBodyMaterials, stair: node }) : null, _jsx(StairRailings, { material: railingMaterial, stair: node }), isSegmentBasedStair ? (_jsx("group", { name: "segments-wrapper", visible: false, children: (node.children ?? []).map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) })) : null] }));
21
65
  };
66
+ function StairRailings({ stair, material }) {
67
+ const nodes = useScene((state) => state.nodes);
68
+ const segments = useMemo(() => (stair.children ?? [])
69
+ .map((childId) => nodes[childId])
70
+ .filter((node) => node?.type === 'stair-segment' && node.visible !== false), [nodes, stair.children]);
71
+ const railPaths = useMemo(() => buildStairRailPaths(segments, stair.railingMode ?? 'none'), [segments, stair.railingMode]);
72
+ const railHeight = stair.railingHeight ?? 0.92;
73
+ const midRailHeight = Math.max(railHeight * 0.45, 0.35);
74
+ const railRadius = 0.022;
75
+ const balusterRadius = 0.018;
76
+ if ((stair.railingMode ?? 'none') === 'none') {
77
+ return null;
78
+ }
79
+ if (stair.stairType === 'curved' || stair.stairType === 'spiral') {
80
+ const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
81
+ const sweepAngle = stair.sweepAngle ?? (stair.stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2);
82
+ const stepSweep = sweepAngle / stepCount;
83
+ const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount;
84
+ const innerRadius = Math.max(stair.stairType === 'spiral' ? 0.05 : 0.2, stair.innerRadius ?? 0.9);
85
+ const outerRadius = innerRadius + Math.max(stair.width ?? 1, 0.4);
86
+ const leftRadius = sweepAngle >= 0 ? innerRadius + 0.04 : outerRadius - 0.04;
87
+ const rightRadius = sweepAngle >= 0 ? outerRadius - 0.04 : innerRadius + 0.04;
88
+ const radii = stair.railingMode === 'both'
89
+ ? [leftRadius, rightRadius]
90
+ : stair.railingMode === 'left'
91
+ ? [leftRadius]
92
+ : stair.railingMode === 'right'
93
+ ? [rightRadius]
94
+ : [];
95
+ return (_jsx("group", { name: "stair-railing", children: radii.map((radius, sideIndex) => {
96
+ const sidePoints = Array.from({ length: stepCount }).map((_, index) => {
97
+ const angle = -sweepAngle / 2 + stepSweep * index + stepSweep / 2;
98
+ return [
99
+ Math.cos(angle) * radius,
100
+ stair.stairType === 'spiral'
101
+ ? stepHeight * index + Math.max(stair.thickness ?? 0.25, 0.02)
102
+ : stepHeight * (index + 1),
103
+ Math.sin(angle) * radius,
104
+ ];
105
+ });
106
+ return (_jsxs("group", { children: [sidePoints.map((point, pointIndex) => (_jsx("mesh", { castShadow: true, geometry: BALUSTER_GEOMETRY, material: material, name: "stair-railing-baluster", 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) => {
107
+ const nextPoint = sidePoints[pointIndex + 1];
108
+ if (!nextPoint)
109
+ return null;
110
+ 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}`));
111
+ })] }, `${stair.id}-curved-railing-${sideIndex}`));
112
+ }) }));
113
+ }
114
+ if (railPaths.length === 0) {
115
+ return null;
116
+ }
117
+ return (_jsxs("group", { name: "stair-railing", children: [railPaths.map((segmentPath, index) => (_jsx("group", { position: [
118
+ segmentPath.layout.center[0],
119
+ segmentPath.layout.elevation,
120
+ segmentPath.layout.center[1],
121
+ ], "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, name: "stair-railing-baluster", 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) => {
122
+ const nextPoint = sidePath.points[pointIndex + 1];
123
+ if (!nextPoint)
124
+ return null;
125
+ 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}`));
126
+ })] }, `${segmentPath.layout.segment.id}-${sidePath.side}-${sideIndex}`))) }, `${segmentPath.layout.segment.id}-railing`))), railPaths.slice(1).map((segmentPath, index) => {
127
+ const previousPath = railPaths[index];
128
+ if (!previousPath || !segmentPath.connectFromPrevious)
129
+ return null;
130
+ if (previousPath.layout.segment.segmentType === 'landing')
131
+ return null;
132
+ if (segmentPath.layout.segment.segmentType === 'landing')
133
+ return null;
134
+ return segmentPath.sidePaths.map((sidePath, sideIndex) => {
135
+ const currentPoint = sidePath.points[0];
136
+ if (!currentPoint)
137
+ return null;
138
+ const currentWorldPoint = toWorldRailPoint(segmentPath.layout, currentPoint);
139
+ const previousSidePath = [...previousPath.sidePaths]
140
+ .map((entry) => {
141
+ const lastPoint = entry.points[entry.points.length - 1];
142
+ return {
143
+ entry,
144
+ distance: lastPoint
145
+ ? distance3(toWorldRailPoint(previousPath.layout, lastPoint), currentWorldPoint)
146
+ : Number.POSITIVE_INFINITY,
147
+ };
148
+ })
149
+ .sort((left, right) => left.distance - right.distance)[0]?.entry;
150
+ const previousPoint = previousSidePath?.points.length
151
+ ? previousSidePath.points[previousSidePath.points.length - 1]
152
+ : null;
153
+ if (!(previousPoint && currentPoint)) {
154
+ return null;
155
+ }
156
+ const previousWorldPoint = toWorldRailPoint(previousPath.layout, previousPoint);
157
+ return (_jsxs("group", { children: [_jsx(RailSegment, { end: [
158
+ currentWorldPoint[0],
159
+ currentWorldPoint[1] + railHeight,
160
+ currentWorldPoint[2],
161
+ ], material: material, radius: railRadius, start: [
162
+ previousWorldPoint[0],
163
+ previousWorldPoint[1] + railHeight,
164
+ previousWorldPoint[2],
165
+ ] }), _jsx(RailSegment, { end: [
166
+ currentWorldPoint[0],
167
+ currentWorldPoint[1] + midRailHeight,
168
+ currentWorldPoint[2],
169
+ ], material: material, radius: railRadius * 0.8, start: [
170
+ previousWorldPoint[0],
171
+ previousWorldPoint[1] + midRailHeight,
172
+ previousWorldPoint[2],
173
+ ] })] }, `${previousPath.layout.segment.id}-${segmentPath.layout.segment.id}-${sideIndex}`));
174
+ });
175
+ })] }));
176
+ }
177
+ const BALUSTER_GEOMETRY = new THREE.CylinderGeometry(1, 1, 1, 8);
178
+ const RAIL_GEOMETRY = new THREE.CylinderGeometry(1, 1, 1, 8);
179
+ const STAIR_TREAD_MATERIAL_INDEX = 0;
180
+ const STAIR_SIDE_MATERIAL_INDEX = 1;
181
+ function RailSegment({ start, end, radius, material, }) {
182
+ const startVector = useMemo(() => new THREE.Vector3(...start), [start]);
183
+ const endVector = useMemo(() => new THREE.Vector3(...end), [end]);
184
+ const direction = useMemo(() => endVector.clone().sub(startVector), [endVector, startVector]);
185
+ const length = Math.max(direction.length(), 0.01);
186
+ const quaternion = useMemo(() => new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize()), [direction]);
187
+ const midpoint = useMemo(() => startVector.clone().add(endVector).multiplyScalar(0.5), [endVector, startVector]);
188
+ return (_jsx("mesh", { castShadow: true, geometry: RAIL_GEOMETRY, material: material, name: "stair-railing-rail", position: [midpoint.x, midpoint.y, midpoint.z], quaternion: quaternion, receiveShadow: true, scale: [Math.max(radius, 0.01), length, Math.max(radius, 0.01)] }));
189
+ }
190
+ function CurvedStairBody({ stair, bodyMaterials, }) {
191
+ const sideMaterial = bodyMaterials[1];
192
+ const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
193
+ const totalRise = Math.max(stair.totalRise ?? 2.5, 0.1);
194
+ const stepHeight = totalRise / stepCount;
195
+ const isSpiral = stair.stairType === 'spiral';
196
+ const innerRadius = Math.max(isSpiral ? 0.05 : 0.2, stair.innerRadius ?? 0.9);
197
+ const outerRadius = innerRadius + Math.max(stair.width ?? 1, 0.4);
198
+ const sweepAngle = stair.sweepAngle ?? (isSpiral ? Math.PI * 2 : Math.PI / 2);
199
+ const stepSweep = sweepAngle / stepCount;
200
+ const thickness = Math.max(stair.thickness ?? 0.25, 0.02);
201
+ const fillToFloor = stair.fillToFloor ?? true;
202
+ const spiralColumnRadius = Math.max(0.05, Math.min(innerRadius * 0.72, innerRadius - 0.03));
203
+ const spiralColumnHeight = totalRise + thickness;
204
+ const spiralLandingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max((stair.width ?? 1) * 0.9, 0.8));
205
+ const spiralLandingSweep = isSpiral && (stair.topLandingMode ?? 'none') === 'integrated'
206
+ ? Math.min(Math.PI * 0.75, spiralLandingDepth / Math.max(innerRadius + (stair.width ?? 1) / 2, 0.1)) * Math.sign(sweepAngle || 1)
207
+ : 0;
208
+ const spiralLastStepTop = stepHeight * Math.max(stepCount - 1, 0) + thickness;
209
+ const spiralLandingThickness = isSpiral && (stair.topLandingMode ?? 'none') === 'integrated'
210
+ ? Math.max(0.02, totalRise - spiralLastStepTop)
211
+ : 0;
212
+ return (_jsxs("group", { name: isSpiral ? 'spiral-stair' : 'curved-stair', children: [isSpiral && (stair.showCenterColumn ?? true) ? (_jsx("mesh", { castShadow: true, receiveShadow: true, material: sideMaterial, name: "stair-side", position: [0, spiralColumnHeight / 2, 0], children: _jsx("cylinderGeometry", { args: [spiralColumnRadius, spiralColumnRadius, spiralColumnHeight, 10] }) })) : null, Array.from({ length: stepCount }).map((_, index) => {
213
+ const currentHeight = stepHeight * (index + 1);
214
+ const actualStepHeight = isSpiral
215
+ ? thickness
216
+ : fillToFloor
217
+ ? Math.max(currentHeight, thickness)
218
+ : thickness;
219
+ const startAngle = -sweepAngle / 2 + stepSweep * index;
220
+ const endAngle = startAngle + stepSweep;
221
+ const stepY = isSpiral
222
+ ? stepHeight * index
223
+ : fillToFloor
224
+ ? 0
225
+ : Math.max(currentHeight - thickness, 0);
226
+ const midAngle = startAngle + stepSweep / 2;
227
+ return (_jsxs("group", { "position-y": stepY, children: [isSpiral && (stair.showStepSupports ?? true) ? (_jsx("mesh", { castShadow: true, material: sideMaterial, name: "stair-side", position: [
228
+ Math.cos(midAngle) *
229
+ (spiralColumnRadius +
230
+ Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 -
231
+ 0.02),
232
+ Math.max(thickness * 0.55, 0.025) / 2,
233
+ Math.sin(midAngle) *
234
+ (spiralColumnRadius +
235
+ Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 -
236
+ 0.02),
237
+ ], receiveShadow: true, "rotation-y": -midAngle, children: _jsx("boxGeometry", { args: [
238
+ Math.max(0.04, innerRadius - spiralColumnRadius + 0.04),
239
+ Math.max(thickness * 0.55, 0.025),
240
+ Math.max(0.04, Math.min(0.12, Math.max(thickness * 0.55, 0.025) * 1.5)),
241
+ ] }) })) : null, _jsx(CurvedStepMesh, { endAngle: endAngle, innerRadius: innerRadius, material: bodyMaterials, outerRadius: outerRadius, positionY: 0, startAngle: startAngle, stepHeight: actualStepHeight, thickness: thickness })] }, `${stair.id}-${isSpiral ? 'spiral' : 'curved'}-step-${index}`));
242
+ }), isSpiral && (stair.topLandingMode ?? 'none') === 'integrated' ? (_jsx(CurvedStepMesh, { endAngle: sweepAngle / 2 + spiralLandingSweep, innerRadius: innerRadius, material: bodyMaterials, outerRadius: outerRadius, positionY: spiralLastStepTop, startAngle: sweepAngle / 2, stepHeight: spiralLandingThickness, thickness: spiralLandingThickness })) : null] }));
243
+ }
244
+ function CurvedStepMesh({ innerRadius, outerRadius, startAngle, endAngle, stepHeight, thickness, positionY, material, }) {
245
+ const geometry = useMemo(() => buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle, Math.max(stepHeight, thickness)), [endAngle, innerRadius, outerRadius, startAngle, stepHeight, thickness]);
246
+ return (_jsx("mesh", { castShadow: true, geometry: geometry, material: material, "position-y": positionY, receiveShadow: true }));
247
+ }
248
+ function buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle, height) {
249
+ const clampedHeight = Math.max(height, 0.02);
250
+ const y0 = 0;
251
+ const y1 = clampedHeight;
252
+ const sweepAngle = endAngle - startAngle;
253
+ const sweepDirection = Math.sign(sweepAngle) || 1;
254
+ const segmentCount = Math.max(4, Math.min(24, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 18) + Math.max(0, (outerRadius - innerRadius) * 3))));
255
+ const positions = [];
256
+ const normals = [];
257
+ const uvs = [];
258
+ const triangleMaterialIndices = [];
259
+ const pointOnArc = (radius, angle, y) => new THREE.Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius);
260
+ const pushUv = (point, normal, materialIndex) => {
261
+ if (materialIndex === STAIR_TREAD_MATERIAL_INDEX) {
262
+ const angle = Math.atan2(point.z, point.x);
263
+ const arcOffset = (angle - startAngle) * Math.max((innerRadius + outerRadius) * 0.5, 0.01);
264
+ uvs.push(arcOffset, Math.sqrt(point.x * point.x + point.z * point.z) - innerRadius);
265
+ return;
266
+ }
267
+ const absX = Math.abs(normal.x);
268
+ const absY = Math.abs(normal.y);
269
+ const absZ = Math.abs(normal.z);
270
+ if (absY >= absX && absY >= absZ) {
271
+ uvs.push(point.x, point.z);
272
+ }
273
+ else if (absX >= absZ) {
274
+ uvs.push(point.z, point.y);
275
+ }
276
+ else {
277
+ uvs.push(point.x, point.y);
278
+ }
279
+ };
280
+ const pushTriangle = (a, b, c, normal, materialIndex) => {
281
+ const edgeAB = b.clone().sub(a);
282
+ const edgeAC = c.clone().sub(a);
283
+ const faceNormal = edgeAB.cross(edgeAC);
284
+ const ordered = faceNormal.dot(normal) >= 0 ? [a, b, c] : [a, c, b];
285
+ for (const point of ordered) {
286
+ positions.push(point.x, point.y, point.z);
287
+ normals.push(normal.x, normal.y, normal.z);
288
+ pushUv(point, normal, materialIndex);
289
+ }
290
+ triangleMaterialIndices.push(materialIndex);
291
+ };
292
+ const pushQuad = (a, b, c, d, normal, materialIndex) => {
293
+ pushTriangle(a, b, c, normal, materialIndex);
294
+ pushTriangle(a, c, d, normal, materialIndex);
295
+ };
296
+ const upNormal = new THREE.Vector3(0, 1, 0);
297
+ const downNormal = new THREE.Vector3(0, -1, 0);
298
+ for (let index = 0; index < segmentCount; index++) {
299
+ const t0 = index / segmentCount;
300
+ const t1 = (index + 1) / segmentCount;
301
+ const segStart = startAngle + sweepAngle * t0;
302
+ const segEnd = startAngle + sweepAngle * t1;
303
+ const midAngle = (segStart + segEnd) / 2;
304
+ const innerStartBottom = pointOnArc(innerRadius, segStart, y0);
305
+ const innerEndBottom = pointOnArc(innerRadius, segEnd, y0);
306
+ const outerStartBottom = pointOnArc(outerRadius, segStart, y0);
307
+ const outerEndBottom = pointOnArc(outerRadius, segEnd, y0);
308
+ const innerStartTop = pointOnArc(innerRadius, segStart, y1);
309
+ const innerEndTop = pointOnArc(innerRadius, segEnd, y1);
310
+ const outerStartTop = pointOnArc(outerRadius, segStart, y1);
311
+ const outerEndTop = pointOnArc(outerRadius, segEnd, y1);
312
+ const outerNormal = new THREE.Vector3(Math.cos(midAngle), 0, Math.sin(midAngle)).normalize();
313
+ const innerNormal = new THREE.Vector3(-Math.cos(midAngle), 0, -Math.sin(midAngle)).normalize();
314
+ pushQuad(innerStartTop, outerStartTop, outerEndTop, innerEndTop, upNormal, STAIR_TREAD_MATERIAL_INDEX);
315
+ pushQuad(innerStartBottom, innerEndBottom, outerEndBottom, outerStartBottom, downNormal, STAIR_SIDE_MATERIAL_INDEX);
316
+ pushQuad(innerStartBottom, innerStartTop, innerEndTop, innerEndBottom, innerNormal, STAIR_SIDE_MATERIAL_INDEX);
317
+ pushQuad(outerStartBottom, outerEndBottom, outerEndTop, outerStartTop, outerNormal, STAIR_SIDE_MATERIAL_INDEX);
318
+ }
319
+ const startInnerBottom = pointOnArc(innerRadius, startAngle, y0);
320
+ const startOuterBottom = pointOnArc(outerRadius, startAngle, y0);
321
+ const startInnerTop = pointOnArc(innerRadius, startAngle, y1);
322
+ const startOuterTop = pointOnArc(outerRadius, startAngle, y1);
323
+ const endInnerBottom = pointOnArc(innerRadius, endAngle, y0);
324
+ const endOuterBottom = pointOnArc(outerRadius, endAngle, y0);
325
+ const endInnerTop = pointOnArc(innerRadius, endAngle, y1);
326
+ const endOuterTop = pointOnArc(outerRadius, endAngle, y1);
327
+ const startNormal = new THREE.Vector3(sweepDirection * Math.sin(startAngle), 0, -sweepDirection * Math.cos(startAngle)).normalize();
328
+ const endNormal = new THREE.Vector3(-sweepDirection * Math.sin(endAngle), 0, sweepDirection * Math.cos(endAngle)).normalize();
329
+ pushQuad(startInnerBottom, startOuterBottom, startOuterTop, startInnerTop, startNormal, STAIR_SIDE_MATERIAL_INDEX);
330
+ pushQuad(endInnerBottom, endInnerTop, endOuterTop, endOuterBottom, endNormal, STAIR_SIDE_MATERIAL_INDEX);
331
+ const geometry = new THREE.BufferGeometry();
332
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
333
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
334
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
335
+ geometry.clearGroups();
336
+ let currentMaterial = triangleMaterialIndices[0];
337
+ let groupStart = 0;
338
+ for (let triangleIndex = 1; triangleIndex < triangleMaterialIndices.length; triangleIndex++) {
339
+ const materialIndex = triangleMaterialIndices[triangleIndex];
340
+ if (materialIndex === currentMaterial)
341
+ continue;
342
+ geometry.addGroup(groupStart * 3, (triangleIndex - groupStart) * 3, currentMaterial);
343
+ groupStart = triangleIndex;
344
+ currentMaterial = materialIndex;
345
+ }
346
+ if (triangleMaterialIndices.length > 0) {
347
+ geometry.addGroup(groupStart * 3, (triangleMaterialIndices.length - groupStart) * 3, currentMaterial ?? STAIR_SIDE_MATERIAL_INDEX);
348
+ }
349
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
350
+ geometry.computeVertexNormals();
351
+ return geometry;
352
+ }
353
+ function buildStairRailPaths(segments, railingMode) {
354
+ if (!segments.length || railingMode === 'none')
355
+ return [];
356
+ const layouts = computeStairRailLayouts(segments);
357
+ const landingInset = 0.08;
358
+ if (railingMode === 'both') {
359
+ const isStraightLineDoubleLandingLayout = layouts.length === 4 &&
360
+ layouts[0]?.segment.segmentType === 'stair' &&
361
+ layouts[1]?.segment.segmentType === 'landing' &&
362
+ layouts[2]?.segment.segmentType === 'stair' &&
363
+ layouts[2]?.segment.attachmentSide === 'front' &&
364
+ layouts[3]?.segment.segmentType === 'landing' &&
365
+ layouts[3]?.segment.attachmentSide === 'front';
366
+ return layouts.map((layout, index) => {
367
+ const previousLayout = index > 0 ? layouts[index - 1] : undefined;
368
+ const nextLayout = layouts[index + 1];
369
+ const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index);
370
+ const hideLandingRailing = layout.segment.segmentType === 'landing' &&
371
+ previousLayout?.segment.segmentType === 'stair' &&
372
+ nextLayout?.segment.segmentType === 'stair';
373
+ const visualTurnSide = isTerminalLandingBeforeStair && nextStairLayout?.segment.attachmentSide
374
+ ? nextStairLayout.segment.attachmentSide
375
+ : nextLayout?.segment.attachmentSide;
376
+ const sideCandidates = isTerminalLandingBeforeStair && layout.segment.segmentType === 'landing'
377
+ ? visualTurnSide === 'left'
378
+ ? ['front', 'right']
379
+ : visualTurnSide === 'right'
380
+ ? ['front', 'left']
381
+ : ['left', 'right']
382
+ : hideLandingRailing
383
+ ? visualTurnSide === 'left'
384
+ ? ['front', 'right']
385
+ : visualTurnSide === 'right'
386
+ ? ['front', 'left']
387
+ : ['left', 'right']
388
+ : layout.segment.segmentType === 'landing'
389
+ ? nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'left'
390
+ ? ['front', 'right']
391
+ : nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'right'
392
+ ? ['front', 'left']
393
+ : visualTurnSide === 'left'
394
+ ? ['right']
395
+ : visualTurnSide === 'right'
396
+ ? ['left']
397
+ : ['left', 'right']
398
+ : ['left', 'right'];
399
+ return {
400
+ layout,
401
+ sidePaths: isStraightLineDoubleLandingLayout && index === 1
402
+ ? ['left', 'right'].map((side) => buildSegmentRailPath(layouts, index, side, landingInset))
403
+ : sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
404
+ connectFromPrevious: index > 0 &&
405
+ !(previousLayout?.segment.segmentType === 'landing' &&
406
+ layout.segment.segmentType === 'landing'),
407
+ };
408
+ });
409
+ }
410
+ const isStraightLineDoubleLandingLayout = layouts.length === 4 &&
411
+ layouts[0]?.segment.segmentType === 'stair' &&
412
+ layouts[1]?.segment.segmentType === 'landing' &&
413
+ layouts[2]?.segment.segmentType === 'stair' &&
414
+ layouts[2]?.segment.attachmentSide === 'front' &&
415
+ layouts[3]?.segment.segmentType === 'landing' &&
416
+ layouts[3]?.segment.attachmentSide === 'front';
417
+ return layouts.map((layout, index) => {
418
+ const previousLayout = index > 0 ? layouts[index - 1] : undefined;
419
+ const nextLayout = layouts[index + 1];
420
+ const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index);
421
+ const isMiddleLandingBetweenFlights = layout.segment.segmentType === 'landing' &&
422
+ previousLayout?.segment.segmentType === 'stair' &&
423
+ nextLayout?.segment.segmentType === 'stair';
424
+ const nextAttachmentSide = nextLayout?.segment.attachmentSide;
425
+ const terminalNextAttachmentSide = nextStairLayout?.segment.attachmentSide;
426
+ const suppressMiddleLandingOnPreferredTurnSide = isMiddleLandingBetweenFlights &&
427
+ nextAttachmentSide != null &&
428
+ nextAttachmentSide !== 'front' &&
429
+ nextAttachmentSide === railingMode;
430
+ const suppressLandingRailing = (layout.segment.segmentType === 'landing' &&
431
+ nextLayout?.segment.segmentType === 'landing' &&
432
+ nextAttachmentSide === railingMode) ||
433
+ suppressMiddleLandingOnPreferredTurnSide;
434
+ const landingContinuesOnPreferredSide = layout.segment.segmentType === 'landing'
435
+ ? nextAttachmentSide == null ||
436
+ nextAttachmentSide === 'front' ||
437
+ nextAttachmentSide === railingMode
438
+ : true;
439
+ const sideCandidates = suppressLandingRailing
440
+ ? []
441
+ : layout.segment.segmentType !== 'landing'
442
+ ? [railingMode]
443
+ : isTerminalLandingBeforeStair
444
+ ? railingMode === 'left'
445
+ ? terminalNextAttachmentSide === 'right'
446
+ ? ['front', 'left']
447
+ : terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
448
+ ? ['left']
449
+ : []
450
+ : railingMode === 'right'
451
+ ? terminalNextAttachmentSide === 'left'
452
+ ? ['front', 'right']
453
+ : terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
454
+ ? ['right']
455
+ : []
456
+ : [railingMode]
457
+ : isStraightLineDoubleLandingLayout
458
+ ? [railingMode]
459
+ : isMiddleLandingBetweenFlights && railingMode === 'left'
460
+ ? nextAttachmentSide === 'right'
461
+ ? ['front', 'left']
462
+ : ['left']
463
+ : isMiddleLandingBetweenFlights && railingMode === 'right'
464
+ ? nextAttachmentSide === 'left'
465
+ ? ['front', 'right']
466
+ : ['right']
467
+ : nextLayout?.segment.segmentType === 'landing' &&
468
+ nextAttachmentSide != null &&
469
+ nextAttachmentSide !== 'front' &&
470
+ nextAttachmentSide !== railingMode
471
+ ? ['front', railingMode]
472
+ : [railingMode];
473
+ return {
474
+ layout,
475
+ sidePaths: sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
476
+ connectFromPrevious: index > 0 &&
477
+ !suppressLandingRailing &&
478
+ sideCandidates.length > 0 &&
479
+ (layout.segment.segmentType === 'landing' ? landingContinuesOnPreferredSide : true),
480
+ };
481
+ });
482
+ }
483
+ function resolveLandingChainNextStair(layouts, index) {
484
+ const layout = layouts[index];
485
+ if (!layout || layout.segment.segmentType !== 'landing') {
486
+ return { isTerminalLandingBeforeStair: false };
487
+ }
488
+ let cursor = index;
489
+ while (cursor + 1 < layouts.length && layouts[cursor + 1]?.segment.segmentType === 'landing') {
490
+ cursor += 1;
491
+ }
492
+ const nextStairLayout = cursor + 1 < layouts.length && layouts[cursor + 1]?.segment.segmentType === 'stair'
493
+ ? layouts[cursor + 1]
494
+ : undefined;
495
+ return {
496
+ nextStairLayout,
497
+ isTerminalLandingBeforeStair: Boolean(nextStairLayout) && cursor === index,
498
+ };
499
+ }
500
+ function computeStairRailLayouts(segments) {
501
+ const transforms = computeSegmentTransforms(segments);
502
+ return segments.map((segment, index) => {
503
+ const transform = transforms[index];
504
+ const [centerOffsetX, centerOffsetZ] = rotateXZ(0, segment.length / 2, transform.rotation);
505
+ return {
506
+ center: [transform.position[0] + centerOffsetX, transform.position[2] + centerOffsetZ],
507
+ elevation: transform.position[1],
508
+ rotation: transform.rotation,
509
+ segment,
510
+ };
511
+ });
512
+ }
513
+ function buildSegmentRailPath(layouts, layoutIndex, side, landingInset) {
514
+ const layout = layouts[layoutIndex];
515
+ const segment = layout.segment;
516
+ const previousLayout = layoutIndex > 0 ? layouts[layoutIndex - 1] : undefined;
517
+ const nextLayout = layoutIndex >= 0 ? layouts[layoutIndex + 1] : undefined;
518
+ const steps = Math.max(1, segment.segmentType === 'landing' ? 1 : segment.stepCount);
519
+ const stepDepth = segment.length / steps;
520
+ const stepHeight = segment.segmentType === 'landing' ? 0 : segment.height / steps;
521
+ const flightSideOffset = side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045;
522
+ const flightStartX = previousLayout?.segment.segmentType === 'landing'
523
+ ? -segment.length / 2 + landingInset
524
+ : -segment.length / 2;
525
+ const flightEndX = nextLayout?.segment.segmentType === 'landing'
526
+ ? segment.length / 2 - landingInset
527
+ : segment.length / 2;
528
+ const landingFrontX = previousLayout?.segment.segmentType === 'stair' &&
529
+ segment.attachmentSide &&
530
+ segment.attachmentSide !== 'front'
531
+ ? -segment.length / 2 + landingInset
532
+ : segment.length / 2 - landingInset;
533
+ if (segment.segmentType === 'landing') {
534
+ const backX = -segment.length / 2 + landingInset;
535
+ const frontX = segment.length / 2 - landingInset;
536
+ const leftZ = segment.width / 2 - landingInset;
537
+ const rightZ = -segment.width / 2 + landingInset;
538
+ return {
539
+ side,
540
+ points: side === 'left'
541
+ ? [
542
+ [backX, 0, leftZ],
543
+ [frontX, 0, leftZ],
544
+ ]
545
+ : side === 'right'
546
+ ? [
547
+ [backX, 0, rightZ],
548
+ [frontX, 0, rightZ],
549
+ ]
550
+ : [
551
+ [landingFrontX, 0, leftZ],
552
+ [landingFrontX, 0, rightZ],
553
+ ],
554
+ };
555
+ }
556
+ return {
557
+ side,
558
+ points: [
559
+ ...(previousLayout?.segment.segmentType === 'landing'
560
+ ? []
561
+ : [[flightStartX, stepHeight > 0 ? stepHeight : 0, flightSideOffset]]),
562
+ ...Array.from({ length: steps }).map((_, index) => [
563
+ -segment.length / 2 + stepDepth * index + stepDepth / 2,
564
+ stepHeight * (index + 1),
565
+ flightSideOffset,
566
+ ]),
567
+ ...(nextLayout?.segment.segmentType === 'landing'
568
+ ? []
569
+ : [[flightEndX, segment.height, flightSideOffset]]),
570
+ ],
571
+ };
572
+ }
573
+ function toWorldRailPoint(layout, point) {
574
+ const [localX, localY, localZ] = point;
575
+ const [offsetX, offsetZ] = rotateXZ(localZ, localX, layout.rotation);
576
+ return [layout.center[0] + offsetX, layout.elevation + localY, layout.center[1] + offsetZ];
577
+ }
578
+ function computeSegmentTransforms(segments) {
579
+ const transforms = [];
580
+ let currentPos = new THREE.Vector3(0, 0, 0);
581
+ let currentRot = 0;
582
+ for (let i = 0; i < segments.length; i++) {
583
+ const segment = segments[i];
584
+ if (i === 0) {
585
+ transforms.push({
586
+ position: [currentPos.x, currentPos.y, currentPos.z],
587
+ rotation: currentRot,
588
+ });
589
+ continue;
590
+ }
591
+ const prev = segments[i - 1];
592
+ const localAttachPos = new THREE.Vector3();
593
+ let rotChange = 0;
594
+ switch (segment.attachmentSide) {
595
+ case 'front':
596
+ localAttachPos.set(0, prev.height, prev.length);
597
+ break;
598
+ case 'left':
599
+ localAttachPos.set(prev.width / 2, prev.height, prev.length / 2);
600
+ rotChange = Math.PI / 2;
601
+ break;
602
+ case 'right':
603
+ localAttachPos.set(-prev.width / 2, prev.height, prev.length / 2);
604
+ rotChange = -Math.PI / 2;
605
+ break;
606
+ }
607
+ localAttachPos.applyAxisAngle(new THREE.Vector3(0, 1, 0), currentRot);
608
+ currentPos = currentPos.clone().add(localAttachPos);
609
+ currentRot += rotChange;
610
+ transforms.push({ position: [currentPos.x, currentPos.y, currentPos.z], rotation: currentRot });
611
+ }
612
+ return transforms;
613
+ }
614
+ function rotateXZ(x, z, angle) {
615
+ const cos = Math.cos(angle);
616
+ const sin = Math.sin(angle);
617
+ return [x * cos + z * sin, -x * sin + z * cos];
618
+ }
619
+ function distance3(a, b) {
620
+ return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
621
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"stair-segment-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/stair-segment/stair-segment-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAyB,MAAM,kBAAkB,CAAA;AAM/E,eAAO,MAAM,oBAAoB,GAAI,UAAU;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,4CA8BxE,CAAA"}
1
+ {"version":3,"file":"stair-segment-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/stair-segment/stair-segment-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,gBAAgB,EAAyB,MAAM,kBAAkB,CAAA;AAM/G,eAAO,MAAM,oBAAoB,GAAI,UAAU;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,4CA4DxE,CAAA"}