@pascal-app/viewer 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/components/error-boundary.d.ts +7 -3
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +3 -1
  4. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/ceiling/ceiling-renderer.js +19 -3
  6. package/dist/components/renderers/column/column-renderer.d.ts +5 -0
  7. package/dist/components/renderers/column/column-renderer.d.ts.map +1 -0
  8. package/dist/components/renderers/column/column-renderer.js +594 -0
  9. package/dist/components/renderers/door/door-renderer.d.ts.map +1 -1
  10. package/dist/components/renderers/door/door-renderer.js +4 -9
  11. package/dist/components/renderers/fence/fence-renderer.d.ts.map +1 -1
  12. package/dist/components/renderers/guide/guide-renderer.js +1 -1
  13. package/dist/components/renderers/item/item-renderer.d.ts.map +1 -1
  14. package/dist/components/renderers/item/item-renderer.js +2 -1
  15. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  16. package/dist/components/renderers/node-renderer.js +3 -1
  17. package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -1
  18. package/dist/components/renderers/roof/roof-renderer.js +1 -13
  19. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +1 -1
  20. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +5 -20
  21. package/dist/components/renderers/slab/slab-renderer.d.ts.map +1 -1
  22. package/dist/components/renderers/slab/slab-renderer.js +39 -27
  23. package/dist/components/renderers/spawn/spawn-renderer.d.ts +5 -0
  24. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +1 -0
  25. package/dist/components/renderers/spawn/spawn-renderer.js +32 -0
  26. package/dist/components/renderers/stair/stair-renderer.d.ts.map +1 -1
  27. package/dist/components/renderers/stair/stair-renderer.js +5 -23
  28. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +1 -1
  29. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +5 -22
  30. package/dist/components/viewer/index.d.ts +2 -0
  31. package/dist/components/viewer/index.d.ts.map +1 -1
  32. package/dist/components/viewer/index.js +88 -18
  33. package/dist/components/viewer/post-processing.d.ts +13 -1
  34. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  35. package/dist/components/viewer/post-processing.js +71 -14
  36. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  37. package/dist/components/viewer/selection-manager.js +19 -2
  38. package/dist/hooks/use-node-events.d.ts +9 -1
  39. package/dist/hooks/use-node-events.d.ts.map +1 -1
  40. package/dist/index.d.ts +6 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +5 -2
  43. package/dist/lib/materials.d.ts +3 -0
  44. package/dist/lib/materials.d.ts.map +1 -1
  45. package/dist/lib/materials.js +14 -0
  46. package/dist/lib/merged-outline-node.d.ts +2 -0
  47. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  48. package/dist/lib/merged-outline-node.js +25 -5
  49. package/dist/store/use-viewer.d.ts +4 -4
  50. package/dist/store/use-viewer.d.ts.map +1 -1
  51. package/dist/store/use-viewer.js +2 -2
  52. package/dist/systems/ceiling/ceiling-system.d.ts +8 -0
  53. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -0
  54. package/dist/systems/ceiling/ceiling-system.js +91 -0
  55. package/dist/systems/column/column-geometry.d.ts +22 -0
  56. package/dist/systems/column/column-geometry.d.ts.map +1 -0
  57. package/dist/systems/column/column-geometry.js +108 -0
  58. package/dist/systems/door/door-animation-system.d.ts +2 -0
  59. package/dist/systems/door/door-animation-system.d.ts.map +1 -0
  60. package/dist/systems/door/door-animation-system.js +53 -0
  61. package/dist/systems/door/door-system.d.ts +2 -0
  62. package/dist/systems/door/door-system.d.ts.map +1 -0
  63. package/dist/systems/door/door-system.js +867 -0
  64. package/dist/systems/fence/fence-system.d.ts +2 -0
  65. package/dist/systems/fence/fence-system.d.ts.map +1 -0
  66. package/dist/systems/fence/fence-system.js +185 -0
  67. package/dist/systems/item/item-system.d.ts +2 -0
  68. package/dist/systems/item/item-system.d.ts.map +1 -0
  69. package/dist/systems/item/item-system.js +44 -0
  70. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts +14 -0
  71. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts.map +1 -0
  72. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.js +155 -0
  73. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts +6 -0
  74. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts.map +1 -0
  75. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.js +75 -0
  76. package/dist/systems/item-mesh-metadata/sync-request.d.ts +7 -0
  77. package/dist/systems/item-mesh-metadata/sync-request.d.ts.map +1 -0
  78. package/dist/systems/item-mesh-metadata/sync-request.js +25 -0
  79. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  80. package/dist/systems/roof/roof-materials.js +18 -1
  81. package/dist/systems/roof/roof-system.d.ts +16 -0
  82. package/dist/systems/roof/roof-system.d.ts.map +1 -0
  83. package/dist/systems/roof/roof-system.js +797 -0
  84. package/dist/systems/slab/slab-system.d.ts +8 -0
  85. package/dist/systems/slab/slab-system.d.ts.map +1 -0
  86. package/dist/systems/slab/slab-system.js +155 -0
  87. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  88. package/dist/systems/stair/stair-materials.js +25 -2
  89. package/dist/systems/stair/stair-system.d.ts +2 -0
  90. package/dist/systems/stair/stair-system.d.ts.map +1 -0
  91. package/dist/systems/stair/stair-system.js +772 -0
  92. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  93. package/dist/systems/wall/wall-materials.js +2 -2
  94. package/dist/systems/wall/wall-system.d.ts +11 -0
  95. package/dist/systems/wall/wall-system.d.ts.map +1 -0
  96. package/dist/systems/wall/wall-system.js +592 -0
  97. package/dist/systems/window/window-animation-system.d.ts +2 -0
  98. package/dist/systems/window/window-animation-system.d.ts.map +1 -0
  99. package/dist/systems/window/window-animation-system.js +135 -0
  100. package/dist/systems/window/window-system.d.ts +12 -0
  101. package/dist/systems/window/window-system.d.ts.map +1 -0
  102. package/dist/systems/window/window-system.js +1430 -0
  103. package/package.json +4 -2
