@pascal-app/viewer 0.7.0 → 0.9.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.
Files changed (216) hide show
  1. package/dist/components/renderers/node-renderer.d.ts +1 -1
  2. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.js +38 -22
  4. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  5. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  6. package/dist/components/renderers/parametric-node-renderer.js +34 -0
  7. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  8. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  9. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  10. package/dist/components/viewer/frame-limiter.js +5 -1
  11. package/dist/components/viewer/ground-occluder.d.ts +1 -1
  12. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  13. package/dist/components/viewer/ground-occluder.js +12 -24
  14. package/dist/components/viewer/index.d.ts +38 -1
  15. package/dist/components/viewer/index.d.ts.map +1 -1
  16. package/dist/components/viewer/index.js +183 -60
  17. package/dist/components/viewer/lights.d.ts +1 -1
  18. package/dist/components/viewer/lights.d.ts.map +1 -1
  19. package/dist/components/viewer/lights.js +170 -43
  20. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  21. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  22. package/dist/components/viewer/perf-monitor.js +99 -11
  23. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  24. package/dist/components/viewer/post-processing.js +217 -61
  25. package/dist/components/viewer/registered-systems.d.ts +12 -0
  26. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  27. package/dist/components/viewer/registered-systems.js +47 -0
  28. package/dist/components/viewer/scene-bvh.d.ts +17 -0
  29. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  30. package/dist/components/viewer/scene-bvh.js +94 -0
  31. package/dist/components/viewer/selection-manager.d.ts +1 -1
  32. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  33. package/dist/components/viewer/selection-manager.js +20 -5
  34. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  35. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  36. package/dist/components/viewer/walkthrough-controls.d.ts +1 -1
  37. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  38. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  39. package/dist/hooks/use-node-events.d.ts +5 -73
  40. package/dist/hooks/use-node-events.d.ts.map +1 -1
  41. package/dist/hooks/use-node-events.js +19 -7
  42. package/dist/index.d.ts +36 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +67 -3
  45. package/dist/lib/csg-utils.d.ts +16 -0
  46. package/dist/lib/csg-utils.d.ts.map +1 -0
  47. package/dist/lib/csg-utils.js +33 -0
  48. package/dist/lib/drawable-geometry.d.ts +21 -0
  49. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  50. package/dist/lib/drawable-geometry.js +22 -0
  51. package/dist/lib/edge-style.d.ts +3 -0
  52. package/dist/lib/edge-style.d.ts.map +1 -0
  53. package/dist/lib/edge-style.js +11 -0
  54. package/dist/lib/gpu-perf.d.ts +4 -0
  55. package/dist/lib/gpu-perf.d.ts.map +1 -0
  56. package/dist/lib/gpu-perf.js +23 -0
  57. package/dist/lib/ink-edges.d.ts +9 -0
  58. package/dist/lib/ink-edges.d.ts.map +1 -0
  59. package/dist/lib/ink-edges.js +43 -0
  60. package/dist/lib/isolation.d.ts +39 -0
  61. package/dist/lib/isolation.d.ts.map +1 -0
  62. package/dist/lib/isolation.js +103 -0
  63. package/dist/lib/layers.d.ts +18 -0
  64. package/dist/lib/layers.d.ts.map +1 -1
  65. package/dist/lib/layers.js +18 -0
  66. package/dist/lib/materials.d.ts +25 -15
  67. package/dist/lib/materials.d.ts.map +1 -1
  68. package/dist/lib/materials.js +277 -63
  69. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  70. package/dist/lib/merged-outline-node.js +11 -11
  71. package/dist/lib/polygon-union.d.ts +4 -0
  72. package/dist/lib/polygon-union.d.ts.map +1 -0
  73. package/dist/lib/polygon-union.js +289 -0
  74. package/dist/lib/polygon-union.test.d.ts +2 -0
  75. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  76. package/dist/lib/polygon-union.test.js +105 -0
  77. package/dist/lib/scene-themes.d.ts +29 -0
  78. package/dist/lib/scene-themes.d.ts.map +1 -0
  79. package/dist/lib/scene-themes.js +193 -0
  80. package/dist/store/use-viewer.d.ts +42 -4
  81. package/dist/store/use-viewer.d.ts.map +1 -1
  82. package/dist/store/use-viewer.js +26 -3
  83. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  84. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  85. package/dist/systems/ceiling/ceiling-system.js +62 -12
  86. package/dist/systems/door/door-system.d.ts.map +1 -1
  87. package/dist/systems/door/door-system.js +373 -22
  88. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  89. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  90. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  91. package/dist/systems/fence/fence-system.d.ts +3 -0
  92. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  93. package/dist/systems/fence/fence-system.js +10 -6
  94. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  95. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  96. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  97. package/dist/systems/geometry/geometry-system.d.ts +34 -0
  98. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  99. package/dist/systems/geometry/geometry-system.js +279 -0
  100. package/dist/systems/guide/guide-system.d.ts.map +1 -1
  101. package/dist/systems/guide/guide-system.js +1 -26
  102. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  103. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  104. package/dist/systems/item/item-system.d.ts +7 -0
  105. package/dist/systems/item/item-system.d.ts.map +1 -1
  106. package/dist/systems/item/item-system.js +8 -11
  107. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  108. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  109. package/dist/systems/item-light/item-light-system.js +18 -5
  110. package/dist/systems/roof/roof-materials.d.ts +3 -2
  111. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  112. package/dist/systems/roof/roof-materials.js +38 -14
  113. package/dist/systems/roof/roof-system.d.ts +27 -0
  114. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  115. package/dist/systems/roof/roof-system.js +411 -71
  116. package/dist/systems/scan/scan-system.d.ts.map +1 -1
  117. package/dist/systems/scan/scan-system.js +1 -26
  118. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  119. package/dist/systems/slab/slab-system.js +167 -49
  120. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  121. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  122. package/dist/systems/slab/slab-system.test.js +62 -0
  123. package/dist/systems/stair/stair-materials.d.ts +4 -3
  124. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  125. package/dist/systems/stair/stair-materials.js +30 -15
  126. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  127. package/dist/systems/stair/stair-system.js +58 -71
  128. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  129. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  130. package/dist/systems/surface-hole-geometry.js +4 -0
  131. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  132. package/dist/systems/wall/wall-cutout.js +18 -2
  133. package/dist/systems/wall/wall-materials.d.ts +4 -3
  134. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  135. package/dist/systems/wall/wall-materials.js +52 -28
  136. package/dist/systems/wall/wall-system.d.ts +1 -1
  137. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  138. package/dist/systems/wall/wall-system.js +179 -31
  139. package/dist/systems/window/window-system.d.ts.map +1 -1
  140. package/dist/systems/window/window-system.js +71 -7
  141. package/package.json +4 -5
  142. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  143. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  144. package/dist/components/renderers/building/building-renderer.js +0 -11
  145. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  146. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  147. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  148. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  149. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  150. package/dist/components/renderers/column/column-renderer.js +0 -594
  151. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  152. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  153. package/dist/components/renderers/door/door-renderer.js +0 -16
  154. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  155. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  156. package/dist/components/renderers/fence/fence-renderer.js +0 -29
  157. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  158. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  159. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  160. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  161. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  162. package/dist/components/renderers/item/item-renderer.js +0 -172
  163. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  164. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  165. package/dist/components/renderers/level/level-renderer.js +0 -11
  166. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  167. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  168. package/dist/components/renderers/roof/roof-materials.js +0 -16
  169. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  170. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  171. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  172. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  173. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  174. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  175. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  176. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  177. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  178. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  179. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  180. package/dist/components/renderers/site/site-renderer.js +0 -106
  181. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  182. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  183. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  184. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  185. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  186. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  187. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  188. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  189. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  190. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  191. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  192. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -32
  193. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  194. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  195. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  196. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  197. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  198. package/dist/components/renderers/window/window-renderer.js +0 -21
  199. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  200. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  201. package/dist/components/renderers/zone/zone-renderer.js +0 -181
  202. package/dist/lib/suppress-three-clock-warning.d.ts +0 -25
  203. package/dist/lib/suppress-three-clock-warning.d.ts.map +0 -1
  204. package/dist/lib/suppress-three-clock-warning.js +0 -51
  205. package/dist/systems/export/export-system.d.ts +0 -2
  206. package/dist/systems/export/export-system.d.ts.map +0 -1
  207. package/dist/systems/export/export-system.js +0 -60
  208. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts +0 -14
  209. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts.map +0 -1
  210. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.js +0 -155
  211. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts +0 -6
  212. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts.map +0 -1
  213. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.js +0 -75
  214. package/dist/systems/item-mesh-metadata/sync-request.d.ts +0 -7
  215. package/dist/systems/item-mesh-metadata/sync-request.d.ts.map +0 -1
  216. package/dist/systems/item-mesh-metadata/sync-request.js +0 -25
