@pascal-app/viewer 1.0.6 → 1.0.7
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/viewer/daylight-atmosphere.d.ts +9 -0
- package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/daylight-atmosphere.js +315 -0
- package/dist/components/viewer/drone-controller.d.ts +15 -0
- package/dist/components/viewer/drone-controller.d.ts.map +1 -0
- package/dist/components/viewer/drone-controller.js +194 -0
- package/dist/components/viewer/glb-interactive.d.ts +4 -18
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +32 -268
- package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
- package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene-motion.js +57 -0
- package/dist/components/viewer/glb-scene.d.ts +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +289 -49
- package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
- package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/glb-selection-callout.js +54 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
- package/dist/components/viewer/index.d.ts +5 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +30 -6
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +192 -5
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +150 -94
- package/dist/components/viewer/scene-environment.js +4 -5
- package/dist/components/viewer/selection-callout.d.ts +51 -0
- package/dist/components/viewer/selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/selection-callout.js +197 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
- package/dist/components/viewer/use-daylight-rooms.js +236 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/index.d.ts +21 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -7
- package/dist/lib/daylight/ground.d.ts +21 -0
- package/dist/lib/daylight/ground.d.ts.map +1 -0
- package/dist/lib/daylight/ground.js +53 -0
- package/dist/lib/daylight/location.d.ts +32 -0
- package/dist/lib/daylight/location.d.ts.map +1 -0
- package/dist/lib/daylight/location.js +74 -0
- package/dist/lib/daylight/look.d.ts +33 -0
- package/dist/lib/daylight/look.d.ts.map +1 -0
- package/dist/lib/daylight/look.js +13 -0
- package/dist/lib/daylight/material-hooks.d.ts +2 -0
- package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
- package/dist/lib/daylight/material-hooks.js +150 -0
- package/dist/lib/daylight/room-context.d.ts +12 -0
- package/dist/lib/daylight/room-context.d.ts.map +1 -0
- package/dist/lib/daylight/room-context.js +53 -0
- package/dist/lib/daylight/room-scene.d.ts +54 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +356 -0
- package/dist/lib/daylight/rooms.d.ts +262 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1223 -0
- package/dist/lib/daylight/runtime.d.ts +33 -0
- package/dist/lib/daylight/runtime.d.ts.map +1 -0
- package/dist/lib/daylight/runtime.js +42 -0
- package/dist/lib/daylight/sky.d.ts +26 -0
- package/dist/lib/daylight/sky.d.ts.map +1 -0
- package/dist/lib/daylight/sky.js +217 -0
- package/dist/lib/daylight/solar.d.ts +46 -0
- package/dist/lib/daylight/solar.d.ts.map +1 -0
- package/dist/lib/daylight/solar.js +156 -0
- package/dist/lib/device-tier.d.ts +29 -0
- package/dist/lib/device-tier.d.ts.map +1 -0
- package/dist/lib/device-tier.js +71 -0
- package/dist/lib/dispose-object3d.d.ts.map +1 -1
- package/dist/lib/dispose-object3d.js +4 -2
- package/dist/lib/drone-flight.d.ts +44 -0
- package/dist/lib/drone-flight.d.ts.map +1 -0
- package/dist/lib/drone-flight.js +121 -0
- package/dist/lib/editor-highlight.d.ts +16 -0
- package/dist/lib/editor-highlight.d.ts.map +1 -0
- package/dist/lib/editor-highlight.js +33 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -1
- package/dist/lib/foundation-materials.js +2 -4
- package/dist/lib/glb-merge.d.ts +23 -0
- package/dist/lib/glb-merge.d.ts.map +1 -0
- package/dist/lib/glb-merge.js +184 -0
- package/dist/lib/hardware-geometry.d.ts +21 -0
- package/dist/lib/hardware-geometry.d.ts.map +1 -0
- package/dist/lib/hardware-geometry.js +283 -0
- package/dist/lib/layer-pass.d.ts +2 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layer-pass.js +20 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +50 -23
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -1
- package/dist/lib/occlusion-culler.d.ts +34 -0
- package/dist/lib/occlusion-culler.d.ts.map +1 -0
- package/dist/lib/occlusion-culler.js +698 -0
- package/dist/lib/post-processing-resources.d.ts +4 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/post-processing-resources.js +5 -0
- package/dist/lib/readback-rows.d.ts +14 -0
- package/dist/lib/readback-rows.d.ts.map +1 -0
- package/dist/lib/readback-rows.js +31 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -1
- package/dist/lib/reveal-pose.js +3 -0
- package/dist/lib/scene-matrices.d.ts +12 -0
- package/dist/lib/scene-matrices.d.ts.map +1 -0
- package/dist/lib/scene-matrices.js +287 -0
- package/dist/lib/scene-pass-mrt.d.ts +5 -2
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
- package/dist/lib/scene-pass-mrt.js +7 -2
- package/dist/lib/scene-themes.d.ts +0 -2
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +0 -2
- package/dist/lib/scene-visibility.d.ts +2 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +12 -3
- package/dist/lib/site-surfaces.d.ts +1 -8
- package/dist/lib/site-surfaces.d.ts.map +1 -1
- package/dist/lib/site-surfaces.js +0 -19
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +24 -40
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +10 -8
- package/dist/lib/underground-ambiance.d.ts +0 -2
- package/dist/lib/underground-ambiance.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.js +0 -2
- package/dist/store/use-item-light-pool.d.ts +3 -6
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +12 -0
- package/dist/store/use-viewer.d.ts +72 -15
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +103 -18
- package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +11 -1
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +501 -145
- package/dist/systems/door/leaf-profile.d.ts +11 -0
- package/dist/systems/door/leaf-profile.d.ts.map +1 -0
- package/dist/systems/door/leaf-profile.js +140 -0
- package/dist/systems/interactive/callout-actions.d.ts +72 -0
- package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
- package/dist/systems/interactive/callout-actions.js +212 -0
- package/dist/systems/interactive/interactive-system.d.ts +5 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +44 -121
- package/dist/systems/interactive/opening-settled.d.ts +16 -0
- package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
- package/dist/systems/interactive/opening-settled.js +53 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +22 -214
- package/dist/systems/item-light/pooled-lights.d.ts +70 -0
- package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
- package/dist/systems/item-light/pooled-lights.js +449 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -1
- package/dist/systems/roof/roof-material-slots.d.ts +14 -0
- package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
- package/dist/systems/roof/roof-material-slots.js +13 -0
- package/dist/systems/roof/roof-materials.d.ts +25 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +85 -26
- package/dist/systems/roof/roof-system.d.ts +34 -1
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +477 -105
- package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
- package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-casing-geometry.js +102 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
- package/dist/systems/wall/opening-cutout-geometry.js +7 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +28 -35
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +34 -19
- package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
- package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
- package/dist/systems/wall/wall-t-junctions.js +149 -0
- package/dist/systems/window/window-animation-system.d.ts +3 -12
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +37 -122
- package/dist/systems/window/window-pose.d.ts +31 -0
- package/dist/systems/window/window-pose.d.ts.map +1 -0
- package/dist/systems/window/window-pose.js +152 -0
- package/dist/systems/window/window-roller-shutter.d.ts +8 -0
- package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
- package/dist/systems/window/window-roller-shutter.js +64 -0
- package/dist/systems/window/window-system.d.ts +2 -11
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +470 -303
- package/package.json +6 -6
- package/dist/systems/interactive/control-widget.d.ts +0 -10
- package/dist/systems/interactive/control-widget.d.ts.map +0 -1
- package/dist/systems/interactive/control-widget.js +0 -35
- package/dist/systems/interactive/procedural-controls.d.ts +0 -13
- package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
- package/dist/systems/interactive/procedural-controls.js +0 -17
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { cornerPair, DEFAULT_WALL_THICKNESS, getEffectiveNode, getOpeningWallPlacement, getWallThickness, sceneRegistry, scriptedSize, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { cornerPair, DEFAULT_WALL_THICKNESS, getEffectiveNode, getOpeningWallPlacement, getWallThickness, openingCasingWidth, openingExteriorFace, openingHostFacing, resolveOpeningCasingHost, resolveOpeningFrameDepth, sceneRegistry, scriptedSize, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
5
|
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
6
|
+
import { hingeJambPart, hingeLeafPart, roundedBox, tube } from '../../lib/hardware-geometry';
|
|
6
7
|
import { createMaterial, createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
|
|
7
8
|
import { timeSpan } from '../../lib/perf-tracks';
|
|
8
9
|
import { settleScriptedOpening } from '../../lib/scripted-opening';
|
|
9
10
|
import useViewer from '../../store/use-viewer';
|
|
11
|
+
import { buildOpeningCasingMeshes } from '../wall/opening-casing-geometry';
|
|
10
12
|
import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
|
|
11
13
|
import { cornerFrameStale, FUSED_CORNER_SIDE } from './corner-frame';
|
|
14
|
+
import { AWNING_WINDOW_SASH_NAME, CASEMENT_WINDOW_SASH_NAME, DOUBLE_HUNG_BOTTOM_SASH_NAME, DOUBLE_HUNG_TOP_SASH_NAME, FRENCH_CASEMENT_LEFT_SASH_NAME, FRENCH_CASEMENT_RIGHT_SASH_NAME, HOPPER_WINDOW_SASH_NAME, LOUVERED_WINDOW_SLATS_NAME, poseWindowMovingParts, SINGLE_HUNG_ACTIVE_SASH_NAME, SLIDING_WINDOW_ACTIVE_PANEL_NAME, TILT_TURN_WINDOW_SASH_NAME, WINDOW_LEFT_SHUTTER_NAME, WINDOW_RIGHT_SHUTTER_NAME, WINDOW_SHUTTER_OUTER_NAME, } from './window-pose';
|
|
15
|
+
import { buildWindowRollerCurtain, getWindowRollerCurtainZ } from './window-roller-shutter';
|
|
16
|
+
export * from './window-pose';
|
|
12
17
|
// Invisible material for root mesh — used as selection hitbox only
|
|
13
18
|
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
14
19
|
let baseMaterial = getBaseMaterial();
|
|
@@ -20,16 +25,6 @@ let currentShading = 'rendered';
|
|
|
20
25
|
let currentTextures = true;
|
|
21
26
|
let currentColorPreset = 'clay';
|
|
22
27
|
let currentSceneMaterials;
|
|
23
|
-
export const CASEMENT_WINDOW_SASH_NAME = 'casement-window-sash';
|
|
24
|
-
export const FRENCH_CASEMENT_LEFT_SASH_NAME = 'french-casement-left-sash';
|
|
25
|
-
export const FRENCH_CASEMENT_RIGHT_SASH_NAME = 'french-casement-right-sash';
|
|
26
|
-
export const SLIDING_WINDOW_ACTIVE_PANEL_NAME = 'sliding-window-active-panel';
|
|
27
|
-
export const SINGLE_HUNG_ACTIVE_SASH_NAME = 'single-hung-active-sash';
|
|
28
|
-
export const DOUBLE_HUNG_TOP_SASH_NAME = 'double-hung-top-sash';
|
|
29
|
-
export const DOUBLE_HUNG_BOTTOM_SASH_NAME = 'double-hung-bottom-sash';
|
|
30
|
-
export const LOUVERED_WINDOW_SLATS_NAME = 'louvered-window-slats';
|
|
31
|
-
export const AWNING_WINDOW_SASH_NAME = 'awning-window-sash';
|
|
32
|
-
export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash';
|
|
33
28
|
const MAX_WINDOW_REBUILDS_PER_FRAME = 16;
|
|
34
29
|
const WINDOW_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WINDOW_REBUILDS_PER_FRAME;
|
|
35
30
|
const WINDOW_PROGRESSIVE_TIME_BUDGET_MS = 8;
|
|
@@ -205,17 +200,30 @@ function windowSlotDefault(slotId) {
|
|
|
205
200
|
// only) → the window-wide `material` for the frame (glass keeps its own) → the
|
|
206
201
|
// role/base default. Textures-off ignores overrides — the monochrome escape hatch.
|
|
207
202
|
function resolveWindowSlotMaterial(node, slotId) {
|
|
208
|
-
const fallback =
|
|
203
|
+
const fallback = slotId === 'hardware' || slotId === 'shutters' || slotId === 'sill'
|
|
204
|
+
? resolveWindowSlotMaterial(node, 'frame')
|
|
205
|
+
: windowSlotDefault(slotId === 'glass' ? 'glass' : 'frame');
|
|
209
206
|
if (!currentTextures)
|
|
210
207
|
return fallback;
|
|
211
208
|
const ref = node.slots?.[slotId];
|
|
212
209
|
const painted = ref ? resolveMaterialRef(ref, currentSceneMaterials, currentShading) : null;
|
|
213
210
|
if (painted)
|
|
214
211
|
return painted;
|
|
212
|
+
if (slotId === 'shutters')
|
|
213
|
+
return (resolveMaterialRef('library:preset-charcoal', currentSceneMaterials, currentShading) ??
|
|
214
|
+
fallback);
|
|
215
215
|
if (node.material && slotId === 'frame')
|
|
216
216
|
return createMaterial(node.material, currentShading);
|
|
217
217
|
return fallback;
|
|
218
218
|
}
|
|
219
|
+
/** Frame depth follows the wall; sash, bars and glass keep buildable section sizes. */
|
|
220
|
+
function windowDepths(frameDepth) {
|
|
221
|
+
return {
|
|
222
|
+
sash: Math.min(frameDepth * 0.72, 0.068),
|
|
223
|
+
bar: Math.min(frameDepth, 0.05),
|
|
224
|
+
glass: Math.min(Math.max(0.004, frameDepth * 0.08), 0.024),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
219
227
|
function addBox(parent, material, w, h, d, x, y, z) {
|
|
220
228
|
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
221
229
|
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
@@ -223,6 +231,30 @@ function addBox(parent, material, w, h, d, x, y, z) {
|
|
|
223
231
|
m.position.set(x, y, z);
|
|
224
232
|
tagWindowSlot(m);
|
|
225
233
|
parent.add(m);
|
|
234
|
+
return m;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* How far the sill's top stands over the opening's bottom: level with it, the top would share the
|
|
238
|
+
* plane of the wall's reveal under the frame and of a face finish's cut edge, and flicker with them.
|
|
239
|
+
*/
|
|
240
|
+
const SILL_LIFT = 0.001;
|
|
241
|
+
/** Both sills sit on finished host faces, independent of the frame's inset or placement face. */
|
|
242
|
+
function windowFaceFrame(node, mesh) {
|
|
243
|
+
const nodes = useScene.getState().nodes;
|
|
244
|
+
const exteriorFace = openingExteriorFace(node, nodes);
|
|
245
|
+
const facing = openingHostFacing({ rotation: [0, mesh.rotation.y, 0] });
|
|
246
|
+
const exterior = (exteriorFace === 'a' ? 1 : -1) * facing;
|
|
247
|
+
const hostId = node.roofSegmentId ?? node.parentId;
|
|
248
|
+
const hostNode = hostId ? nodes[hostId] : undefined;
|
|
249
|
+
const host = resolveOpeningCasingHost(node, nodes, hostNode ? getEffectiveNode(hostNode) : undefined);
|
|
250
|
+
const wallDepth = host?.thickness ?? (hostNode?.type === 'wall' ? getWallThickness(hostNode) : node.frameDepth);
|
|
251
|
+
const cased = (face) => face === 'a' ? node.casingFaceA === true : node.casingFaceB === true;
|
|
252
|
+
return { exterior, exteriorFace, cased, wallDepth, offset: (node.position[2] ?? 0) * facing };
|
|
253
|
+
}
|
|
254
|
+
function addSill(parent, node) {
|
|
255
|
+
const { exterior, wallDepth, offset } = windowFaceFrame(node, parent);
|
|
256
|
+
currentWindowSlot = 'sill';
|
|
257
|
+
addBox(parent, resolveWindowSlotMaterial(node, 'sill'), node.width + 2 * (node.sillOverhang ?? node.sillDepth * 0.4), node.sillThickness, node.sillDepth, 0, -node.height / 2 - node.sillThickness / 2 + SILL_LIFT, exterior * (wallDepth / 2 + node.sillDepth / 2) - offset).name = 'window-exterior-sill';
|
|
226
258
|
}
|
|
227
259
|
function addShape(parent, material, shape, depth, z = 0) {
|
|
228
260
|
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
@@ -237,7 +269,7 @@ function addShape(parent, material, shape, depth, z = 0) {
|
|
|
237
269
|
}
|
|
238
270
|
function disposeObjectGeometry(object) {
|
|
239
271
|
object.traverse((child) => {
|
|
240
|
-
if (child instanceof THREE.Mesh)
|
|
272
|
+
if (child instanceof THREE.Mesh && !child.geometry.userData.shared)
|
|
241
273
|
child.geometry.dispose();
|
|
242
274
|
});
|
|
243
275
|
}
|
|
@@ -436,7 +468,7 @@ function getRoundedHorizontalBoundsAtY(y, left, right, top, radii) {
|
|
|
436
468
|
return { minX, maxX };
|
|
437
469
|
}
|
|
438
470
|
function addRoundedWindowVisuals(node, mesh) {
|
|
439
|
-
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill,
|
|
471
|
+
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, } = node;
|
|
440
472
|
const halfWidth = width / 2;
|
|
441
473
|
const bottom = -height / 2;
|
|
442
474
|
const top = height / 2;
|
|
@@ -452,7 +484,7 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
452
484
|
currentWindowSlot = 'frame';
|
|
453
485
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, inset, outerRadii), frameDepth);
|
|
454
486
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
455
|
-
const glassDepth =
|
|
487
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
456
488
|
currentWindowSlot = 'glass';
|
|
457
489
|
addShape(mesh, glassMaterial, createRoundedShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii), glassDepth);
|
|
458
490
|
const numCols = columnRatios.length;
|
|
@@ -471,7 +503,7 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
471
503
|
const x2 = x + columnDividerThickness;
|
|
472
504
|
const dividerTop = Math.min(getRoundedBoundaryYAtX(x1, innerLeft, innerRight, innerTop, innerRadii), getRoundedBoundaryYAtX(x2, innerLeft, innerRight, innerTop, innerRadii));
|
|
473
505
|
if (dividerTop > innerBottom + 0.01) {
|
|
474
|
-
addShape(mesh, baseMaterial, createRectShape(x1, x2, innerBottom, dividerTop), frameDepth
|
|
506
|
+
addShape(mesh, baseMaterial, createRectShape(x1, x2, innerBottom, dividerTop), windowDepths(frameDepth).bar);
|
|
475
507
|
}
|
|
476
508
|
x += columnDividerThickness;
|
|
477
509
|
}
|
|
@@ -483,20 +515,16 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
483
515
|
const yBottom = y - rowDividerThickness;
|
|
484
516
|
const { minX, maxX } = getRoundedHorizontalBoundsAtY(yTop, innerLeft, innerRight, innerTop, innerRadii);
|
|
485
517
|
if (maxX - minX > 0.01 && yTop > innerBottom) {
|
|
486
|
-
addShape(mesh, baseMaterial, createRectShape(minX, maxX, Math.max(yBottom, innerBottom), yTop), frameDepth
|
|
518
|
+
addShape(mesh, baseMaterial, createRectShape(minX, maxX, Math.max(yBottom, innerBottom), yTop), windowDepths(frameDepth).bar);
|
|
487
519
|
}
|
|
488
520
|
y -= rowDividerThickness;
|
|
489
521
|
}
|
|
490
522
|
}
|
|
491
|
-
if (sill)
|
|
492
|
-
|
|
493
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
494
|
-
currentWindowSlot = 'frame';
|
|
495
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
496
|
-
}
|
|
523
|
+
if (sill)
|
|
524
|
+
addSill(mesh, node);
|
|
497
525
|
}
|
|
498
526
|
function addArchedWindowVisuals(node, mesh) {
|
|
499
|
-
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill,
|
|
527
|
+
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, } = node;
|
|
500
528
|
const halfWidth = width / 2;
|
|
501
529
|
const bottom = -height / 2;
|
|
502
530
|
const top = height / 2;
|
|
@@ -513,7 +541,7 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
513
541
|
currentWindowSlot = 'frame';
|
|
514
542
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, archHeight, inset), frameDepth);
|
|
515
543
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
516
|
-
const glassDepth =
|
|
544
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
517
545
|
currentWindowSlot = 'glass';
|
|
518
546
|
addShape(mesh, glassMaterial, createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight), glassDepth);
|
|
519
547
|
const numCols = columnRatios.length;
|
|
@@ -533,7 +561,7 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
533
561
|
const x2 = x + columnDividerThickness;
|
|
534
562
|
const dividerTop = Math.min(getArchBoundaryY(x1, innerHalfWidth, innerSpringY, innerArchHeight), getArchBoundaryY(x2, innerHalfWidth, innerSpringY, innerArchHeight));
|
|
535
563
|
if (dividerTop > innerBottom + 0.01) {
|
|
536
|
-
addShape(mesh, baseMaterial, createRectShape(x1, x2, innerBottom, dividerTop), frameDepth
|
|
564
|
+
addShape(mesh, baseMaterial, createRectShape(x1, x2, innerBottom, dividerTop), windowDepths(frameDepth).bar);
|
|
537
565
|
}
|
|
538
566
|
x += columnDividerThickness;
|
|
539
567
|
}
|
|
@@ -547,34 +575,38 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
547
575
|
const x1 = -halfAtTop;
|
|
548
576
|
const x2 = halfAtTop;
|
|
549
577
|
if (x2 - x1 > 0.01 && yTop > innerBottom) {
|
|
550
|
-
addShape(mesh, baseMaterial, createRectShape(x1, x2, Math.max(yBottom, innerBottom), yTop), frameDepth
|
|
578
|
+
addShape(mesh, baseMaterial, createRectShape(x1, x2, Math.max(yBottom, innerBottom), yTop), windowDepths(frameDepth).bar);
|
|
551
579
|
}
|
|
552
580
|
y -= rowDividerThickness;
|
|
553
581
|
}
|
|
554
582
|
}
|
|
555
|
-
if (sill)
|
|
556
|
-
|
|
557
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
558
|
-
currentWindowSlot = 'frame';
|
|
559
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
560
|
-
}
|
|
583
|
+
if (sill)
|
|
584
|
+
addSill(mesh, node);
|
|
561
585
|
}
|
|
562
586
|
function getWindowRenderOpenAmount(node) {
|
|
563
587
|
const runtimeValue = useInteractive.getState().windows[node.id]?.operationState;
|
|
564
588
|
return Math.min(Math.max(runtimeValue ?? node.operationState ?? 0, 0), 1);
|
|
565
589
|
}
|
|
590
|
+
function getWindowRenderShutterAmount(node, field) {
|
|
591
|
+
const interactive = useInteractive.getState();
|
|
592
|
+
return Math.min(Math.max(interactive.windows[node.id]?.[field] ??
|
|
593
|
+
interactive.windowAnimations[node.id]?.[field]?.from ??
|
|
594
|
+
node[field] ??
|
|
595
|
+
1, 0), 1);
|
|
596
|
+
}
|
|
566
597
|
function getAwningDirection(node) {
|
|
567
598
|
return node.windowType === 'hopper' ? 'down' : (node.awningDirection ?? 'up');
|
|
568
599
|
}
|
|
569
600
|
function isRectangleOnlyWindowType(node) {
|
|
570
|
-
return (node.windowType === '
|
|
601
|
+
return (node.windowType === 'tilt-turn' ||
|
|
602
|
+
node.windowType === 'sliding' ||
|
|
571
603
|
node.windowType === 'single-hung' ||
|
|
572
604
|
node.windowType === 'double-hung' ||
|
|
573
605
|
node.windowType === 'bay' ||
|
|
574
606
|
node.windowType === 'bow');
|
|
575
607
|
}
|
|
576
608
|
function addSlidingWindowVisuals(node, mesh) {
|
|
577
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
609
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
578
610
|
const innerW = width - 2 * frameThickness;
|
|
579
611
|
const innerH = height - 2 * frameThickness;
|
|
580
612
|
// Outer frame.
|
|
@@ -584,7 +616,7 @@ function addSlidingWindowVisuals(node, mesh) {
|
|
|
584
616
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
585
617
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
586
618
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
587
|
-
const glassDepth =
|
|
619
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
588
620
|
const railThickness = Math.max(frameThickness * 0.55, 0.025);
|
|
589
621
|
const trackThickness = Math.max(frameThickness * 0.35, 0.018);
|
|
590
622
|
const panelOverlap = Math.min(Math.max(frameThickness * 0.9, 0.04), innerW * 0.12);
|
|
@@ -610,23 +642,19 @@ function addSlidingWindowVisuals(node, mesh) {
|
|
|
610
642
|
addBox(mesh, glassMaterial, panelWidth, panelH, glassDepth, rightPanelX, 0, rightZ);
|
|
611
643
|
// The right sash stays fixed. The left sash is the active panel that slides across it.
|
|
612
644
|
currentWindowSlot = 'frame';
|
|
613
|
-
addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth
|
|
614
|
-
addBox(mesh, baseMaterial, railThickness, panelH, frameDepth
|
|
615
|
-
addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth
|
|
616
|
-
addBox(mesh, baseMaterial, railThickness, panelH, frameDepth
|
|
617
|
-
}
|
|
618
|
-
if (sill) {
|
|
619
|
-
const sillW = width + sillDepth * 0.4;
|
|
620
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
621
|
-
currentWindowSlot = 'frame';
|
|
622
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
645
|
+
addBox(activePanel, baseMaterial, railThickness, panelH, windowDepths(frameDepth).sash, -panelWidth / 2 + railThickness / 2, 0, 0);
|
|
646
|
+
addBox(mesh, baseMaterial, railThickness, panelH, windowDepths(frameDepth).sash, rightPanelX + panelWidth / 2 - railThickness / 2, 0, rightZ);
|
|
647
|
+
addBox(activePanel, baseMaterial, railThickness, panelH, windowDepths(frameDepth).sash, panelWidth / 2 - railThickness / 2, 0, 0);
|
|
648
|
+
addBox(mesh, baseMaterial, railThickness, panelH, windowDepths(frameDepth).sash, rightPanelX - panelWidth / 2 + railThickness / 2, 0, rightZ);
|
|
623
649
|
}
|
|
650
|
+
if (sill)
|
|
651
|
+
addSill(mesh, node);
|
|
624
652
|
}
|
|
625
653
|
function addRectCasementSash(parent, name, panelW, panelH, frameThickness, frameDepth, pivotX, sashCenterX, rotationY) {
|
|
626
654
|
const sash = new THREE.Group();
|
|
627
655
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
628
|
-
const sashDepth = frameDepth
|
|
629
|
-
const glassDepth =
|
|
656
|
+
const sashDepth = windowDepths(frameDepth).sash;
|
|
657
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
630
658
|
const glassW = Math.max(panelW - 2 * sashFrameThickness, 0.01);
|
|
631
659
|
const glassH = Math.max(panelH - 2 * sashFrameThickness, 0.01);
|
|
632
660
|
sash.name = name;
|
|
@@ -642,15 +670,6 @@ function addRectCasementSash(parent, name, panelW, panelH, frameThickness, frame
|
|
|
642
670
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
|
|
643
671
|
disableSubtreeRaycastIfSwung(sash, rotationY);
|
|
644
672
|
}
|
|
645
|
-
function addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth) {
|
|
646
|
-
const markerW = Math.max(frameThickness * 0.38, 0.018);
|
|
647
|
-
const markerH = innerH * 0.24;
|
|
648
|
-
currentWindowSlot = 'frame';
|
|
649
|
-
for (const pivotX of [-innerW / 2, innerW / 2]) {
|
|
650
|
-
addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, innerH * 0.25, frameDepth * 0.08);
|
|
651
|
-
addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, -innerH * 0.25, frameDepth * 0.08);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
673
|
function createFrenchArchLeafShape(leafW, leafH, fullW, springY, archHeight, side, inset = 0) {
|
|
655
674
|
const left = -leafW / 2 + inset;
|
|
656
675
|
const right = leafW / 2 - inset;
|
|
@@ -724,8 +743,8 @@ function addShapedFrenchCasementSash(parent, node, name, side, fullW, leafW, lea
|
|
|
724
743
|
const sash = new THREE.Group();
|
|
725
744
|
const sashVisual = new THREE.Group();
|
|
726
745
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
727
|
-
const sashDepth = frameDepth
|
|
728
|
-
const glassDepth =
|
|
746
|
+
const sashDepth = windowDepths(frameDepth).sash;
|
|
747
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
729
748
|
sash.name = name;
|
|
730
749
|
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
731
750
|
sash.rotation.y = rotationY;
|
|
@@ -757,7 +776,7 @@ function addShapedFrenchCasementSash(parent, node, name, side, fullW, leafW, lea
|
|
|
757
776
|
disableSubtreeRaycastIfSwung(sash, rotationY);
|
|
758
777
|
}
|
|
759
778
|
function addFrenchCasementWindowVisuals(node, mesh) {
|
|
760
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
779
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
761
780
|
const innerW = width - 2 * frameThickness;
|
|
762
781
|
const innerH = height - 2 * frameThickness;
|
|
763
782
|
// Fixed outer frame.
|
|
@@ -771,17 +790,12 @@ function addFrenchCasementWindowVisuals(node, mesh) {
|
|
|
771
790
|
const leafW = innerW / 2;
|
|
772
791
|
addRectCasementSash(mesh, FRENCH_CASEMENT_LEFT_SASH_NAME, leafW, innerH, frameThickness, frameDepth, -innerW / 2, leafW / 2, -openAngle);
|
|
773
792
|
addRectCasementSash(mesh, FRENCH_CASEMENT_RIGHT_SASH_NAME, leafW, innerH, frameThickness, frameDepth, innerW / 2, -leafW / 2, openAngle);
|
|
774
|
-
addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth);
|
|
775
|
-
}
|
|
776
|
-
if (sill) {
|
|
777
|
-
const sillW = width + sillDepth * 0.4;
|
|
778
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
779
|
-
currentWindowSlot = 'frame';
|
|
780
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
781
793
|
}
|
|
794
|
+
if (sill)
|
|
795
|
+
addSill(mesh, node);
|
|
782
796
|
}
|
|
783
797
|
function addShapedCasementWindowVisuals(node, mesh) {
|
|
784
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
798
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
785
799
|
const innerW = width - 2 * frameThickness;
|
|
786
800
|
const innerH = height - 2 * frameThickness;
|
|
787
801
|
if (node.openingShape === 'arch') {
|
|
@@ -798,14 +812,9 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
798
812
|
const leafW = innerW / 2;
|
|
799
813
|
addShapedFrenchCasementSash(mesh, node, FRENCH_CASEMENT_LEFT_SASH_NAME, 'left', innerW, leafW, innerH, frameThickness, frameDepth, -innerW / 2, leafW / 2, -openAngle);
|
|
800
814
|
addShapedFrenchCasementSash(mesh, node, FRENCH_CASEMENT_RIGHT_SASH_NAME, 'right', innerW, leafW, innerH, frameThickness, frameDepth, innerW / 2, -leafW / 2, openAngle);
|
|
801
|
-
addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth);
|
|
802
|
-
}
|
|
803
|
-
if (sill) {
|
|
804
|
-
const sillW = width + sillDepth * 0.4;
|
|
805
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
806
|
-
currentWindowSlot = 'frame';
|
|
807
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
808
815
|
}
|
|
816
|
+
if (sill)
|
|
817
|
+
addSill(mesh, node);
|
|
809
818
|
return;
|
|
810
819
|
}
|
|
811
820
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
@@ -816,8 +825,8 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
816
825
|
const pivotX = hingeSide === 'left' ? -innerW / 2 : innerW / 2;
|
|
817
826
|
const sashCenterX = hingeSide === 'left' ? innerW / 2 : -innerW / 2;
|
|
818
827
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
819
|
-
const sashDepth = frameDepth
|
|
820
|
-
const glassDepth =
|
|
828
|
+
const sashDepth = windowDepths(frameDepth).sash;
|
|
829
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
821
830
|
const sash = new THREE.Group();
|
|
822
831
|
const sashVisual = new THREE.Group();
|
|
823
832
|
sash.name = CASEMENT_WINDOW_SASH_NAME;
|
|
@@ -851,19 +860,12 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
851
860
|
}
|
|
852
861
|
}
|
|
853
862
|
disableSubtreeRaycastIfSwung(sash, sash.rotation.y);
|
|
854
|
-
currentWindowSlot = 'frame';
|
|
855
|
-
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
|
|
856
|
-
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
|
|
857
|
-
}
|
|
858
|
-
if (sill) {
|
|
859
|
-
const sillW = width + sillDepth * 0.4;
|
|
860
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
861
|
-
currentWindowSlot = 'frame';
|
|
862
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
863
863
|
}
|
|
864
|
+
if (sill)
|
|
865
|
+
addSill(mesh, node);
|
|
864
866
|
}
|
|
865
867
|
function addCasementWindowVisuals(node, mesh) {
|
|
866
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
868
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
867
869
|
if (node.openingShape === 'rounded' || node.openingShape === 'arch') {
|
|
868
870
|
addShapedCasementWindowVisuals(node, mesh);
|
|
869
871
|
return;
|
|
@@ -889,8 +891,8 @@ function addCasementWindowVisuals(node, mesh) {
|
|
|
889
891
|
const pivotX = hingeSide === 'left' ? -innerW / 2 : innerW / 2;
|
|
890
892
|
const sashCenterX = hingeSide === 'left' ? innerW / 2 : -innerW / 2;
|
|
891
893
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
892
|
-
const sashDepth = frameDepth
|
|
893
|
-
const glassDepth =
|
|
894
|
+
const sashDepth = windowDepths(frameDepth).sash;
|
|
895
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
894
896
|
const glassW = Math.max(innerW - 2 * sashFrameThickness, 0.01);
|
|
895
897
|
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
896
898
|
sash.name = CASEMENT_WINDOW_SASH_NAME;
|
|
@@ -905,20 +907,37 @@ function addCasementWindowVisuals(node, mesh) {
|
|
|
905
907
|
currentWindowSlot = 'glass';
|
|
906
908
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
|
|
907
909
|
disableSubtreeRaycastIfSwung(sash, sash.rotation.y);
|
|
908
|
-
// Small hinge markers make the pivot side legible when the sash is closed.
|
|
909
|
-
currentWindowSlot = 'frame';
|
|
910
|
-
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
|
|
911
|
-
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
|
|
912
910
|
}
|
|
913
|
-
if (sill)
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
911
|
+
if (sill)
|
|
912
|
+
addSill(mesh, node);
|
|
913
|
+
}
|
|
914
|
+
function addTiltTurnWindowVisuals(node, mesh) {
|
|
915
|
+
const { width, height, frameDepth, frameThickness: rail } = node;
|
|
916
|
+
const innerW = width - 2 * rail;
|
|
917
|
+
const innerH = height - 2 * rail;
|
|
918
|
+
currentWindowSlot = 'frame';
|
|
919
|
+
for (const side of [-1, 1]) {
|
|
920
|
+
addBox(mesh, baseMaterial, width, rail, frameDepth, 0, (side * (height - rail)) / 2, 0);
|
|
921
|
+
addBox(mesh, baseMaterial, rail, innerH, frameDepth, (side * (width - rail)) / 2, 0, 0);
|
|
922
|
+
}
|
|
923
|
+
if (innerW > 0.01 && innerH > 0.01) {
|
|
924
|
+
const turn = node.tiltTurnMode === 'turn';
|
|
925
|
+
const side = node.hingesSide === 'right' ? 1 : -1;
|
|
926
|
+
const sash = new THREE.Group();
|
|
927
|
+
sash.name = TILT_TURN_WINDOW_SASH_NAME;
|
|
928
|
+
sash.position.set(turn ? (side * innerW) / 2 : 0, turn ? 0 : -innerH / 2, frameDepth * 0.06);
|
|
929
|
+
const content = new THREE.Group();
|
|
930
|
+
content.position.set(turn ? (-side * innerW) / 2 : 0, turn ? 0 : innerH / 2, 0);
|
|
931
|
+
sash.add(content);
|
|
932
|
+
mesh.add(sash);
|
|
933
|
+
const sashRail = Math.max(rail * 0.72, 0.032);
|
|
934
|
+
addHungSash(content, innerW, innerH, sashRail, frameDepth, windowDepths(frameDepth).glass, Math.max(innerW - 2 * sashRail, 0.01), Math.max(innerH - 2 * sashRail, 0.01));
|
|
918
935
|
}
|
|
936
|
+
if (node.sill)
|
|
937
|
+
addSill(mesh, node);
|
|
919
938
|
}
|
|
920
939
|
function addAwningWindowVisuals(node, mesh) {
|
|
921
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
940
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
922
941
|
if (node.openingShape === 'rounded' || node.openingShape === 'arch') {
|
|
923
942
|
addShapedAwningWindowVisuals(node, mesh);
|
|
924
943
|
return;
|
|
@@ -939,8 +958,8 @@ function addAwningWindowVisuals(node, mesh) {
|
|
|
939
958
|
const pivotY = isDownward ? -innerH / 2 : innerH / 2;
|
|
940
959
|
const sashCenterY = isDownward ? innerH / 2 : -innerH / 2;
|
|
941
960
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
942
|
-
const sashDepth = frameDepth
|
|
943
|
-
const glassDepth =
|
|
961
|
+
const sashDepth = windowDepths(frameDepth).sash;
|
|
962
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
944
963
|
const glassW = Math.max(innerW - 2 * sashFrameThickness, 0.01);
|
|
945
964
|
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
946
965
|
sash.name = AWNING_WINDOW_SASH_NAME;
|
|
@@ -954,19 +973,12 @@ function addAwningWindowVisuals(node, mesh) {
|
|
|
954
973
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, sashCenterY, 0);
|
|
955
974
|
currentWindowSlot = 'glass';
|
|
956
975
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, sashCenterY, sashDepth * 0.08);
|
|
957
|
-
// Compact hinge rail, visible even when the sash is closed.
|
|
958
|
-
currentWindowSlot = 'frame';
|
|
959
|
-
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
960
|
-
}
|
|
961
|
-
if (sill) {
|
|
962
|
-
const sillW = width + sillDepth * 0.4;
|
|
963
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
964
|
-
currentWindowSlot = 'frame';
|
|
965
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
966
976
|
}
|
|
977
|
+
if (sill)
|
|
978
|
+
addSill(mesh, node);
|
|
967
979
|
}
|
|
968
980
|
function addShapedAwningWindowVisuals(node, mesh) {
|
|
969
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
981
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
970
982
|
const innerW = width - 2 * frameThickness;
|
|
971
983
|
const innerH = height - 2 * frameThickness;
|
|
972
984
|
if (node.openingShape === 'arch') {
|
|
@@ -983,8 +995,8 @@ function addShapedAwningWindowVisuals(node, mesh) {
|
|
|
983
995
|
const isDownward = getAwningDirection(node) === 'down';
|
|
984
996
|
const pivotY = isDownward ? -innerH / 2 : innerH / 2;
|
|
985
997
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
986
|
-
const sashDepth = frameDepth
|
|
987
|
-
const glassDepth =
|
|
998
|
+
const sashDepth = windowDepths(frameDepth).sash;
|
|
999
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
988
1000
|
const sash = new THREE.Group();
|
|
989
1001
|
const sashVisual = new THREE.Group();
|
|
990
1002
|
sash.name = AWNING_WINDOW_SASH_NAME;
|
|
@@ -1017,134 +1029,21 @@ function addShapedAwningWindowVisuals(node, mesh) {
|
|
|
1017
1029
|
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
1018
1030
|
}
|
|
1019
1031
|
}
|
|
1020
|
-
currentWindowSlot = 'frame';
|
|
1021
|
-
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
1022
|
-
}
|
|
1023
|
-
if (sill) {
|
|
1024
|
-
const sillW = width + sillDepth * 0.4;
|
|
1025
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1026
|
-
currentWindowSlot = 'frame';
|
|
1027
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
function addHopperWindowVisuals(node, mesh) {
|
|
1031
|
-
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
1032
|
-
if (node.openingShape === 'rounded' || node.openingShape === 'arch') {
|
|
1033
|
-
addShapedHopperWindowVisuals(node, mesh);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
const innerW = width - 2 * frameThickness;
|
|
1037
|
-
const innerH = height - 2 * frameThickness;
|
|
1038
|
-
// Fixed outer frame.
|
|
1039
|
-
currentWindowSlot = 'frame';
|
|
1040
|
-
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1041
|
-
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1042
|
-
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
1043
|
-
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
1044
|
-
if (innerW > 0.01 && innerH > 0.01) {
|
|
1045
|
-
const openAmount = getWindowRenderOpenAmount(node);
|
|
1046
|
-
const openAngle = openAmount * (Math.PI / 3);
|
|
1047
|
-
const sash = new THREE.Group();
|
|
1048
|
-
const pivotY = -innerH / 2;
|
|
1049
|
-
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
1050
|
-
const sashDepth = frameDepth * 0.72;
|
|
1051
|
-
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
1052
|
-
const glassW = Math.max(innerW - 2 * sashFrameThickness, 0.01);
|
|
1053
|
-
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
1054
|
-
sash.name = HOPPER_WINDOW_SASH_NAME;
|
|
1055
|
-
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
1056
|
-
sash.rotation.x = -openAngle;
|
|
1057
|
-
mesh.add(sash);
|
|
1058
|
-
currentWindowSlot = 'frame';
|
|
1059
|
-
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, innerH - sashFrameThickness / 2, 0);
|
|
1060
|
-
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashFrameThickness / 2, 0);
|
|
1061
|
-
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, innerH / 2, 0);
|
|
1062
|
-
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, innerH / 2, 0);
|
|
1063
|
-
currentWindowSlot = 'glass';
|
|
1064
|
-
addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, innerH / 2, sashDepth * 0.08);
|
|
1065
|
-
// Compact bottom hinge rail, visible even when the sash is closed.
|
|
1066
|
-
currentWindowSlot = 'frame';
|
|
1067
|
-
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
1068
|
-
}
|
|
1069
|
-
if (sill) {
|
|
1070
|
-
const sillW = width + sillDepth * 0.4;
|
|
1071
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1072
|
-
currentWindowSlot = 'frame';
|
|
1073
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
function addShapedHopperWindowVisuals(node, mesh) {
|
|
1077
|
-
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
1078
|
-
const innerW = width - 2 * frameThickness;
|
|
1079
|
-
const innerH = height - 2 * frameThickness;
|
|
1080
|
-
if (node.openingShape === 'arch') {
|
|
1081
|
-
currentWindowSlot = 'frame';
|
|
1082
|
-
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
1083
|
-
}
|
|
1084
|
-
else {
|
|
1085
|
-
currentWindowSlot = 'frame';
|
|
1086
|
-
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
1087
|
-
}
|
|
1088
|
-
if (innerW > 0.01 && innerH > 0.01) {
|
|
1089
|
-
const openAmount = getWindowRenderOpenAmount(node);
|
|
1090
|
-
const openAngle = openAmount * (Math.PI / 3);
|
|
1091
|
-
const pivotY = -innerH / 2;
|
|
1092
|
-
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
1093
|
-
const sashDepth = frameDepth * 0.72;
|
|
1094
|
-
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
1095
|
-
const sash = new THREE.Group();
|
|
1096
|
-
const sashVisual = new THREE.Group();
|
|
1097
|
-
sash.name = HOPPER_WINDOW_SASH_NAME;
|
|
1098
|
-
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
1099
|
-
sash.rotation.x = -openAngle;
|
|
1100
|
-
sashVisual.position.y = innerH / 2;
|
|
1101
|
-
sash.add(sashVisual);
|
|
1102
|
-
mesh.add(sash);
|
|
1103
|
-
if (node.openingShape === 'arch') {
|
|
1104
|
-
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
1105
|
-
currentWindowSlot = 'frame';
|
|
1106
|
-
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
1107
|
-
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
1108
|
-
if (glassInset > 0.001) {
|
|
1109
|
-
const glassW = innerW - 2 * glassInset;
|
|
1110
|
-
const glassH = innerH - 2 * glassInset;
|
|
1111
|
-
currentWindowSlot = 'glass';
|
|
1112
|
-
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
else {
|
|
1116
|
-
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
1117
|
-
currentWindowSlot = 'frame';
|
|
1118
|
-
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
1119
|
-
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
1120
|
-
if (glassInset > 0.001) {
|
|
1121
|
-
const glassW = innerW - 2 * glassInset;
|
|
1122
|
-
const glassH = innerH - 2 * glassInset;
|
|
1123
|
-
currentWindowSlot = 'glass';
|
|
1124
|
-
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
currentWindowSlot = 'frame';
|
|
1128
|
-
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
1129
|
-
}
|
|
1130
|
-
if (sill) {
|
|
1131
|
-
const sillW = width + sillDepth * 0.4;
|
|
1132
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1133
|
-
currentWindowSlot = 'frame';
|
|
1134
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1135
1032
|
}
|
|
1033
|
+
if (sill)
|
|
1034
|
+
addSill(mesh, node);
|
|
1136
1035
|
}
|
|
1137
1036
|
function addHungSash(parent, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH) {
|
|
1138
1037
|
currentWindowSlot = 'frame';
|
|
1139
|
-
addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth
|
|
1140
|
-
addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth
|
|
1141
|
-
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth
|
|
1142
|
-
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth
|
|
1038
|
+
addBox(parent, baseMaterial, panelW, sashFrameThickness, windowDepths(frameDepth).sash, 0, panelHeight / 2 - sashFrameThickness / 2, 0);
|
|
1039
|
+
addBox(parent, baseMaterial, panelW, sashFrameThickness, windowDepths(frameDepth).sash, 0, -panelHeight / 2 + sashFrameThickness / 2, 0);
|
|
1040
|
+
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, windowDepths(frameDepth).sash, -panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
1041
|
+
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, windowDepths(frameDepth).sash, panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
1143
1042
|
currentWindowSlot = 'glass';
|
|
1144
1043
|
addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, 0);
|
|
1145
1044
|
}
|
|
1146
1045
|
function addSingleHungWindowVisuals(node, mesh) {
|
|
1147
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
1046
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
1148
1047
|
const innerW = width - 2 * frameThickness;
|
|
1149
1048
|
const innerH = height - 2 * frameThickness;
|
|
1150
1049
|
// Fixed outer frame.
|
|
@@ -1154,7 +1053,7 @@ function addSingleHungWindowVisuals(node, mesh) {
|
|
|
1154
1053
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
1155
1054
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
1156
1055
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
1157
|
-
const glassDepth =
|
|
1056
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
1158
1057
|
const railThickness = Math.max(frameThickness * 0.55, 0.025);
|
|
1159
1058
|
const trackThickness = Math.max(frameThickness * 0.35, 0.018);
|
|
1160
1059
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
@@ -1184,18 +1083,14 @@ function addSingleHungWindowVisuals(node, mesh) {
|
|
|
1184
1083
|
addHungSash(activeSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
1185
1084
|
// Meeting rails: top sash fixed, bottom sash moves upward over it.
|
|
1186
1085
|
currentWindowSlot = 'frame';
|
|
1187
|
-
addBox(mesh, baseMaterial, panelW, railThickness, frameDepth
|
|
1188
|
-
addBox(activeSash, baseMaterial, panelW, railThickness, frameDepth
|
|
1189
|
-
}
|
|
1190
|
-
if (sill) {
|
|
1191
|
-
const sillW = width + sillDepth * 0.4;
|
|
1192
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1193
|
-
currentWindowSlot = 'frame';
|
|
1194
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1086
|
+
addBox(mesh, baseMaterial, panelW, railThickness, windowDepths(frameDepth).sash, 0, topPanelY - panelHeight / 2 + railThickness / 2, topZ);
|
|
1087
|
+
addBox(activeSash, baseMaterial, panelW, railThickness, windowDepths(frameDepth).sash, 0, panelHeight / 2 - railThickness / 2, 0);
|
|
1195
1088
|
}
|
|
1089
|
+
if (sill)
|
|
1090
|
+
addSill(mesh, node);
|
|
1196
1091
|
}
|
|
1197
1092
|
function addDoubleHungWindowVisuals(node, mesh) {
|
|
1198
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
1093
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
1199
1094
|
const innerW = width - 2 * frameThickness;
|
|
1200
1095
|
const innerH = height - 2 * frameThickness;
|
|
1201
1096
|
// Fixed outer frame.
|
|
@@ -1205,7 +1100,7 @@ function addDoubleHungWindowVisuals(node, mesh) {
|
|
|
1205
1100
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
1206
1101
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
1207
1102
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
1208
|
-
const glassDepth =
|
|
1103
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
1209
1104
|
const railThickness = Math.max(frameThickness * 0.55, 0.025);
|
|
1210
1105
|
const trackThickness = Math.max(frameThickness * 0.35, 0.018);
|
|
1211
1106
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
@@ -1236,18 +1131,14 @@ function addDoubleHungWindowVisuals(node, mesh) {
|
|
|
1236
1131
|
addHungSash(bottomSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
1237
1132
|
// Opposing meeting rails: top sash descends while bottom sash rises.
|
|
1238
1133
|
currentWindowSlot = 'frame';
|
|
1239
|
-
addBox(topSash, baseMaterial, panelW, railThickness, frameDepth
|
|
1240
|
-
addBox(bottomSash, baseMaterial, panelW, railThickness, frameDepth
|
|
1241
|
-
}
|
|
1242
|
-
if (sill) {
|
|
1243
|
-
const sillW = width + sillDepth * 0.4;
|
|
1244
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1245
|
-
currentWindowSlot = 'frame';
|
|
1246
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1134
|
+
addBox(topSash, baseMaterial, panelW, railThickness, windowDepths(frameDepth).sash, 0, -panelHeight / 2 + railThickness / 2, 0);
|
|
1135
|
+
addBox(bottomSash, baseMaterial, panelW, railThickness, windowDepths(frameDepth).sash, 0, panelHeight / 2 - railThickness / 2, 0);
|
|
1247
1136
|
}
|
|
1137
|
+
if (sill)
|
|
1138
|
+
addSill(mesh, node);
|
|
1248
1139
|
}
|
|
1249
1140
|
function addBayWindowVisuals(node, mesh) {
|
|
1250
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
1141
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
1251
1142
|
const innerW = width - 2 * frameThickness;
|
|
1252
1143
|
const innerH = height - 2 * frameThickness;
|
|
1253
1144
|
currentWindowSlot = 'frame';
|
|
@@ -1261,9 +1152,9 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1261
1152
|
const sideRun = Math.max((innerW - centerW) / 2, 0.01);
|
|
1262
1153
|
const sideW = Math.hypot(sideRun, projectionDepth);
|
|
1263
1154
|
const sideAngle = Math.atan2(projectionDepth, sideRun);
|
|
1264
|
-
const panelDepth =
|
|
1155
|
+
const panelDepth = windowDepths(frameDepth).sash;
|
|
1265
1156
|
const sashFrameThickness = Math.max(frameThickness * 0.72, 0.032);
|
|
1266
|
-
const glassDepth =
|
|
1157
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
1267
1158
|
const bayFootprint = [
|
|
1268
1159
|
[-innerW / 2, 0],
|
|
1269
1160
|
[-centerW / 2, projectionDepth],
|
|
@@ -1316,15 +1207,11 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1316
1207
|
addBayCap(innerH / 2);
|
|
1317
1208
|
addBayCap(-innerH / 2);
|
|
1318
1209
|
}
|
|
1319
|
-
if (sill)
|
|
1320
|
-
|
|
1321
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1322
|
-
currentWindowSlot = 'frame';
|
|
1323
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1324
|
-
}
|
|
1210
|
+
if (sill)
|
|
1211
|
+
addSill(mesh, node);
|
|
1325
1212
|
}
|
|
1326
1213
|
function addBowWindowVisuals(node, mesh) {
|
|
1327
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
1214
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
1328
1215
|
const innerW = width - 2 * frameThickness;
|
|
1329
1216
|
const innerH = height - 2 * frameThickness;
|
|
1330
1217
|
currentWindowSlot = 'frame';
|
|
@@ -1396,18 +1283,14 @@ function addBowWindowVisuals(node, mesh) {
|
|
|
1396
1283
|
currentWindowSlot = 'frame';
|
|
1397
1284
|
for (let index = 0; index <= mullionCount; index += 1) {
|
|
1398
1285
|
const x = -halfSpan + (innerW * index) / mullionCount;
|
|
1399
|
-
addBox(mesh, baseMaterial, sashFrameThickness, innerH, frameDepth
|
|
1286
|
+
addBox(mesh, baseMaterial, sashFrameThickness, innerH, windowDepths(frameDepth).bar, x, 0, arcZAt(x));
|
|
1400
1287
|
}
|
|
1401
1288
|
}
|
|
1402
|
-
if (sill)
|
|
1403
|
-
|
|
1404
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1405
|
-
currentWindowSlot = 'frame';
|
|
1406
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1407
|
-
}
|
|
1289
|
+
if (sill)
|
|
1290
|
+
addSill(mesh, node);
|
|
1408
1291
|
}
|
|
1409
1292
|
function addLouveredWindowVisuals(node, mesh) {
|
|
1410
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
1293
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
1411
1294
|
if (node.openingShape === 'rounded' || node.openingShape === 'arch') {
|
|
1412
1295
|
addShapedLouveredWindowVisuals(node, mesh);
|
|
1413
1296
|
return;
|
|
@@ -1424,15 +1307,15 @@ function addLouveredWindowVisuals(node, mesh) {
|
|
|
1424
1307
|
const slatCount = Math.max(4, Math.min(9, Math.round(height / 0.22)));
|
|
1425
1308
|
const slatGap = innerH / slatCount;
|
|
1426
1309
|
const slatHeight = Math.max(Math.min(slatGap * 0.62, 0.14), 0.045);
|
|
1427
|
-
const slatDepth =
|
|
1310
|
+
const slatDepth = windowDepths(frameDepth).glass;
|
|
1428
1311
|
const slatAngle = -openAmount * (Math.PI / 3);
|
|
1429
1312
|
const railThickness = Math.max(frameThickness * 0.45, 0.022);
|
|
1430
1313
|
const slats = new THREE.Group();
|
|
1431
1314
|
slats.name = LOUVERED_WINDOW_SLATS_NAME;
|
|
1432
1315
|
mesh.add(slats);
|
|
1433
1316
|
currentWindowSlot = 'frame';
|
|
1434
|
-
addBox(mesh, baseMaterial, railThickness, innerH, frameDepth
|
|
1435
|
-
addBox(mesh, baseMaterial, railThickness, innerH, frameDepth
|
|
1317
|
+
addBox(mesh, baseMaterial, railThickness, innerH, windowDepths(frameDepth).bar, -innerW / 2 + railThickness / 2, 0, 0);
|
|
1318
|
+
addBox(mesh, baseMaterial, railThickness, innerH, windowDepths(frameDepth).bar, innerW / 2 - railThickness / 2, 0, 0);
|
|
1436
1319
|
currentWindowSlot = 'glass';
|
|
1437
1320
|
for (let index = 0; index < slatCount; index += 1) {
|
|
1438
1321
|
const y = innerH / 2 - slatGap * (index + 0.5);
|
|
@@ -1443,15 +1326,11 @@ function addLouveredWindowVisuals(node, mesh) {
|
|
|
1443
1326
|
addBox(slat, glassMaterial, Math.max(innerW - 2 * railThickness, 0.01), slatHeight, slatDepth, 0, 0, 0);
|
|
1444
1327
|
}
|
|
1445
1328
|
}
|
|
1446
|
-
if (sill)
|
|
1447
|
-
|
|
1448
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1449
|
-
currentWindowSlot = 'frame';
|
|
1450
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1451
|
-
}
|
|
1329
|
+
if (sill)
|
|
1330
|
+
addSill(mesh, node);
|
|
1452
1331
|
}
|
|
1453
1332
|
function addShapedLouveredWindowVisuals(node, mesh) {
|
|
1454
|
-
const { width, height, frameDepth, frameThickness, sill
|
|
1333
|
+
const { width, height, frameDepth, frameThickness, sill } = node;
|
|
1455
1334
|
const halfWidth = width / 2;
|
|
1456
1335
|
const bottom = -height / 2;
|
|
1457
1336
|
const top = height / 2;
|
|
@@ -1475,7 +1354,7 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1475
1354
|
const slatCount = Math.max(4, Math.min(9, Math.round(height / 0.22)));
|
|
1476
1355
|
const slatGap = innerH / slatCount;
|
|
1477
1356
|
const slatHeight = Math.max(Math.min(slatGap * 0.62, 0.14), 0.045);
|
|
1478
|
-
const slatDepth =
|
|
1357
|
+
const slatDepth = windowDepths(frameDepth).glass;
|
|
1479
1358
|
const slatAngle = -openAmount * (Math.PI / 3);
|
|
1480
1359
|
const railThickness = Math.max(frameThickness * 0.45, 0.022);
|
|
1481
1360
|
const slatInset = railThickness + 0.004;
|
|
@@ -1504,7 +1383,7 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1504
1383
|
const railTop = node.openingShape === 'arch'
|
|
1505
1384
|
? getArchBoundaryY(sampleX, innerW / 2, top - getClampedArchHeight(width, height, node.archHeight), getClampedArchHeight(innerW, innerH, getClampedArchHeight(width, height, node.archHeight) - inset))
|
|
1506
1385
|
: getRoundedBoundaryYAtX(sampleX, innerLeft, innerRight, innerTop, insetCornerRadii(getWindowRoundedRadii(node, width, height), inset, innerW, innerH));
|
|
1507
|
-
addShape(mesh, baseMaterial, createRectShape(Math.min(railX1, railX2), Math.max(railX1, railX2), innerBottom, railTop), frameDepth
|
|
1386
|
+
addShape(mesh, baseMaterial, createRectShape(Math.min(railX1, railX2), Math.max(railX1, railX2), innerBottom, railTop), windowDepths(frameDepth).bar);
|
|
1508
1387
|
};
|
|
1509
1388
|
addVerticalRail(innerLeft);
|
|
1510
1389
|
addVerticalRail(innerRight);
|
|
@@ -1525,12 +1404,8 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1525
1404
|
addBox(slat, glassMaterial, slatW, slatHeight, slatDepth, 0, 0, 0);
|
|
1526
1405
|
}
|
|
1527
1406
|
}
|
|
1528
|
-
if (sill)
|
|
1529
|
-
|
|
1530
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1531
|
-
currentWindowSlot = 'frame';
|
|
1532
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1533
|
-
}
|
|
1407
|
+
if (sill)
|
|
1408
|
+
addSill(mesh, node);
|
|
1534
1409
|
}
|
|
1535
1410
|
/**
|
|
1536
1411
|
* The side of a corner window fused at the corner, in the window's own frame: 1 its +x side, -1 its
|
|
@@ -1544,17 +1419,26 @@ function fusedCornerSide(node, rotationY) {
|
|
|
1544
1419
|
const flipped = Math.abs(rotationY) > Math.PI / 2;
|
|
1545
1420
|
return (pair.end === 'end') !== flipped ? 1 : -1;
|
|
1546
1421
|
}
|
|
1547
|
-
function updateWindowMesh(
|
|
1422
|
+
function updateWindowMesh(rawNode, mesh) {
|
|
1548
1423
|
currentWindowSlot = undefined;
|
|
1549
1424
|
mesh.userData[FUSED_CORNER_SIDE] = 0;
|
|
1425
|
+
// The frame runs through its host wall: an auto depth is the wall's thickness
|
|
1426
|
+
// exactly, so every builder below reads one resolved number.
|
|
1427
|
+
const parent = rawNode.parentId
|
|
1428
|
+
? useScene.getState().nodes[rawNode.parentId]
|
|
1429
|
+
: undefined;
|
|
1430
|
+
const wall = parent?.type === 'wall' ? getEffectiveNode(parent) : undefined;
|
|
1431
|
+
const node = {
|
|
1432
|
+
...rawNode,
|
|
1433
|
+
frameDepth: resolveOpeningFrameDepth(rawNode, wall ? getWallThickness(wall) : DEFAULT_WALL_THICKNESS),
|
|
1434
|
+
};
|
|
1550
1435
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
1551
1436
|
mesh.geometry.dispose();
|
|
1552
1437
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
1553
1438
|
mesh.material = hitboxMaterial;
|
|
1554
1439
|
// Sync transform from node (React may lag behind the system by a frame during drag)
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
? getOpeningWallPlacement(getEffectiveNode(parent), node, useScene.getState().nodes)
|
|
1440
|
+
const placement = wall && !node.roofSegmentId
|
|
1441
|
+
? getOpeningWallPlacement(wall, node, useScene.getState().nodes)
|
|
1558
1442
|
: node;
|
|
1559
1443
|
mesh.position.set(...placement.position);
|
|
1560
1444
|
mesh.rotation.set(...placement.rotation);
|
|
@@ -1577,7 +1461,9 @@ function updateWindowMesh(node, mesh) {
|
|
|
1577
1461
|
// window resets cleanly without a restore).
|
|
1578
1462
|
baseMaterial = resolveWindowSlotMaterial(node, 'frame');
|
|
1579
1463
|
glassMaterial = resolveWindowSlotMaterial(node, 'glass');
|
|
1580
|
-
|
|
1464
|
+
// The casing sits on the wall faces around the opening, whatever fills it.
|
|
1465
|
+
addWindowCasing(node, mesh);
|
|
1466
|
+
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, openingKind, openingShape, windowType, } = node;
|
|
1581
1467
|
if (openingKind === 'opening') {
|
|
1582
1468
|
syncWindowCutout(node, mesh);
|
|
1583
1469
|
return true;
|
|
@@ -1597,6 +1483,11 @@ function updateWindowMesh(node, mesh) {
|
|
|
1597
1483
|
syncWindowCutout(node, mesh);
|
|
1598
1484
|
return true;
|
|
1599
1485
|
}
|
|
1486
|
+
if (windowType === 'tilt-turn') {
|
|
1487
|
+
addTiltTurnWindowVisuals(node, mesh);
|
|
1488
|
+
syncWindowCutout(node, mesh);
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1600
1491
|
if (windowType === 'hopper') {
|
|
1601
1492
|
addAwningWindowVisuals(node, mesh);
|
|
1602
1493
|
syncWindowCutout(node, mesh);
|
|
@@ -1685,7 +1576,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1685
1576
|
currentWindowSlot = 'frame';
|
|
1686
1577
|
for (let c = 0; c < numCols - 1; c++) {
|
|
1687
1578
|
cx += colWidths[c];
|
|
1688
|
-
addBox(mesh, baseMaterial, columnDividerThickness, innerH, frameDepth, cx + columnDividerThickness / 2, 0, 0);
|
|
1579
|
+
addBox(mesh, baseMaterial, columnDividerThickness, innerH, windowDepths(frameDepth).bar, cx + columnDividerThickness / 2, 0, 0);
|
|
1689
1580
|
cx += columnDividerThickness;
|
|
1690
1581
|
}
|
|
1691
1582
|
// Row dividers — per column width, so they don't overlap column dividers (top to bottom)
|
|
@@ -1695,12 +1586,12 @@ function updateWindowMesh(node, mesh) {
|
|
|
1695
1586
|
cy -= rowHeights[r];
|
|
1696
1587
|
const divY = cy - rowDividerThickness / 2;
|
|
1697
1588
|
for (let c = 0; c < numCols; c++) {
|
|
1698
|
-
addBox(mesh, baseMaterial, colWidths[c], rowDividerThickness, frameDepth, colXCenters[c], divY, 0);
|
|
1589
|
+
addBox(mesh, baseMaterial, colWidths[c], rowDividerThickness, windowDepths(frameDepth).bar, colXCenters[c], divY, 0);
|
|
1699
1590
|
}
|
|
1700
1591
|
cy -= rowDividerThickness;
|
|
1701
1592
|
}
|
|
1702
1593
|
// Glass panes
|
|
1703
|
-
const glassDepth =
|
|
1594
|
+
const glassDepth = windowDepths(frameDepth).glass;
|
|
1704
1595
|
currentWindowSlot = 'glass';
|
|
1705
1596
|
for (let c = 0; c < numCols; c++) {
|
|
1706
1597
|
for (let r = 0; r < numRows; r++) {
|
|
@@ -1708,17 +1599,293 @@ function updateWindowMesh(node, mesh) {
|
|
|
1708
1599
|
}
|
|
1709
1600
|
}
|
|
1710
1601
|
// ── Sill ──
|
|
1711
|
-
if (sill)
|
|
1712
|
-
|
|
1713
|
-
// Protrudes from the front face of the frame (+Z)
|
|
1714
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1715
|
-
currentWindowSlot = 'frame';
|
|
1716
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1717
|
-
}
|
|
1602
|
+
if (sill)
|
|
1603
|
+
addSill(mesh, node);
|
|
1718
1604
|
syncWindowCutout(node, mesh);
|
|
1719
1605
|
return true;
|
|
1720
1606
|
}
|
|
1607
|
+
/**
|
|
1608
|
+
* The window's casing on the faces of its wall (or gable) around the cutout,
|
|
1609
|
+
* painted with the frame. Read from the live host, so it stays on the faces
|
|
1610
|
+
* while the wall's thickness is dragged.
|
|
1611
|
+
*/
|
|
1612
|
+
function addWindowCasing(node, mesh) {
|
|
1613
|
+
const nodes = useScene.getState().nodes;
|
|
1614
|
+
const hostId = node.roofSegmentId ?? node.parentId;
|
|
1615
|
+
const hostNode = hostId ? nodes[hostId] : undefined;
|
|
1616
|
+
const host = resolveOpeningCasingHost(node, nodes, hostNode ? getEffectiveNode(hostNode) : undefined);
|
|
1617
|
+
if (!host)
|
|
1618
|
+
return;
|
|
1619
|
+
for (const casing of buildOpeningCasingMeshes(node, host, baseMaterial)) {
|
|
1620
|
+
const ref = node.slots?.[casing.userData.slotId];
|
|
1621
|
+
casing.material =
|
|
1622
|
+
(currentTextures && ref
|
|
1623
|
+
? resolveMaterialRef(ref, currentSceneMaterials, currentShading)
|
|
1624
|
+
: null) ?? baseMaterial;
|
|
1625
|
+
mesh.add(casing);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function addSashGrid(node, glass) {
|
|
1629
|
+
const geometry = glass.geometry;
|
|
1630
|
+
geometry.computeBoundingBox();
|
|
1631
|
+
const bounds = geometry.boundingBox;
|
|
1632
|
+
const size = bounds.getSize(new THREE.Vector3());
|
|
1633
|
+
const center = bounds.getCenter(new THREE.Vector3()).add(glass.position);
|
|
1634
|
+
const parent = glass.parent;
|
|
1635
|
+
const barDepth = windowDepths(node.frameDepth).bar;
|
|
1636
|
+
const vertical = node.columnRatios;
|
|
1637
|
+
const horizontal = node.rowRatios;
|
|
1638
|
+
const usableW = size.x - (vertical.length - 1) * node.columnDividerThickness;
|
|
1639
|
+
const usableH = size.y - (horizontal.length - 1) * node.rowDividerThickness;
|
|
1640
|
+
const colSum = vertical.reduce((a, b) => a + b, 0);
|
|
1641
|
+
const rowSum = horizontal.reduce((a, b) => a + b, 0);
|
|
1642
|
+
if (usableW <= 0 || usableH <= 0 || colSum <= 0 || rowSum <= 0)
|
|
1643
|
+
return;
|
|
1644
|
+
const bars = new THREE.Group();
|
|
1645
|
+
bars.name = 'window-sash-grid';
|
|
1646
|
+
bars.position.copy(center);
|
|
1647
|
+
parent.add(bars);
|
|
1648
|
+
currentWindowSlot = 'frame';
|
|
1649
|
+
let x = -size.x / 2;
|
|
1650
|
+
for (const ratio of vertical.slice(0, -1)) {
|
|
1651
|
+
x += (ratio / colSum) * usableW;
|
|
1652
|
+
addBox(bars, baseMaterial, node.columnDividerThickness, size.y, barDepth, x + node.columnDividerThickness / 2, 0, 0);
|
|
1653
|
+
x += node.columnDividerThickness;
|
|
1654
|
+
}
|
|
1655
|
+
let y = size.y / 2;
|
|
1656
|
+
for (const ratio of horizontal.slice(0, -1)) {
|
|
1657
|
+
y -= (ratio / rowSum) * usableH;
|
|
1658
|
+
let left = -size.x / 2;
|
|
1659
|
+
for (const column of vertical) {
|
|
1660
|
+
const width = (column / colSum) * usableW;
|
|
1661
|
+
addBox(bars, baseMaterial, width, node.rowDividerThickness, barDepth, left + width / 2, y - node.rowDividerThickness / 2, 0);
|
|
1662
|
+
left += width + node.columnDividerThickness;
|
|
1663
|
+
}
|
|
1664
|
+
y -= node.rowDividerThickness;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
function addSashHardware(node, sash, interior) {
|
|
1668
|
+
const rail = Math.max(node.frameThickness * 0.72, 0.032);
|
|
1669
|
+
const tiltTurn = node.windowType === 'tilt-turn';
|
|
1670
|
+
const horizontalHinge = node.windowType !== 'casement' && !tiltTurn;
|
|
1671
|
+
const innerW = node.width - 2 * node.frameThickness;
|
|
1672
|
+
const innerH = node.height - 2 * node.frameThickness;
|
|
1673
|
+
const french = !tiltTurn && node.casementStyle === 'french';
|
|
1674
|
+
const panelW = french && !horizontalHinge ? innerW / 2 : innerW;
|
|
1675
|
+
const leftHinge = sash.name === FRENCH_CASEMENT_LEFT_SASH_NAME ||
|
|
1676
|
+
([CASEMENT_WINDOW_SASH_NAME, TILT_TURN_WINDOW_SASH_NAME].includes(sash.name) &&
|
|
1677
|
+
node.hingesSide === 'left');
|
|
1678
|
+
const downward = getAwningDirection(node) === 'down';
|
|
1679
|
+
const tilt = tiltTurn && node.tiltTurnMode !== 'turn';
|
|
1680
|
+
const centerX = tilt || horizontalHinge ? 0 : ((leftHinge ? 1 : -1) * panelW) / 2;
|
|
1681
|
+
const centerY = tilt ? innerH / 2 : horizontalHinge ? ((downward ? 1 : -1) * innerH) / 2 : 0;
|
|
1682
|
+
const surfaceZ = (interior * windowDepths(node.frameDepth).sash) / 2;
|
|
1683
|
+
const material = resolveWindowSlotMaterial(node, 'hardware');
|
|
1684
|
+
const hardware = new THREE.Group();
|
|
1685
|
+
hardware.name = 'window-sash-hardware';
|
|
1686
|
+
sash.add(hardware);
|
|
1687
|
+
const part = (parent, geometry, x, y, z) => {
|
|
1688
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
1689
|
+
mesh.position.set(x, y, z);
|
|
1690
|
+
mesh.userData.slotId = 'hardware';
|
|
1691
|
+
parent.add(mesh);
|
|
1692
|
+
return mesh;
|
|
1693
|
+
};
|
|
1694
|
+
if (node.windowHandle !== false) {
|
|
1695
|
+
const handle = new THREE.Group();
|
|
1696
|
+
handle.name = 'window-handle';
|
|
1697
|
+
handle.position.set(horizontalHinge ? 0 : centerX + (leftHinge ? 1 : -1) * (panelW / 2 - rail / 2), horizontalHinge ? centerY + (downward ? 1 : -1) * (innerH / 2 - rail / 2) : centerY, surfaceZ);
|
|
1698
|
+
if (interior < 0)
|
|
1699
|
+
handle.rotation.y = Math.PI;
|
|
1700
|
+
if (tiltTurn)
|
|
1701
|
+
handle.rotation.z = Math.PI / 2;
|
|
1702
|
+
hardware.add(handle);
|
|
1703
|
+
part(handle, roundedBox(0.028, 0.12, 0.006, 0.003), 0, 0, 0.003);
|
|
1704
|
+
part(handle, tube(0.006, 0.028), 0, 0, 0.02).rotation.x = Math.PI / 2;
|
|
1705
|
+
part(handle, tube(0.006, 0.09), 0, -0.045, 0.034);
|
|
1706
|
+
}
|
|
1707
|
+
for (const side of tiltTurn ? [-1, 1, 0] : [-1, 1]) {
|
|
1708
|
+
const bottom = tiltTurn && side === 0;
|
|
1709
|
+
const hinge = new THREE.Group();
|
|
1710
|
+
hinge.name = 'window-hinge';
|
|
1711
|
+
hinge.position.set(bottom
|
|
1712
|
+
? centerX
|
|
1713
|
+
: horizontalHinge
|
|
1714
|
+
? side * panelW * 0.3
|
|
1715
|
+
: centerX - ((leftHinge ? 1 : -1) * panelW) / 2, bottom ? centerY - innerH / 2 : horizontalHinge ? 0 : centerY + side * innerH * 0.3, surfaceZ);
|
|
1716
|
+
// Mirroring uses rotation so the hardware keeps outward-facing triangles.
|
|
1717
|
+
if (horizontalHinge || bottom)
|
|
1718
|
+
hinge.rotation.z = Math.PI / 2;
|
|
1719
|
+
if (interior < 0)
|
|
1720
|
+
hinge.rotation.y = Math.PI;
|
|
1721
|
+
hardware.add(hinge);
|
|
1722
|
+
for (const geometry of [hingeLeafPart(0.01, 0.06), hingeJambPart(0.01, 0.06)]) {
|
|
1723
|
+
part(hinge, geometry, 0, 0, 0);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/** Finish existing sash groups so pose/export animate every attached part together. */
|
|
1728
|
+
function addWindowDetails(node, mesh) {
|
|
1729
|
+
const hasHardware = !node.corner && ['casement', 'awning', 'hopper', 'tilt-turn'].includes(node.windowType);
|
|
1730
|
+
const { exterior, exteriorFace, cased, wallDepth, offset } = windowFaceFrame(node, mesh);
|
|
1731
|
+
const interior = -exterior;
|
|
1732
|
+
mesh.userData.windowExteriorSign = exterior;
|
|
1733
|
+
if (node.sillInterior) {
|
|
1734
|
+
const interiorFace = exteriorFace === 'a' ? 'b' : 'a';
|
|
1735
|
+
const depth = node.sillInteriorDepth ?? 0.06;
|
|
1736
|
+
const thickness = node.sillInteriorThickness ?? 0.03;
|
|
1737
|
+
const overhang = (cased(interiorFace) ? openingCasingWidth(node) : 0) + (node.sillInteriorOverhang ?? 0.015);
|
|
1738
|
+
currentWindowSlot = 'sill';
|
|
1739
|
+
addBox(mesh, resolveWindowSlotMaterial(node, 'sill'), node.width + 2 * overhang, thickness, depth, 0, -node.height / 2 - thickness / 2 + SILL_LIFT, interior * (wallDepth / 2 + depth / 2) - offset).name = 'window-interior-stool';
|
|
1740
|
+
}
|
|
1741
|
+
if (node.shutters && node.shutters !== 'none') {
|
|
1742
|
+
currentWindowSlot = 'shutters';
|
|
1743
|
+
const material = resolveWindowSlotMaterial(node, 'shutters');
|
|
1744
|
+
const sideWidth = node.shutterWidth ??
|
|
1745
|
+
node.width / (node.shutterSides === 'left' || node.shutterSides === 'right' ? 1 : 2);
|
|
1746
|
+
const bifold = sideWidth > 0.75;
|
|
1747
|
+
const width = sideWidth / (bifold ? 2 : 1);
|
|
1748
|
+
const height = node.height;
|
|
1749
|
+
const border = Math.min(0.055, width / 4, height / 8);
|
|
1750
|
+
for (const side of [-1, 1]) {
|
|
1751
|
+
if ((node.shutterSides === 'left' && side === 1) ||
|
|
1752
|
+
(node.shutterSides === 'right' && side === -1))
|
|
1753
|
+
continue;
|
|
1754
|
+
const shutter = new THREE.Group();
|
|
1755
|
+
shutter.name = side === -1 ? WINDOW_LEFT_SHUTTER_NAME : WINDOW_RIGHT_SHUTTER_NAME;
|
|
1756
|
+
shutter.userData.side = side === -1 ? 'left' : 'right';
|
|
1757
|
+
shutter.position.set((side * node.width) / 2, 0, exterior * (wallDepth / 2 + 0.02 + 0.015) - offset);
|
|
1758
|
+
mesh.add(shutter);
|
|
1759
|
+
const leaves = [shutter];
|
|
1760
|
+
if (bifold) {
|
|
1761
|
+
const outer = new THREE.Group();
|
|
1762
|
+
outer.name = WINDOW_SHUTTER_OUTER_NAME;
|
|
1763
|
+
outer.userData.slotId = 'shutters';
|
|
1764
|
+
// Offset the hinge axis so the folded 30 mm leaves stack without intersecting.
|
|
1765
|
+
outer.position.set(side * width, 0, exterior * 0.02);
|
|
1766
|
+
shutter.add(outer);
|
|
1767
|
+
leaves.push(outer);
|
|
1768
|
+
}
|
|
1769
|
+
for (const leaf of leaves) {
|
|
1770
|
+
const board = (w, h, d, x, y, z = 0) => addBox(leaf, material, w, h, d, (side * width) / 2 + x, y, z - (leaf === shutter ? 0 : exterior * 0.02));
|
|
1771
|
+
if (node.shutters === 'board-batten') {
|
|
1772
|
+
const count = Math.ceil(width / 0.1);
|
|
1773
|
+
for (let i = 0; i < count; i++) {
|
|
1774
|
+
const boardWidth = Math.min(0.1, width - i * 0.1);
|
|
1775
|
+
board(boardWidth - Math.min(0.002, boardWidth * 0.05), height, 0.02, -width / 2 + i * 0.1 + boardWidth / 2, 0);
|
|
1776
|
+
}
|
|
1777
|
+
for (const y of [-height * 0.3, height * 0.3])
|
|
1778
|
+
board(width, 0.07, 0.01, 0, y, -exterior * 0.01);
|
|
1779
|
+
if (height > 1.2)
|
|
1780
|
+
board(width, 0.07, 0.01, 0, 0, -exterior * 0.01);
|
|
1781
|
+
}
|
|
1782
|
+
else {
|
|
1783
|
+
for (const edge of [-1, 1]) {
|
|
1784
|
+
board(border, height, 0.03, (edge * (width - border)) / 2, 0);
|
|
1785
|
+
board(width - 2 * border, border, 0.03, 0, (edge * (height - border)) / 2);
|
|
1786
|
+
}
|
|
1787
|
+
const innerW = width - 2 * border;
|
|
1788
|
+
const innerH = height - 2 * border;
|
|
1789
|
+
if (node.shutters === 'louvered') {
|
|
1790
|
+
for (let y = -innerH / 2 + 0.02; y < innerH / 2; y += 0.04)
|
|
1791
|
+
board(innerW, Math.min(0.034, innerH), 0.012, 0, y).rotation.x =
|
|
1792
|
+
(-exterior * Math.PI) / 6;
|
|
1793
|
+
if (height > 1.2)
|
|
1794
|
+
board(innerW, border, 0.03, 0, 0);
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
if (height > 1.2) {
|
|
1798
|
+
board(innerW, border, 0.03, 0, 0);
|
|
1799
|
+
for (const half of [-1, 1])
|
|
1800
|
+
board(innerW, (innerH - border) / 2, 0.012, 0, (half * (innerH + border)) / 4, -exterior * 0.009);
|
|
1801
|
+
}
|
|
1802
|
+
else
|
|
1803
|
+
board(innerW, innerH, 0.012, 0, 0, -exterior * 0.009);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (side === -1 && (node.shutterSides ?? 'both') === 'both') {
|
|
1808
|
+
const tip = leaves[leaves.length - 1];
|
|
1809
|
+
addBox(tip, material, 0.01, height, 0.01, side * (width + 0.005), 0, -exterior * (tip === shutter ? 0.02 : 0.04)).name = 'window-shutter-overlap';
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (!node.corner) {
|
|
1814
|
+
const gridTypes = [
|
|
1815
|
+
'casement',
|
|
1816
|
+
'sliding',
|
|
1817
|
+
'single-hung',
|
|
1818
|
+
'double-hung',
|
|
1819
|
+
'awning',
|
|
1820
|
+
'hopper',
|
|
1821
|
+
'tilt-turn',
|
|
1822
|
+
];
|
|
1823
|
+
// Curved sash outlines need clipped grids; rectangular sashes cover the initial grid contract.
|
|
1824
|
+
if (node.sashGrid &&
|
|
1825
|
+
gridTypes.includes(node.windowType) &&
|
|
1826
|
+
(node.openingShape === 'rectangle' || isRectangleOnlyWindowType(node))) {
|
|
1827
|
+
const panes = [];
|
|
1828
|
+
mesh.traverse((child) => {
|
|
1829
|
+
if (child instanceof THREE.Mesh && child.userData.slotId === 'glass')
|
|
1830
|
+
panes.push(child);
|
|
1831
|
+
});
|
|
1832
|
+
for (const pane of panes)
|
|
1833
|
+
addSashGrid(node, pane);
|
|
1834
|
+
}
|
|
1835
|
+
if (hasHardware) {
|
|
1836
|
+
for (const name of [
|
|
1837
|
+
CASEMENT_WINDOW_SASH_NAME,
|
|
1838
|
+
FRENCH_CASEMENT_LEFT_SASH_NAME,
|
|
1839
|
+
FRENCH_CASEMENT_RIGHT_SASH_NAME,
|
|
1840
|
+
AWNING_WINDOW_SASH_NAME,
|
|
1841
|
+
HOPPER_WINDOW_SASH_NAME,
|
|
1842
|
+
TILT_TURN_WINDOW_SASH_NAME,
|
|
1843
|
+
]) {
|
|
1844
|
+
const sash = mesh.getObjectByName(name);
|
|
1845
|
+
if (!sash)
|
|
1846
|
+
continue;
|
|
1847
|
+
addSashHardware(node, sash, interior);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (node.rollerShutter === 'box' || node.rollerShutter === 'concealed') {
|
|
1852
|
+
currentWindowSlot = 'shutters';
|
|
1853
|
+
const material = resolveWindowSlotMaterial(node, 'shutters');
|
|
1854
|
+
const concealed = node.rollerShutter === 'concealed';
|
|
1855
|
+
const faceZ = (exterior * wallDepth) / 2 - offset;
|
|
1856
|
+
if (!concealed) {
|
|
1857
|
+
const boxHeight = node.rollerShutterBoxHeight ?? 0.18;
|
|
1858
|
+
addBox(mesh, material, node.width + 0.08, boxHeight, 0.18, 0, node.height / 2 + boxHeight / 2, faceZ + exterior * 0.11).name = 'window-roller-box';
|
|
1859
|
+
}
|
|
1860
|
+
// Shutter clearance follows the closed unit, independent of its rendered opening angle.
|
|
1861
|
+
poseWindowMovingParts(node, mesh, 0);
|
|
1862
|
+
const guideZ = getWindowRollerCurtainZ(mesh, exterior);
|
|
1863
|
+
// The concealed head strip shares the curtain plane: inside the frame head it would fight its face.
|
|
1864
|
+
if (concealed) {
|
|
1865
|
+
addBox(mesh, material, node.width - 0.002, 0.03, 0.03, 0, node.height / 2 - 0.016, guideZ).name = 'window-roller-lintel';
|
|
1866
|
+
}
|
|
1867
|
+
for (const side of [-1, 1])
|
|
1868
|
+
addBox(mesh, material, concealed ? 0.025 : 0.04, node.height, 0.03, side * (node.width / 2 + (concealed ? -0.0125 : 0.02)), 0, guideZ).name = 'window-roller-guide';
|
|
1869
|
+
mesh.add(buildWindowRollerCurtain(node, material, guideZ, getWindowRenderShutterAmount(node, 'rollerShutterOpen')));
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1721
1872
|
function syncWindowCutout(node, mesh) {
|
|
1873
|
+
if (node.openingKind !== 'opening') {
|
|
1874
|
+
addWindowDetails(node, mesh);
|
|
1875
|
+
poseWindowMovingParts(node, mesh, getWindowRenderOpenAmount(node), getWindowRenderShutterAmount(node, 'shutterOpen'));
|
|
1876
|
+
for (const name of [
|
|
1877
|
+
CASEMENT_WINDOW_SASH_NAME,
|
|
1878
|
+
FRENCH_CASEMENT_LEFT_SASH_NAME,
|
|
1879
|
+
FRENCH_CASEMENT_RIGHT_SASH_NAME,
|
|
1880
|
+
AWNING_WINDOW_SASH_NAME,
|
|
1881
|
+
HOPPER_WINDOW_SASH_NAME,
|
|
1882
|
+
TILT_TURN_WINDOW_SASH_NAME,
|
|
1883
|
+
]) {
|
|
1884
|
+
const sash = mesh.getObjectByName(name);
|
|
1885
|
+
if (sash)
|
|
1886
|
+
disableSubtreeRaycastIfSwung(sash, sash.rotation.y || sash.rotation.x);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1722
1889
|
// ── Cutout: invisible raycast hit target for the whole opening ──
|
|
1723
1890
|
let cutout = mesh.getObjectByName('cutout');
|
|
1724
1891
|
if (!cutout) {
|