@pascal-app/viewer 0.5.1 → 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 (53) hide show
  1. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/ceiling/ceiling-renderer.js +4 -3
  3. package/dist/components/renderers/door/door-renderer.d.ts.map +1 -1
  4. package/dist/components/renderers/door/door-renderer.js +5 -2
  5. package/dist/components/renderers/fence/fence-renderer.d.ts.map +1 -1
  6. package/dist/components/renderers/fence/fence-renderer.js +16 -2
  7. package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -1
  8. package/dist/components/renderers/roof/roof-renderer.js +31 -9
  9. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +1 -1
  10. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +42 -8
  11. package/dist/components/renderers/site/site-renderer.d.ts.map +1 -1
  12. package/dist/components/renderers/site/site-renderer.js +7 -1
  13. package/dist/components/renderers/slab/slab-renderer.d.ts.map +1 -1
  14. package/dist/components/renderers/slab/slab-renderer.js +37 -8
  15. package/dist/components/renderers/stair/stair-renderer.d.ts.map +1 -1
  16. package/dist/components/renderers/stair/stair-renderer.js +172 -39
  17. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +1 -1
  18. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +37 -8
  19. package/dist/components/renderers/wall/wall-renderer.d.ts.map +1 -1
  20. package/dist/components/renderers/wall/wall-renderer.js +4 -9
  21. package/dist/components/renderers/window/window-renderer.d.ts.map +1 -1
  22. package/dist/components/renderers/window/window-renderer.js +5 -2
  23. package/dist/components/viewer/frame-limiter.d.ts +6 -0
  24. package/dist/components/viewer/frame-limiter.d.ts.map +1 -0
  25. package/dist/components/viewer/frame-limiter.js +31 -0
  26. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  27. package/dist/components/viewer/ground-occluder.js +5 -1
  28. package/dist/components/viewer/index.d.ts.map +1 -1
  29. package/dist/components/viewer/index.js +11 -11
  30. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  31. package/dist/components/viewer/post-processing.js +33 -36
  32. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  33. package/dist/components/viewer/selection-manager.js +12 -1
  34. package/dist/hooks/use-node-events.d.ts.map +1 -1
  35. package/dist/hooks/use-node-events.js +2 -0
  36. package/dist/lib/materials.d.ts +4 -1
  37. package/dist/lib/materials.d.ts.map +1 -1
  38. package/dist/lib/materials.js +178 -2
  39. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  40. package/dist/lib/merged-outline-node.js +11 -4
  41. package/dist/store/use-viewer.d.ts +2 -2
  42. package/dist/systems/roof/roof-materials.d.ts +5 -0
  43. package/dist/systems/roof/roof-materials.d.ts.map +1 -0
  44. package/dist/systems/roof/roof-materials.js +29 -0
  45. package/dist/systems/stair/stair-materials.d.ts +7 -0
  46. package/dist/systems/stair/stair-materials.d.ts.map +1 -0
  47. package/dist/systems/stair/stair-materials.js +33 -0
  48. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  49. package/dist/systems/wall/wall-cutout.js +4 -123
  50. package/dist/systems/wall/wall-materials.d.ts +16 -0
  51. package/dist/systems/wall/wall-materials.d.ts.map +1 -0
  52. package/dist/systems/wall/wall-materials.js +148 -0
  53. package/package.json +4 -4
@@ -1,24 +1,67 @@
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
4
  import * as THREE from 'three';
5
5
  import { useNodeEvents } from '../../../hooks/use-node-events';
6
- 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';
7
8
  import { NodeRenderer } from '../node-renderer';
8
9
  export const StairRenderer = ({ node }) => {
9
10
  const ref = useRef(null);
11
+ const isSegmentBasedStair = node.stairType === 'straight';
10
12
  useRegistry(node.id, 'stair', ref);
11
13
  useLayoutEffect(() => {
12
14
  useScene.getState().markDirty(node.id);
13
15
  }, [node.id]);
14
16
  const handlers = useNodeEvents(node, 'stair');
15
17
  const material = useMemo(() => {
18
+ const presetMaterial = createMaterialFromPresetRef(node.materialPreset);
19
+ if (presetMaterial)
20
+ return presetMaterial;
16
21
  const mat = node.material;
17
22
  if (!mat)
18
23
  return DEFAULT_STAIR_MATERIAL;
19
24
  return createMaterial(mat);
20
- }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
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))) })] }));
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] }));
22
65
  };