@@ -1,31 +1,109 @@
1
- import { clampDoorOperationState, getDoorRenderOpenAmount, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
1
+ import { clampDoorOperationState, DoorNode as DoorNodeSchema, getDoorRenderOpenAmount, getEffectiveNode, sceneRegistry, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
+ import { useEffect, useRef } from 'react';
3
4
  import * as THREE from 'three';
4
- import { baseMaterial, glassMaterial } from '../../lib/materials';
5
+ import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, } from '../../lib/materials';
6
+ import useViewer from '../../store/use-viewer';
5
7
  // Invisible material for root mesh — used as selection hitbox only
6
8
  const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
7
- const revealMaterial = new THREE.MeshBasicMaterial({ color: '#7f766c' });
9
+ const defaultRevealMaterial = new THREE.MeshBasicMaterial({ color: '#7f766c' });
10
+ let baseMaterial = getBaseMaterial();
11
+ let revealMaterial = defaultRevealMaterial;
12
+ let glassMaterial = defaultGlassMaterial;
13
+ const DOOR_RENDER_DEFAULTS = DoorNodeSchema.parse({ id: 'door_render_default' });
14
+ const MAX_DOOR_REBUILDS_PER_FRAME = 16;
15
+ const DOOR_PROGRESSIVE_DIRTY_THRESHOLD = MAX_DOOR_REBUILDS_PER_FRAME;
16
+ const DOOR_PROGRESSIVE_TIME_BUDGET_MS = 8;
17
+ // Legacy/unparsed door nodes can miss schema-defaulted fields (segments,
18
+ // columnRatios, dividerThickness, …) and crash the geometry build. Re-apply the
19
+ // Zod defaults; if the node is structurally invalid (e.g. a segment missing a
20
+ // required field) drop the bad segments, then fall back to defaults entirely.
21
+ function normalizeDoorNodeForRender(node) {
22
+ const parsed = DoorNodeSchema.safeParse(node);
23
+ if (parsed.success)
24
+ return parsed.data;
25
+ const retry = DoorNodeSchema.safeParse({ ...node, segments: undefined });
26
+ if (retry.success)
27
+ return retry.data;
28
+ return { ...DOOR_RENDER_DEFAULTS, id: node.id, parentId: node.parentId };
29
+ }
8
30
  export const DoorSystem = () => {
9
31
  const dirtyNodes = useScene((state) => state.dirtyNodes);
10
32
  const clearDirty = useScene((state) => state.clearDirty);
33
+ const shading = useViewer((state) => state.shading);
34
+ const textures = useViewer((state) => state.textures);
35
+ const colorPreset = useViewer((state) => state.colorPreset);
36
+ const materialRevisionRef = useRef(null);
37
+ // Subscribe so an override-only update (no scene write) still re-runs
38
+ // the component, letting the gate below pick up the latest dirtyNodes
39
+ // set from the same render pass that received the override-publishing
40
+ // `markDirty` call. Mirrors WallSystem.
41
+ useLiveNodeOverrides((s) => s.overrides);
42
+ const joineryMaterial = createSurfaceRoleMaterial('joinery', colorPreset);
43
+ baseMaterial = textures ? getBaseMaterial(shading) : joineryMaterial;
44
+ revealMaterial = textures ? defaultRevealMaterial : joineryMaterial;
45
+ glassMaterial = textures ? defaultGlassMaterial : joineryMaterial;
46
+ useEffect(() => {
47
+ const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`;
48
+ if (materialRevisionRef.current === materialRevision)
49
+ return;
50
+ materialRevisionRef.current = materialRevision;
51
+ const nodes = useScene.getState().nodes;
52
+ for (const node of Object.values(nodes)) {
53
+ if (node?.type === 'door') {
54
+ useScene.getState().dirtyNodes.add(node.id);
55
+ }
56
+ }
57
+ });
11
58
  useFrame(() => {
12
59
  if (dirtyNodes.size === 0)
13
60
  return;
61
+ const frameJoineryMaterial = createSurfaceRoleMaterial('joinery', colorPreset);
62
+ baseMaterial = textures ? getBaseMaterial(shading) : frameJoineryMaterial;
63
+ revealMaterial = textures ? defaultRevealMaterial : frameJoineryMaterial;
64
+ glassMaterial = textures ? defaultGlassMaterial : frameJoineryMaterial;
14
65
  const nodes = useScene.getState().nodes;
66
+ const dirtyDoorIds = [];
15
67
  dirtyNodes.forEach((id) => {
16
68
  const node = nodes[id];
17
69
  if (!node || node.type !== 'door')
18
70
  return;
71
+ dirtyDoorIds.push(id);
72
+ });
73
+ const useProgressiveDoorRebuilds = dirtyDoorIds.length > DOOR_PROGRESSIVE_DIRTY_THRESHOLD;
74
+ const frameStartedAt = performance.now();
75
+ let rebuiltDoorsThisFrame = 0;
76
+ for (const id of dirtyDoorIds) {
77
+ if (useProgressiveDoorRebuilds) {
78
+ if (rebuiltDoorsThisFrame >= MAX_DOOR_REBUILDS_PER_FRAME) {
79
+ break;
80
+ }
81
+ if (rebuiltDoorsThisFrame > 0 &&
82
+ performance.now() - frameStartedAt >= DOOR_PROGRESSIVE_TIME_BUDGET_MS) {
83
+ break;
84
+ }
85
+ }
86
+ const node = nodes[id];
87
+ if (!node || node.type !== 'door')
88
+ continue;
19
89
  const mesh = sceneRegistry.nodes.get(id);
20
90
  if (!mesh)
21
- return; // Keep dirty until mesh mounts
22
- updateDoorMesh(node, mesh);
91
+ continue; // Keep dirty until mesh mounts
92
+ // Merge any live override (width / height / position) so the mesh
93
+ // rebuild reflects the in-flight drag without zustand churn. When
94
+ // no override is set this returns the scene node unchanged.
95
+ const effectiveNode = getEffectiveNode(node);
96
+ updateDoorMesh(effectiveNode, mesh);
23
97
  clearDirty(id);
98
+ rebuiltDoorsThisFrame += 1;
24
99
  // Rebuild the parent wall so its cutout reflects the updated door geometry
25
- if (node.parentId) {
26
- useScene.getState().dirtyNodes.add(node.parentId);
100
+ // Avoid triggering expensive wall CSG rebuilds while the door is being interactively moved/duplicated.
101
+ // The editor tools will request a final wall rebuild on commit.
102
+ const isTransient = !!node.metadata?.isTransient;
103
+ if (!isTransient && effectiveNode.parentId) {
104
+ useScene.getState().dirtyNodes.add(effectiveNode.parentId);
27
105
  }
28
- });
106
+ }
29
107
  }, 3);
30
108
  return null;
31
109
  };
@@ -56,6 +134,16 @@ function addShape(parent, material, shape, depth) {
56
134
  const mesh = new THREE.Mesh(geometry, material);
57
135
  parent.add(mesh);
58
136
  }
137
+ function addShapeAt(parent, material, shape, depth, x, y, z) {
138
+ const geometry = new THREE.ExtrudeGeometry(shape, {
139
+ depth,
140
+ bevelEnabled: false,
141
+ curveSegments: 24,
142
+ });
143
+ geometry.translate(x, y, z - depth / 2);
144
+ const mesh = new THREE.Mesh(geometry, material);
145
+ parent.add(mesh);
146
+ }
59
147
  function getClampedArchHeight(width, height, archHeight) {
60
148
  return Math.min(Math.max(archHeight ?? width / 2, 0.01), Math.max(height, 0.01));
61
149
  }
@@ -83,6 +171,32 @@ function getArchBoundaryY(x, halfWidth, springY, archHeight) {
83
171
  const t = Math.min(Math.abs(x) / halfWidth, 1);
84
172
  return springY + archHeight * Math.sqrt(Math.max(1 - t * t, 0));
85
173
  }
174
+ function getOneSidedArchBoundaryYAtX(x, left, right, top, archHeight, curvedSide) {
175
+ const width = right - left;
176
+ if (width <= 1e-6)
177
+ return top;
178
+ const clampedArchHeight = getClampedArchHeight(width, Number.MAX_SAFE_INTEGER, archHeight);
179
+ const springY = top - clampedArchHeight;
180
+ const distanceFromApex = curvedSide === 'left'
181
+ ? Math.max(0, Math.min((right - x) / width, 1))
182
+ : Math.max(0, Math.min((x - left) / width, 1));
183
+ return (springY + clampedArchHeight * Math.sqrt(Math.max(1 - distanceFromApex * distanceFromApex, 0)));
184
+ }
185
+ function getRoundedBoundaryYAtX(x, left, right, top, radii) {
186
+ if (radii.topLeft > 1e-6 && x < left + radii.topLeft) {
187
+ const centerX = left + radii.topLeft;
188
+ const centerY = top - radii.topLeft;
189
+ const dx = x - centerX;
190
+ return centerY + Math.sqrt(Math.max(radii.topLeft * radii.topLeft - dx * dx, 0));
191
+ }
192
+ if (radii.topRight > 1e-6 && x > right - radii.topRight) {
193
+ const centerX = right - radii.topRight;
194
+ const centerY = top - radii.topRight;
195
+ const dx = x - centerX;
196
+ return centerY + Math.sqrt(Math.max(radii.topRight * radii.topRight - dx * dx, 0));
197
+ }
198
+ return top;
199
+ }
86
200
  function createArchBandShape(width, outerSpringY, outerTopY, innerSpringY, innerTopY, insetX) {
87
201
  const halfWidth = width / 2;
88
202
  const innerHalfWidth = Math.max(halfWidth - insetX, 0);
@@ -215,6 +329,87 @@ function createRoundedLeafFrameShape(width, bottom, top, radii, insetX, insetY)
215
329
  outer.holes.push(shapeToReversedPath(createRoundedTopShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii)));
216
330
  return outer;
217
331
  }
332
+ function createRoundedClippedLeafFrameShape(left, right, bottom, top, fullLeft, fullRight, radii, insetX, insetY) {
333
+ const outerRadii = normalizeTopCornerRadii(radii, fullRight - fullLeft, top - bottom);
334
+ const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getRoundedBoundaryYAtX(x, fullLeft, fullRight, top, {
335
+ topLeft: outerRadii.topLeft,
336
+ topRight: outerRadii.topRight,
337
+ }));
338
+ if (!outer)
339
+ return null;
340
+ const innerLeft = left + insetX;
341
+ const innerRight = right - insetX;
342
+ const innerBottom = bottom + insetY;
343
+ const innerTop = top - insetY;
344
+ if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
345
+ return outer;
346
+ const innerFullLeft = fullLeft + insetX;
347
+ const innerFullRight = fullRight - insetX;
348
+ const innerRadii = normalizeTopCornerRadii({
349
+ topLeft: Math.max(outerRadii.topLeft - Math.max(insetX, insetY), 0),
350
+ topRight: Math.max(outerRadii.topRight - Math.max(insetX, insetY), 0),
351
+ }, innerFullRight - innerFullLeft, innerTop - innerBottom);
352
+ const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getRoundedBoundaryYAtX(x, innerFullLeft, innerFullRight, innerTop, {
353
+ topLeft: innerRadii.topLeft,
354
+ topRight: innerRadii.topRight,
355
+ }));
356
+ if (holeShape)
357
+ outer.holes.push(shapeToReversedPath(holeShape));
358
+ return outer;
359
+ }
360
+ function createArchedLeafFrameShape(width, bottom, top, archHeight, insetX, insetY) {
361
+ const halfWidth = width / 2;
362
+ const outer = createArchShape(-halfWidth, halfWidth, bottom, top, archHeight);
363
+ const innerLeft = -halfWidth + insetX;
364
+ const innerRight = halfWidth - insetX;
365
+ const innerBottom = bottom + insetY;
366
+ const innerTop = top - insetY;
367
+ if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
368
+ return outer;
369
+ const innerArchHeight = getClampedArchHeight(innerRight - innerLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
370
+ outer.holes.push(shapeToReversedPath(createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight)));
371
+ return outer;
372
+ }
373
+ function createArchedClippedLeafFrameShape(left, right, bottom, top, fullLeft, fullRight, archHeight, insetX, insetY) {
374
+ const fullCenterX = (fullLeft + fullRight) / 2;
375
+ const fullHalfWidth = (fullRight - fullLeft) / 2;
376
+ const springY = top - archHeight;
377
+ const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getArchBoundaryY(x - fullCenterX, fullHalfWidth, springY, archHeight));
378
+ if (!outer)
379
+ return null;
380
+ const innerLeft = left + insetX;
381
+ const innerRight = right - insetX;
382
+ const innerBottom = bottom + insetY;
383
+ const innerTop = top - insetY;
384
+ if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
385
+ return outer;
386
+ const innerFullLeft = fullLeft + insetX;
387
+ const innerFullRight = fullRight - insetX;
388
+ const innerArchHeight = getClampedArchHeight(innerFullRight - innerFullLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
389
+ const innerFullCenterX = (innerFullLeft + innerFullRight) / 2;
390
+ const innerFullHalfWidth = (innerFullRight - innerFullLeft) / 2;
391
+ const innerSpringY = innerTop - innerArchHeight;
392
+ const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getArchBoundaryY(x - innerFullCenterX, innerFullHalfWidth, innerSpringY, innerArchHeight));
393
+ if (holeShape)
394
+ outer.holes.push(shapeToReversedPath(holeShape));
395
+ return outer;
396
+ }
397
+ function createOneSidedArchLeafFrameShape(left, right, bottom, top, archHeight, insetX, insetY, curvedSide) {
398
+ const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getOneSidedArchBoundaryYAtX(x, left, right, top, archHeight, curvedSide));
399
+ if (!outer)
400
+ return null;
401
+ const innerLeft = left + insetX;
402
+ const innerRight = right - insetX;
403
+ const innerBottom = bottom + insetY;
404
+ const innerTop = top - insetY;
405
+ if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
406
+ return outer;
407
+ const innerArchHeight = getClampedArchHeight(innerRight - innerLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
408
+ const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getOneSidedArchBoundaryYAtX(x, innerLeft, innerRight, innerTop, innerArchHeight, curvedSide));
409
+ if (holeShape)
410
+ outer.holes.push(shapeToReversedPath(holeShape));
411
+ return outer;
412
+ }
218
413
  function createTopClippedRectShape(left, right, bottom, top, getBoundaryY) {
219
414
  const segments = 20;
220
415
  const points = [];
@@ -242,16 +437,16 @@ function disposeObject(object) {
242
437
  child.geometry.dispose();
243
438
  });
244
439
  }
245
- function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, }) {
440
+ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, renderPerimeterFrame = true, openingShape = 'rectangle', openingTopRadii, archHeight, archOuterSide, }) {
246
441
  const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
247
442
  const shouldRenderFrame = hasLeafContent || keepFrameWhenEmpty;
248
443
  const cpX = contentPadding[0];
249
444
  const cpY = contentPadding[1];
250
- if (shouldRenderFrame && cpY > 0) {
445
+ if (renderPerimeterFrame && shouldRenderFrame && cpY > 0) {
251
446
  addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY + leafHeight / 2 - cpY / 2, 0);
252
447
  addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY - leafHeight / 2 + cpY / 2, 0);
253
448
  }
254
- if (shouldRenderFrame && cpX > 0) {
449
+ if (renderPerimeterFrame && shouldRenderFrame && cpX > 0) {
255
450
  const innerH = leafHeight - 2 * cpY;
256
451
  addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX - leafWidth / 2 + cpX / 2, leafCenterY, 0);
257
452
  addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX + leafWidth / 2 - cpX / 2, leafCenterY, 0);
@@ -261,9 +456,32 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
261
456
  const totalRatio = segments.reduce((sum, s) => sum + s.heightRatio, 0);
262
457
  const contentTop = leafCenterY + contentH / 2;
263
458
  let segY = contentTop;
459
+ const leafLeft = leafCenterX - leafWidth / 2;
460
+ const leafRight = leafCenterX + leafWidth / 2;
461
+ const leafTop = leafCenterY + leafHeight / 2;
462
+ const hasShapedTop = openingShape === 'rounded' || openingShape === 'arch';
463
+ const clampedLeafArchHeight = openingShape === 'arch' ? getClampedArchHeight(leafWidth, leafHeight, archHeight) : 0;
464
+ const leafSpringY = leafTop - clampedLeafArchHeight;
465
+ const topBoundaryAtX = (x) => {
466
+ if (openingShape === 'rounded' && openingTopRadii) {
467
+ return getRoundedBoundaryYAtX(x, leafLeft, leafRight, leafTop, {
468
+ topLeft: openingTopRadii.topLeft,
469
+ topRight: openingTopRadii.topRight,
470
+ });
471
+ }
472
+ if (openingShape === 'arch') {
473
+ if (archOuterSide) {
474
+ return getOneSidedArchBoundaryYAtX(x, leafLeft, leafRight, leafTop, clampedLeafArchHeight, archOuterSide);
475
+ }
476
+ return getArchBoundaryY(x - leafCenterX, leafWidth / 2, leafSpringY, clampedLeafArchHeight);
477
+ }
478
+ return leafTop;
479
+ };
264
480
  for (const seg of segments) {
265
481
  const segH = (seg.heightRatio / totalRatio) * contentH;
266
482
  const segCenterY = segY - segH / 2;
483
+ const segTop = segY;
484
+ const segBottom = segY - segH;
267
485
  const numCols = seg.columnRatios.length;
268
486
  const colSum = seg.columnRatios.reduce((a, b) => a + b, 0);
269
487
  const usableW = contentW - (numCols - 1) * seg.dividerThickness;
@@ -280,7 +498,17 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
280
498
  cx = leafCenterX - contentW / 2;
281
499
  for (let c = 0; c < numCols - 1; c++) {
282
500
  cx += colWidths[c];
283
- addLeafBox(baseMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0);
501
+ const dividerLeft = cx;
502
+ const dividerRight = cx + seg.dividerThickness;
503
+ const dividerShape = hasShapedTop && addLeafShape
504
+ ? createTopClippedRectShape(dividerLeft, dividerRight, segBottom, segTop, topBoundaryAtX)
505
+ : null;
506
+ if (dividerShape && addLeafShape) {
507
+ addLeafShape(baseMaterial, dividerShape, leafDepth + 0.001);
508
+ }
509
+ else {
510
+ addLeafBox(baseMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0);
511
+ }
284
512
  cx += seg.dividerThickness;
285
513
  }
286
514
  }
@@ -289,31 +517,99 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
289
517
  const colX = colXCenters[c];
290
518
  if (seg.type === 'glass') {
291
519
  const glassDepth = Math.max(0.004, leafDepth * 0.15);
292
- addLeafBox(glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0);
520
+ const segmentLeft = colX - colW / 2;
521
+ const segmentRight = colX + colW / 2;
522
+ const glassShape = hasShapedTop && addLeafShape
523
+ ? createTopClippedRectShape(segmentLeft, segmentRight, segBottom, segTop, topBoundaryAtX)
524
+ : null;
525
+ if (glassShape && addLeafShape) {
526
+ addLeafShape(glassMaterial, glassShape, glassDepth);
527
+ }
528
+ else {
529
+ addLeafBox(glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0);
530
+ }
293
531
  }
294
532
  else if (seg.type === 'panel') {
295
- addLeafBox(baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0);
533
+ const segmentLeft = colX - colW / 2;
534
+ const segmentRight = colX + colW / 2;
535
+ const outerPanelShape = hasShapedTop && addLeafShape
536
+ ? createTopClippedRectShape(segmentLeft, segmentRight, segBottom, segTop, topBoundaryAtX)
537
+ : null;
538
+ if (outerPanelShape && addLeafShape) {
539
+ addLeafShape(baseMaterial, outerPanelShape, leafDepth);
540
+ }
541
+ else {
542
+ addLeafBox(baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0);
543
+ }
296
544
  const panelW = colW - 2 * seg.panelInset;
297
545
  const panelH = segH - 2 * seg.panelInset;
298
546
  if (panelW > 0.01 && panelH > 0.01) {
299
547
  const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth);
300
548
  const panelZ = leafDepth / 2 + effectiveDepth / 2;
301
- addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ);
549
+ const insetLeft = colX - panelW / 2;
550
+ const insetRight = colX + panelW / 2;
551
+ const insetTop = segTop - seg.panelInset;
552
+ const insetBottom = segBottom + seg.panelInset;
553
+ const innerPanelShape = hasShapedTop && addLeafShape
554
+ ? createTopClippedRectShape(insetLeft, insetRight, insetBottom, insetTop, topBoundaryAtX)
555
+ : null;
556
+ if (innerPanelShape && addLeafShape) {
557
+ addLeafShape(baseMaterial, innerPanelShape, effectiveDepth);
558
+ }
559
+ else {
560
+ addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ);
561
+ }
302
562
  }
303
563
  }
304
564
  }
305
565
  segY -= segH;
306
566
  }
307
567
  }
308
- function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, hingeX, hingeSide, swingRotation, segments, contentPadding, handle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, doorHeight, }) {
568
+ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, hingeX, hingeSide, swingRotation, segments, contentPadding, handle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, doorHeight, openingShape, openingTopRadii, archHeight, roundedBoundary, archedBoundary, archOuterSide, }) {
309
569
  const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
310
570
  const leafGroup = new THREE.Group();
311
571
  leafGroup.position.set(hingeX, 0, 0);
312
572
  leafGroup.rotation.y = swingRotation;
313
573
  mesh.add(leafGroup);
314
574
  const addLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z);
575
+ const addLeafShape = (material, shape, depth) => addShapeAt(leafGroup, material, shape, depth, -hingeX, 0, 0);
576
+ const localLeafCenterX = leafCenterX - hingeX;
577
+ const leafBottom = leafCenterY - leafHeight / 2;
578
+ const leafTop = leafCenterY + leafHeight / 2;
579
+ const usesShapedLeafFrame = openingShape === 'rounded' || openingShape === 'arch';
580
+ if (usesShapedLeafFrame && hasLeafContent) {
581
+ if (openingShape === 'rounded') {
582
+ const roundedLeafShape = roundedBoundary
583
+ ? createRoundedClippedLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, roundedBoundary.fullLeft, roundedBoundary.fullRight, roundedBoundary.radii, contentPadding[0], contentPadding[1])
584
+ : createRoundedLeafFrameShape(leafWidth, leafBottom, leafTop, openingTopRadii, contentPadding[0], contentPadding[1]);
585
+ if (roundedLeafShape) {
586
+ if (roundedBoundary) {
587
+ addLeafShape(baseMaterial, roundedLeafShape, leafDepth);
588
+ }
589
+ else {
590
+ addShapeAt(leafGroup, baseMaterial, roundedLeafShape, leafDepth, localLeafCenterX, 0, 0);
591
+ }
592
+ }
593
+ }
594
+ else if (openingShape === 'arch') {
595
+ const archedLeafShape = archOuterSide
596
+ ? createOneSidedArchLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, archHeight, contentPadding[0], contentPadding[1], archOuterSide)
597
+ : archedBoundary
598
+ ? createArchedClippedLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, archedBoundary.fullLeft, archedBoundary.fullRight, archedBoundary.archHeight, contentPadding[0], contentPadding[1])
599
+ : createArchedLeafFrameShape(leafWidth, leafBottom, leafTop, getClampedArchHeight(leafWidth, leafHeight, archHeight), contentPadding[0], contentPadding[1]);
600
+ if (archedLeafShape) {
601
+ if (archOuterSide || archedBoundary) {
602
+ addLeafShape(baseMaterial, archedLeafShape, leafDepth);
603
+ }
604
+ else {
605
+ addShapeAt(leafGroup, baseMaterial, archedLeafShape, leafDepth, localLeafCenterX, 0, 0);
606
+ }
607
+ }
608
+ }
609
+ }
315
610
  addLeafSegmentContent({
316
611
  addLeafBox,
612
+ addLeafShape,
317
613
  leafWidth,
318
614
  leafHeight,
319
615
  leafCenterX,
@@ -321,6 +617,11 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
321
617
  leafDepth,
322
618
  segments,
323
619
  contentPadding,
620
+ renderPerimeterFrame: !usesShapedLeafFrame,
621
+ openingShape,
622
+ openingTopRadii,
623
+ archHeight,
624
+ archOuterSide,
324
625
  });
325
626
  if (hasLeafContent && handle) {
326
627
  const handleY = handleHeight - doorHeight / 2;
@@ -349,8 +650,6 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
349
650
  const hingeH = 0.1;
350
651
  const hingeW = 0.024;
351
652
  const hingeD = leafDepth + 0.016;
352
- const leafBottom = leafCenterY - leafHeight / 2;
353
- const leafTop = leafCenterY + leafHeight / 2;
354
653
  addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafBottom + 0.25, 0);
355
654
  addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, (leafBottom + leafTop) / 2, 0);
356
655
  addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafTop - 0.25, 0);
@@ -607,7 +906,16 @@ function addGarageTiltupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafD
607
906
  addTiltupTrim(insetWidth / 2, 0, 0.018, insetHeight);
608
907
  addBox(mesh, revealMaterial, insideWidth, 0.026, Math.max(frameDepth * 0.4, 0.035), 0, hingeY, 0);
609
908
  }
610
- function updateDoorMesh(node, mesh) {
909
+ function getEffectiveOpeningShape(node) {
910
+ return node.doorType === 'folding' ||
911
+ node.doorType === 'pocket' ||
912
+ node.doorType === 'barn' ||
913
+ node.doorType === 'sliding'
914
+ ? 'rectangle'
915
+ : (node.openingShape ?? 'rectangle');
916
+ }
917
+ function updateDoorMesh(rawNode, mesh) {
918
+ const node = normalizeDoorNodeForRender(rawNode);
611
919
  // Root mesh is an invisible hitbox; all visuals live in child meshes
612
920
  mesh.geometry.dispose();
613
921
  mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
@@ -622,7 +930,8 @@ function updateDoorMesh(node, mesh) {
622
930
  disposeObject(child);
623
931
  mesh.remove(child);
624
932
  }
625
- const { width, height, openingKind, openingShape, frameThickness, frameDepth, threshold, thresholdHeight, segments, handle, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, contentPadding, hingesSide, swingDirection, swingAngle: nodeSwingAngle = 0, doorType = 'hinged', operationState: nodeOperationState = 0, leafCount = 1, slideDirection = 'left', garagePanelCount = 4, } = node;
933
+ const { width, height, openingKind, openingShape: rawOpeningShape, frameThickness, frameDepth, threshold, thresholdHeight, segments, handle, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, contentPadding, hingesSide, swingDirection, swingAngle: nodeSwingAngle = 0, doorType = 'hinged', operationState: nodeOperationState = 0, leafCount = 1, slideDirection = 'left', garagePanelCount = 4, } = node;
934
+ const openingShape = getEffectiveOpeningShape(node) ?? rawOpeningShape;
626
935
  const runtimeDoorState = useInteractive.getState().doors[node.id];
627
936
  const swingAngle = runtimeDoorState?.swingAngle ?? nodeSwingAngle;
628
937
  const operationState = runtimeDoorState?.operationState ?? nodeOperationState;
@@ -768,6 +1077,14 @@ function updateDoorMesh(node, mesh) {
768
1077
  }
769
1078
  else if (doorType === 'double' || doorType === 'french') {
770
1079
  const doubleLeafW = insideWidth / 2;
1080
+ const fullLeafTopRadii = getDoorTopRadii(node, insideWidth, leafH);
1081
+ const roundedBoundary = openingShape === 'rounded'
1082
+ ? {
1083
+ fullLeft: -insideWidth / 2,
1084
+ fullRight: insideWidth / 2,
1085
+ radii: fullLeafTopRadii,
1086
+ }
1087
+ : undefined;
771
1088
  addDoorLeaf(mesh, {
772
1089
  leafWidth: doubleLeafW,
773
1090
  leafHeight: leafH,
@@ -787,6 +1104,13 @@ function updateDoorMesh(node, mesh) {
787
1104
  panicBar,
788
1105
  panicBarHeight,
789
1106
  doorHeight: height,
1107
+ openingShape,
1108
+ openingTopRadii: openingShape === 'rounded'
1109
+ ? { topLeft: fullLeafTopRadii.topLeft, topRight: 0 }
1110
+ : fullLeafTopRadii,
1111
+ archHeight: node.archHeight ?? 0.45,
1112
+ roundedBoundary,
1113
+ archOuterSide: openingShape === 'arch' ? 'left' : undefined,
790
1114
  });
791
1115
  addDoorLeaf(mesh, {
792
1116
  leafWidth: doubleLeafW,
@@ -807,6 +1131,13 @@ function updateDoorMesh(node, mesh) {
807
1131
  panicBar,
808
1132
  panicBarHeight,
809
1133
  doorHeight: height,
1134
+ openingShape,
1135
+ openingTopRadii: openingShape === 'rounded'
1136
+ ? { topLeft: 0, topRight: fullLeafTopRadii.topRight }
1137
+ : fullLeafTopRadii,
1138
+ archHeight: node.archHeight ?? 0.45,
1139
+ roundedBoundary,
1140
+ archOuterSide: openingShape === 'arch' ? 'right' : undefined,
810
1141
  });
811
1142
  }
812
1143
  else {
@@ -831,9 +1162,28 @@ function updateDoorMesh(node, mesh) {
831
1162
  panicBar,
832
1163
  panicBarHeight,
833
1164
  doorHeight: height,
1165
+ openingShape,
1166
+ openingTopRadii: getDoorTopRadii(node, insideWidth, leafH),
1167
+ archHeight: node.archHeight ?? 0.45,
834
1168
  });
835
1169
  }
836
1170
  syncDoorCutout(node, mesh);
1171
+ // Guard: some degenerate door configs can leave a child mesh with an
1172
+ // empty (0-vertex) geometry — e.g. a zero-area extruded leaf frame.
1173
+ // Submitting such a mesh trips a WebGPU error ("Vertex buffer slot 0
1174
+ // … was not set" on a Draw(0, …)). Hide any empty mesh so it is never
1175
+ // drawn (it would render nothing anyway).
1176
+ hideEmptyGeometryMeshes(mesh);
1177
+ }
1178
+ function hideEmptyGeometryMeshes(root) {
1179
+ root.traverse((obj) => {
1180
+ const child = obj;
1181
+ if (!child.isMesh || !child.geometry)
1182
+ return;
1183
+ const position = child.geometry.getAttribute('position');
1184
+ if (!position || position.count === 0)
1185
+ child.visible = false;
1186
+ });
837
1187
  }
838
1188
  function syncDoorCutout(node, mesh) {
839
1189
  // ── Cutout (for wall CSG) — always full door dimensions, 1m deep ──
@@ -844,7 +1194,8 @@ function syncDoorCutout(node, mesh) {
844
1194
  mesh.add(cutout);
845
1195
  }
846
1196
  cutout.geometry.dispose();
847
- if (node.openingShape === 'arch') {
1197
+ const openingShape = getEffectiveOpeningShape(node);
1198
+ if (openingShape === 'arch') {
848
1199
  cutout.geometry = new THREE.ExtrudeGeometry(createArchShape(-node.width / 2, node.width / 2, -node.height / 2, node.height / 2, getClampedArchHeight(node.width, node.height, node.archHeight)), {
849
1200
  depth: 1,
850
1201
  bevelEnabled: false,
@@ -852,7 +1203,7 @@ function syncDoorCutout(node, mesh) {
852
1203
  });
853
1204
  cutout.geometry.translate(0, 0, -0.5);
854
1205
  }
855
- else if (node.openingShape === 'rounded') {
1206
+ else if (openingShape === 'rounded') {
856
1207
  cutout.geometry = new THREE.ExtrudeGeometry(createRoundedTopShape(-node.width / 2, node.width / 2, -node.height / 2, node.height / 2, getDoorTopRadii(node, node.width, node.height)), {
857
1208
  depth: 1,
858
1209
  bevelEnabled: false,
@@ -0,0 +1,2 @@
1
+ export declare function ElevatorInteractionSystem(): null;
2
+ //# sourceMappingURL=elevator-interaction-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elevator-interaction-system.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-interaction-system.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,yBAAyB,SA0DxC"}
@@ -0,0 +1,62 @@
1
+ import { openElevatorDoor, requestElevatorLevel, resolveElevatorDispatchTarget, useInteractive, useScene, } from '@pascal-app/core';
2
+ import { useThree } from '@react-three/fiber';
3
+ import { useEffect, useRef } from 'react';
4
+ import { Raycaster, Vector2 } from 'three';
5
+ function getElevatorButtonData(object) {
6
+ let current = object;
7
+ while (current) {
8
+ const data = current.userData.elevatorButton;
9
+ if (data)
10
+ return data;
11
+ current = current.parent;
12
+ }
13
+ return null;
14
+ }
15
+ export function ElevatorInteractionSystem() {
16
+ const camera = useThree((state) => state.camera);
17
+ const gl = useThree((state) => state.gl);
18
+ const scene = useThree((state) => state.scene);
19
+ const raycasterRef = useRef(new Raycaster());
20
+ const pointerRef = useRef(new Vector2());
21
+ useEffect(() => {
22
+ const canvas = gl.domElement;
23
+ const handlePointerDown = (event) => {
24
+ if (event.button !== 0)
25
+ return;
26
+ const rect = canvas.getBoundingClientRect();
27
+ const pointer = pointerRef.current;
28
+ pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
29
+ pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
30
+ const raycaster = raycasterRef.current;
31
+ raycaster.setFromCamera(pointer, camera);
32
+ const button = raycaster
33
+ .intersectObjects(scene.children, true)
34
+ .map((intersection) => getElevatorButtonData(intersection.object))
35
+ .find((data) => data !== null);
36
+ if (!button || button.disabled)
37
+ return;
38
+ event.preventDefault();
39
+ event.stopPropagation();
40
+ if (button.action === 'open-door') {
41
+ openElevatorDoor(button.elevatorId);
42
+ return;
43
+ }
44
+ if (!button.levelId)
45
+ return;
46
+ const targetElevatorId = button.kind === 'landing'
47
+ ? resolveElevatorDispatchTarget({
48
+ elevators: useInteractive.getState().elevators,
49
+ levelId: button.levelId,
50
+ nodes: useScene.getState().nodes,
51
+ requestedElevatorId: button.elevatorId,
52
+ })
53
+ : button.elevatorId;
54
+ requestElevatorLevel(targetElevatorId, button.levelId);
55
+ };
56
+ canvas.addEventListener('pointerdown', handlePointerDown, true);
57
+ return () => {
58
+ canvas.removeEventListener('pointerdown', handlePointerDown, true);
59
+ };
60
+ }, [camera, gl, scene]);
61
+ return null;
62
+ }
@@ -1,2 +1,5 @@
1
+ import { type FenceNode } from '@pascal-app/core';
2
+ import * as THREE from 'three';
3
+ export declare function generateFenceGeometry(fence: FenceNode): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
1
4
  export declare const FenceSystem: () => null;
2
5
  //# sourceMappingURL=fence-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"AA6QA,eAAO,MAAM,WAAW,YAiBvB,CAAA"}
1
+ {"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA2O9B,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,oFAcrD;AAgBD,eAAO,MAAM,WAAW,YAiBvB,CAAA"}