@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.
@@ -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,4CAYhE,CAAA"}
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,5 @@
1
+ import { type FenceNode } from '@pascal-app/core';
2
+ export declare const FenceRenderer: ({ node }: {
3
+ node: FenceNode;
4
+ }) => import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=fence-renderer.d.ts.map
@@ -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;AAkBzD,eAAO,MAAM,YAAY,GAAI,YAAY;IAAE,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,mDAyBjE,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,EAAE,KAAK,SAAS,EAAyB,MAAM,kBAAkB,CAAA;AAOxE,eAAO,MAAM,aAAa,GAAI,UAAU;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,4CAoC1D,CAAA"}
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":"AAaA,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;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,YAoQzB,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;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 count when project changes
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
- }, [renderer, scene, camera, hoverHighlightMode, isInitialized, zoneLayers]);
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
- setTimeout(requestPipelineRebuild, RETRY_DELAY_MS);
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":"AAwQA,eAAO,MAAM,gBAAgB,+CAsE5B,CAAA"}
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';
@@ -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,YAavB,CAAA"}
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,YAatB,CAAA"}
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":"AAoMA,eAAO,MAAM,UAAU,YAgFtB,CAAA"}
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.4.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.4",
25
+ "@pascal-app/core": "^0.5.1",
26
26
  "@react-three/drei": "^10",
27
27
  "@react-three/fiber": "^9",
28
28
  "react": "^18 || ^19",