23
66
  function StairRailings({ stair, material }) {
24
67
  const nodes = useScene((state) => state.nodes);
@@ -60,7 +103,7 @@ function StairRailings({ stair, material }) {
60
103
  Math.sin(angle) * radius,
61
104
  ];
62
105
  });
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) => {
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) => {
64
107
  const nextPoint = sidePoints[pointIndex + 1];
65
108
  if (!nextPoint)
66
109
  return null;
@@ -71,7 +114,11 @@ function StairRailings({ stair, material }) {
71
114
  if (railPaths.length === 0) {
72
115
  return null;
73
116
  }
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) => {
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) => {
75
122
  const nextPoint = sidePath.points[pointIndex + 1];
76
123
  if (!nextPoint)
77
124
  return null;
@@ -94,23 +141,43 @@ function StairRailings({ stair, material }) {
94
141
  const lastPoint = entry.points[entry.points.length - 1];
95
142
  return {
96
143
  entry,
97
- distance: lastPoint ? distance3(toWorldRailPoint(previousPath.layout, lastPoint), currentWorldPoint) : Number.POSITIVE_INFINITY,
144
+ distance: lastPoint
145
+ ? distance3(toWorldRailPoint(previousPath.layout, lastPoint), currentWorldPoint)
146
+ : Number.POSITIVE_INFINITY,
98
147
  };
99
148
  })
100
149
  .sort((left, right) => left.distance - right.distance)[0]?.entry;
101
- const previousPoint = previousSidePath && previousSidePath.points.length
150
+ const previousPoint = previousSidePath?.points.length
102
151
  ? previousSidePath.points[previousSidePath.points.length - 1]
103
152
  : null;
104
153
  if (!(previousPoint && currentPoint)) {
105
154
  return null;
106
155
  }
107
156
  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}`));
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}`));
109
174
  });
110
175
  })] }));
111
176
  }
112
177
  const BALUSTER_GEOMETRY = new THREE.CylinderGeometry(1, 1, 1, 8);
113
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;
114
181
  function RailSegment({ start, end, radius, material, }) {
115
182
  const startVector = useMemo(() => new THREE.Vector3(...start), [start]);
116
183
  const endVector = useMemo(() => new THREE.Vector3(...end), [end]);
@@ -118,9 +185,10 @@ function RailSegment({ start, end, radius, material, }) {
118
185
  const length = Math.max(direction.length(), 0.01);
119
186
  const quaternion = useMemo(() => new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize()), [direction]);
120
187
  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)] }));
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)] }));
122
189
  }
123
- function CurvedStairBody({ stair, material }) {
190
+ function CurvedStairBody({ stair, bodyMaterials, }) {
191
+ const sideMaterial = bodyMaterials[1];
124
192
  const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
125
193
  const totalRise = Math.max(stair.totalRise ?? 2.5, 0.1);
126
194
  const stepHeight = totalRise / stepCount;
@@ -135,34 +203,47 @@ function CurvedStairBody({ stair, material }) {
135
203
  const spiralColumnHeight = totalRise + thickness;
136
204
  const spiralLandingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max((stair.width ?? 1) * 0.9, 0.8));
137
205
  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)
206
+ ? Math.min(Math.PI * 0.75, spiralLandingDepth / Math.max(innerRadius + (stair.width ?? 1) / 2, 0.1)) * Math.sign(sweepAngle || 1)
140
207
  : 0;
141
208
  const spiralLastStepTop = stepHeight * Math.max(stepCount - 1, 0) + thickness;
142
209
  const spiralLandingThickness = isSpiral && (stair.topLandingMode ?? 'none') === 'integrated'
143
210
  ? Math.max(0.02, totalRise - spiralLastStepTop)
144
211
  : 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) => {
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) => {
146
213
  const currentHeight = stepHeight * (index + 1);
147
- const actualStepHeight = isSpiral ? thickness : fillToFloor ? Math.max(currentHeight, thickness) : thickness;
214
+ const actualStepHeight = isSpiral
215
+ ? thickness
216
+ : fillToFloor
217
+ ? Math.max(currentHeight, thickness)
218
+ : thickness;
148
219
  const startAngle = -sweepAngle / 2 + stepSweep * index;
149
220
  const endAngle = startAngle + stepSweep;
150
- const stepY = isSpiral ? stepHeight * index : fillToFloor ? 0 : Math.max(currentHeight - thickness, 0);
221
+ const stepY = isSpiral
222
+ ? stepHeight * index
223
+ : fillToFloor
224
+ ? 0
225
+ : Math.max(currentHeight - thickness, 0);
151
226
  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),
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),
154
232
  Math.max(thickness * 0.55, 0.025) / 2,
