@pascal-app/viewer 0.6.0 → 0.8.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.
- package/dist/components/error-boundary.d.ts +7 -3
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +3 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.js +19 -3
- package/dist/components/renderers/column/column-renderer.d.ts +5 -0
- package/dist/components/renderers/column/column-renderer.d.ts.map +1 -0
- package/dist/components/renderers/column/column-renderer.js +597 -0
- package/dist/components/renderers/door/door-renderer.d.ts.map +1 -1
- package/dist/components/renderers/door/door-renderer.js +4 -9
- package/dist/components/renderers/fence/fence-renderer.d.ts.map +1 -1
- package/dist/components/renderers/fence/fence-renderer.js +2 -16
- package/dist/components/renderers/guide/guide-renderer.js +1 -1
- package/dist/components/renderers/item/item-renderer.d.ts.map +1 -1
- package/dist/components/renderers/item/item-renderer.js +2 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +3 -1
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -1
- package/dist/components/renderers/roof/roof-renderer.js +1 -13
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +1 -1
- package/dist/components/renderers/roof-segment/roof-segment-renderer.js +5 -20
- package/dist/components/renderers/slab/slab-renderer.d.ts.map +1 -1
- package/dist/components/renderers/slab/slab-renderer.js +38 -26
- package/dist/components/renderers/spawn/spawn-renderer.d.ts +5 -0
- package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +1 -0
- package/dist/components/renderers/spawn/spawn-renderer.js +32 -0
- package/dist/components/renderers/stair/stair-renderer.d.ts.map +1 -1
- package/dist/components/renderers/stair/stair-renderer.js +7 -25
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +1 -1
- package/dist/components/renderers/stair-segment/stair-segment-renderer.js +4 -2
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +5 -1
- package/dist/components/viewer/index.d.ts +3 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +89 -19
- package/dist/components/viewer/post-processing.d.ts +13 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +71 -35
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +19 -2
- package/dist/hooks/use-node-events.d.ts +9 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/lib/materials.d.ts +3 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +16 -3
- package/dist/lib/merged-outline-node.d.ts +2 -0
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +29 -16
- package/dist/store/use-viewer.d.ts +4 -4
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +2 -2
- package/dist/systems/ceiling/ceiling-system.d.ts +8 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -0
- package/dist/systems/ceiling/ceiling-system.js +91 -0
- package/dist/systems/column/column-geometry.d.ts +22 -0
- package/dist/systems/column/column-geometry.d.ts.map +1 -0
- package/dist/systems/column/column-geometry.js +108 -0
- package/dist/systems/door/door-animation-system.d.ts +2 -0
- package/dist/systems/door/door-animation-system.d.ts.map +1 -0
- package/dist/systems/door/door-animation-system.js +53 -0
- package/dist/systems/door/door-system.d.ts +2 -0
- package/dist/systems/door/door-system.d.ts.map +1 -0
- package/dist/systems/door/door-system.js +867 -0
- package/dist/systems/fence/fence-system.d.ts +2 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -0
- package/dist/systems/fence/fence-system.js +185 -0
- package/dist/systems/guide/guide-system.d.ts.map +1 -1
- package/dist/systems/guide/guide-system.js +1 -26
- package/dist/systems/item/item-system.d.ts +2 -0
- package/dist/systems/item/item-system.d.ts.map +1 -0
- package/dist/systems/item/item-system.js +44 -0
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +18 -1
- package/dist/systems/roof/roof-system.d.ts +16 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -0
- package/dist/systems/roof/roof-system.js +800 -0
- package/dist/systems/scan/scan-system.d.ts.map +1 -1
- package/dist/systems/scan/scan-system.js +1 -26
- package/dist/systems/slab/slab-system.d.ts +8 -0
- package/dist/systems/slab/slab-system.d.ts.map +1 -0
- package/dist/systems/slab/slab-system.js +155 -0
- package/dist/systems/stair/stair-materials.d.ts.map +1 -1
- package/dist/systems/stair/stair-materials.js +25 -2
- package/dist/systems/stair/stair-system.d.ts +2 -0
- package/dist/systems/stair/stair-system.d.ts.map +1 -0
- package/dist/systems/stair/stair-system.js +770 -0
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +2 -2
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -0
- package/dist/systems/wall/wall-system.js +595 -0
- package/dist/systems/window/window-animation-system.d.ts +2 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -0
- package/dist/systems/window/window-animation-system.js +135 -0
- package/dist/systems/window/window-system.d.ts +12 -0
- package/dist/systems/window/window-system.d.ts.map +1 -0
- package/dist/systems/window/window-system.js +1430 -0
- package/package.json +6 -4
- package/dist/lib/suppress-three-clock-warning.d.ts +0 -25
- package/dist/lib/suppress-three-clock-warning.d.ts.map +0 -1
- package/dist/lib/suppress-three-clock-warning.js +0 -51
- package/dist/systems/export/export-system.d.ts +0 -2
- package/dist/systems/export/export-system.d.ts.map +0 -1
- package/dist/systems/export/export-system.js +0 -60
|
@@ -0,0 +1,1430 @@
|
|
|
1
|
+
import { 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
|
+
export const CASEMENT_WINDOW_SASH_NAME = 'casement-window-sash';
|
|
8
|
+
export const FRENCH_CASEMENT_LEFT_SASH_NAME = 'french-casement-left-sash';
|
|
9
|
+
export const FRENCH_CASEMENT_RIGHT_SASH_NAME = 'french-casement-right-sash';
|
|
10
|
+
export const SLIDING_WINDOW_ACTIVE_PANEL_NAME = 'sliding-window-active-panel';
|
|
11
|
+
export const SINGLE_HUNG_ACTIVE_SASH_NAME = 'single-hung-active-sash';
|
|
12
|
+
export const DOUBLE_HUNG_TOP_SASH_NAME = 'double-hung-top-sash';
|
|
13
|
+
export const DOUBLE_HUNG_BOTTOM_SASH_NAME = 'double-hung-bottom-sash';
|
|
14
|
+
export const LOUVERED_WINDOW_SLATS_NAME = 'louvered-window-slats';
|
|
15
|
+
export const AWNING_WINDOW_SASH_NAME = 'awning-window-sash';
|
|
16
|
+
export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash';
|
|
17
|
+
export const WindowSystem = () => {
|
|
18
|
+
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
19
|
+
const clearDirty = useScene((state) => state.clearDirty);
|
|
20
|
+
useFrame(() => {
|
|
21
|
+
if (dirtyNodes.size === 0)
|
|
22
|
+
return;
|
|
23
|
+
const nodes = useScene.getState().nodes;
|
|
24
|
+
dirtyNodes.forEach((id) => {
|
|
25
|
+
const node = nodes[id];
|
|
26
|
+
if (!node || node.type !== 'window')
|
|
27
|
+
return;
|
|
28
|
+
const mesh = sceneRegistry.nodes.get(id);
|
|
29
|
+
if (!mesh)
|
|
30
|
+
return; // Keep dirty until mesh mounts
|
|
31
|
+
updateWindowMesh(node, mesh);
|
|
32
|
+
clearDirty(id);
|
|
33
|
+
// Rebuild the parent wall so its cutout reflects the updated window geometry
|
|
34
|
+
if (node.parentId) {
|
|
35
|
+
useScene.getState().dirtyNodes.add(node.parentId);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}, 3);
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
function addBox(parent, material, w, h, d, x, y, z) {
|
|
42
|
+
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
|
|
43
|
+
m.position.set(x, y, z);
|
|
44
|
+
parent.add(m);
|
|
45
|
+
}
|
|
46
|
+
function addShape(parent, material, shape, depth, z = 0) {
|
|
47
|
+
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
48
|
+
depth,
|
|
49
|
+
bevelEnabled: false,
|
|
50
|
+
curveSegments: 24,
|
|
51
|
+
});
|
|
52
|
+
geometry.translate(0, 0, -depth / 2 + z);
|
|
53
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
54
|
+
parent.add(mesh);
|
|
55
|
+
}
|
|
56
|
+
function disposeObjectGeometry(object) {
|
|
57
|
+
object.traverse((child) => {
|
|
58
|
+
if (child instanceof THREE.Mesh)
|
|
59
|
+
child.geometry.dispose();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function createRectShape(left, right, bottom, top) {
|
|
63
|
+
const shape = new THREE.Shape();
|
|
64
|
+
shape.moveTo(left, bottom);
|
|
65
|
+
shape.lineTo(right, bottom);
|
|
66
|
+
shape.lineTo(right, top);
|
|
67
|
+
shape.lineTo(left, top);
|
|
68
|
+
shape.closePath();
|
|
69
|
+
return shape;
|
|
70
|
+
}
|
|
71
|
+
function normalizeCornerRadii(radii, width, height) {
|
|
72
|
+
const next = { ...radii };
|
|
73
|
+
const scale = Math.min(1, width / Math.max(next.topLeft + next.topRight, 1e-6), width / Math.max(next.bottomLeft + next.bottomRight, 1e-6), height / Math.max(next.topLeft + next.bottomLeft, 1e-6), height / Math.max(next.topRight + next.bottomRight, 1e-6));
|
|
74
|
+
if (scale < 1) {
|
|
75
|
+
next.topLeft *= scale;
|
|
76
|
+
next.topRight *= scale;
|
|
77
|
+
next.bottomRight *= scale;
|
|
78
|
+
next.bottomLeft *= scale;
|
|
79
|
+
}
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
function getWindowRoundedRadii(node, width, height) {
|
|
83
|
+
if (node.openingRadiusMode === 'individual') {
|
|
84
|
+
const [topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0] = node.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15];
|
|
85
|
+
return normalizeCornerRadii({
|
|
86
|
+
topLeft: Math.max(topLeft, 0),
|
|
87
|
+
topRight: Math.max(topRight, 0),
|
|
88
|
+
bottomRight: Math.max(bottomRight, 0),
|
|
89
|
+
bottomLeft: Math.max(bottomLeft, 0),
|
|
90
|
+
}, width, height);
|
|
91
|
+
}
|
|
92
|
+
const maxRadius = Math.min(width / 2, height / 2);
|
|
93
|
+
const radius = Math.min(Math.max(node.cornerRadius ?? 0.15, 0), maxRadius);
|
|
94
|
+
return { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius };
|
|
95
|
+
}
|
|
96
|
+
function insetCornerRadii(radii, inset, width, height) {
|
|
97
|
+
return normalizeCornerRadii({
|
|
98
|
+
topLeft: Math.max(radii.topLeft - inset, 0),
|
|
99
|
+
topRight: Math.max(radii.topRight - inset, 0),
|
|
100
|
+
bottomRight: Math.max(radii.bottomRight - inset, 0),
|
|
101
|
+
bottomLeft: Math.max(radii.bottomLeft - inset, 0),
|
|
102
|
+
}, width, height);
|
|
103
|
+
}
|
|
104
|
+
function createRoundedShape(left, right, bottom, top, radii) {
|
|
105
|
+
const shape = new THREE.Shape();
|
|
106
|
+
const { topLeft, topRight, bottomRight, bottomLeft } = radii;
|
|
107
|
+
shape.moveTo(left + bottomLeft, bottom);
|
|
108
|
+
shape.lineTo(right - bottomRight, bottom);
|
|
109
|
+
if (bottomRight > 1e-6) {
|
|
110
|
+
shape.absarc(right - bottomRight, bottom + bottomRight, bottomRight, -Math.PI / 2, 0, false);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
shape.lineTo(right, bottom);
|
|
114
|
+
}
|
|
115
|
+
shape.lineTo(right, top - topRight);
|
|
116
|
+
if (topRight > 1e-6) {
|
|
117
|
+
shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
shape.lineTo(right, top);
|
|
121
|
+
}
|
|
122
|
+
shape.lineTo(left + topLeft, top);
|
|
123
|
+
if (topLeft > 1e-6) {
|
|
124
|
+
shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
shape.lineTo(left, top);
|
|
128
|
+
}
|
|
129
|
+
shape.lineTo(left, bottom + bottomLeft);
|
|
130
|
+
if (bottomLeft > 1e-6) {
|
|
131
|
+
shape.absarc(left + bottomLeft, bottom + bottomLeft, bottomLeft, Math.PI, Math.PI * 1.5, false);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
shape.lineTo(left, bottom);
|
|
135
|
+
}
|
|
136
|
+
shape.closePath();
|
|
137
|
+
return shape;
|
|
138
|
+
}
|
|
139
|
+
function createRoundedFrameShape(width, height, frameThickness, outerRadii) {
|
|
140
|
+
const halfWidth = width / 2;
|
|
141
|
+
const bottom = -height / 2;
|
|
142
|
+
const top = height / 2;
|
|
143
|
+
const outer = createRoundedShape(-halfWidth, halfWidth, bottom, top, outerRadii);
|
|
144
|
+
const inset = Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005);
|
|
145
|
+
if (inset <= 0.001)
|
|
146
|
+
return outer;
|
|
147
|
+
const innerLeft = -halfWidth + inset;
|
|
148
|
+
const innerRight = halfWidth - inset;
|
|
149
|
+
const innerBottom = bottom + inset;
|
|
150
|
+
const innerTop = top - inset;
|
|
151
|
+
const innerRadii = insetCornerRadii(outerRadii, inset, innerRight - innerLeft, innerTop - innerBottom);
|
|
152
|
+
const holeShape = createRoundedShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii);
|
|
153
|
+
const hole = new THREE.Path(holeShape.getPoints(32).reverse());
|
|
154
|
+
outer.holes.push(hole);
|
|
155
|
+
return outer;
|
|
156
|
+
}
|
|
157
|
+
function getClampedArchHeight(width, height, archHeight) {
|
|
158
|
+
return Math.min(Math.max(archHeight ?? width / 2, 0.01), Math.max(height, 0.01));
|
|
159
|
+
}
|
|
160
|
+
function createArchShape(left, right, bottom, top, archHeight) {
|
|
161
|
+
const centerX = (left + right) / 2;
|
|
162
|
+
const halfWidth = (right - left) / 2;
|
|
163
|
+
const clampedArchHeight = getClampedArchHeight(right - left, top - bottom, archHeight);
|
|
164
|
+
const springY = top - clampedArchHeight;
|
|
165
|
+
const shape = new THREE.Shape();
|
|
166
|
+
const segments = 32;
|
|
167
|
+
shape.moveTo(left, bottom);
|
|
168
|
+
shape.lineTo(right, bottom);
|
|
169
|
+
shape.lineTo(right, springY);
|
|
170
|
+
for (let index = 1; index <= segments; index += 1) {
|
|
171
|
+
const x = right + (left - right) * (index / segments);
|
|
172
|
+
shape.lineTo(x, getArchBoundaryY(x - centerX, halfWidth, springY, clampedArchHeight));
|
|
173
|
+
}
|
|
174
|
+
shape.lineTo(left, bottom);
|
|
175
|
+
shape.closePath();
|
|
176
|
+
return shape;
|
|
177
|
+
}
|
|
178
|
+
function createArchedFrameShape(width, height, archHeight, frameThickness) {
|
|
179
|
+
const halfWidth = width / 2;
|
|
180
|
+
const bottom = -height / 2;
|
|
181
|
+
const top = height / 2;
|
|
182
|
+
const outer = createArchShape(-halfWidth, halfWidth, bottom, top, archHeight);
|
|
183
|
+
const inset = Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005);
|
|
184
|
+
if (inset <= 0.001)
|
|
185
|
+
return outer;
|
|
186
|
+
const innerLeft = -halfWidth + inset;
|
|
187
|
+
const innerRight = halfWidth - inset;
|
|
188
|
+
const innerBottom = bottom + inset;
|
|
189
|
+
const innerTop = top - inset;
|
|
190
|
+
const innerArchHeight = getClampedArchHeight(innerRight - innerLeft, innerTop - innerBottom, archHeight - inset);
|
|
191
|
+
const hole = new THREE.Path(createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight)
|
|
192
|
+
.getPoints(32)
|
|
193
|
+
.reverse());
|
|
194
|
+
outer.holes.push(hole);
|
|
195
|
+
return outer;
|
|
196
|
+
}
|
|
197
|
+
function getArchBoundaryY(x, halfWidth, springY, archHeight) {
|
|
198
|
+
if (halfWidth <= 1e-6)
|
|
199
|
+
return springY;
|
|
200
|
+
const t = Math.min(Math.abs(x) / halfWidth, 1);
|
|
201
|
+
return springY + archHeight * Math.sqrt(Math.max(1 - t * t, 0));
|
|
202
|
+
}
|
|
203
|
+
function getArchedOpeningHalfWidthAtY(y, halfWidth, springY, archHeight) {
|
|
204
|
+
if (y <= springY || archHeight <= 1e-6)
|
|
205
|
+
return halfWidth;
|
|
206
|
+
const normalizedY = Math.min(Math.max((y - springY) / archHeight, 0), 1);
|
|
207
|
+
return halfWidth * Math.sqrt(Math.max(1 - normalizedY * normalizedY, 0));
|
|
208
|
+
}
|
|
209
|
+
function getRoundedBoundaryYAtX(x, left, right, top, radii) {
|
|
210
|
+
if (radii.topLeft > 1e-6 && x < left + radii.topLeft) {
|
|
211
|
+
const centerX = left + radii.topLeft;
|
|
212
|
+
const centerY = top - radii.topLeft;
|
|
213
|
+
const dx = x - centerX;
|
|
214
|
+
return centerY + Math.sqrt(Math.max(radii.topLeft * radii.topLeft - dx * dx, 0));
|
|
215
|
+
}
|
|
216
|
+
if (radii.topRight > 1e-6 && x > right - radii.topRight) {
|
|
217
|
+
const centerX = right - radii.topRight;
|
|
218
|
+
const centerY = top - radii.topRight;
|
|
219
|
+
const dx = x - centerX;
|
|
220
|
+
return centerY + Math.sqrt(Math.max(radii.topRight * radii.topRight - dx * dx, 0));
|
|
221
|
+
}
|
|
222
|
+
return top;
|
|
223
|
+
}
|
|
224
|
+
function getRoundedBottomBoundaryYAtX(x, left, right, bottom, radii) {
|
|
225
|
+
if (radii.bottomLeft > 1e-6 && x < left + radii.bottomLeft) {
|
|
226
|
+
const centerX = left + radii.bottomLeft;
|
|
227
|
+
const centerY = bottom + radii.bottomLeft;
|
|
228
|
+
const dx = x - centerX;
|
|
229
|
+
return centerY - Math.sqrt(Math.max(radii.bottomLeft * radii.bottomLeft - dx * dx, 0));
|
|
230
|
+
}
|
|
231
|
+
if (radii.bottomRight > 1e-6 && x > right - radii.bottomRight) {
|
|
232
|
+
const centerX = right - radii.bottomRight;
|
|
233
|
+
const centerY = bottom + radii.bottomRight;
|
|
234
|
+
const dx = x - centerX;
|
|
235
|
+
return centerY - Math.sqrt(Math.max(radii.bottomRight * radii.bottomRight - dx * dx, 0));
|
|
236
|
+
}
|
|
237
|
+
return bottom;
|
|
238
|
+
}
|
|
239
|
+
function getRoundedHorizontalBoundsAtY(y, left, right, top, radii) {
|
|
240
|
+
let minX = left;
|
|
241
|
+
let maxX = right;
|
|
242
|
+
if (radii.topLeft > 1e-6 && y > top - radii.topLeft) {
|
|
243
|
+
const centerX = left + radii.topLeft;
|
|
244
|
+
const centerY = top - radii.topLeft;
|
|
245
|
+
const dy = y - centerY;
|
|
246
|
+
minX = centerX - Math.sqrt(Math.max(radii.topLeft * radii.topLeft - dy * dy, 0));
|
|
247
|
+
}
|
|
248
|
+
if (radii.topRight > 1e-6 && y > top - radii.topRight) {
|
|
249
|
+
const centerX = right - radii.topRight;
|
|
250
|
+
const centerY = top - radii.topRight;
|
|
251
|
+
const dy = y - centerY;
|
|
252
|
+
maxX = centerX + Math.sqrt(Math.max(radii.topRight * radii.topRight - dy * dy, 0));
|
|
253
|
+
}
|
|
254
|
+
return { minX, maxX };
|
|
255
|
+
}
|
|
256
|
+
function addRoundedWindowVisuals(node, mesh) {
|
|
257
|
+
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, } = node;
|
|
258
|
+
const halfWidth = width / 2;
|
|
259
|
+
const bottom = -height / 2;
|
|
260
|
+
const top = height / 2;
|
|
261
|
+
const outerRadii = getWindowRoundedRadii(node, width, height);
|
|
262
|
+
const inset = Math.max(0, Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005));
|
|
263
|
+
const innerLeft = -halfWidth + inset;
|
|
264
|
+
const innerRight = halfWidth - inset;
|
|
265
|
+
const innerBottom = bottom + inset;
|
|
266
|
+
const innerTop = top - inset;
|
|
267
|
+
const innerW = innerRight - innerLeft;
|
|
268
|
+
const innerH = innerTop - innerBottom;
|
|
269
|
+
const innerRadii = insetCornerRadii(outerRadii, inset, innerW, innerH);
|
|
270
|
+
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, inset, outerRadii), frameDepth);
|
|
271
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
272
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
273
|
+
addShape(mesh, glassMaterial, createRoundedShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii), glassDepth);
|
|
274
|
+
const numCols = columnRatios.length;
|
|
275
|
+
const numRows = rowRatios.length;
|
|
276
|
+
const usableW = innerW - (numCols - 1) * columnDividerThickness;
|
|
277
|
+
const usableH = innerH - (numRows - 1) * rowDividerThickness;
|
|
278
|
+
const colSum = columnRatios.reduce((a, b) => a + b, 0);
|
|
279
|
+
const rowSum = rowRatios.reduce((a, b) => a + b, 0);
|
|
280
|
+
const colWidths = columnRatios.map((r) => (r / colSum) * usableW);
|
|
281
|
+
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
|
|
282
|
+
let x = innerLeft;
|
|
283
|
+
for (let c = 0; c < numCols - 1; c++) {
|
|
284
|
+
x += colWidths[c];
|
|
285
|
+
const x1 = x;
|
|
286
|
+
const x2 = x + columnDividerThickness;
|
|
287
|
+
const dividerTop = Math.min(getRoundedBoundaryYAtX(x1, innerLeft, innerRight, innerTop, innerRadii), getRoundedBoundaryYAtX(x2, innerLeft, innerRight, innerTop, innerRadii));
|
|
288
|
+
if (dividerTop > innerBottom + 0.01) {
|
|
289
|
+
addShape(mesh, baseMaterial, createRectShape(x1, x2, innerBottom, dividerTop), frameDepth + 0.001);
|
|
290
|
+
}
|
|
291
|
+
x += columnDividerThickness;
|
|
292
|
+
}
|
|
293
|
+
let y = innerTop;
|
|
294
|
+
for (let r = 0; r < numRows - 1; r++) {
|
|
295
|
+
y -= rowHeights[r];
|
|
296
|
+
const yTop = y;
|
|
297
|
+
const yBottom = y - rowDividerThickness;
|
|
298
|
+
const { minX, maxX } = getRoundedHorizontalBoundsAtY(yTop, innerLeft, innerRight, innerTop, innerRadii);
|
|
299
|
+
if (maxX - minX > 0.01 && yTop > innerBottom) {
|
|
300
|
+
addShape(mesh, baseMaterial, createRectShape(minX, maxX, Math.max(yBottom, innerBottom), yTop), frameDepth + 0.001);
|
|
301
|
+
}
|
|
302
|
+
y -= rowDividerThickness;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (sill) {
|
|
306
|
+
const sillW = width + sillDepth * 0.4;
|
|
307
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
308
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function addArchedWindowVisuals(node, mesh) {
|
|
312
|
+
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, } = node;
|
|
313
|
+
const halfWidth = width / 2;
|
|
314
|
+
const bottom = -height / 2;
|
|
315
|
+
const top = height / 2;
|
|
316
|
+
const archHeight = getClampedArchHeight(width, height, node.archHeight);
|
|
317
|
+
const inset = Math.max(0, Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005));
|
|
318
|
+
const innerLeft = -halfWidth + inset;
|
|
319
|
+
const innerRight = halfWidth - inset;
|
|
320
|
+
const innerBottom = bottom + inset;
|
|
321
|
+
const innerTop = top - inset;
|
|
322
|
+
const innerW = innerRight - innerLeft;
|
|
323
|
+
const innerH = innerTop - innerBottom;
|
|
324
|
+
const innerArchHeight = getClampedArchHeight(innerW, innerH, archHeight - inset);
|
|
325
|
+
const innerSpringY = innerTop - innerArchHeight;
|
|
326
|
+
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, archHeight, inset), frameDepth);
|
|
327
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
328
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
329
|
+
addShape(mesh, glassMaterial, createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight), glassDepth);
|
|
330
|
+
const numCols = columnRatios.length;
|
|
331
|
+
const numRows = rowRatios.length;
|
|
332
|
+
const usableW = innerW - (numCols - 1) * columnDividerThickness;
|
|
333
|
+
const usableH = innerH - (numRows - 1) * rowDividerThickness;
|
|
334
|
+
const colSum = columnRatios.reduce((a, b) => a + b, 0);
|
|
335
|
+
const rowSum = rowRatios.reduce((a, b) => a + b, 0);
|
|
336
|
+
const colWidths = columnRatios.map((r) => (r / colSum) * usableW);
|
|
337
|
+
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
|
|
338
|
+
const innerHalfWidth = innerW / 2;
|
|
339
|
+
let x = innerLeft;
|
|
340
|
+
for (let c = 0; c < numCols - 1; c++) {
|
|
341
|
+
x += colWidths[c];
|
|
342
|
+
const x1 = x;
|
|
343
|
+
const x2 = x + columnDividerThickness;
|
|
344
|
+
const dividerTop = Math.min(getArchBoundaryY(x1, innerHalfWidth, innerSpringY, innerArchHeight), getArchBoundaryY(x2, innerHalfWidth, innerSpringY, innerArchHeight));
|
|
345
|
+
if (dividerTop > innerBottom + 0.01) {
|
|
346
|
+
addShape(mesh, baseMaterial, createRectShape(x1, x2, innerBottom, dividerTop), frameDepth + 0.001);
|
|
347
|
+
}
|
|
348
|
+
x += columnDividerThickness;
|
|
349
|
+
}
|
|
350
|
+
let y = innerTop;
|
|
351
|
+
for (let r = 0; r < numRows - 1; r++) {
|
|
352
|
+
y -= rowHeights[r];
|
|
353
|
+
const yTop = y;
|
|
354
|
+
const yBottom = y - rowDividerThickness;
|
|
355
|
+
const halfAtTop = getArchedOpeningHalfWidthAtY(yTop, innerHalfWidth, innerSpringY, innerArchHeight);
|
|
356
|
+
const x1 = -halfAtTop;
|
|
357
|
+
const x2 = halfAtTop;
|
|
358
|
+
if (x2 - x1 > 0.01 && yTop > innerBottom) {
|
|
359
|
+
addShape(mesh, baseMaterial, createRectShape(x1, x2, Math.max(yBottom, innerBottom), yTop), frameDepth + 0.001);
|
|
360
|
+
}
|
|
361
|
+
y -= rowDividerThickness;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (sill) {
|
|
365
|
+
const sillW = width + sillDepth * 0.4;
|
|
366
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
367
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function getWindowRenderOpenAmount(node) {
|
|
371
|
+
const runtimeValue = useInteractive.getState().windows[node.id]?.operationState;
|
|
372
|
+
return Math.min(Math.max(runtimeValue ?? node.operationState ?? 0, 0), 1);
|
|
373
|
+
}
|
|
374
|
+
function getAwningDirection(node) {
|
|
375
|
+
return node.windowType === 'hopper' ? 'down' : (node.awningDirection ?? 'up');
|
|
376
|
+
}
|
|
377
|
+
function isRectangleOnlyWindowType(node) {
|
|
378
|
+
return (node.windowType === 'sliding' ||
|
|
379
|
+
node.windowType === 'single-hung' ||
|
|
380
|
+
node.windowType === 'double-hung' ||
|
|
381
|
+
node.windowType === 'bay' ||
|
|
382
|
+
node.windowType === 'bow');
|
|
383
|
+
}
|
|
384
|
+
function addSlidingWindowVisuals(node, mesh) {
|
|
385
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
386
|
+
const innerW = width - 2 * frameThickness;
|
|
387
|
+
const innerH = height - 2 * frameThickness;
|
|
388
|
+
// Outer frame.
|
|
389
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
390
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
391
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
392
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
393
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
394
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
395
|
+
const railThickness = Math.max(frameThickness * 0.55, 0.025);
|
|
396
|
+
const trackThickness = Math.max(frameThickness * 0.35, 0.018);
|
|
397
|
+
const panelOverlap = Math.min(Math.max(frameThickness * 0.9, 0.04), innerW * 0.12);
|
|
398
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
399
|
+
const travel = Math.max(innerW / 2 - panelOverlap, 0) * openAmount;
|
|
400
|
+
const panelWidth = (innerW + panelOverlap) / 2;
|
|
401
|
+
const leftPanelBaseX = -innerW / 4 - panelOverlap / 4;
|
|
402
|
+
const leftPanelX = leftPanelBaseX + travel;
|
|
403
|
+
const rightPanelX = innerW / 4 + panelOverlap / 4;
|
|
404
|
+
const leftZ = frameDepth * 0.16;
|
|
405
|
+
const rightZ = -frameDepth * 0.12;
|
|
406
|
+
const panelH = Math.max(innerH - trackThickness * 2, 0.01);
|
|
407
|
+
const activePanel = new THREE.Group();
|
|
408
|
+
activePanel.name = SLIDING_WINDOW_ACTIVE_PANEL_NAME;
|
|
409
|
+
activePanel.position.set(leftPanelX, 0, leftZ);
|
|
410
|
+
mesh.add(activePanel);
|
|
411
|
+
// Twin tracks signal the sliding operation without adding editor-only state.
|
|
412
|
+
addBox(mesh, baseMaterial, innerW, trackThickness, frameDepth, 0, innerH / 2 - trackThickness / 2, 0);
|
|
413
|
+
addBox(mesh, baseMaterial, innerW, trackThickness, frameDepth, 0, -innerH / 2 + trackThickness / 2, 0);
|
|
414
|
+
addBox(activePanel, glassMaterial, panelWidth, panelH, glassDepth, 0, 0, 0);
|
|
415
|
+
addBox(mesh, glassMaterial, panelWidth, panelH, glassDepth, rightPanelX, 0, rightZ);
|
|
416
|
+
// The right sash stays fixed. The left sash is the active panel that slides across it.
|
|
417
|
+
addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth * 0.72, -panelWidth / 2 + railThickness / 2, 0, 0);
|
|
418
|
+
addBox(mesh, baseMaterial, railThickness, panelH, frameDepth * 0.72, rightPanelX + panelWidth / 2 - railThickness / 2, 0, rightZ);
|
|
419
|
+
addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth * 0.78, panelWidth / 2 - railThickness / 2, 0, 0);
|
|
420
|
+
addBox(mesh, baseMaterial, railThickness, panelH, frameDepth * 0.78, rightPanelX - panelWidth / 2 + railThickness / 2, 0, rightZ);
|
|
421
|
+
}
|
|
422
|
+
if (sill) {
|
|
423
|
+
const sillW = width + sillDepth * 0.4;
|
|
424
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
425
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function addRectCasementSash(parent, name, panelW, panelH, frameThickness, frameDepth, pivotX, sashCenterX, rotationY) {
|
|
429
|
+
const sash = new THREE.Group();
|
|
430
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
431
|
+
const sashDepth = frameDepth * 0.72;
|
|
432
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
433
|
+
const glassW = Math.max(panelW - 2 * sashFrameThickness, 0.01);
|
|
434
|
+
const glassH = Math.max(panelH - 2 * sashFrameThickness, 0.01);
|
|
435
|
+
sash.name = name;
|
|
436
|
+
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
437
|
+
sash.rotation.y = rotationY;
|
|
438
|
+
parent.add(sash);
|
|
439
|
+
addBox(sash, baseMaterial, panelW, sashFrameThickness, sashDepth, sashCenterX, panelH / 2 - sashFrameThickness / 2, 0);
|
|
440
|
+
addBox(sash, baseMaterial, panelW, sashFrameThickness, sashDepth, sashCenterX, -panelH / 2 + sashFrameThickness / 2, 0);
|
|
441
|
+
addBox(sash, baseMaterial, sashFrameThickness, panelH, sashDepth, sashCenterX - panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
442
|
+
addBox(sash, baseMaterial, sashFrameThickness, panelH, sashDepth, sashCenterX + panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
443
|
+
addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
|
|
444
|
+
}
|
|
445
|
+
function addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth) {
|
|
446
|
+
const markerW = Math.max(frameThickness * 0.38, 0.018);
|
|
447
|
+
const markerH = innerH * 0.24;
|
|
448
|
+
for (const pivotX of [-innerW / 2, innerW / 2]) {
|
|
449
|
+
addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, innerH * 0.25, frameDepth * 0.08);
|
|
450
|
+
addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, -innerH * 0.25, frameDepth * 0.08);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function createFrenchArchLeafShape(leafW, leafH, fullW, springY, archHeight, side, inset = 0) {
|
|
454
|
+
const left = -leafW / 2 + inset;
|
|
455
|
+
const right = leafW / 2 - inset;
|
|
456
|
+
const bottom = -leafH / 2 + inset;
|
|
457
|
+
const halfFullW = fullW / 2;
|
|
458
|
+
const xOffset = side === 'left' ? -leafW / 2 : leafW / 2;
|
|
459
|
+
const shape = new THREE.Shape();
|
|
460
|
+
const segments = 32;
|
|
461
|
+
const topAt = (localX) => Math.max(bottom + 0.01, getArchBoundaryY(localX + xOffset, halfFullW, springY, archHeight) - inset);
|
|
462
|
+
shape.moveTo(left, bottom);
|
|
463
|
+
shape.lineTo(right, bottom);
|
|
464
|
+
shape.lineTo(right, topAt(right));
|
|
465
|
+
for (let index = 1; index <= segments; index += 1) {
|
|
466
|
+
const x = right + (left - right) * (index / segments);
|
|
467
|
+
shape.lineTo(x, topAt(x));
|
|
468
|
+
}
|
|
469
|
+
shape.lineTo(left, bottom);
|
|
470
|
+
shape.closePath();
|
|
471
|
+
return shape;
|
|
472
|
+
}
|
|
473
|
+
function createFrenchArchLeafFrameShape(leafW, leafH, fullW, springY, archHeight, frameThickness, side) {
|
|
474
|
+
const outer = createFrenchArchLeafShape(leafW, leafH, fullW, springY, archHeight, side);
|
|
475
|
+
const inset = Math.min(frameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
|
|
476
|
+
if (inset <= 0.001)
|
|
477
|
+
return outer;
|
|
478
|
+
const hole = new THREE.Path(createFrenchArchLeafShape(leafW, leafH, fullW, springY, archHeight, side, inset)
|
|
479
|
+
.getPoints(32)
|
|
480
|
+
.reverse());
|
|
481
|
+
outer.holes.push(hole);
|
|
482
|
+
return outer;
|
|
483
|
+
}
|
|
484
|
+
function createFrenchRoundedLeafShape(leafW, leafH, fullW, fullRadii, side, inset = 0) {
|
|
485
|
+
const left = -leafW / 2 + inset;
|
|
486
|
+
const right = leafW / 2 - inset;
|
|
487
|
+
const bottom = -leafH / 2 + inset;
|
|
488
|
+
const top = leafH / 2 - inset;
|
|
489
|
+
const fullLeft = -fullW / 2 + inset;
|
|
490
|
+
const fullRight = fullW / 2 - inset;
|
|
491
|
+
const globalOffset = side === 'left' ? -leafW / 2 : leafW / 2;
|
|
492
|
+
const radii = inset > 0 ? insetCornerRadii(fullRadii, inset, fullRight - fullLeft, top - bottom) : fullRadii;
|
|
493
|
+
const shape = new THREE.Shape();
|
|
494
|
+
const segments = 32;
|
|
495
|
+
const topAt = (localX) => getRoundedBoundaryYAtX(localX + globalOffset, fullLeft, fullRight, top, radii);
|
|
496
|
+
const bottomAt = (localX) => getRoundedBottomBoundaryYAtX(localX + globalOffset, fullLeft, fullRight, bottom, radii);
|
|
497
|
+
shape.moveTo(right, bottomAt(right));
|
|
498
|
+
shape.lineTo(right, topAt(right));
|
|
499
|
+
for (let index = 1; index <= segments; index += 1) {
|
|
500
|
+
const x = right + (left - right) * (index / segments);
|
|
501
|
+
shape.lineTo(x, topAt(x));
|
|
502
|
+
}
|
|
503
|
+
shape.lineTo(left, bottomAt(left));
|
|
504
|
+
for (let index = 1; index <= segments; index += 1) {
|
|
505
|
+
const x = left + (right - left) * (index / segments);
|
|
506
|
+
shape.lineTo(x, bottomAt(x));
|
|
507
|
+
}
|
|
508
|
+
shape.closePath();
|
|
509
|
+
return shape;
|
|
510
|
+
}
|
|
511
|
+
function createFrenchRoundedLeafFrameShape(leafW, leafH, fullW, fullRadii, frameThickness, side) {
|
|
512
|
+
const outer = createFrenchRoundedLeafShape(leafW, leafH, fullW, fullRadii, side);
|
|
513
|
+
const inset = Math.min(frameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
|
|
514
|
+
if (inset <= 0.001)
|
|
515
|
+
return outer;
|
|
516
|
+
const hole = new THREE.Path(createFrenchRoundedLeafShape(leafW, leafH, fullW, fullRadii, side, inset)
|
|
517
|
+
.getPoints(32)
|
|
518
|
+
.reverse());
|
|
519
|
+
outer.holes.push(hole);
|
|
520
|
+
return outer;
|
|
521
|
+
}
|
|
522
|
+
function addShapedFrenchCasementSash(parent, node, name, side, fullW, leafW, leafH, frameThickness, frameDepth, pivotX, sashCenterX, rotationY) {
|
|
523
|
+
const sash = new THREE.Group();
|
|
524
|
+
const sashVisual = new THREE.Group();
|
|
525
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
526
|
+
const sashDepth = frameDepth * 0.72;
|
|
527
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
528
|
+
sash.name = name;
|
|
529
|
+
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
530
|
+
sash.rotation.y = rotationY;
|
|
531
|
+
sashVisual.position.x = sashCenterX;
|
|
532
|
+
sash.add(sashVisual);
|
|
533
|
+
parent.add(sash);
|
|
534
|
+
if (node.openingShape === 'arch') {
|
|
535
|
+
const outerArchHeight = getClampedArchHeight(node.width, node.height, node.archHeight);
|
|
536
|
+
const sashArchHeight = getClampedArchHeight(fullW, leafH, outerArchHeight - frameThickness);
|
|
537
|
+
const sashSpringY = node.height / 2 - outerArchHeight;
|
|
538
|
+
addShape(sashVisual, baseMaterial, createFrenchArchLeafFrameShape(leafW, leafH, fullW, sashSpringY, sashArchHeight, sashFrameThickness, side), sashDepth);
|
|
539
|
+
const glassInset = Math.min(sashFrameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
|
|
540
|
+
if (glassInset > 0.001) {
|
|
541
|
+
addShape(sashVisual, glassMaterial, createFrenchArchLeafShape(leafW, leafH, fullW, sashSpringY, sashArchHeight, side, glassInset), glassDepth, sashDepth * 0.08);
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const frameRadii = insetCornerRadii(getWindowRoundedRadii(node, node.width, node.height), frameThickness, fullW, leafH);
|
|
546
|
+
addShape(sashVisual, baseMaterial, createFrenchRoundedLeafFrameShape(leafW, leafH, fullW, frameRadii, sashFrameThickness, side), sashDepth);
|
|
547
|
+
const glassInset = Math.min(sashFrameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
|
|
548
|
+
if (glassInset > 0.001) {
|
|
549
|
+
addShape(sashVisual, glassMaterial, createFrenchRoundedLeafShape(leafW, leafH, fullW, frameRadii, side, glassInset), glassDepth, sashDepth * 0.08);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function addFrenchCasementWindowVisuals(node, mesh) {
|
|
553
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
554
|
+
const innerW = width - 2 * frameThickness;
|
|
555
|
+
const innerH = height - 2 * frameThickness;
|
|
556
|
+
// Fixed outer frame.
|
|
557
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
558
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
559
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
560
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
561
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
562
|
+
const openAngle = getWindowRenderOpenAmount(node) * (Math.PI / 2);
|
|
563
|
+
const leafW = innerW / 2;
|
|
564
|
+
addRectCasementSash(mesh, FRENCH_CASEMENT_LEFT_SASH_NAME, leafW, innerH, frameThickness, frameDepth, -innerW / 2, leafW / 2, -openAngle);
|
|
565
|
+
addRectCasementSash(mesh, FRENCH_CASEMENT_RIGHT_SASH_NAME, leafW, innerH, frameThickness, frameDepth, innerW / 2, -leafW / 2, openAngle);
|
|
566
|
+
addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth);
|
|
567
|
+
}
|
|
568
|
+
if (sill) {
|
|
569
|
+
const sillW = width + sillDepth * 0.4;
|
|
570
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
571
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function addShapedCasementWindowVisuals(node, mesh) {
|
|
575
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
576
|
+
const innerW = width - 2 * frameThickness;
|
|
577
|
+
const innerH = height - 2 * frameThickness;
|
|
578
|
+
if (node.openingShape === 'arch') {
|
|
579
|
+
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
583
|
+
}
|
|
584
|
+
if ((node.casementStyle ?? 'single') === 'french') {
|
|
585
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
586
|
+
const openAngle = getWindowRenderOpenAmount(node) * (Math.PI / 2);
|
|
587
|
+
const leafW = innerW / 2;
|
|
588
|
+
addShapedFrenchCasementSash(mesh, node, FRENCH_CASEMENT_LEFT_SASH_NAME, 'left', innerW, leafW, innerH, frameThickness, frameDepth, -innerW / 2, leafW / 2, -openAngle);
|
|
589
|
+
addShapedFrenchCasementSash(mesh, node, FRENCH_CASEMENT_RIGHT_SASH_NAME, 'right', innerW, leafW, innerH, frameThickness, frameDepth, innerW / 2, -leafW / 2, openAngle);
|
|
590
|
+
addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth);
|
|
591
|
+
}
|
|
592
|
+
if (sill) {
|
|
593
|
+
const sillW = width + sillDepth * 0.4;
|
|
594
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
595
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
596
|
+
}
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
600
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
601
|
+
const openAngle = openAmount * (Math.PI / 2);
|
|
602
|
+
const hingeSide = node.hingesSide ?? 'left';
|
|
603
|
+
const hingeSign = hingeSide === 'left' ? -1 : 1;
|
|
604
|
+
const pivotX = hingeSide === 'left' ? -innerW / 2 : innerW / 2;
|
|
605
|
+
const sashCenterX = hingeSide === 'left' ? innerW / 2 : -innerW / 2;
|
|
606
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
607
|
+
const sashDepth = frameDepth * 0.72;
|
|
608
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
609
|
+
const sash = new THREE.Group();
|
|
610
|
+
const sashVisual = new THREE.Group();
|
|
611
|
+
sash.name = CASEMENT_WINDOW_SASH_NAME;
|
|
612
|
+
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
613
|
+
sash.rotation.y = hingeSign * openAngle;
|
|
614
|
+
sashVisual.position.x = sashCenterX;
|
|
615
|
+
sash.add(sashVisual);
|
|
616
|
+
mesh.add(sash);
|
|
617
|
+
if (node.openingShape === 'arch') {
|
|
618
|
+
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
619
|
+
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
620
|
+
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
621
|
+
if (glassInset > 0.001) {
|
|
622
|
+
const glassW = innerW - 2 * glassInset;
|
|
623
|
+
const glassH = innerH - 2 * glassInset;
|
|
624
|
+
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
629
|
+
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
630
|
+
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
631
|
+
if (glassInset > 0.001) {
|
|
632
|
+
const glassW = innerW - 2 * glassInset;
|
|
633
|
+
const glassH = innerH - 2 * glassInset;
|
|
634
|
+
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
|
|
638
|
+
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
|
|
639
|
+
}
|
|
640
|
+
if (sill) {
|
|
641
|
+
const sillW = width + sillDepth * 0.4;
|
|
642
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
643
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function addCasementWindowVisuals(node, mesh) {
|
|
647
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
648
|
+
if (node.openingShape === 'rounded' || node.openingShape === 'arch') {
|
|
649
|
+
addShapedCasementWindowVisuals(node, mesh);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if ((node.casementStyle ?? 'single') === 'french') {
|
|
653
|
+
addFrenchCasementWindowVisuals(node, mesh);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const innerW = width - 2 * frameThickness;
|
|
657
|
+
const innerH = height - 2 * frameThickness;
|
|
658
|
+
// Fixed outer frame.
|
|
659
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
660
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
661
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
662
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
663
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
664
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
665
|
+
const openAngle = openAmount * (Math.PI / 2);
|
|
666
|
+
const hingeSide = node.hingesSide ?? 'left';
|
|
667
|
+
const hingeSign = hingeSide === 'left' ? -1 : 1;
|
|
668
|
+
const sash = new THREE.Group();
|
|
669
|
+
const pivotX = hingeSide === 'left' ? -innerW / 2 : innerW / 2;
|
|
670
|
+
const sashCenterX = hingeSide === 'left' ? innerW / 2 : -innerW / 2;
|
|
671
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
672
|
+
const sashDepth = frameDepth * 0.72;
|
|
673
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
674
|
+
const glassW = Math.max(innerW - 2 * sashFrameThickness, 0.01);
|
|
675
|
+
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
676
|
+
sash.name = CASEMENT_WINDOW_SASH_NAME;
|
|
677
|
+
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
678
|
+
sash.rotation.y = hingeSign * openAngle;
|
|
679
|
+
mesh.add(sash);
|
|
680
|
+
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, sashCenterX, innerH / 2 - sashFrameThickness / 2, 0);
|
|
681
|
+
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, sashCenterX, -innerH / 2 + sashFrameThickness / 2, 0);
|
|
682
|
+
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, sashCenterX - innerW / 2 + sashFrameThickness / 2, 0, 0);
|
|
683
|
+
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, sashCenterX + innerW / 2 - sashFrameThickness / 2, 0, 0);
|
|
684
|
+
addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
|
|
685
|
+
// Small hinge markers make the pivot side legible when the sash is closed.
|
|
686
|
+
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
|
|
687
|
+
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
|
|
688
|
+
}
|
|
689
|
+
if (sill) {
|
|
690
|
+
const sillW = width + sillDepth * 0.4;
|
|
691
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
692
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function addAwningWindowVisuals(node, mesh) {
|
|
696
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
697
|
+
if (node.openingShape === 'rounded' || node.openingShape === 'arch') {
|
|
698
|
+
addShapedAwningWindowVisuals(node, mesh);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const innerW = width - 2 * frameThickness;
|
|
702
|
+
const innerH = height - 2 * frameThickness;
|
|
703
|
+
// Fixed outer frame.
|
|
704
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
705
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
706
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
707
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
708
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
709
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
710
|
+
const openAngle = openAmount * (Math.PI / 3);
|
|
711
|
+
const isDownward = getAwningDirection(node) === 'down';
|
|
712
|
+
const sash = new THREE.Group();
|
|
713
|
+
const pivotY = isDownward ? -innerH / 2 : innerH / 2;
|
|
714
|
+
const sashCenterY = isDownward ? innerH / 2 : -innerH / 2;
|
|
715
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
716
|
+
const sashDepth = frameDepth * 0.72;
|
|
717
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
718
|
+
const glassW = Math.max(innerW - 2 * sashFrameThickness, 0.01);
|
|
719
|
+
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
720
|
+
sash.name = AWNING_WINDOW_SASH_NAME;
|
|
721
|
+
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
722
|
+
sash.rotation.x = -openAngle;
|
|
723
|
+
mesh.add(sash);
|
|
724
|
+
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashCenterY + innerH / 2 - sashFrameThickness / 2, 0);
|
|
725
|
+
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashCenterY - innerH / 2 + sashFrameThickness / 2, 0);
|
|
726
|
+
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, sashCenterY, 0);
|
|
727
|
+
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, sashCenterY, 0);
|
|
728
|
+
addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, sashCenterY, sashDepth * 0.08);
|
|
729
|
+
// Compact hinge rail, visible even when the sash is closed.
|
|
730
|
+
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
731
|
+
}
|
|
732
|
+
if (sill) {
|
|
733
|
+
const sillW = width + sillDepth * 0.4;
|
|
734
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
735
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function addShapedAwningWindowVisuals(node, mesh) {
|
|
739
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
740
|
+
const innerW = width - 2 * frameThickness;
|
|
741
|
+
const innerH = height - 2 * frameThickness;
|
|
742
|
+
if (node.openingShape === 'arch') {
|
|
743
|
+
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
747
|
+
}
|
|
748
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
749
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
750
|
+
const openAngle = openAmount * (Math.PI / 3);
|
|
751
|
+
const isDownward = getAwningDirection(node) === 'down';
|
|
752
|
+
const pivotY = isDownward ? -innerH / 2 : innerH / 2;
|
|
753
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
754
|
+
const sashDepth = frameDepth * 0.72;
|
|
755
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
756
|
+
const sash = new THREE.Group();
|
|
757
|
+
const sashVisual = new THREE.Group();
|
|
758
|
+
sash.name = AWNING_WINDOW_SASH_NAME;
|
|
759
|
+
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
760
|
+
sash.rotation.x = -openAngle;
|
|
761
|
+
sashVisual.position.y = isDownward ? innerH / 2 : -innerH / 2;
|
|
762
|
+
sash.add(sashVisual);
|
|
763
|
+
mesh.add(sash);
|
|
764
|
+
if (node.openingShape === 'arch') {
|
|
765
|
+
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
766
|
+
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
767
|
+
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
768
|
+
if (glassInset > 0.001) {
|
|
769
|
+
const glassW = innerW - 2 * glassInset;
|
|
770
|
+
const glassH = innerH - 2 * glassInset;
|
|
771
|
+
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
776
|
+
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
777
|
+
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
778
|
+
if (glassInset > 0.001) {
|
|
779
|
+
const glassW = innerW - 2 * glassInset;
|
|
780
|
+
const glassH = innerH - 2 * glassInset;
|
|
781
|
+
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
785
|
+
}
|
|
786
|
+
if (sill) {
|
|
787
|
+
const sillW = width + sillDepth * 0.4;
|
|
788
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
789
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function addHopperWindowVisuals(node, mesh) {
|
|
793
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
794
|
+
if (node.openingShape === 'rounded' || node.openingShape === 'arch') {
|
|
795
|
+
addShapedHopperWindowVisuals(node, mesh);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const innerW = width - 2 * frameThickness;
|
|
799
|
+
const innerH = height - 2 * frameThickness;
|
|
800
|
+
// Fixed outer frame.
|
|
801
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
802
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
803
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
804
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
805
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
806
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
807
|
+
const openAngle = openAmount * (Math.PI / 3);
|
|
808
|
+
const sash = new THREE.Group();
|
|
809
|
+
const pivotY = -innerH / 2;
|
|
810
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
811
|
+
const sashDepth = frameDepth * 0.72;
|
|
812
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
813
|
+
const glassW = Math.max(innerW - 2 * sashFrameThickness, 0.01);
|
|
814
|
+
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
815
|
+
sash.name = HOPPER_WINDOW_SASH_NAME;
|
|
816
|
+
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
817
|
+
sash.rotation.x = -openAngle;
|
|
818
|
+
mesh.add(sash);
|
|
819
|
+
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, innerH - sashFrameThickness / 2, 0);
|
|
820
|
+
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashFrameThickness / 2, 0);
|
|
821
|
+
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, innerH / 2, 0);
|
|
822
|
+
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, innerH / 2, 0);
|
|
823
|
+
addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, innerH / 2, sashDepth * 0.08);
|
|
824
|
+
// Compact bottom hinge rail, visible even when the sash is closed.
|
|
825
|
+
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
826
|
+
}
|
|
827
|
+
if (sill) {
|
|
828
|
+
const sillW = width + sillDepth * 0.4;
|
|
829
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
830
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function addShapedHopperWindowVisuals(node, mesh) {
|
|
834
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
835
|
+
const innerW = width - 2 * frameThickness;
|
|
836
|
+
const innerH = height - 2 * frameThickness;
|
|
837
|
+
if (node.openingShape === 'arch') {
|
|
838
|
+
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
842
|
+
}
|
|
843
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
844
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
845
|
+
const openAngle = openAmount * (Math.PI / 3);
|
|
846
|
+
const pivotY = -innerH / 2;
|
|
847
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
848
|
+
const sashDepth = frameDepth * 0.72;
|
|
849
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
850
|
+
const sash = new THREE.Group();
|
|
851
|
+
const sashVisual = new THREE.Group();
|
|
852
|
+
sash.name = HOPPER_WINDOW_SASH_NAME;
|
|
853
|
+
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
854
|
+
sash.rotation.x = -openAngle;
|
|
855
|
+
sashVisual.position.y = innerH / 2;
|
|
856
|
+
sash.add(sashVisual);
|
|
857
|
+
mesh.add(sash);
|
|
858
|
+
if (node.openingShape === 'arch') {
|
|
859
|
+
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
860
|
+
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
861
|
+
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
862
|
+
if (glassInset > 0.001) {
|
|
863
|
+
const glassW = innerW - 2 * glassInset;
|
|
864
|
+
const glassH = innerH - 2 * glassInset;
|
|
865
|
+
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
870
|
+
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
871
|
+
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
872
|
+
if (glassInset > 0.001) {
|
|
873
|
+
const glassW = innerW - 2 * glassInset;
|
|
874
|
+
const glassH = innerH - 2 * glassInset;
|
|
875
|
+
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
879
|
+
}
|
|
880
|
+
if (sill) {
|
|
881
|
+
const sillW = width + sillDepth * 0.4;
|
|
882
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
883
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function addHungSash(parent, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH) {
|
|
887
|
+
addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth * 0.72, 0, panelHeight / 2 - sashFrameThickness / 2, 0);
|
|
888
|
+
addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth * 0.72, 0, -panelHeight / 2 + sashFrameThickness / 2, 0);
|
|
889
|
+
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth * 0.72, -panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
890
|
+
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth * 0.72, panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
891
|
+
addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, 0);
|
|
892
|
+
}
|
|
893
|
+
function addSingleHungWindowVisuals(node, mesh) {
|
|
894
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
895
|
+
const innerW = width - 2 * frameThickness;
|
|
896
|
+
const innerH = height - 2 * frameThickness;
|
|
897
|
+
// Fixed outer frame.
|
|
898
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
899
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
900
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
901
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
902
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
903
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
904
|
+
const railThickness = Math.max(frameThickness * 0.55, 0.025);
|
|
905
|
+
const trackThickness = Math.max(frameThickness * 0.35, 0.018);
|
|
906
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
907
|
+
const panelOverlap = Math.min(Math.max(frameThickness * 0.9, 0.04), innerH * 0.12);
|
|
908
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
909
|
+
const travel = Math.max(innerH / 2 - panelOverlap, 0) * openAmount;
|
|
910
|
+
const panelHeight = (innerH + panelOverlap) / 2;
|
|
911
|
+
const topPanelY = innerH / 4 + panelOverlap / 4;
|
|
912
|
+
const bottomPanelY = -innerH / 4 - panelOverlap / 4 + travel;
|
|
913
|
+
const topZ = -frameDepth * 0.12;
|
|
914
|
+
const bottomZ = frameDepth * 0.16;
|
|
915
|
+
const panelW = Math.max(innerW - trackThickness * 2, 0.01);
|
|
916
|
+
const glassW = Math.max(panelW - 2 * sashFrameThickness, 0.01);
|
|
917
|
+
const glassH = Math.max(panelHeight - 2 * sashFrameThickness, 0.01);
|
|
918
|
+
const activeSash = new THREE.Group();
|
|
919
|
+
activeSash.name = SINGLE_HUNG_ACTIVE_SASH_NAME;
|
|
920
|
+
activeSash.position.set(0, bottomPanelY, bottomZ);
|
|
921
|
+
mesh.add(activeSash);
|
|
922
|
+
// Side tracks show the lower sash is the moving element.
|
|
923
|
+
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, -innerW / 2 + trackThickness / 2, 0, 0);
|
|
924
|
+
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, innerW / 2 - trackThickness / 2, 0, 0);
|
|
925
|
+
const topSash = new THREE.Group();
|
|
926
|
+
topSash.position.set(0, topPanelY, topZ);
|
|
927
|
+
mesh.add(topSash);
|
|
928
|
+
addHungSash(topSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
929
|
+
addHungSash(activeSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
930
|
+
// Meeting rails: top sash fixed, bottom sash moves upward over it.
|
|
931
|
+
addBox(mesh, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, topPanelY - panelHeight / 2 + railThickness / 2, topZ);
|
|
932
|
+
addBox(activeSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, panelHeight / 2 - railThickness / 2, 0);
|
|
933
|
+
}
|
|
934
|
+
if (sill) {
|
|
935
|
+
const sillW = width + sillDepth * 0.4;
|
|
936
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
937
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function addDoubleHungWindowVisuals(node, mesh) {
|
|
941
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
942
|
+
const innerW = width - 2 * frameThickness;
|
|
943
|
+
const innerH = height - 2 * frameThickness;
|
|
944
|
+
// Fixed outer frame.
|
|
945
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
946
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
947
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
948
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
949
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
950
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
951
|
+
const railThickness = Math.max(frameThickness * 0.55, 0.025);
|
|
952
|
+
const trackThickness = Math.max(frameThickness * 0.35, 0.018);
|
|
953
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
954
|
+
const panelOverlap = Math.min(Math.max(frameThickness * 0.9, 0.04), innerH * 0.12);
|
|
955
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
956
|
+
const travel = Math.max(innerH / 2 - panelOverlap, 0) * openAmount;
|
|
957
|
+
const panelHeight = (innerH + panelOverlap) / 2;
|
|
958
|
+
const topPanelY = innerH / 4 + panelOverlap / 4 - travel;
|
|
959
|
+
const bottomPanelY = -innerH / 4 - panelOverlap / 4 + travel;
|
|
960
|
+
const topZ = -frameDepth * 0.12;
|
|
961
|
+
const bottomZ = frameDepth * 0.16;
|
|
962
|
+
const panelW = Math.max(innerW - trackThickness * 2, 0.01);
|
|
963
|
+
const glassW = Math.max(panelW - 2 * sashFrameThickness, 0.01);
|
|
964
|
+
const glassH = Math.max(panelHeight - 2 * sashFrameThickness, 0.01);
|
|
965
|
+
const topSash = new THREE.Group();
|
|
966
|
+
const bottomSash = new THREE.Group();
|
|
967
|
+
topSash.name = DOUBLE_HUNG_TOP_SASH_NAME;
|
|
968
|
+
topSash.position.set(0, topPanelY, topZ);
|
|
969
|
+
mesh.add(topSash);
|
|
970
|
+
bottomSash.name = DOUBLE_HUNG_BOTTOM_SASH_NAME;
|
|
971
|
+
bottomSash.position.set(0, bottomPanelY, bottomZ);
|
|
972
|
+
mesh.add(bottomSash);
|
|
973
|
+
// Side tracks show both sashes move vertically.
|
|
974
|
+
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, -innerW / 2 + trackThickness / 2, 0, 0);
|
|
975
|
+
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, innerW / 2 - trackThickness / 2, 0, 0);
|
|
976
|
+
addHungSash(topSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
977
|
+
addHungSash(bottomSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
978
|
+
// Opposing meeting rails: top sash descends while bottom sash rises.
|
|
979
|
+
addBox(topSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, -panelHeight / 2 + railThickness / 2, 0);
|
|
980
|
+
addBox(bottomSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, panelHeight / 2 - railThickness / 2, 0);
|
|
981
|
+
}
|
|
982
|
+
if (sill) {
|
|
983
|
+
const sillW = width + sillDepth * 0.4;
|
|
984
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
985
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function addBayWindowVisuals(node, mesh) {
|
|
989
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
990
|
+
const innerW = width - 2 * frameThickness;
|
|
991
|
+
const innerH = height - 2 * frameThickness;
|
|
992
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
993
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
994
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
995
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
996
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
997
|
+
const projectionDepth = Math.max(width * 0.22, 0.28);
|
|
998
|
+
const centerW = innerW * 0.48;
|
|
999
|
+
const sideRun = Math.max((innerW - centerW) / 2, 0.01);
|
|
1000
|
+
const sideW = Math.hypot(sideRun, projectionDepth);
|
|
1001
|
+
const sideAngle = Math.atan2(projectionDepth, sideRun);
|
|
1002
|
+
const panelDepth = Math.max(frameDepth * 0.72, 0.04);
|
|
1003
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
1004
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
1005
|
+
const bayFootprint = [
|
|
1006
|
+
[-innerW / 2, 0],
|
|
1007
|
+
[-centerW / 2, projectionDepth],
|
|
1008
|
+
[centerW / 2, projectionDepth],
|
|
1009
|
+
[innerW / 2, 0],
|
|
1010
|
+
];
|
|
1011
|
+
const addBayPanel = (parent, panelW) => {
|
|
1012
|
+
const glassW = Math.max(panelW - 2 * sashFrameThickness, 0.01);
|
|
1013
|
+
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
1014
|
+
addBox(parent, baseMaterial, panelW, sashFrameThickness, panelDepth, 0, innerH / 2 - sashFrameThickness / 2, 0);
|
|
1015
|
+
addBox(parent, baseMaterial, panelW, sashFrameThickness, panelDepth, 0, -innerH / 2 + sashFrameThickness / 2, 0);
|
|
1016
|
+
addBox(parent, baseMaterial, sashFrameThickness, innerH, panelDepth, -panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
1017
|
+
addBox(parent, baseMaterial, sashFrameThickness, innerH, panelDepth, panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
1018
|
+
addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, panelDepth * 0.08);
|
|
1019
|
+
};
|
|
1020
|
+
const addBayCap = (centerY) => {
|
|
1021
|
+
const halfThickness = frameThickness / 2;
|
|
1022
|
+
const vertices = [];
|
|
1023
|
+
const indices = [];
|
|
1024
|
+
for (const [x, z] of bayFootprint) {
|
|
1025
|
+
vertices.push(x, centerY - halfThickness, z);
|
|
1026
|
+
}
|
|
1027
|
+
for (const [x, z] of bayFootprint) {
|
|
1028
|
+
vertices.push(x, centerY + halfThickness, z);
|
|
1029
|
+
}
|
|
1030
|
+
indices.push(0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 1, 5, 6, 1, 6, 2, 2, 6, 7, 2, 7, 3, 3, 7, 4, 3, 4, 0);
|
|
1031
|
+
const geometry = new THREE.BufferGeometry();
|
|
1032
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
1033
|
+
geometry.setIndex(indices);
|
|
1034
|
+
geometry.computeVertexNormals();
|
|
1035
|
+
mesh.add(new THREE.Mesh(geometry, baseMaterial));
|
|
1036
|
+
};
|
|
1037
|
+
const center = new THREE.Group();
|
|
1038
|
+
center.position.set(0, 0, projectionDepth);
|
|
1039
|
+
mesh.add(center);
|
|
1040
|
+
addBayPanel(center, centerW);
|
|
1041
|
+
const left = new THREE.Group();
|
|
1042
|
+
left.position.set((-innerW / 2 - centerW / 2) / 2, 0, projectionDepth / 2);
|
|
1043
|
+
left.rotation.y = -sideAngle;
|
|
1044
|
+
mesh.add(left);
|
|
1045
|
+
addBayPanel(left, sideW);
|
|
1046
|
+
const right = new THREE.Group();
|
|
1047
|
+
right.position.set((innerW / 2 + centerW / 2) / 2, 0, projectionDepth / 2);
|
|
1048
|
+
right.rotation.y = sideAngle;
|
|
1049
|
+
mesh.add(right);
|
|
1050
|
+
addBayPanel(right, sideW);
|
|
1051
|
+
addBayCap(innerH / 2);
|
|
1052
|
+
addBayCap(-innerH / 2);
|
|
1053
|
+
}
|
|
1054
|
+
if (sill) {
|
|
1055
|
+
const sillW = width + sillDepth * 0.4;
|
|
1056
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1057
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function addBowWindowVisuals(node, mesh) {
|
|
1061
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
1062
|
+
const innerW = width - 2 * frameThickness;
|
|
1063
|
+
const innerH = height - 2 * frameThickness;
|
|
1064
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1065
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1066
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
1067
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
1068
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
1069
|
+
const mullionCount = 5;
|
|
1070
|
+
const curveSegments = 28;
|
|
1071
|
+
const projectionDepth = Math.max(width * 0.18, 0.22);
|
|
1072
|
+
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
1073
|
+
const halfSpan = innerW / 2;
|
|
1074
|
+
const arcZAt = (x) => projectionDepth * (1 - (x / halfSpan) ** 2);
|
|
1075
|
+
const slabYTop = innerH / 2;
|
|
1076
|
+
const slabYBottom = -innerH / 2;
|
|
1077
|
+
const glassTop = innerH / 2 - sashFrameThickness;
|
|
1078
|
+
const glassBottom = -innerH / 2 + sashFrameThickness;
|
|
1079
|
+
const createCurvedVerticalBand = (yBottom, yTop, zOffset = 0) => {
|
|
1080
|
+
const positions = [];
|
|
1081
|
+
const indices = [];
|
|
1082
|
+
for (let index = 0; index <= curveSegments; index += 1) {
|
|
1083
|
+
const x = -halfSpan + (innerW * index) / curveSegments;
|
|
1084
|
+
const z = arcZAt(x) + zOffset;
|
|
1085
|
+
positions.push(x, yBottom, z, x, yTop, z);
|
|
1086
|
+
}
|
|
1087
|
+
for (let index = 0; index < curveSegments; index += 1) {
|
|
1088
|
+
const a = index * 2;
|
|
1089
|
+
indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2);
|
|
1090
|
+
}
|
|
1091
|
+
const geometry = new THREE.BufferGeometry();
|
|
1092
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
1093
|
+
geometry.setIndex(indices);
|
|
1094
|
+
geometry.computeVertexNormals();
|
|
1095
|
+
return geometry;
|
|
1096
|
+
};
|
|
1097
|
+
const createCurvedCap = (centerY, thickness) => {
|
|
1098
|
+
const positions = [];
|
|
1099
|
+
const indices = [];
|
|
1100
|
+
const yBottom = centerY - thickness / 2;
|
|
1101
|
+
const yTop = centerY + thickness / 2;
|
|
1102
|
+
for (let index = 0; index <= curveSegments; index += 1) {
|
|
1103
|
+
const x = -halfSpan + (innerW * index) / curveSegments;
|
|
1104
|
+
const z = arcZAt(x);
|
|
1105
|
+
positions.push(x, yBottom, 0, x, yBottom, z, x, yTop, 0, x, yTop, z);
|
|
1106
|
+
}
|
|
1107
|
+
for (let index = 0; index < curveSegments; index += 1) {
|
|
1108
|
+
const a = index * 4;
|
|
1109
|
+
const b = a + 4;
|
|
1110
|
+
indices.push(a, b, a + 2, b, b + 2, a + 2, a + 1, a + 3, b + 1, b + 1, a + 3, b + 3, a + 2, b + 2, a + 3, b + 2, b + 3, a + 3, a, a + 1, b, b, a + 1, b + 1);
|
|
1111
|
+
}
|
|
1112
|
+
const geometry = new THREE.BufferGeometry();
|
|
1113
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
1114
|
+
geometry.setIndex(indices);
|
|
1115
|
+
geometry.computeVertexNormals();
|
|
1116
|
+
return geometry;
|
|
1117
|
+
};
|
|
1118
|
+
const addCurvedMesh = (material, geometry) => {
|
|
1119
|
+
mesh.add(new THREE.Mesh(geometry, material));
|
|
1120
|
+
};
|
|
1121
|
+
addCurvedMesh(baseMaterial, createCurvedVerticalBand(glassTop, innerH / 2));
|
|
1122
|
+
addCurvedMesh(baseMaterial, createCurvedVerticalBand(-innerH / 2, glassBottom));
|
|
1123
|
+
addCurvedMesh(glassMaterial, createCurvedVerticalBand(glassBottom, glassTop, frameDepth * 0.04));
|
|
1124
|
+
addCurvedMesh(baseMaterial, createCurvedCap(slabYTop, frameThickness));
|
|
1125
|
+
addCurvedMesh(baseMaterial, createCurvedCap(slabYBottom, frameThickness));
|
|
1126
|
+
for (let index = 0; index <= mullionCount; index += 1) {
|
|
1127
|
+
const x = -halfSpan + (innerW * index) / mullionCount;
|
|
1128
|
+
addBox(mesh, baseMaterial, sashFrameThickness, innerH, frameDepth * 0.72, x, 0, arcZAt(x));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (sill) {
|
|
1132
|
+
const sillW = width + sillDepth * 0.4;
|
|
1133
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1134
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function addLouveredWindowVisuals(node, mesh) {
|
|
1138
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
1139
|
+
if (node.openingShape === 'rounded' || node.openingShape === 'arch') {
|
|
1140
|
+
addShapedLouveredWindowVisuals(node, mesh);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const innerW = width - 2 * frameThickness;
|
|
1144
|
+
const innerH = height - 2 * frameThickness;
|
|
1145
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1146
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1147
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
1148
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
1149
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
1150
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
1151
|
+
const slatCount = Math.max(4, Math.min(9, Math.round(height / 0.22)));
|
|
1152
|
+
const slatGap = innerH / slatCount;
|
|
1153
|
+
const slatHeight = Math.max(Math.min(slatGap * 0.62, 0.14), 0.045);
|
|
1154
|
+
const slatDepth = Math.max(frameDepth * 0.16, 0.012);
|
|
1155
|
+
const slatAngle = -openAmount * (Math.PI / 3);
|
|
1156
|
+
const railThickness = Math.max(frameThickness * 0.45, 0.022);
|
|
1157
|
+
const slats = new THREE.Group();
|
|
1158
|
+
slats.name = LOUVERED_WINDOW_SLATS_NAME;
|
|
1159
|
+
mesh.add(slats);
|
|
1160
|
+
addBox(mesh, baseMaterial, railThickness, innerH, frameDepth * 0.95, -innerW / 2 + railThickness / 2, 0, 0);
|
|
1161
|
+
addBox(mesh, baseMaterial, railThickness, innerH, frameDepth * 0.95, innerW / 2 - railThickness / 2, 0, 0);
|
|
1162
|
+
for (let index = 0; index < slatCount; index += 1) {
|
|
1163
|
+
const y = innerH / 2 - slatGap * (index + 0.5);
|
|
1164
|
+
const slat = new THREE.Group();
|
|
1165
|
+
slat.position.set(0, y, 0);
|
|
1166
|
+
slat.rotation.x = slatAngle;
|
|
1167
|
+
slats.add(slat);
|
|
1168
|
+
addBox(slat, glassMaterial, Math.max(innerW - 2 * railThickness, 0.01), slatHeight, slatDepth, 0, 0, 0);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (sill) {
|
|
1172
|
+
const sillW = width + sillDepth * 0.4;
|
|
1173
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1174
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function addShapedLouveredWindowVisuals(node, mesh) {
|
|
1178
|
+
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
1179
|
+
const halfWidth = width / 2;
|
|
1180
|
+
const bottom = -height / 2;
|
|
1181
|
+
const top = height / 2;
|
|
1182
|
+
const inset = Math.max(0, Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005));
|
|
1183
|
+
const innerLeft = -halfWidth + inset;
|
|
1184
|
+
const innerRight = halfWidth - inset;
|
|
1185
|
+
const innerBottom = bottom + inset;
|
|
1186
|
+
const innerTop = top - inset;
|
|
1187
|
+
const innerW = innerRight - innerLeft;
|
|
1188
|
+
const innerH = innerTop - innerBottom;
|
|
1189
|
+
if (node.openingShape === 'arch') {
|
|
1190
|
+
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
1194
|
+
}
|
|
1195
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
1196
|
+
const openAmount = getWindowRenderOpenAmount(node);
|
|
1197
|
+
const slatCount = Math.max(4, Math.min(9, Math.round(height / 0.22)));
|
|
1198
|
+
const slatGap = innerH / slatCount;
|
|
1199
|
+
const slatHeight = Math.max(Math.min(slatGap * 0.62, 0.14), 0.045);
|
|
1200
|
+
const slatDepth = Math.max(frameDepth * 0.16, 0.012);
|
|
1201
|
+
const slatAngle = -openAmount * (Math.PI / 3);
|
|
1202
|
+
const railThickness = Math.max(frameThickness * 0.45, 0.022);
|
|
1203
|
+
const slatInset = railThickness + 0.004;
|
|
1204
|
+
const slats = new THREE.Group();
|
|
1205
|
+
slats.name = LOUVERED_WINDOW_SLATS_NAME;
|
|
1206
|
+
mesh.add(slats);
|
|
1207
|
+
const getBoundsAtY = node.openingShape === 'arch'
|
|
1208
|
+
? (() => {
|
|
1209
|
+
const outerArchHeight = getClampedArchHeight(width, height, node.archHeight);
|
|
1210
|
+
const archHeight = getClampedArchHeight(innerW, innerH, outerArchHeight - inset);
|
|
1211
|
+
const springY = top - outerArchHeight;
|
|
1212
|
+
return (y) => {
|
|
1213
|
+
const half = getArchedOpeningHalfWidthAtY(y, innerW / 2, springY, archHeight);
|
|
1214
|
+
return { minX: -half, maxX: half };
|
|
1215
|
+
};
|
|
1216
|
+
})()
|
|
1217
|
+
: (() => {
|
|
1218
|
+
const innerRadii = insetCornerRadii(getWindowRoundedRadii(node, width, height), inset, innerW, innerH);
|
|
1219
|
+
return (y) => getRoundedHorizontalBoundsAtY(y, innerLeft, innerRight, innerTop, innerRadii);
|
|
1220
|
+
})();
|
|
1221
|
+
const addVerticalRail = (x) => {
|
|
1222
|
+
const railX1 = x;
|
|
1223
|
+
const railX2 = x + (x < 0 ? railThickness : -railThickness);
|
|
1224
|
+
const sampleX = x < 0 ? Math.max(railX1, railX2) : Math.min(railX1, railX2);
|
|
1225
|
+
const railTop = node.openingShape === 'arch'
|
|
1226
|
+
? getArchBoundaryY(sampleX, innerW / 2, top - getClampedArchHeight(width, height, node.archHeight), getClampedArchHeight(innerW, innerH, getClampedArchHeight(width, height, node.archHeight) - inset))
|
|
1227
|
+
: getRoundedBoundaryYAtX(sampleX, innerLeft, innerRight, innerTop, insetCornerRadii(getWindowRoundedRadii(node, width, height), inset, innerW, innerH));
|
|
1228
|
+
addShape(mesh, baseMaterial, createRectShape(Math.min(railX1, railX2), Math.max(railX1, railX2), innerBottom, railTop), frameDepth * 0.95);
|
|
1229
|
+
};
|
|
1230
|
+
addVerticalRail(innerLeft);
|
|
1231
|
+
addVerticalRail(innerRight);
|
|
1232
|
+
for (let index = 0; index < slatCount; index += 1) {
|
|
1233
|
+
const y = innerTop - slatGap * (index + 0.5);
|
|
1234
|
+
const topBounds = getBoundsAtY(Math.min(y + slatHeight / 2, innerTop));
|
|
1235
|
+
const bottomBounds = getBoundsAtY(Math.max(y - slatHeight / 2, innerBottom));
|
|
1236
|
+
const minX = Math.max(topBounds.minX, bottomBounds.minX) + slatInset;
|
|
1237
|
+
const maxX = Math.min(topBounds.maxX, bottomBounds.maxX) - slatInset;
|
|
1238
|
+
const slatW = Math.max(maxX - minX, 0);
|
|
1239
|
+
if (slatW <= 0.01)
|
|
1240
|
+
continue;
|
|
1241
|
+
const slat = new THREE.Group();
|
|
1242
|
+
slat.position.set((minX + maxX) / 2, y, 0);
|
|
1243
|
+
slat.rotation.x = slatAngle;
|
|
1244
|
+
slats.add(slat);
|
|
1245
|
+
addBox(slat, glassMaterial, slatW, slatHeight, slatDepth, 0, 0, 0);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (sill) {
|
|
1249
|
+
const sillW = width + sillDepth * 0.4;
|
|
1250
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1251
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function updateWindowMesh(node, mesh) {
|
|
1255
|
+
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
1256
|
+
mesh.geometry.dispose();
|
|
1257
|
+
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
1258
|
+
mesh.material = hitboxMaterial;
|
|
1259
|
+
// Sync transform from node (React may lag behind the system by a frame during drag)
|
|
1260
|
+
mesh.position.set(node.position[0], node.position[1], node.position[2]);
|
|
1261
|
+
mesh.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2]);
|
|
1262
|
+
// Dispose and remove all old visual children; preserve 'cutout'
|
|
1263
|
+
for (const child of [...mesh.children]) {
|
|
1264
|
+
if (child.name === 'cutout')
|
|
1265
|
+
continue;
|
|
1266
|
+
disposeObjectGeometry(child);
|
|
1267
|
+
mesh.remove(child);
|
|
1268
|
+
}
|
|
1269
|
+
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, openingKind, openingShape, windowType, } = node;
|
|
1270
|
+
if (openingKind === 'opening') {
|
|
1271
|
+
syncWindowCutout(node, mesh);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (windowType === 'sliding') {
|
|
1275
|
+
addSlidingWindowVisuals(node, mesh);
|
|
1276
|
+
syncWindowCutout(node, mesh);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (windowType === 'casement') {
|
|
1280
|
+
addCasementWindowVisuals(node, mesh);
|
|
1281
|
+
syncWindowCutout(node, mesh);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (windowType === 'awning') {
|
|
1285
|
+
addAwningWindowVisuals(node, mesh);
|
|
1286
|
+
syncWindowCutout(node, mesh);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (windowType === 'hopper') {
|
|
1290
|
+
addAwningWindowVisuals(node, mesh);
|
|
1291
|
+
syncWindowCutout(node, mesh);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (windowType === 'single-hung') {
|
|
1295
|
+
addSingleHungWindowVisuals(node, mesh);
|
|
1296
|
+
syncWindowCutout(node, mesh);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (windowType === 'double-hung') {
|
|
1300
|
+
addDoubleHungWindowVisuals(node, mesh);
|
|
1301
|
+
syncWindowCutout(node, mesh);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (windowType === 'bay') {
|
|
1305
|
+
addBayWindowVisuals(node, mesh);
|
|
1306
|
+
syncWindowCutout(node, mesh);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (windowType === 'bow') {
|
|
1310
|
+
addBowWindowVisuals(node, mesh);
|
|
1311
|
+
syncWindowCutout(node, mesh);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (windowType === 'louvered') {
|
|
1315
|
+
addLouveredWindowVisuals(node, mesh);
|
|
1316
|
+
syncWindowCutout(node, mesh);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (openingShape === 'arch') {
|
|
1320
|
+
addArchedWindowVisuals(node, mesh);
|
|
1321
|
+
syncWindowCutout(node, mesh);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (openingShape === 'rounded') {
|
|
1325
|
+
addRoundedWindowVisuals(node, mesh);
|
|
1326
|
+
syncWindowCutout(node, mesh);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const innerW = width - 2 * frameThickness;
|
|
1330
|
+
const innerH = height - 2 * frameThickness;
|
|
1331
|
+
// ── Frame members ──
|
|
1332
|
+
// Top / bottom — full width
|
|
1333
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1334
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1335
|
+
// Left / right — inner height to avoid corner overlap
|
|
1336
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
1337
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
1338
|
+
// ── Pane grid ──
|
|
1339
|
+
const numCols = columnRatios.length;
|
|
1340
|
+
const numRows = rowRatios.length;
|
|
1341
|
+
const usableW = innerW - (numCols - 1) * columnDividerThickness;
|
|
1342
|
+
const usableH = innerH - (numRows - 1) * rowDividerThickness;
|
|
1343
|
+
const colSum = columnRatios.reduce((a, b) => a + b, 0);
|
|
1344
|
+
const rowSum = rowRatios.reduce((a, b) => a + b, 0);
|
|
1345
|
+
const colWidths = columnRatios.map((r) => (r / colSum) * usableW);
|
|
1346
|
+
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
|
|
1347
|
+
// Compute column x-centers starting from left edge of inner area
|
|
1348
|
+
const colXCenters = [];
|
|
1349
|
+
let cx = -innerW / 2;
|
|
1350
|
+
for (let c = 0; c < numCols; c++) {
|
|
1351
|
+
colXCenters.push(cx + colWidths[c] / 2);
|
|
1352
|
+
cx += colWidths[c];
|
|
1353
|
+
if (c < numCols - 1)
|
|
1354
|
+
cx += columnDividerThickness;
|
|
1355
|
+
}
|
|
1356
|
+
// Compute row y-centers starting from top edge of inner area (R1 = top)
|
|
1357
|
+
const rowYCenters = [];
|
|
1358
|
+
let cy = innerH / 2;
|
|
1359
|
+
for (let r = 0; r < numRows; r++) {
|
|
1360
|
+
rowYCenters.push(cy - rowHeights[r] / 2);
|
|
1361
|
+
cy -= rowHeights[r];
|
|
1362
|
+
if (r < numRows - 1)
|
|
1363
|
+
cy -= rowDividerThickness;
|
|
1364
|
+
}
|
|
1365
|
+
// Column dividers — full inner height
|
|
1366
|
+
cx = -innerW / 2;
|
|
1367
|
+
for (let c = 0; c < numCols - 1; c++) {
|
|
1368
|
+
cx += colWidths[c];
|
|
1369
|
+
addBox(mesh, baseMaterial, columnDividerThickness, innerH, frameDepth, cx + columnDividerThickness / 2, 0, 0);
|
|
1370
|
+
cx += columnDividerThickness;
|
|
1371
|
+
}
|
|
1372
|
+
// Row dividers — per column width, so they don't overlap column dividers (top to bottom)
|
|
1373
|
+
cy = innerH / 2;
|
|
1374
|
+
for (let r = 0; r < numRows - 1; r++) {
|
|
1375
|
+
cy -= rowHeights[r];
|
|
1376
|
+
const divY = cy - rowDividerThickness / 2;
|
|
1377
|
+
for (let c = 0; c < numCols; c++) {
|
|
1378
|
+
addBox(mesh, baseMaterial, colWidths[c], rowDividerThickness, frameDepth, colXCenters[c], divY, 0);
|
|
1379
|
+
}
|
|
1380
|
+
cy -= rowDividerThickness;
|
|
1381
|
+
}
|
|
1382
|
+
// Glass panes
|
|
1383
|
+
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
1384
|
+
for (let c = 0; c < numCols; c++) {
|
|
1385
|
+
for (let r = 0; r < numRows; r++) {
|
|
1386
|
+
addBox(mesh, glassMaterial, colWidths[c], rowHeights[r], glassDepth, colXCenters[c], rowYCenters[r], 0);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
// ── Sill ──
|
|
1390
|
+
if (sill) {
|
|
1391
|
+
const sillW = width + sillDepth * 0.4; // slightly wider than frame
|
|
1392
|
+
// Protrudes from the front face of the frame (+Z)
|
|
1393
|
+
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1394
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1395
|
+
}
|
|
1396
|
+
syncWindowCutout(node, mesh);
|
|
1397
|
+
}
|
|
1398
|
+
function syncWindowCutout(node, mesh) {
|
|
1399
|
+
// ── Cutout (for wall CSG) — always full window dimensions, 1m deep ──
|
|
1400
|
+
let cutout = mesh.getObjectByName('cutout');
|
|
1401
|
+
if (!cutout) {
|
|
1402
|
+
cutout = new THREE.Mesh();
|
|
1403
|
+
cutout.name = 'cutout';
|
|
1404
|
+
mesh.add(cutout);
|
|
1405
|
+
}
|
|
1406
|
+
cutout.geometry.dispose();
|
|
1407
|
+
if (isRectangleOnlyWindowType(node)) {
|
|
1408
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0);
|
|
1409
|
+
}
|
|
1410
|
+
else if (node.openingShape === 'arch') {
|
|
1411
|
+
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)), {
|
|
1412
|
+
depth: 1,
|
|
1413
|
+
bevelEnabled: false,
|
|
1414
|
+
curveSegments: 24,
|
|
1415
|
+
});
|
|
1416
|
+
cutout.geometry.translate(0, 0, -0.5);
|
|
1417
|
+
}
|
|
1418
|
+
else if (node.openingShape === 'rounded') {
|
|
1419
|
+
cutout.geometry = new THREE.ExtrudeGeometry(createRoundedShape(-node.width / 2, node.width / 2, -node.height / 2, node.height / 2, getWindowRoundedRadii(node, node.width, node.height)), {
|
|
1420
|
+
depth: 1,
|
|
1421
|
+
bevelEnabled: false,
|
|
1422
|
+
curveSegments: 24,
|
|
1423
|
+
});
|
|
1424
|
+
cutout.geometry.translate(0, 0, -0.5);
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0);
|
|
1428
|
+
}
|
|
1429
|
+
cutout.visible = false;
|
|
1430
|
+
}
|