@@ -0,0 +1,867 @@
1
+ import { clampDoorOperationState, getDoorRenderOpenAmount, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
2
+ import { useFrame } from '@react-three/fiber';
3
+ import * as THREE from 'three';
4
+ import { baseMaterial, glassMaterial } from '../../lib/materials';
5
+ // Invisible material for root mesh — used as selection hitbox only
6
+ const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
7
+ const revealMaterial = new THREE.MeshBasicMaterial({ color: '#7f766c' });
8
+ export const DoorSystem = () => {
9
+ const dirtyNodes = useScene((state) => state.dirtyNodes);
10
+ const clearDirty = useScene((state) => state.clearDirty);
11
+ useFrame(() => {
12
+ if (dirtyNodes.size === 0)
13
+ return;
14
+ const nodes = useScene.getState().nodes;
15
+ dirtyNodes.forEach((id) => {
16
+ const node = nodes[id];
17
+ if (!node || node.type !== 'door')
18
+ return;
19
+ const mesh = sceneRegistry.nodes.get(id);
20
+ if (!mesh)
21
+ return; // Keep dirty until mesh mounts
22
+ updateDoorMesh(node, mesh);
23
+ clearDirty(id);
24
+ // Rebuild the parent wall so its cutout reflects the updated door geometry
25
+ if (node.parentId) {
26
+ useScene.getState().dirtyNodes.add(node.parentId);
27
+ }
28
+ });
29
+ }, 3);
30
+ return null;
31
+ };
32
+ function addBox(parent, material, w, h, d, x, y, z) {
33
+ const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
34
+ m.position.set(x, y, z);
35
+ parent.add(m);
36
+ }
37
+ function addRotatedBox(parent, material, w, h, d, x, y, z, rotationY) {
38
+ const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
39
+ m.position.set(x, y, z);
40
+ m.rotation.y = rotationY;
41
+ parent.add(m);
42
+ }
43
+ function addBoxWithRotation(parent, material, w, h, d, x, y, z, rotation) {
44
+ const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
45
+ m.position.set(x, y, z);
46
+ m.rotation.set(rotation[0], rotation[1], rotation[2]);
47
+ parent.add(m);
48
+ }
49
+ function addShape(parent, material, shape, depth) {
50
+ const geometry = new THREE.ExtrudeGeometry(shape, {
51
+ depth,
52
+ bevelEnabled: false,
53
+ curveSegments: 24,
54
+ });
55
+ geometry.translate(0, 0, -depth / 2);
56
+ const mesh = new THREE.Mesh(geometry, material);
57
+ parent.add(mesh);
58
+ }
59
+ function getClampedArchHeight(width, height, archHeight) {
60
+ return Math.min(Math.max(archHeight ?? width / 2, 0.01), Math.max(height, 0.01));
61
+ }
62
+ function createArchShape(left, right, bottom, top, archHeight) {
63
+ const centerX = (left + right) / 2;
64
+ const halfWidth = (right - left) / 2;
65
+ const clampedArchHeight = getClampedArchHeight(right - left, top - bottom, archHeight);
66
+ const springY = top - clampedArchHeight;
67
+ const shape = new THREE.Shape();
68
+ const segments = 32;
69
+ shape.moveTo(left, bottom);
70
+ shape.lineTo(right, bottom);
71
+ shape.lineTo(right, springY);
72
+ for (let index = 1; index <= segments; index += 1) {
73
+ const x = right + (left - right) * (index / segments);
74
+ shape.lineTo(x, getArchBoundaryY(x - centerX, halfWidth, springY, clampedArchHeight));
75
+ }
76
+ shape.lineTo(left, bottom);
77
+ shape.closePath();
78
+ return shape;
79
+ }
80
+ function getArchBoundaryY(x, halfWidth, springY, archHeight) {
81
+ if (halfWidth <= 1e-6)
82
+ return springY;
83
+ const t = Math.min(Math.abs(x) / halfWidth, 1);
84
+ return springY + archHeight * Math.sqrt(Math.max(1 - t * t, 0));
85
+ }
86
+ function createArchBandShape(width, outerSpringY, outerTopY, innerSpringY, innerTopY, insetX) {
87
+ const halfWidth = width / 2;
88
+ const innerHalfWidth = Math.max(halfWidth - insetX, 0);
89
+ const outerArchHeight = Math.max(outerTopY - outerSpringY, 0);
90
+ const safeInnerTopY = Math.min(innerTopY, outerTopY - 0.001);
91
+ const safeInnerSpringY = Math.min(innerSpringY, safeInnerTopY - 0.001);
92
+ const innerArchHeight = Math.max(safeInnerTopY - safeInnerSpringY, 0);
93
+ const shape = new THREE.Shape();
94
+ const segments = 32;
95
+ const getSafeInnerBoundaryY = (x) => Math.min(getArchBoundaryY(x, innerHalfWidth, safeInnerSpringY, innerArchHeight), getArchBoundaryY(x, halfWidth, outerSpringY, outerArchHeight) - 0.001);
96
+ shape.moveTo(-halfWidth, outerSpringY);
97
+ for (let index = 1; index <= segments; index += 1) {
98
+ const x = -halfWidth + width * (index / segments);
99
+ shape.lineTo(x, getArchBoundaryY(x, halfWidth, outerSpringY, outerArchHeight));
100
+ }
101
+ if (innerHalfWidth <= 0.001 || safeInnerTopY <= safeInnerSpringY + 0.001) {
102
+ shape.lineTo(halfWidth, outerSpringY);
103
+ shape.closePath();
104
+ return shape;
105
+ }
106
+ shape.lineTo(innerHalfWidth, outerSpringY);
107
+ shape.lineTo(innerHalfWidth, getSafeInnerBoundaryY(innerHalfWidth));
108
+ for (let index = segments - 1; index >= 0; index -= 1) {
109
+ const x = -innerHalfWidth + innerHalfWidth * 2 * (index / segments);
110
+ shape.lineTo(x, getSafeInnerBoundaryY(x));
111
+ }
112
+ shape.lineTo(-innerHalfWidth, outerSpringY);
113
+ shape.lineTo(-halfWidth, outerSpringY);
114
+ shape.closePath();
115
+ return shape;
116
+ }
117
+ function createArchHeadBarShape(width, bottomY, springY, topY) {
118
+ const halfWidth = width / 2;
119
+ const archHeight = Math.max(topY - springY, 0);
120
+ const shape = new THREE.Shape();
121
+ const segments = 32;
122
+ shape.moveTo(-halfWidth, bottomY);
123
+ shape.lineTo(halfWidth, bottomY);
124
+ shape.lineTo(halfWidth, springY);
125
+ for (let index = 1; index <= segments; index += 1) {
126
+ const x = halfWidth - width * (index / segments);
127
+ shape.lineTo(x, getArchBoundaryY(x, halfWidth, springY, archHeight));
128
+ }
129
+ shape.lineTo(-halfWidth, bottomY);
130
+ shape.closePath();
131
+ return shape;
132
+ }
133
+ function normalizeTopCornerRadii(radii, width, height) {
134
+ const next = { ...radii };
135
+ const scale = Math.min(1, width / Math.max(next.topLeft + next.topRight, 1e-6), height / Math.max(next.topLeft, 1e-6), height / Math.max(next.topRight, 1e-6));
136
+ if (scale < 1) {
137
+ next.topLeft *= scale;
138
+ next.topRight *= scale;
139
+ }
140
+ return next;
141
+ }
142
+ function getDoorTopRadii(node, width, height) {
143
+ if (node.openingRadiusMode === 'individual') {
144
+ const [topLeft = 0, topRight = 0] = node.openingTopRadii ?? [0.15, 0.15];
145
+ return normalizeTopCornerRadii({
146
+ topLeft: Math.max(topLeft, 0),
147
+ topRight: Math.max(topRight, 0),
148
+ }, width, height);
149
+ }
150
+ const maxRadius = Math.min(width / 2, height);
151
+ const radius = Math.min(Math.max(node.cornerRadius ?? 0.15, 0), maxRadius);
152
+ return { topLeft: radius, topRight: radius };
153
+ }
154
+ function createRoundedTopShape(left, right, bottom, top, radii) {
155
+ const shape = new THREE.Shape();
156
+ const { topLeft, topRight } = normalizeTopCornerRadii(radii, right - left, top - bottom);
157
+ shape.moveTo(left, bottom);
158
+ shape.lineTo(right, bottom);
159
+ shape.lineTo(right, top - topRight);
160
+ if (topRight > 1e-6) {
161
+ shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false);
162
+ }
163
+ else {
164
+ shape.lineTo(right, top);
165
+ }
166
+ shape.lineTo(left + topLeft, top);
167
+ if (topLeft > 1e-6) {
168
+ shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false);
169
+ }
170
+ else {
171
+ shape.lineTo(left, top);
172
+ }
173
+ shape.lineTo(left, bottom);
174
+ shape.closePath();
175
+ return shape;
176
+ }
177
+ function createRoundedDoorFrameShape(width, height, frameThickness, radii) {
178
+ const halfWidth = width / 2;
179
+ const bottom = -height / 2;
180
+ const top = height / 2;
181
+ const outerRadii = normalizeTopCornerRadii(radii, width, height);
182
+ const outer = createRoundedTopShape(-halfWidth, halfWidth, bottom, top, outerRadii);
183
+ const inset = Math.min(frameThickness, width / 2 - 0.005, height - 0.005);
184
+ if (inset <= 0.001)
185
+ return outer;
186
+ const innerLeft = -halfWidth + inset;
187
+ const innerRight = halfWidth - inset;
188
+ const innerTop = top - inset;
189
+ const innerRadii = normalizeTopCornerRadii({
190
+ topLeft: Math.max(outerRadii.topLeft - inset, 0),
191
+ topRight: Math.max(outerRadii.topRight - inset, 0),
192
+ }, innerRight - innerLeft, innerTop - bottom);
193
+ const holeShape = createRoundedTopShape(innerLeft, innerRight, bottom, innerTop, innerRadii);
194
+ const hole = new THREE.Path(holeShape.getPoints(32).reverse());
195
+ outer.holes.push(hole);
196
+ return outer;
197
+ }
198
+ function shapeToReversedPath(shape) {
199
+ return new THREE.Path(shape.getPoints(40).reverse());
200
+ }
201
+ function createRoundedLeafFrameShape(width, bottom, top, radii, insetX, insetY) {
202
+ const halfWidth = width / 2;
203
+ const outerRadii = normalizeTopCornerRadii(radii, width, top - bottom);
204
+ const outer = createRoundedTopShape(-halfWidth, halfWidth, bottom, top, outerRadii);
205
+ const innerLeft = -halfWidth + insetX;
206
+ const innerRight = halfWidth - insetX;
207
+ const innerBottom = bottom + insetY;
208
+ const innerTop = top - insetY;
209
+ if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
210
+ return outer;
211
+ const innerRadii = normalizeTopCornerRadii({
212
+ topLeft: Math.max(outerRadii.topLeft - Math.max(insetX, insetY), 0),
213
+ topRight: Math.max(outerRadii.topRight - Math.max(insetX, insetY), 0),
214
+ }, innerRight - innerLeft, innerTop - innerBottom);
215
+ outer.holes.push(shapeToReversedPath(createRoundedTopShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii)));
216
+ return outer;
217
+ }
218
+ function createTopClippedRectShape(left, right, bottom, top, getBoundaryY) {
219
+ const segments = 20;
220
+ const points = [];
221
+ for (let index = 0; index <= segments; index += 1) {
222
+ const t = index / segments;
223
+ const x = right + (left - right) * t;
224
+ const y = Math.min(top, getBoundaryY(x));
225
+ if (y > bottom + 0.001)
226
+ points.push({ x, y });
227
+ }
228
+ if (points.length < 2)
229
+ return null;
230
+ const shape = new THREE.Shape();
231
+ shape.moveTo(left, bottom);
232
+ shape.lineTo(right, bottom);
233
+ for (const point of points) {
234
+ shape.lineTo(point.x, point.y);
235
+ }
236
+ shape.closePath();
237
+ return shape;
238
+ }
239
+ function disposeObject(object) {
240
+ object.traverse((child) => {
241
+ if (child instanceof THREE.Mesh)
242
+ child.geometry.dispose();
243
+ });
244
+ }
245
+ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, }) {
246
+ const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
247
+ const shouldRenderFrame = hasLeafContent || keepFrameWhenEmpty;
248
+ const cpX = contentPadding[0];
249
+ const cpY = contentPadding[1];
250
+ if (shouldRenderFrame && cpY > 0) {
251
+ addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY + leafHeight / 2 - cpY / 2, 0);
252
+ addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY - leafHeight / 2 + cpY / 2, 0);
253
+ }
254
+ if (shouldRenderFrame && cpX > 0) {
255
+ const innerH = leafHeight - 2 * cpY;
256
+ addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX - leafWidth / 2 + cpX / 2, leafCenterY, 0);
257
+ addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX + leafWidth / 2 - cpX / 2, leafCenterY, 0);
258
+ }
259
+ const contentW = leafWidth - 2 * cpX;
260
+ const contentH = leafHeight - 2 * cpY;
261
+ const totalRatio = segments.reduce((sum, s) => sum + s.heightRatio, 0);
262
+ const contentTop = leafCenterY + contentH / 2;
263
+ let segY = contentTop;
264
+ for (const seg of segments) {
265
+ const segH = (seg.heightRatio / totalRatio) * contentH;
266
+ const segCenterY = segY - segH / 2;
267
+ const numCols = seg.columnRatios.length;
268
+ const colSum = seg.columnRatios.reduce((a, b) => a + b, 0);
269
+ const usableW = contentW - (numCols - 1) * seg.dividerThickness;
270
+ const colWidths = seg.columnRatios.map((r) => (r / colSum) * usableW);
271
+ const colXCenters = [];
272
+ let cx = leafCenterX - contentW / 2;
273
+ for (let c = 0; c < numCols; c++) {
274
+ colXCenters.push(cx + colWidths[c] / 2);
275
+ cx += colWidths[c];
276
+ if (c < numCols - 1)
277
+ cx += seg.dividerThickness;
278
+ }
279
+ if (seg.type !== 'empty') {
280
+ cx = leafCenterX - contentW / 2;
281
+ for (let c = 0; c < numCols - 1; c++) {
282
+ cx += colWidths[c];
283
+ addLeafBox(baseMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0);
284
+ cx += seg.dividerThickness;
285
+ }
286
+ }
287
+ for (let c = 0; c < numCols; c++) {
288
+ const colW = colWidths[c];
289
+ const colX = colXCenters[c];
290
+ if (seg.type === 'glass') {
291
+ const glassDepth = Math.max(0.004, leafDepth * 0.15);
292
+ addLeafBox(glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0);
293
+ }
294
+ else if (seg.type === 'panel') {
295
+ addLeafBox(baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0);
296
+ const panelW = colW - 2 * seg.panelInset;
297
+ const panelH = segH - 2 * seg.panelInset;
298
+ if (panelW > 0.01 && panelH > 0.01) {
299
+ const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth);
300
+ const panelZ = leafDepth / 2 + effectiveDepth / 2;
301
+ addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ);
302
+ }
303
+ }
304
+ }
305
+ segY -= segH;
306
+ }
307
+ }
308
+ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, hingeX, hingeSide, swingRotation, segments, contentPadding, handle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, doorHeight, }) {
309
+ const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
310
+ const leafGroup = new THREE.Group();
311
+ leafGroup.position.set(hingeX, 0, 0);
312
+ leafGroup.rotation.y = swingRotation;
313
+ mesh.add(leafGroup);
314
+ const addLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z);
315
+ addLeafSegmentContent({
316
+ addLeafBox,
317
+ leafWidth,
318
+ leafHeight,
319
+ leafCenterX,
320
+ leafCenterY,
321
+ leafDepth,
322
+ segments,
323
+ contentPadding,
324
+ });
325
+ if (hasLeafContent && handle) {
326
+ const handleY = handleHeight - doorHeight / 2;
327
+ const faceZ = leafDepth / 2;
328
+ const handleX = handleSide === 'right'
329
+ ? leafCenterX + leafWidth / 2 - 0.045
330
+ : leafCenterX - leafWidth / 2 + 0.045;
331
+ addLeafBox(baseMaterial, 0.028, 0.14, 0.01, handleX, handleY, faceZ + 0.005);
332
+ addLeafBox(baseMaterial, 0.022, 0.1, 0.035, handleX, handleY, faceZ + 0.025);
333
+ if (handleBothSides) {
334
+ addLeafBox(baseMaterial, 0.028, 0.14, 0.01, handleX, handleY, -faceZ - 0.005);
335
+ addLeafBox(baseMaterial, 0.022, 0.1, 0.035, handleX, handleY, -faceZ - 0.025);
336
+ }
337
+ }
338
+ if (hasLeafContent && doorCloser) {
339
+ const closerY = leafCenterY + leafHeight / 2 - 0.04;
340
+ addLeafBox(baseMaterial, 0.28, 0.055, 0.055, leafCenterX, closerY, leafDepth / 2 + 0.03);
341
+ addLeafBox(baseMaterial, 0.14, 0.015, 0.015, leafCenterX + leafWidth / 4, closerY + 0.025, leafDepth / 2 + 0.015);
342
+ }
343
+ if (hasLeafContent && panicBar) {
344
+ const barY = panicBarHeight - doorHeight / 2;
345
+ addLeafBox(baseMaterial, leafWidth * 0.72, 0.04, 0.055, leafCenterX, barY, leafDepth / 2 + 0.03);
346
+ }
347
+ if (hasLeafContent) {
348
+ const hingeMarkerX = hingeSide === 'right' ? hingeX - 0.012 : hingeX + 0.012;
349
+ const hingeH = 0.1;
350
+ const hingeW = 0.024;
351
+ const hingeD = leafDepth + 0.016;
352
+ const leafBottom = leafCenterY - leafHeight / 2;
353
+ const leafTop = leafCenterY + leafHeight / 2;
354
+ addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafBottom + 0.25, 0);
355
+ addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, (leafBottom + leafTop) / 2, 0);
356
+ addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafTop - 0.25, 0);
357
+ }
358
+ }
359
+ function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, leafCount, doorHeight, handleHeight, segments, contentPadding, }) {
360
+ const panelCount = leafCount === 2 ? 2 : 4;
361
+ const foldAmount = clampDoorOperationState(operationState);
362
+ const panelLength = insideWidth / panelCount;
363
+ const foldAngle = Math.PI * 0.44 * foldAmount;
364
+ addBox(mesh, baseMaterial, insideWidth, Math.min(frameThickness * 0.5, 0.025), Math.max(frameDepth * 0.45, 0.035), 0, leafCenterY + leafHeight / 2 - 0.018, 0);
365
+ const vertices = [{ x: -insideWidth / 2, z: 0 }];
366
+ for (let index = 0; index < panelCount; index++) {
367
+ const previous = vertices[index];
368
+ const direction = index % 2 === 0 ? -1 : 1;
369
+ const angle = direction * foldAngle;
370
+ vertices.push({
371
+ x: previous.x + panelLength * Math.cos(angle),
372
+ z: previous.z + panelLength * Math.sin(angle),
373
+ });
374
+ }
375
+ for (let index = 0; index < panelCount; index++) {
376
+ const start = vertices[index];
377
+ const end = vertices[index + 1];
378
+ const dx = end.x - start.x;
379
+ const dz = end.z - start.z;
380
+ const centerX = (start.x + end.x) / 2;
381
+ const centerZ = (start.z + end.z) / 2;
382
+ const rotationY = Math.atan2(-dz, dx);
383
+ const localX = {
384
+ x: Math.cos(rotationY),
385
+ z: -Math.sin(rotationY),
386
+ };
387
+ const addFoldingLeafBox = (material, w, h, d, x, y, z) => {
388
+ addRotatedBox(mesh, material, w, h, d, centerX + localX.x * x + Math.sin(rotationY) * z, y, centerZ + localX.z * x + Math.cos(rotationY) * z, rotationY);
389
+ };
390
+ addLeafSegmentContent({
391
+ addLeafBox: addFoldingLeafBox,
392
+ leafWidth: Math.max(0.08, panelLength),
393
+ leafHeight,
394
+ leafCenterX: 0,
395
+ leafCenterY,
396
+ leafDepth,
397
+ segments,
398
+ contentPadding,
399
+ keepFrameWhenEmpty: true,
400
+ });
401
+ for (const point of [start, end]) {
402
+ addBox(mesh, revealMaterial, 0.018, leafHeight * 0.92, leafDepth + 0.016, point.x, leafCenterY, point.z);
403
+ }
404
+ }
405
+ const handlePoint = vertices[vertices.length - 1];
406
+ const handleY = handleHeight - doorHeight / 2;
407
+ addBox(mesh, baseMaterial, 0.035, 0.16, leafDepth + 0.035, handlePoint.x - 0.035, handleY, handlePoint.z + 0.045);
408
+ addBox(mesh, baseMaterial, 0.035, 0.16, leafDepth + 0.035, handlePoint.x - 0.035, handleY, handlePoint.z - 0.045);
409
+ }
410
+ function addPocketDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
411
+ const openAmount = clampDoorOperationState(operationState);
412
+ const slideSign = slideDirection === 'right' ? 1 : -1;
413
+ const leafWidth = insideWidth;
414
+ const leafCenterX = slideSign * insideWidth * openAmount;
415
+ const topY = leafCenterY + leafHeight / 2;
416
+ const pocketCenterX = slideSign * insideWidth;
417
+ const handleY = handleHeight - doorHeight / 2;
418
+ const handleX = leafCenterX - slideSign * (leafWidth / 2 - 0.055);
419
+ addBox(mesh, baseMaterial, insideWidth * 2, Math.min(frameThickness * 0.45, 0.024), Math.max(frameDepth * 0.38, 0.03), slideSign * (insideWidth / 2), topY - 0.018, 0);
420
+ addBox(mesh, revealMaterial, insideWidth * 0.9, 0.018, Math.max(frameDepth * 0.32, 0.026), pocketCenterX, topY - 0.055, 0);
421
+ addBox(mesh, revealMaterial, 0.018, leafHeight * 0.94, leafDepth + 0.014, slideSign * insideWidth * 0.5, leafCenterY, 0);
422
+ const addPocketLeafBox = (material, w, h, d, x, y, z) => addBox(mesh, material, w, h, d, x, y, z);
423
+ addLeafSegmentContent({
424
+ addLeafBox: addPocketLeafBox,
425
+ leafWidth,
426
+ leafHeight,
427
+ leafCenterX,
428
+ leafCenterY,
429
+ leafDepth,
430
+ segments,
431
+ contentPadding,
432
+ });
433
+ addBox(mesh, baseMaterial, 0.03, 0.18, leafDepth + 0.03, handleX, handleY, leafDepth / 2 + 0.02);
434
+ addBox(mesh, baseMaterial, 0.03, 0.18, leafDepth + 0.03, handleX, handleY, -leafDepth / 2 - 0.02);
435
+ }
436
+ function addBarnDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
437
+ const openAmount = clampDoorOperationState(operationState);
438
+ const slideSign = slideDirection === 'right' ? 1 : -1;
439
+ const leafWidth = insideWidth * 1.06;
440
+ const leafCenterX = slideSign * insideWidth * openAmount;
441
+ const faceZ = frameDepth / 2 + leafDepth / 2 + 0.028;
442
+ const trackY = leafCenterY + leafHeight / 2 + Math.max(frameThickness * 0.55, 0.045);
443
+ const railLength = insideWidth * 2.25;
444
+ const railCenterX = slideSign * (insideWidth * 0.56);
445
+ const handleY = handleHeight - doorHeight / 2;
446
+ const handleX = leafCenterX - slideSign * (leafWidth / 2 - 0.075);
447
+ const wheelY = trackY - 0.075;
448
+ addBox(mesh, revealMaterial, railLength, 0.035, 0.035, railCenterX, trackY, faceZ + 0.01);
449
+ addBox(mesh, revealMaterial, 0.05, 0.13, 0.035, -insideWidth / 2, trackY - 0.02, faceZ + 0.01);
450
+ addBox(mesh, revealMaterial, 0.05, 0.13, 0.035, insideWidth / 2, trackY - 0.02, faceZ + 0.01);
451
+ const addBarnLeafBox = (material, w, h, d, x, y, z) => addBox(mesh, material, w, h, d, x, y, faceZ + z);
452
+ addLeafSegmentContent({
453
+ addLeafBox: addBarnLeafBox,
454
+ leafWidth,
455
+ leafHeight,
456
+ leafCenterX,
457
+ leafCenterY,
458
+ leafDepth,
459
+ segments,
460
+ contentPadding,
461
+ keepFrameWhenEmpty: true,
462
+ });
463
+ addRotatedBox(mesh, revealMaterial, 0.018, leafHeight * 0.86, 0.012, leafCenterX, leafCenterY, faceZ + leafDepth / 2 + 0.014, -0.52);
464
+ addRotatedBox(mesh, revealMaterial, 0.018, leafHeight * 0.86, 0.012, leafCenterX, leafCenterY, faceZ + leafDepth / 2 + 0.014, 0.52);
465
+ for (const offset of [-leafWidth * 0.28, leafWidth * 0.28]) {
466
+ addBox(mesh, revealMaterial, 0.085, 0.085, 0.035, leafCenterX + offset, wheelY, faceZ + 0.022);
467
+ addBox(mesh, revealMaterial, 0.026, 0.16, 0.026, leafCenterX + offset, wheelY - 0.075, faceZ + 0.022);
468
+ }
469
+ addBox(mesh, baseMaterial, 0.032, 0.22, leafDepth + 0.034, handleX, handleY, faceZ + leafDepth / 2 + 0.02);
470
+ addBox(mesh, baseMaterial, 0.032, 0.22, leafDepth + 0.034, handleX, handleY, faceZ - leafDepth / 2 - 0.02);
471
+ }
472
+ function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
473
+ const openAmount = clampDoorOperationState(operationState);
474
+ const activeOnRight = slideDirection === 'left';
475
+ const fixedSign = activeOnRight ? -1 : 1;
476
+ const activeSign = activeOnRight ? 1 : -1;
477
+ const panelWidth = insideWidth * 0.54;
478
+ const panelHeight = leafHeight;
479
+ const closedActiveX = activeSign * insideWidth * 0.23;
480
+ const fixedX = fixedSign * insideWidth * 0.23;
481
+ const activeX = closedActiveX - activeSign * insideWidth * 0.44 * openAmount;
482
+ const frontZ = leafDepth / 2 + 0.016;
483
+ const backZ = -leafDepth / 2 - 0.006;
484
+ const railY = leafCenterY + panelHeight / 2 - Math.min(frameThickness * 0.35, 0.02);
485
+ const handleY = handleHeight - doorHeight / 2;
486
+ const handleX = activeX + activeSign * (panelWidth / 2 - 0.06);
487
+ addBox(mesh, revealMaterial, insideWidth, 0.024, Math.max(frameDepth * 0.32, 0.026), 0, railY, 0);
488
+ addBox(mesh, revealMaterial, insideWidth, 0.018, Math.max(frameDepth * 0.28, 0.022), 0, -leafHeight / 2 + 0.04, 0);
489
+ const addFixedPanelBox = (material, w, h, d, x, y, z) => addBox(mesh, material, w, h, d, x + fixedX, y, z + backZ);
490
+ const addActivePanelBox = (material, w, h, d, x, y, z) => addBox(mesh, material, w, h, d, x + activeX, y, z + frontZ);
491
+ addLeafSegmentContent({
492
+ addLeafBox: addFixedPanelBox,
493
+ leafWidth: panelWidth,
494
+ leafHeight: panelHeight,
495
+ leafCenterX: 0,
496
+ leafCenterY,
497
+ leafDepth,
498
+ segments,
499
+ contentPadding,
500
+ keepFrameWhenEmpty: true,
501
+ });
502
+ addLeafSegmentContent({
503
+ addLeafBox: addActivePanelBox,
504
+ leafWidth: panelWidth,
505
+ leafHeight: panelHeight,
506
+ leafCenterX: 0,
507
+ leafCenterY,
508
+ leafDepth,
509
+ segments,
510
+ contentPadding,
511
+ keepFrameWhenEmpty: true,
512
+ });
513
+ addBox(mesh, baseMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ + leafDepth / 2 + 0.01);
514
+ addBox(mesh, baseMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ - leafDepth / 2 - 0.01);
515
+ }
516
+ function addGarageSectionalDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, garagePanelCount, }) {
517
+ const openAmount = getDoorRenderOpenAmount('garage-sectional', operationState);
518
+ const panelCount = Math.max(3, Math.min(12, Math.round(garagePanelCount)));
519
+ const panelHeight = leafHeight / panelCount;
520
+ const panelGap = Math.min(0.012, panelHeight * 0.08);
521
+ const travelDepth = Math.max(leafHeight, 1.4);
522
+ const curveRadius = panelHeight * 0.58;
523
+ const curveLength = (Math.PI / 2) * curveRadius;
524
+ const travel = openAmount * ((panelCount - 1) * panelHeight + curveLength + panelHeight * 0.65);
525
+ const overheadY = leafCenterY + leafHeight / 2 - panelHeight / 2;
526
+ const railY = leafCenterY + leafHeight / 2 - 0.04;
527
+ const railZ = -travelDepth / 2;
528
+ addBox(mesh, revealMaterial, 0.035, Math.max(0.04, frameThickness * 0.75), travelDepth, -insideWidth / 2 + 0.035, railY, railZ);
529
+ addBox(mesh, revealMaterial, 0.035, Math.max(0.04, frameThickness * 0.75), travelDepth, insideWidth / 2 - 0.035, railY, railZ);
530
+ for (let index = 0; index < panelCount; index++) {
531
+ const orderFromTop = panelCount - 1 - index;
532
+ const pathPosition = travel - orderFromTop * panelHeight;
533
+ let y = overheadY + pathPosition;
534
+ let z = 0;
535
+ let rotationX = 0;
536
+ if (pathPosition > 0 && pathPosition <= curveLength) {
537
+ const theta = pathPosition / curveRadius;
538
+ rotationX = -theta;
539
+ y = overheadY + curveRadius * Math.sin(theta);
540
+ z = -curveRadius * (1 - Math.cos(theta));
541
+ }
542
+ else if (pathPosition > curveLength) {
543
+ rotationX = -Math.PI / 2;
544
+ y = overheadY + curveRadius;
545
+ z = -(curveRadius + pathPosition - curveLength);
546
+ }
547
+ const revealOffset = (panelHeight - panelGap) * 0.22;
548
+ const trimDepth = 0.01;
549
+ const trimFaceOffset = leafDepth / 2 + trimDepth + 0.006;
550
+ const addSectionalTrim = (localY) => {
551
+ addBoxWithRotation(mesh, revealMaterial, insideWidth - 0.16, 0.012, trimDepth, 0, y + localY * Math.cos(rotationX) - trimFaceOffset * Math.sin(rotationX), z + localY * Math.sin(rotationX) + trimFaceOffset * Math.cos(rotationX), [rotationX, 0, 0]);
552
+ };
553
+ addBoxWithRotation(mesh, baseMaterial, insideWidth, Math.max(0.04, panelHeight - panelGap), leafDepth, 0, y, z, [rotationX, 0, 0]);
554
+ addSectionalTrim(revealOffset);
555
+ addSectionalTrim(-revealOffset);
556
+ }
557
+ addBox(mesh, revealMaterial, insideWidth, 0.032, Math.max(frameDepth * 0.36, 0.03), 0, railY, 0);
558
+ }
559
+ function addGarageRollupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, }) {
560
+ const openAmount = clampDoorOperationState(operationState);
561
+ const slatHeight = Math.max(0.055, Math.min(0.11, leafHeight / 22));
562
+ const visibleHeight = leafHeight * (1 - openAmount);
563
+ const visibleSlatCount = Math.ceil(visibleHeight / slatHeight);
564
+ const topY = leafCenterY + leafHeight / 2;
565
+ const curtainCenterY = topY - visibleHeight / 2;
566
+ const drumMaxRadius = Math.max(0.12, Math.min(0.22, leafHeight * 0.075));
567
+ const drumY = topY + drumMaxRadius * 0.12;
568
+ const drumZ = -frameDepth / 2 - drumMaxRadius * 0.72;
569
+ addBox(mesh, revealMaterial, 0.032, leafHeight, Math.max(frameDepth * 0.48, 0.035), -insideWidth / 2 + 0.03, leafCenterY, 0);
570
+ addBox(mesh, revealMaterial, 0.032, leafHeight, Math.max(frameDepth * 0.48, 0.035), insideWidth / 2 - 0.03, leafCenterY, 0);
571
+ if (visibleHeight > 0.01) {
572
+ addBox(mesh, baseMaterial, insideWidth, visibleHeight, leafDepth, 0, curtainCenterY, 0);
573
+ for (let index = 0; index < visibleSlatCount; index++) {
574
+ const y = topY - Math.min(visibleHeight, index * slatHeight);
575
+ addBox(mesh, revealMaterial, insideWidth - 0.08, 0.01, 0.012, 0, y, leafDepth / 2 + 0.012);
576
+ }
577
+ addBox(mesh, revealMaterial, insideWidth - 0.04, 0.028, leafDepth + 0.018, 0, topY - visibleHeight, leafDepth / 2 + 0.004);
578
+ }
579
+ const drum = new THREE.Mesh(new THREE.CylinderGeometry(drumMaxRadius, drumMaxRadius, insideWidth + frameThickness, 36), baseMaterial);
580
+ drum.position.set(0, drumY, drumZ);
581
+ drum.rotation.z = Math.PI / 2;
582
+ mesh.add(drum);
583
+ addBox(mesh, revealMaterial, insideWidth + frameThickness, 0.026, Math.max(frameDepth * 0.52, 0.04), 0, topY + 0.02, 0);
584
+ }
585
+ function addGarageTiltupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, }) {
586
+ const openAmount = clampDoorOperationState(operationState);
587
+ const angle = (Math.PI / 2) * openAmount;
588
+ const hingeY = leafCenterY + leafHeight / 2;
589
+ const panelCenterY = hingeY - Math.cos(angle) * (leafHeight / 2);
590
+ const panelCenterZ = -Math.sin(angle) * (leafHeight / 2);
591
+ const railLength = Math.max(leafHeight * 0.72, 1.2);
592
+ const railY = hingeY - frameThickness * 0.35;
593
+ const railZ = -railLength / 2;
594
+ addBox(mesh, revealMaterial, 0.03, Math.max(frameThickness * 0.7, 0.035), railLength, -insideWidth / 2 + 0.04, railY, railZ);
595
+ addBox(mesh, revealMaterial, 0.03, Math.max(frameThickness * 0.7, 0.035), railLength, insideWidth / 2 - 0.04, railY, railZ);
596
+ addBoxWithRotation(mesh, baseMaterial, insideWidth, leafHeight, leafDepth, 0, panelCenterY, panelCenterZ, [-angle, 0, 0]);
597
+ const insetWidth = Math.max(0.1, insideWidth - 0.22);
598
+ const insetHeight = Math.max(0.1, leafHeight - 0.28);
599
+ const trimDepth = 0.012;
600
+ const trimFaceOffset = leafDepth / 2 + trimDepth + 0.006;
601
+ const addTiltupTrim = (localX, localY, trimWidth, trimHeight) => {
602
+ addBoxWithRotation(mesh, revealMaterial, trimWidth, trimHeight, trimDepth, localX, panelCenterY + localY * Math.cos(angle) + trimFaceOffset * Math.sin(angle), panelCenterZ - localY * Math.sin(angle) + trimFaceOffset * Math.cos(angle), [-angle, 0, 0]);
603
+ };
604
+ addTiltupTrim(0, insetHeight / 2, insetWidth, 0.018);
605
+ addTiltupTrim(0, -insetHeight / 2, insetWidth, 0.018);
606
+ addTiltupTrim(-insetWidth / 2, 0, 0.018, insetHeight);
607
+ addTiltupTrim(insetWidth / 2, 0, 0.018, insetHeight);
608
+ addBox(mesh, revealMaterial, insideWidth, 0.026, Math.max(frameDepth * 0.4, 0.035), 0, hingeY, 0);
609
+ }
610
+ function updateDoorMesh(node, mesh) {
611
+ // Root mesh is an invisible hitbox; all visuals live in child meshes
612
+ mesh.geometry.dispose();
613
+ mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
614
+ mesh.material = hitboxMaterial;
615
+ // Sync transform from node (React may lag behind the system by a frame during drag)
616
+ mesh.position.set(node.position[0], node.position[1], node.position[2]);
617
+ mesh.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2]);
618
+ // Dispose and remove all old visual children; preserve 'cutout'
619
+ for (const child of [...mesh.children]) {
620
+ if (child.name === 'cutout')
621
+ continue;
622
+ disposeObject(child);
623
+ mesh.remove(child);
624
+ }
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;
626
+ const runtimeDoorState = useInteractive.getState().doors[node.id];
627
+ const swingAngle = runtimeDoorState?.swingAngle ?? nodeSwingAngle;
628
+ const operationState = runtimeDoorState?.operationState ?? nodeOperationState;
629
+ const clampedSwingAngle = Math.max(0, Math.min(Math.PI / 2, swingAngle));
630
+ if (openingKind === 'opening') {
631
+ syncDoorCutout(node, mesh);
632
+ return;
633
+ }
634
+ const insideWidth = width - 2 * frameThickness;
635
+ const leafH = height - frameThickness; // only top frame
636
+ const leafDepth = 0.04;
637
+ const leafCenterY = -frameThickness / 2;
638
+ const swingDirectionSign = swingDirection === 'inward' ? 1 : -1;
639
+ // ── Frame members ──
640
+ if (openingShape === 'arch') {
641
+ const frameBottom = -height / 2;
642
+ const frameTop = height / 2;
643
+ const frameArchHeight = getClampedArchHeight(width, height, node.archHeight);
644
+ const frameSpringY = frameTop - frameArchHeight;
645
+ const frameInnerTopY = frameTop - frameThickness;
646
+ const frameInnerSpringY = Math.min(frameSpringY + frameThickness, frameInnerTopY);
647
+ const useShallowHeadBar = frameArchHeight <= frameThickness * 2;
648
+ const frameHeadBottomY = useShallowHeadBar ? frameSpringY - frameThickness : frameSpringY;
649
+ const postHeight = Math.max(frameHeadBottomY - frameBottom, 0.01);
650
+ addBox(mesh, baseMaterial, frameThickness, postHeight, frameDepth, -width / 2 + frameThickness / 2, frameBottom + postHeight / 2, 0);
651
+ addBox(mesh, baseMaterial, frameThickness, postHeight, frameDepth, width / 2 - frameThickness / 2, frameBottom + postHeight / 2, 0);
652
+ addShape(mesh, baseMaterial, useShallowHeadBar
653
+ ? createArchHeadBarShape(width, frameHeadBottomY, frameSpringY, frameTop)
654
+ : createArchBandShape(width, frameSpringY, frameTop, frameInnerSpringY, frameInnerTopY, frameThickness), frameDepth);
655
+ }
656
+ else if (openingShape === 'rounded') {
657
+ addShape(mesh, baseMaterial, createRoundedDoorFrameShape(width, height, frameThickness, getDoorTopRadii(node, width, height)), frameDepth);
658
+ }
659
+ else {
660
+ // Left post — full height
661
+ addBox(mesh, baseMaterial, frameThickness, height, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
662
+ // Right post — full height
663
+ addBox(mesh, baseMaterial, frameThickness, height, frameDepth, width / 2 - frameThickness / 2, 0, 0);
664
+ // Head (top bar) — full width
665
+ addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
666
+ }
667
+ // ── Threshold (inside the frame) ──
668
+ if (threshold) {
669
+ addBox(mesh, baseMaterial, insideWidth, thresholdHeight, frameDepth, 0, -height / 2 + thresholdHeight / 2, 0);
670
+ }
671
+ if (doorType === 'garage-sectional') {
672
+ addGarageSectionalDoor(mesh, {
673
+ insideWidth,
674
+ leafHeight: leafH,
675
+ leafCenterY,
676
+ leafDepth,
677
+ frameThickness,
678
+ frameDepth,
679
+ operationState,
680
+ garagePanelCount,
681
+ });
682
+ }
683
+ else if (doorType === 'garage-rollup') {
684
+ addGarageRollupDoor(mesh, {
685
+ insideWidth,
686
+ leafHeight: leafH,
687
+ leafCenterY,
688
+ leafDepth,
689
+ frameThickness,
690
+ frameDepth,
691
+ operationState,
692
+ });
693
+ }
694
+ else if (doorType === 'garage-tiltup') {
695
+ addGarageTiltupDoor(mesh, {
696
+ insideWidth,
697
+ leafHeight: leafH,
698
+ leafCenterY,
699
+ leafDepth,
700
+ frameThickness,
701
+ frameDepth,
702
+ operationState,
703
+ });
704
+ }
705
+ else if (doorType === 'folding') {
706
+ addFoldingDoor(mesh, {
707
+ insideWidth,
708
+ leafHeight: leafH,
709
+ leafCenterY,
710
+ leafDepth,
711
+ frameThickness,
712
+ frameDepth,
713
+ operationState,
714
+ leafCount,
715
+ doorHeight: height,
716
+ handleHeight,
717
+ segments,
718
+ contentPadding,
719
+ });
720
+ }
721
+ else if (doorType === 'pocket') {
722
+ addPocketDoor(mesh, {
723
+ insideWidth,
724
+ leafHeight: leafH,
725
+ leafCenterY,
726
+ leafDepth,
727
+ frameThickness,
728
+ frameDepth,
729
+ operationState,
730
+ slideDirection,
731
+ doorHeight: height,
732
+ handleHeight,
733
+ segments,
734
+ contentPadding,
735
+ });
736
+ }
737
+ else if (doorType === 'barn') {
738
+ addBarnDoor(mesh, {
739
+ insideWidth,
740
+ leafHeight: leafH,
741
+ leafCenterY,
742
+ leafDepth,
743
+ frameThickness,
744
+ frameDepth,
745
+ operationState,
746
+ slideDirection,
747
+ doorHeight: height,
748
+ handleHeight,
749
+ segments,
750
+ contentPadding,
751
+ });
752
+ }
753
+ else if (doorType === 'sliding') {
754
+ addSlidingDoor(mesh, {
755
+ insideWidth,
756
+ leafHeight: leafH,
757
+ leafCenterY,
758
+ leafDepth,
759
+ frameThickness,
760
+ frameDepth,
761
+ operationState,
762
+ slideDirection,
763
+ doorHeight: height,
764
+ handleHeight,
765
+ segments,
766
+ contentPadding,
767
+ });
768
+ }
769
+ else if (doorType === 'double' || doorType === 'french') {
770
+ const doubleLeafW = insideWidth / 2;
771
+ addDoorLeaf(mesh, {
772
+ leafWidth: doubleLeafW,
773
+ leafHeight: leafH,
774
+ leafCenterX: -insideWidth / 4,
775
+ leafCenterY,
776
+ leafDepth,
777
+ hingeX: -insideWidth / 2,
778
+ hingeSide: 'left',
779
+ swingRotation: -clampedSwingAngle * swingDirectionSign,
780
+ segments,
781
+ contentPadding,
782
+ handle,
783
+ handleBothSides: doorType === 'double' || doorType === 'french',
784
+ handleHeight,
785
+ handleSide: 'right',
786
+ doorCloser,
787
+ panicBar,
788
+ panicBarHeight,
789
+ doorHeight: height,
790
+ });
791
+ addDoorLeaf(mesh, {
792
+ leafWidth: doubleLeafW,
793
+ leafHeight: leafH,
794
+ leafCenterX: insideWidth / 4,
795
+ leafCenterY,
796
+ leafDepth,
797
+ hingeX: insideWidth / 2,
798
+ hingeSide: 'right',
799
+ swingRotation: clampedSwingAngle * swingDirectionSign,
800
+ segments,
801
+ contentPadding,
802
+ handle,
803
+ handleBothSides: doorType === 'double' || doorType === 'french',
804
+ handleHeight,
805
+ handleSide: 'left',
806
+ doorCloser: false,
807
+ panicBar,
808
+ panicBarHeight,
809
+ doorHeight: height,
810
+ });
811
+ }
812
+ else {
813
+ const hingeX = hingesSide === 'right' ? insideWidth / 2 : -insideWidth / 2;
814
+ const hingeDirectionSign = hingesSide === 'right' ? 1 : -1;
815
+ addDoorLeaf(mesh, {
816
+ leafWidth: insideWidth,
817
+ leafHeight: leafH,
818
+ leafCenterX: 0,
819
+ leafCenterY,
820
+ leafDepth,
821
+ hingeX,
822
+ hingeSide: hingesSide,
823
+ swingRotation: clampedSwingAngle * swingDirectionSign * hingeDirectionSign,
824
+ segments,
825
+ contentPadding,
826
+ handle,
827
+ handleBothSides: doorType === 'hinged',
828
+ handleHeight,
829
+ handleSide,
830
+ doorCloser,
831
+ panicBar,
832
+ panicBarHeight,
833
+ doorHeight: height,
834
+ });
835
+ }
836
+ syncDoorCutout(node, mesh);
837
+ }
838
+ function syncDoorCutout(node, mesh) {
839
+ // ── Cutout (for wall CSG) — always full door dimensions, 1m deep ──
840
+ let cutout = mesh.getObjectByName('cutout');
841
+ if (!cutout) {
842
+ cutout = new THREE.Mesh();
843
+ cutout.name = 'cutout';
844
+ mesh.add(cutout);
845
+ }
846
+ cutout.geometry.dispose();
847
+ if (node.openingShape === 'arch') {
848
+ 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
+ depth: 1,
850
+ bevelEnabled: false,
851
+ curveSegments: 24,
852
+ });
853
+ cutout.geometry.translate(0, 0, -0.5);
854
+ }
855
+ else if (node.openingShape === 'rounded') {
856
+ 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
+ depth: 1,
858
+ bevelEnabled: false,
859
+ curveSegments: 24,
860
+ });
861
+ cutout.geometry.translate(0, 0, -0.5);
862
+ }
863
+ else {
864
+ cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0);
865
+ }
866
+ cutout.visible = false;
867
+ }