155
- Math.sin(midAngle) * (spiralColumnRadius + Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 - 0.02),
233
+ Math.sin(midAngle) *
234
+ (spiralColumnRadius +
235
+ Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 -
236
+ 0.02),
156
237
  ], receiveShadow: true, "rotation-y": -midAngle, children: _jsx("boxGeometry", { args: [
157
238
  Math.max(0.04, innerRadius - spiralColumnRadius + 0.04),
158
239
  Math.max(thickness * 0.55, 0.025),
159
240
  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] }));
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] }));
162
243
  }
163
244
  function CurvedStepMesh({ innerRadius, outerRadius, startAngle, endAngle, stepHeight, thickness, positionY, material, }) {
164
245
  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 });
246
+ return (_jsx("mesh", { castShadow: true, geometry: geometry, material: material, "position-y": positionY, receiveShadow: true }));
166
247
  }
167
248
  function buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle, height) {
168
249
  const clampedHeight = Math.max(height, 0.02);
@@ -173,8 +254,30 @@ function buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle,
173
254
  const segmentCount = Math.max(4, Math.min(24, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 18) + Math.max(0, (outerRadius - innerRadius) * 3))));
174
255
  const positions = [];
175
256
  const normals = [];
257
+ const uvs = [];
258
+ const triangleMaterialIndices = [];
176
259
  const pointOnArc = (radius, angle, y) => new THREE.Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius);
177
- const pushTriangle = (a, b, c, normal) => {
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) => {
178
281
  const edgeAB = b.clone().sub(a);
179
282
  const edgeAC = c.clone().sub(a);
180
283
  const faceNormal = edgeAB.cross(edgeAC);
@@ -182,11 +285,13 @@ function buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle,
182
285
  for (const point of ordered) {
183
286
  positions.push(point.x, point.y, point.z);
184
287
  normals.push(normal.x, normal.y, normal.z);
288
+ pushUv(point, normal, materialIndex);
185
289
  }
290
+ triangleMaterialIndices.push(materialIndex);
186
291
  };
187
- const pushQuad = (a, b, c, d, normal) => {
188
- pushTriangle(a, b, c, normal);
189
- pushTriangle(a, c, d, normal);
292
+ const pushQuad = (a, b, c, d, normal, materialIndex) => {
293
+ pushTriangle(a, b, c, normal, materialIndex);
294
+ pushTriangle(a, c, d, normal, materialIndex);
190
295
  };
191
296
  const upNormal = new THREE.Vector3(0, 1, 0);
192
297
  const downNormal = new THREE.Vector3(0, -1, 0);
@@ -206,10 +311,10 @@ function buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle,
206
311
  const outerEndTop = pointOnArc(outerRadius, segEnd, y1);
207
312
  const outerNormal = new THREE.Vector3(Math.cos(midAngle), 0, Math.sin(midAngle)).normalize();
208
313
  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);
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);
213
318
  }
214
319
  const startInnerBottom = pointOnArc(innerRadius, startAngle, y0);
215
320
  const startOuterBottom = pointOnArc(outerRadius, startAngle, y0);
@@ -221,11 +326,27 @@ function buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle,
221
326
  const endOuterTop = pointOnArc(outerRadius, endAngle, y1);
222
327
  const startNormal = new THREE.Vector3(sweepDirection * Math.sin(startAngle), 0, -sweepDirection * Math.cos(startAngle)).normalize();
223
328
  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);
329
+ pushQuad(startInnerBottom, startOuterBottom, startOuterTop, startInnerTop, startNormal, STAIR_SIDE_MATERIAL_INDEX);
330
+ pushQuad(endInnerBottom, endInnerTop, endOuterTop, endOuterBottom, endNormal, STAIR_SIDE_MATERIAL_INDEX);
226
331
  const geometry = new THREE.BufferGeometry();
227
332
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
228
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));
229
350
  geometry.computeVertexNormals();
230
351
  return geometry;
231
352
  }
@@ -281,7 +402,8 @@ function buildStairRailPaths(segments, railingMode) {
281
402
  ? ['left', 'right'].map((side) => buildSegmentRailPath(layouts, index, side, landingInset))
282
403
  : sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
283
404
  connectFromPrevious: index > 0 &&
284
- !(previousLayout?.segment.segmentType === 'landing' && layout.segment.segmentType === 'landing'),
405
+ !(previousLayout?.segment.segmentType === 'landing' &&
406
+ layout.segment.segmentType === 'landing'),
285
407
  };
286
408
  });
287
409
  }
@@ -310,7 +432,9 @@ function buildStairRailPaths(segments, railingMode) {
310
432
  nextAttachmentSide === railingMode) ||
311
433
  suppressMiddleLandingOnPreferredTurnSide;
312
434
  const landingContinuesOnPreferredSide = layout.segment.segmentType === 'landing'
313
- ? nextAttachmentSide == null || nextAttachmentSide === 'front' || nextAttachmentSide === railingMode
435
+ ? nextAttachmentSide == null ||
436
+ nextAttachmentSide === 'front' ||
437
+ nextAttachmentSide === railingMode
314
438
  : true;
315
439
  const sideCandidates = suppressLandingRailing
316
440
  ? []
@@ -395,8 +519,12 @@ function buildSegmentRailPath(layouts, layoutIndex, side, landingInset) {
395
519
  const stepDepth = segment.length / steps;
396
520
  const stepHeight = segment.segmentType === 'landing' ? 0 : segment.height / steps;
397
521
  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;
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;
400
528
  const landingFrontX = previousLayout?.segment.segmentType === 'stair' &&
401
529
  segment.attachmentSide &&
402
530
  segment.attachmentSide !== 'front'
@@ -428,7 +556,9 @@ function buildSegmentRailPath(layouts, layoutIndex, side, landingInset) {
428
556
  return {
429
557
  side,
430
558
  points: [
431
- ...(previousLayout?.segment.segmentType === 'landing' ? [] : [[flightStartX, stepHeight > 0 ? stepHeight : 0, flightSideOffset]]),
559
+ ...(previousLayout?.segment.segmentType === 'landing'
560
+ ? []
561
+ : [[flightStartX, stepHeight > 0 ? stepHeight : 0, flightSideOffset]]),
432
562
  ...Array.from({ length: steps }).map((_, index) => [
433
563
  -segment.length / 2 + stepDepth * index + stepDepth / 2,
434
564
  stepHeight * (index + 1),
@@ -452,7 +582,10 @@ function computeSegmentTransforms(segments) {
452
582
  for (let i = 0; i < segments.length; i++) {
453
583
  const segment = segments[i];
454
584
  if (i === 0) {
455
- transforms.push({ position: [currentPos.x, currentPos.y, currentPos.z], rotation: currentRot });
585
+ transforms.push({
586
+ position: [currentPos.x, currentPos.y, currentPos.z],
587
+ rotation: currentRot,
588
+ });
456
589
  continue;
457
590
  }
458
591
  const prev = segments[i - 1];
@@ -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"}
@@ -1,20 +1,49 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRegistry, useScene } from '@pascal-app/core';
3
- import { useLayoutEffect, useMemo, useRef } from 'react';
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 { getStraightStairSegmentBodyMaterials } from '../../../systems/stair/stair-materials';
6
7
  export const StairSegmentRenderer = ({ node }) => {
7
8
  const ref = useRef(null);
9
+ const nodes = useScene((state) => state.nodes);
8
10
  useRegistry(node.id, 'stair-segment', ref);
9
11
  useLayoutEffect(() => {
10
12
  useScene.getState().markDirty(node.id);
11
13
  }, [node.id]);
12
14
  const handlers = useNodeEvents(node, 'stair-segment');
15
+ const parentNode = node.parentId ? nodes[node.parentId] : undefined;
13
16
  const material = useMemo(() => {
14
- const mat = node.material;
15
- if (!mat)
16
- return DEFAULT_STAIR_MATERIAL;
17
- return createMaterial(mat);
18
- }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
19
- return (_jsx("mesh", { material: material, position: node.position, ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }));
17
+ return getStraightStairSegmentBodyMaterials(node, parentNode);
18
+ }, [
19
+ node.materialPreset,
20
+ node.material,
21
+ node.material?.preset,
22
+ node.material?.properties,
23
+ node.material?.texture,
24
+ parentNode?.materialPreset,
25
+ parentNode?.material,
26
+ parentNode?.material?.preset,
27
+ parentNode?.material?.properties,
28
+ parentNode?.material?.texture,
29
+ parentNode?.railingMaterialPreset,
30
+ parentNode?.railingMaterial,
31
+ parentNode?.sideMaterialPreset,
32
+ parentNode?.sideMaterial,
33
+ parentNode?.treadMaterialPreset,
34
+ parentNode?.treadMaterial,
35
+ ]);
36
+ const placeholderGeometry = useMemo(() => {
37
+ const geometry = new THREE.BufferGeometry();
38
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
39
+ geometry.addGroup(0, 0, 0);
40
+ geometry.addGroup(0, 0, 1);
41
+ return geometry;
42
+ }, []);
43
+ useEffect(() => {
44
+ return () => {
45
+ placeholderGeometry.dispose();
46
+ };
47
+ }, [placeholderGeometry]);
48
+ return (_jsx("mesh", { geometry: placeholderGeometry, material: material, position: node.position, ref: ref, "rotation-y": node.rotation, visible: node.visible, ...handlers }));
20
49
  };
@@ -1 +1 @@
1
- {"version":3,"file":"wall-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/wall/wall-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAOvE,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CA6BxD,CAAA"}
1
+ {"version":3,"file":"wall-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/wall/wall-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAOvE,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,4CAwBxD,CAAA"}
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRegistry, useScene } from '@pascal-app/core';
3
- import { useLayoutEffect, useMemo, useRef } from 'react';
3
+ import { useLayoutEffect, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
- import { createMaterial, DEFAULT_WALL_MATERIAL } from '../../../lib/materials';
5
+ import { getVisibleWallMaterials } from '../../../systems/wall/wall-materials';
6
6
  import { NodeRenderer } from '../node-renderer';
7
7
  export const WallRenderer = ({ node }) => {
8
8
  const ref = useRef(null);
@@ -11,11 +11,6 @@ export const WallRenderer = ({ node }) => {
11
11
  useScene.getState().markDirty(node.id);
12
12
  }, [node.id]);
13
13
  const handlers = useNodeEvents(node, 'wall');
14
- const material = useMemo(() => {
15
- const mat = node.material;
16
- if (!mat)
17
- return DEFAULT_WALL_MATERIAL;
18
- return createMaterial(mat);
19
- }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]);
20
- return (_jsxs("mesh", { castShadow: true, material: material, receiveShadow: true, ref: ref, visible: node.visible, children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("mesh", { name: "collision-mesh", visible: false, ...handlers, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId)))] }));
14
+ const material = getVisibleWallMaterials(node);
15
+ return (_jsxs("mesh", { castShadow: true, material: material, receiveShadow: true, ref: ref, visible: node.visible, children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("mesh", { name: "collision-mesh", visible: false, ...handlers, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, `${node.id}:${childId}`)))] }));
21
16
  };
@@ -1 +1 @@
1
- {"version":3,"file":"window-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/window/window-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAM/D,eAAO,MAAM,cAAc,GAAI,UAAU;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,4CA2B5D,CAAA"}
1
+ {"version":3,"file":"window-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/window/window-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAMzE,eAAO,MAAM,cAAc,GAAI,UAAU;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,4CA8B5D,CAAA"}
@@ -1,11 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useRegistry } from '@pascal-app/core';
3
- import { useMemo, useRef } from 'react';
2
+ import { useRegistry, useScene } from '@pascal-app/core';
3
+ import { useLayoutEffect, useMemo, useRef } from 'react';
4
4
  import { useNodeEvents } from '../../../hooks/use-node-events';
5
5
  import { createMaterial, DEFAULT_WINDOW_MATERIAL } from '../../../lib/materials';
6
6
  export const WindowRenderer = ({ node }) => {
7
7
  const ref = useRef(null);
8
8
  useRegistry(node.id, 'window', ref);
9
+ useLayoutEffect(() => {
10
+ useScene.getState().markDirty(node.id);
11
+ }, [node.id]);
9
12
  const handlers = useNodeEvents(node, 'window');
10
13
  const isTransient = !!node.metadata?.isTransient;
11
14
  const material = useMemo(() => {
@@ -0,0 +1,6 @@
1
+ type FrameLimiterProps = {
2
+ fps?: number;
3
+ };
4
+ declare const FrameLimiter: React.FC<FrameLimiterProps>;
5
+ export default FrameLimiter;
6
+ //# sourceMappingURL=frame-limiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-limiter.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/frame-limiter.tsx"],"names":[],"mappings":"AAGA,KAAK,iBAAiB,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,QAAA,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAgC7C,CAAA;AAED,eAAe,YAAY,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { useThree } from '@react-three/fiber';
2
+ import { useLayoutEffect } from 'react';
3
+ const FrameLimiter = ({ fps = 50 }) => {
4
+ const { advance, set, frameloop: initFrameloop } = useThree();
5
+ useLayoutEffect(() => {
6
+ let elapsed = 0;
7
+ let then = 0;
8
+ let i = 0;
9
+ let raf = null;
10
+ const interval = 1000 / fps;
11
+ function tick(t) {
12
+ raf = requestAnimationFrame(tick);
13
+ elapsed = t - then;
14
+ if (elapsed > interval) {
15
+ advance(i);
16
+ i += elapsed / 1000 - (elapsed % interval) / 1000;
17
+ then = t - (elapsed % interval);
18
+ }
19
+ }
20
+ set({ frameloop: 'never' });
21
+ raf = requestAnimationFrame(tick);
22
+ return () => {
23
+ if (raf) {
24
+ cancelAnimationFrame(raf);
25
+ }
26
+ set({ frameloop: initFrameloop });
27
+ };
28
+ }, [fps, advance, set, initFrameloop]);
29
+ return null;
30
+ };
31
+ export default FrameLimiter;
@@ -1 +1 @@
1
- {"version":3,"file":"ground-occluder.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/ground-occluder.tsx"],"names":[],"mappings":"AAMA,eAAO,MAAM,cAAc,+CA+F1B,CAAA"}
1
+ {"version":3,"file":"ground-occluder.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/ground-occluder.tsx"],"names":[],"mappings":"AAMA,eAAO,MAAM,cAAc,+CAuG1B,CAAA"}
@@ -31,7 +31,11 @@ export const GroundOccluder = () => {
31
31
  // reveal their footprint on the level-zero ground material.
32
32
  const polygons = [];
33
33
  Object.values(nodes).forEach((node) => {
34
- if (!(node.type === 'slab' && node.visible && node.polygon.length >= 3)) {
34
+ if (!(node.type === 'slab' &&
35
+ node.visible &&
36
+ node.polygon.length >= 3 &&
37
+ // Only recessed slabs should punch through the ground plane.
38
+ (node.elevation ?? 0.05) < 0)) {
35
39
  return;
36
40
  }
37
41
  if (Number.isFinite(lowestLevelIndex)) {
@@ -1 +1 @@
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
+ {"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;AA2CrC,OAAO,QAAQ,oBAAoB,CAAC;IAClC,UAAU,aAAc,SAAQ,kBAAkB,CAAC,OAAO,KAAK,CAAC;KAAG;CACpE;AAwCD,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,CAsEjC,CAAA;AASD,eAAe,MAAM,CAAA"}
@@ -13,6 +13,7 @@ import { ScanSystem } from '../../systems/scan/scan-system';
13
13
  import { WallCutout } from '../../systems/wall/wall-cutout';
14
14
  import { ZoneSystem } from '../../systems/zone/zone-system';
15
15
  import { SceneRenderer } from '../renderers/scene-renderer';
16
+ import FrameLimiter from './frame-limiter';
16
17
  import { Lights } from './lights';
17
18
  import { PerfMonitor } from './perf-monitor';
18
19
  import PostProcessing from './post-processing';
@@ -40,13 +41,6 @@ function AnimatedBackground({ isDark }) {
40
41
  return null;
41
42
  }
42
43
  extend(THREE);
43
- /**
44
- * Monitors the WebGPU device for loss events and logs them.
45
- * WebGPU device loss can happen when:
46
- * - Tab is backgrounded and OS reclaims GPU
47
- * - Driver crash or GPU reset
48
- * - Browser security policy kills the context
49
- */
50
44
  function GPUDeviceWatcher() {
51
45
  const gl = useThree((s) => s.gl);
52
46
  useEffect(() => {
@@ -55,7 +49,7 @@ function GPUDeviceWatcher() {
55
49
  if (!device)
56
50
  return;
57
51
  device.lost.then((info) => {
58
- console.error(`[viewer] WebGPU device lost: reason="${info.reason}", message="${info.message}". ` +
52
+ console.error(`[viewer] WebGPU device lost: reason="${info.reason ?? 'unknown'}", message="${info.message ?? ''}". ` +
59
53
  'The page must be reloaded to recover the GPU context.');
60
54
  });
61
55
  }, [gl]);
@@ -63,18 +57,24 @@ function GPUDeviceWatcher() {
63
57
  }
64
58
  const Viewer = ({ children, selectionManager = 'default', perf = false, }) => {
65
59
  const theme = useViewer((state) => state.theme);
66
- return (_jsxs(Canvas, { camera: { position: [50, 50, 50], fov: 50 }, className: `transition-colors duration-700 ${theme === 'dark' ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`, dpr: [1, 1.5], gl: (props) => {
60
+ return (_jsxs(Canvas, { camera: { position: [50, 50, 50], fov: 50 }, className: `transition-colors duration-700 ${theme === 'dark' ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`, dpr: [1, 1.5], frameloop: "never", gl: async (props) => {
67
61
  const renderer = new THREE.WebGPURenderer(props);
68
62
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
69
63
  renderer.toneMappingExposure = 0.9;
70
- // renderer.init() // Only use when using <DebugRenderer />
64
+ // Awaiting init() is required when the browser falls back to the
65
+ // WebGL2 backend (Safari without the WebGPU flag, older Chrome on
66
+ // machines without a WebGPU device). In native WebGPU mode the
67
+ // init resolves almost instantly. Without this await, the first
68
+ // render throws "Renderer: .render() called before the backend is
69
+ // initialized" from the post-processing fallback path.
70
+ await renderer.init();
71
71
  return renderer;
72
72
  }, resize: {
73
73
  debounce: 100,
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(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] }));
77
+ }, children: [_jsx(FrameLimiter, { fps: 50 }), _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,YAmTzB,CAAA;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA4BA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AAuBD,QAAA,MAAM,oBAAoB,YAoSzB,CAAA;AAED,eAAe,oBAAoB,CAAA"}