@pascal-app/viewer 0.9.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- package/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +5 -2
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/parametric-node-renderer.js +7 -4
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +59 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
- package/dist/components/viewer/index.d.ts +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +113 -12
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +36 -5
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +17 -2
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +180 -24
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +6 -4
- package/dist/components/viewer/scene-bvh.d.ts +2 -1
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +3 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +23 -12
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -24
- package/dist/hooks/use-node-events.d.ts +1 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +22 -14
- package/dist/index.d.ts +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -13
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +3 -9
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +65 -5
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/edge-style.d.ts +1 -0
- package/dist/lib/edge-style.d.ts.map +1 -1
- package/dist/lib/edge-style.js +26 -6
- package/dist/lib/hero-pose.d.ts +47 -0
- package/dist/lib/hero-pose.d.ts.map +1 -0
- package/dist/lib/hero-pose.js +227 -0
- package/dist/lib/ink-edges.d.ts +1 -1
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +13 -7
- package/dist/lib/ktx2-loader.d.ts +46 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +142 -0
- package/dist/lib/layers.d.ts +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +16 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +158 -34
- package/dist/lib/materials.test.d.ts +2 -0
- package/dist/lib/materials.test.d.ts.map +1 -0
- package/dist/lib/materials.test.js +25 -0
- package/dist/lib/merged-outline-node.test.d.ts +2 -0
- package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
- package/dist/lib/merged-outline-node.test.js +22 -0
- package/dist/lib/scene-themes.d.ts +1 -0
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +25 -16
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/snapshot-pipeline.d.ts +42 -0
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
- package/dist/lib/snapshot-pipeline.js +254 -0
- package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
- package/dist/lib/terrain-perimeter-fill.js +75 -0
- package/dist/lib/texture-node-guard.d.ts +13 -0
- package/dist/lib/texture-node-guard.d.ts.map +1 -0
- package/dist/lib/texture-node-guard.js +39 -0
- package/dist/lib/texture-reference.d.ts +31 -0
- package/dist/lib/texture-reference.d.ts.map +1 -0
- package/dist/lib/texture-reference.js +139 -0
- package/dist/lib/texture-reference.test.d.ts +2 -0
- package/dist/lib/texture-reference.test.d.ts.map +1 -0
- package/dist/lib/texture-reference.test.js +86 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +73 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +191 -4
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +61 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +12 -5
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +494 -161
- package/dist/systems/fence/fence-system.d.ts +17 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +221 -53
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
- package/dist/systems/geometry/geometry-system.d.ts +7 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +95 -26
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +20 -6
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +34 -9
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +7 -45
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +29 -8
- package/dist/systems/roof/roof-system.d.ts +21 -8
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1079 -221
- package/dist/systems/slab/slab-system.d.ts +7 -4
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +31 -83
- package/dist/systems/slab/slab-system.test.js +54 -14
- package/dist/systems/stair/stair-system.js +7 -4
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +49 -17
- package/dist/systems/wall/wall-materials.d.ts +11 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +244 -35
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +116 -0
- package/dist/systems/wall/wall-system.d.ts +4 -8
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +397 -177
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +240 -14
- package/package.json +4 -4
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- package/dist/components/viewer/ground-occluder.js +0 -67
- package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
- package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
- package/dist/components/viewer/walkthrough-controls.js +0 -131
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import { getEffectiveNode, sceneRegistry, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { DEFAULT_WALL_THICKNESS, getEffectiveNode, getWallThickness, sceneRegistry, 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
|
-
import {
|
|
5
|
+
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
6
|
+
import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
|
|
6
7
|
import useViewer from '../../store/use-viewer';
|
|
8
|
+
import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
|
|
7
9
|
// Invisible material for root mesh — used as selection hitbox only
|
|
8
10
|
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
9
11
|
let baseMaterial = getBaseMaterial();
|
|
10
12
|
let glassMaterial = defaultGlassMaterial;
|
|
13
|
+
let currentWindowSlot;
|
|
14
|
+
// Per-frame viewer state, captured so the per-node mesh builder (which runs
|
|
15
|
+
// outside React) can resolve each window's slot materials.
|
|
16
|
+
let currentShading = 'rendered';
|
|
17
|
+
let currentTextures = true;
|
|
18
|
+
let currentColorPreset = 'clay';
|
|
19
|
+
let currentSceneMaterials;
|
|
11
20
|
export const CASEMENT_WINDOW_SASH_NAME = 'casement-window-sash';
|
|
12
21
|
export const FRENCH_CASEMENT_LEFT_SASH_NAME = 'french-casement-left-sash';
|
|
13
22
|
export const FRENCH_CASEMENT_RIGHT_SASH_NAME = 'french-casement-right-sash';
|
|
@@ -27,6 +36,7 @@ export const WindowSystem = () => {
|
|
|
27
36
|
const shading = useViewer((state) => state.shading);
|
|
28
37
|
const textures = useViewer((state) => state.textures);
|
|
29
38
|
const colorPreset = useViewer((state) => state.colorPreset);
|
|
39
|
+
const sceneMaterials = useScene((state) => state.materials);
|
|
30
40
|
const materialRevisionRef = useRef(null);
|
|
31
41
|
// Subscribe so override-only updates re-run this component. Mirrors
|
|
32
42
|
// WallSystem + DoorSystem.
|
|
@@ -49,6 +59,20 @@ export const WindowSystem = () => {
|
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
});
|
|
62
|
+
// Editing a scene material a window slot references must rebuild that window
|
|
63
|
+
// (window meshes are built by this system, not <GeometrySystem>, so its
|
|
64
|
+
// scene-material re-dirty doesn't cover them).
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
void sceneMaterials;
|
|
67
|
+
const nodes = useScene.getState().nodes;
|
|
68
|
+
for (const node of Object.values(nodes)) {
|
|
69
|
+
if (node?.type !== 'window')
|
|
70
|
+
continue;
|
|
71
|
+
if (!nodeReferencesSceneMaterial(node))
|
|
72
|
+
continue;
|
|
73
|
+
useScene.getState().dirtyNodes.add(node.id);
|
|
74
|
+
}
|
|
75
|
+
}, [sceneMaterials]);
|
|
52
76
|
useFrame(() => {
|
|
53
77
|
if (dirtyNodes.size === 0)
|
|
54
78
|
return;
|
|
@@ -58,11 +82,15 @@ export const WindowSystem = () => {
|
|
|
58
82
|
glassMaterial = textures
|
|
59
83
|
? defaultGlassMaterial
|
|
60
84
|
: createSurfaceRoleMaterial('glazing', colorPreset);
|
|
85
|
+
currentShading = shading;
|
|
86
|
+
currentTextures = textures;
|
|
87
|
+
currentColorPreset = colorPreset;
|
|
88
|
+
currentSceneMaterials = sceneMaterials;
|
|
61
89
|
const nodes = useScene.getState().nodes;
|
|
62
90
|
const dirtyWindowIds = [];
|
|
63
91
|
dirtyNodes.forEach((id) => {
|
|
64
92
|
const node = nodes[id];
|
|
65
|
-
if (
|
|
93
|
+
if (node?.type !== 'window')
|
|
66
94
|
return;
|
|
67
95
|
dirtyWindowIds.push(id);
|
|
68
96
|
});
|
|
@@ -80,7 +108,7 @@ export const WindowSystem = () => {
|
|
|
80
108
|
}
|
|
81
109
|
}
|
|
82
110
|
const node = nodes[id];
|
|
83
|
-
if (
|
|
111
|
+
if (node?.type !== 'window')
|
|
84
112
|
continue;
|
|
85
113
|
const mesh = sceneRegistry.nodes.get(id);
|
|
86
114
|
if (!mesh)
|
|
@@ -102,9 +130,70 @@ export const WindowSystem = () => {
|
|
|
102
130
|
}, 3);
|
|
103
131
|
return null;
|
|
104
132
|
};
|
|
133
|
+
function tagWindowSlot(mesh) {
|
|
134
|
+
mesh.userData.slotId = currentWindowSlot;
|
|
135
|
+
return mesh;
|
|
136
|
+
}
|
|
137
|
+
const NO_RAYCAST = () => { };
|
|
138
|
+
// An open casement sash swings perpendicular to the wall, so in a top-down view
|
|
139
|
+
// its flat panel blankets the room interior and wins the selection raycast over
|
|
140
|
+
// the slab/items beneath it. Drop the swung sash out of the raycast so a floor
|
|
141
|
+
// click falls through; the window stays selectable via its proud invisible
|
|
142
|
+
// cutout proxy at the opening (see syncWindowCutout). Skipped while closed so
|
|
143
|
+
// paint-by-slot still resolves on the sash.
|
|
144
|
+
function disableSubtreeRaycastIfSwung(object, rotationY) {
|
|
145
|
+
if (Math.abs(rotationY) <= 1e-3)
|
|
146
|
+
return;
|
|
147
|
+
object.traverse((child) => {
|
|
148
|
+
;
|
|
149
|
+
child.raycast = NO_RAYCAST;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function nodeReferencesSceneMaterial(node) {
|
|
153
|
+
const slots = node.slots;
|
|
154
|
+
if (!slots)
|
|
155
|
+
return false;
|
|
156
|
+
for (const ref of Object.values(slots)) {
|
|
157
|
+
if (typeof ref === 'string' && ref.startsWith('scene:'))
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
// Window frame/glass default to catalog finishes (generic approach). `preset-glass`
|
|
163
|
+
// is now FrontSide (it was the only glass we use), so it's safe for the WebGPU
|
|
164
|
+
// MRT scene pass.
|
|
165
|
+
const FRAME_DEFAULT_REF = 'library:preset-softwhite';
|
|
166
|
+
const GLASS_DEFAULT_REF = 'library:preset-glass';
|
|
167
|
+
function windowSlotDefault(slotId) {
|
|
168
|
+
if (slotId === 'glass') {
|
|
169
|
+
if (!currentTextures)
|
|
170
|
+
return createSurfaceRoleMaterial('glazing', currentColorPreset);
|
|
171
|
+
return (resolveMaterialRef(GLASS_DEFAULT_REF, currentSceneMaterials, currentShading) ??
|
|
172
|
+
defaultGlassMaterial);
|
|
173
|
+
}
|
|
174
|
+
if (!currentTextures)
|
|
175
|
+
return createSurfaceRoleMaterial('joinery', currentColorPreset);
|
|
176
|
+
return (resolveMaterialRef(FRAME_DEFAULT_REF, currentSceneMaterials, currentShading) ??
|
|
177
|
+
getBaseMaterial(currentShading));
|
|
178
|
+
}
|
|
179
|
+
// Resolve a window's slot to a material: the `node.slots` override (colored mode
|
|
180
|
+
// only) → the role/base default. Textures-off ignores overrides — the monochrome
|
|
181
|
+
// escape hatch.
|
|
182
|
+
function resolveWindowSlotMaterial(node, slotId) {
|
|
183
|
+
const fallback = windowSlotDefault(slotId);
|
|
184
|
+
if (!currentTextures)
|
|
185
|
+
return fallback;
|
|
186
|
+
const ref = node.slots?.[slotId];
|
|
187
|
+
if (!ref)
|
|
188
|
+
return fallback;
|
|
189
|
+
return resolveMaterialRef(ref, currentSceneMaterials, currentShading) ?? fallback;
|
|
190
|
+
}
|
|
105
191
|
function addBox(parent, material, w, h, d, x, y, z) {
|
|
106
|
-
const
|
|
192
|
+
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
193
|
+
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
194
|
+
const m = new THREE.Mesh(geometry, material);
|
|
107
195
|
m.position.set(x, y, z);
|
|
196
|
+
tagWindowSlot(m);
|
|
108
197
|
parent.add(m);
|
|
109
198
|
}
|
|
110
199
|
function addShape(parent, material, shape, depth, z = 0) {
|
|
@@ -115,6 +204,7 @@ function addShape(parent, material, shape, depth, z = 0) {
|
|
|
115
204
|
});
|
|
116
205
|
geometry.translate(0, 0, -depth / 2 + z);
|
|
117
206
|
const mesh = new THREE.Mesh(geometry, material);
|
|
207
|
+
tagWindowSlot(mesh);
|
|
118
208
|
parent.add(mesh);
|
|
119
209
|
}
|
|
120
210
|
function disposeObjectGeometry(object) {
|
|
@@ -331,9 +421,11 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
331
421
|
const innerW = innerRight - innerLeft;
|
|
332
422
|
const innerH = innerTop - innerBottom;
|
|
333
423
|
const innerRadii = insetCornerRadii(outerRadii, inset, innerW, innerH);
|
|
424
|
+
currentWindowSlot = 'frame';
|
|
334
425
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, inset, outerRadii), frameDepth);
|
|
335
426
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
336
427
|
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
428
|
+
currentWindowSlot = 'glass';
|
|
337
429
|
addShape(mesh, glassMaterial, createRoundedShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii), glassDepth);
|
|
338
430
|
const numCols = columnRatios.length;
|
|
339
431
|
const numRows = rowRatios.length;
|
|
@@ -344,6 +436,7 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
344
436
|
const colWidths = columnRatios.map((r) => (r / colSum) * usableW);
|
|
345
437
|
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
|
|
346
438
|
let x = innerLeft;
|
|
439
|
+
currentWindowSlot = 'frame';
|
|
347
440
|
for (let c = 0; c < numCols - 1; c++) {
|
|
348
441
|
x += colWidths[c];
|
|
349
442
|
const x1 = x;
|
|
@@ -355,6 +448,7 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
355
448
|
x += columnDividerThickness;
|
|
356
449
|
}
|
|
357
450
|
let y = innerTop;
|
|
451
|
+
currentWindowSlot = 'frame';
|
|
358
452
|
for (let r = 0; r < numRows - 1; r++) {
|
|
359
453
|
y -= rowHeights[r];
|
|
360
454
|
const yTop = y;
|
|
@@ -369,6 +463,7 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
369
463
|
if (sill) {
|
|
370
464
|
const sillW = width + sillDepth * 0.4;
|
|
371
465
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
466
|
+
currentWindowSlot = 'frame';
|
|
372
467
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
373
468
|
}
|
|
374
469
|
}
|
|
@@ -387,9 +482,11 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
387
482
|
const innerH = innerTop - innerBottom;
|
|
388
483
|
const innerArchHeight = getClampedArchHeight(innerW, innerH, archHeight - inset);
|
|
389
484
|
const innerSpringY = innerTop - innerArchHeight;
|
|
485
|
+
currentWindowSlot = 'frame';
|
|
390
486
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, archHeight, inset), frameDepth);
|
|
391
487
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
392
488
|
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
489
|
+
currentWindowSlot = 'glass';
|
|
393
490
|
addShape(mesh, glassMaterial, createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight), glassDepth);
|
|
394
491
|
const numCols = columnRatios.length;
|
|
395
492
|
const numRows = rowRatios.length;
|
|
@@ -401,6 +498,7 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
401
498
|
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
|
|
402
499
|
const innerHalfWidth = innerW / 2;
|
|
403
500
|
let x = innerLeft;
|
|
501
|
+
currentWindowSlot = 'frame';
|
|
404
502
|
for (let c = 0; c < numCols - 1; c++) {
|
|
405
503
|
x += colWidths[c];
|
|
406
504
|
const x1 = x;
|
|
@@ -412,6 +510,7 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
412
510
|
x += columnDividerThickness;
|
|
413
511
|
}
|
|
414
512
|
let y = innerTop;
|
|
513
|
+
currentWindowSlot = 'frame';
|
|
415
514
|
for (let r = 0; r < numRows - 1; r++) {
|
|
416
515
|
y -= rowHeights[r];
|
|
417
516
|
const yTop = y;
|
|
@@ -428,6 +527,7 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
428
527
|
if (sill) {
|
|
429
528
|
const sillW = width + sillDepth * 0.4;
|
|
430
529
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
530
|
+
currentWindowSlot = 'frame';
|
|
431
531
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
432
532
|
}
|
|
433
533
|
}
|
|
@@ -450,6 +550,7 @@ function addSlidingWindowVisuals(node, mesh) {
|
|
|
450
550
|
const innerW = width - 2 * frameThickness;
|
|
451
551
|
const innerH = height - 2 * frameThickness;
|
|
452
552
|
// Outer frame.
|
|
553
|
+
currentWindowSlot = 'frame';
|
|
453
554
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
454
555
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
455
556
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -473,11 +574,14 @@ function addSlidingWindowVisuals(node, mesh) {
|
|
|
473
574
|
activePanel.position.set(leftPanelX, 0, leftZ);
|
|
474
575
|
mesh.add(activePanel);
|
|
475
576
|
// Twin tracks signal the sliding operation without adding editor-only state.
|
|
577
|
+
currentWindowSlot = 'frame';
|
|
476
578
|
addBox(mesh, baseMaterial, innerW, trackThickness, frameDepth, 0, innerH / 2 - trackThickness / 2, 0);
|
|
477
579
|
addBox(mesh, baseMaterial, innerW, trackThickness, frameDepth, 0, -innerH / 2 + trackThickness / 2, 0);
|
|
580
|
+
currentWindowSlot = 'glass';
|
|
478
581
|
addBox(activePanel, glassMaterial, panelWidth, panelH, glassDepth, 0, 0, 0);
|
|
479
582
|
addBox(mesh, glassMaterial, panelWidth, panelH, glassDepth, rightPanelX, 0, rightZ);
|
|
480
583
|
// The right sash stays fixed. The left sash is the active panel that slides across it.
|
|
584
|
+
currentWindowSlot = 'frame';
|
|
481
585
|
addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth * 0.72, -panelWidth / 2 + railThickness / 2, 0, 0);
|
|
482
586
|
addBox(mesh, baseMaterial, railThickness, panelH, frameDepth * 0.72, rightPanelX + panelWidth / 2 - railThickness / 2, 0, rightZ);
|
|
483
587
|
addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth * 0.78, panelWidth / 2 - railThickness / 2, 0, 0);
|
|
@@ -486,6 +590,7 @@ function addSlidingWindowVisuals(node, mesh) {
|
|
|
486
590
|
if (sill) {
|
|
487
591
|
const sillW = width + sillDepth * 0.4;
|
|
488
592
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
593
|
+
currentWindowSlot = 'frame';
|
|
489
594
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
490
595
|
}
|
|
491
596
|
}
|
|
@@ -500,15 +605,19 @@ function addRectCasementSash(parent, name, panelW, panelH, frameThickness, frame
|
|
|
500
605
|
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
501
606
|
sash.rotation.y = rotationY;
|
|
502
607
|
parent.add(sash);
|
|
608
|
+
currentWindowSlot = 'frame';
|
|
503
609
|
addBox(sash, baseMaterial, panelW, sashFrameThickness, sashDepth, sashCenterX, panelH / 2 - sashFrameThickness / 2, 0);
|
|
504
610
|
addBox(sash, baseMaterial, panelW, sashFrameThickness, sashDepth, sashCenterX, -panelH / 2 + sashFrameThickness / 2, 0);
|
|
505
611
|
addBox(sash, baseMaterial, sashFrameThickness, panelH, sashDepth, sashCenterX - panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
506
612
|
addBox(sash, baseMaterial, sashFrameThickness, panelH, sashDepth, sashCenterX + panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
613
|
+
currentWindowSlot = 'glass';
|
|
507
614
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
|
|
615
|
+
disableSubtreeRaycastIfSwung(sash, rotationY);
|
|
508
616
|
}
|
|
509
617
|
function addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth) {
|
|
510
618
|
const markerW = Math.max(frameThickness * 0.38, 0.018);
|
|
511
619
|
const markerH = innerH * 0.24;
|
|
620
|
+
currentWindowSlot = 'frame';
|
|
512
621
|
for (const pivotX of [-innerW / 2, innerW / 2]) {
|
|
513
622
|
addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, innerH * 0.25, frameDepth * 0.08);
|
|
514
623
|
addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, -innerH * 0.25, frameDepth * 0.08);
|
|
@@ -599,25 +708,32 @@ function addShapedFrenchCasementSash(parent, node, name, side, fullW, leafW, lea
|
|
|
599
708
|
const outerArchHeight = getClampedArchHeight(node.width, node.height, node.archHeight);
|
|
600
709
|
const sashArchHeight = getClampedArchHeight(fullW, leafH, outerArchHeight - frameThickness);
|
|
601
710
|
const sashSpringY = node.height / 2 - outerArchHeight;
|
|
711
|
+
currentWindowSlot = 'frame';
|
|
602
712
|
addShape(sashVisual, baseMaterial, createFrenchArchLeafFrameShape(leafW, leafH, fullW, sashSpringY, sashArchHeight, sashFrameThickness, side), sashDepth);
|
|
603
713
|
const glassInset = Math.min(sashFrameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
|
|
604
714
|
if (glassInset > 0.001) {
|
|
715
|
+
currentWindowSlot = 'glass';
|
|
605
716
|
addShape(sashVisual, glassMaterial, createFrenchArchLeafShape(leafW, leafH, fullW, sashSpringY, sashArchHeight, side, glassInset), glassDepth, sashDepth * 0.08);
|
|
606
717
|
}
|
|
718
|
+
disableSubtreeRaycastIfSwung(sash, rotationY);
|
|
607
719
|
return;
|
|
608
720
|
}
|
|
609
721
|
const frameRadii = insetCornerRadii(getWindowRoundedRadii(node, node.width, node.height), frameThickness, fullW, leafH);
|
|
722
|
+
currentWindowSlot = 'frame';
|
|
610
723
|
addShape(sashVisual, baseMaterial, createFrenchRoundedLeafFrameShape(leafW, leafH, fullW, frameRadii, sashFrameThickness, side), sashDepth);
|
|
611
724
|
const glassInset = Math.min(sashFrameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
|
|
612
725
|
if (glassInset > 0.001) {
|
|
726
|
+
currentWindowSlot = 'glass';
|
|
613
727
|
addShape(sashVisual, glassMaterial, createFrenchRoundedLeafShape(leafW, leafH, fullW, frameRadii, side, glassInset), glassDepth, sashDepth * 0.08);
|
|
614
728
|
}
|
|
729
|
+
disableSubtreeRaycastIfSwung(sash, rotationY);
|
|
615
730
|
}
|
|
616
731
|
function addFrenchCasementWindowVisuals(node, mesh) {
|
|
617
732
|
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
618
733
|
const innerW = width - 2 * frameThickness;
|
|
619
734
|
const innerH = height - 2 * frameThickness;
|
|
620
735
|
// Fixed outer frame.
|
|
736
|
+
currentWindowSlot = 'frame';
|
|
621
737
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
622
738
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
623
739
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -632,6 +748,7 @@ function addFrenchCasementWindowVisuals(node, mesh) {
|
|
|
632
748
|
if (sill) {
|
|
633
749
|
const sillW = width + sillDepth * 0.4;
|
|
634
750
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
751
|
+
currentWindowSlot = 'frame';
|
|
635
752
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
636
753
|
}
|
|
637
754
|
}
|
|
@@ -640,9 +757,11 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
640
757
|
const innerW = width - 2 * frameThickness;
|
|
641
758
|
const innerH = height - 2 * frameThickness;
|
|
642
759
|
if (node.openingShape === 'arch') {
|
|
760
|
+
currentWindowSlot = 'frame';
|
|
643
761
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
644
762
|
}
|
|
645
763
|
else {
|
|
764
|
+
currentWindowSlot = 'frame';
|
|
646
765
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
647
766
|
}
|
|
648
767
|
if ((node.casementStyle ?? 'single') === 'french') {
|
|
@@ -656,6 +775,7 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
656
775
|
if (sill) {
|
|
657
776
|
const sillW = width + sillDepth * 0.4;
|
|
658
777
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
778
|
+
currentWindowSlot = 'frame';
|
|
659
779
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
660
780
|
}
|
|
661
781
|
return;
|
|
@@ -680,30 +800,37 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
680
800
|
mesh.add(sash);
|
|
681
801
|
if (node.openingShape === 'arch') {
|
|
682
802
|
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
803
|
+
currentWindowSlot = 'frame';
|
|
683
804
|
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
684
805
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
685
806
|
if (glassInset > 0.001) {
|
|
686
807
|
const glassW = innerW - 2 * glassInset;
|
|
687
808
|
const glassH = innerH - 2 * glassInset;
|
|
809
|
+
currentWindowSlot = 'glass';
|
|
688
810
|
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
689
811
|
}
|
|
690
812
|
}
|
|
691
813
|
else {
|
|
692
814
|
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
815
|
+
currentWindowSlot = 'frame';
|
|
693
816
|
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
694
817
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
695
818
|
if (glassInset > 0.001) {
|
|
696
819
|
const glassW = innerW - 2 * glassInset;
|
|
697
820
|
const glassH = innerH - 2 * glassInset;
|
|
821
|
+
currentWindowSlot = 'glass';
|
|
698
822
|
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
699
823
|
}
|
|
700
824
|
}
|
|
825
|
+
disableSubtreeRaycastIfSwung(sash, sash.rotation.y);
|
|
826
|
+
currentWindowSlot = 'frame';
|
|
701
827
|
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
|
|
702
828
|
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
|
|
703
829
|
}
|
|
704
830
|
if (sill) {
|
|
705
831
|
const sillW = width + sillDepth * 0.4;
|
|
706
832
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
833
|
+
currentWindowSlot = 'frame';
|
|
707
834
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
708
835
|
}
|
|
709
836
|
}
|
|
@@ -720,6 +847,7 @@ function addCasementWindowVisuals(node, mesh) {
|
|
|
720
847
|
const innerW = width - 2 * frameThickness;
|
|
721
848
|
const innerH = height - 2 * frameThickness;
|
|
722
849
|
// Fixed outer frame.
|
|
850
|
+
currentWindowSlot = 'frame';
|
|
723
851
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
724
852
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
725
853
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -741,18 +869,23 @@ function addCasementWindowVisuals(node, mesh) {
|
|
|
741
869
|
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
742
870
|
sash.rotation.y = hingeSign * openAngle;
|
|
743
871
|
mesh.add(sash);
|
|
872
|
+
currentWindowSlot = 'frame';
|
|
744
873
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, sashCenterX, innerH / 2 - sashFrameThickness / 2, 0);
|
|
745
874
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, sashCenterX, -innerH / 2 + sashFrameThickness / 2, 0);
|
|
746
875
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, sashCenterX - innerW / 2 + sashFrameThickness / 2, 0, 0);
|
|
747
876
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, sashCenterX + innerW / 2 - sashFrameThickness / 2, 0, 0);
|
|
877
|
+
currentWindowSlot = 'glass';
|
|
748
878
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
|
|
879
|
+
disableSubtreeRaycastIfSwung(sash, sash.rotation.y);
|
|
749
880
|
// Small hinge markers make the pivot side legible when the sash is closed.
|
|
881
|
+
currentWindowSlot = 'frame';
|
|
750
882
|
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
|
|
751
883
|
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
|
|
752
884
|
}
|
|
753
885
|
if (sill) {
|
|
754
886
|
const sillW = width + sillDepth * 0.4;
|
|
755
887
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
888
|
+
currentWindowSlot = 'frame';
|
|
756
889
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
757
890
|
}
|
|
758
891
|
}
|
|
@@ -765,6 +898,7 @@ function addAwningWindowVisuals(node, mesh) {
|
|
|
765
898
|
const innerW = width - 2 * frameThickness;
|
|
766
899
|
const innerH = height - 2 * frameThickness;
|
|
767
900
|
// Fixed outer frame.
|
|
901
|
+
currentWindowSlot = 'frame';
|
|
768
902
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
769
903
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
770
904
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -785,17 +919,21 @@ function addAwningWindowVisuals(node, mesh) {
|
|
|
785
919
|
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
786
920
|
sash.rotation.x = -openAngle;
|
|
787
921
|
mesh.add(sash);
|
|
922
|
+
currentWindowSlot = 'frame';
|
|
788
923
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashCenterY + innerH / 2 - sashFrameThickness / 2, 0);
|
|
789
924
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashCenterY - innerH / 2 + sashFrameThickness / 2, 0);
|
|
790
925
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, sashCenterY, 0);
|
|
791
926
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, sashCenterY, 0);
|
|
927
|
+
currentWindowSlot = 'glass';
|
|
792
928
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, sashCenterY, sashDepth * 0.08);
|
|
793
929
|
// Compact hinge rail, visible even when the sash is closed.
|
|
930
|
+
currentWindowSlot = 'frame';
|
|
794
931
|
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
795
932
|
}
|
|
796
933
|
if (sill) {
|
|
797
934
|
const sillW = width + sillDepth * 0.4;
|
|
798
935
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
936
|
+
currentWindowSlot = 'frame';
|
|
799
937
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
800
938
|
}
|
|
801
939
|
}
|
|
@@ -804,9 +942,11 @@ function addShapedAwningWindowVisuals(node, mesh) {
|
|
|
804
942
|
const innerW = width - 2 * frameThickness;
|
|
805
943
|
const innerH = height - 2 * frameThickness;
|
|
806
944
|
if (node.openingShape === 'arch') {
|
|
945
|
+
currentWindowSlot = 'frame';
|
|
807
946
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
808
947
|
}
|
|
809
948
|
else {
|
|
949
|
+
currentWindowSlot = 'frame';
|
|
810
950
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
811
951
|
}
|
|
812
952
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
@@ -827,29 +967,35 @@ function addShapedAwningWindowVisuals(node, mesh) {
|
|
|
827
967
|
mesh.add(sash);
|
|
828
968
|
if (node.openingShape === 'arch') {
|
|
829
969
|
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
970
|
+
currentWindowSlot = 'frame';
|
|
830
971
|
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
831
972
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
832
973
|
if (glassInset > 0.001) {
|
|
833
974
|
const glassW = innerW - 2 * glassInset;
|
|
834
975
|
const glassH = innerH - 2 * glassInset;
|
|
976
|
+
currentWindowSlot = 'glass';
|
|
835
977
|
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
836
978
|
}
|
|
837
979
|
}
|
|
838
980
|
else {
|
|
839
981
|
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
982
|
+
currentWindowSlot = 'frame';
|
|
840
983
|
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
841
984
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
842
985
|
if (glassInset > 0.001) {
|
|
843
986
|
const glassW = innerW - 2 * glassInset;
|
|
844
987
|
const glassH = innerH - 2 * glassInset;
|
|
988
|
+
currentWindowSlot = 'glass';
|
|
845
989
|
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
846
990
|
}
|
|
847
991
|
}
|
|
992
|
+
currentWindowSlot = 'frame';
|
|
848
993
|
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
849
994
|
}
|
|
850
995
|
if (sill) {
|
|
851
996
|
const sillW = width + sillDepth * 0.4;
|
|
852
997
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
998
|
+
currentWindowSlot = 'frame';
|
|
853
999
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
854
1000
|
}
|
|
855
1001
|
}
|
|
@@ -862,6 +1008,7 @@ function addHopperWindowVisuals(node, mesh) {
|
|
|
862
1008
|
const innerW = width - 2 * frameThickness;
|
|
863
1009
|
const innerH = height - 2 * frameThickness;
|
|
864
1010
|
// Fixed outer frame.
|
|
1011
|
+
currentWindowSlot = 'frame';
|
|
865
1012
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
866
1013
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
867
1014
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -880,17 +1027,21 @@ function addHopperWindowVisuals(node, mesh) {
|
|
|
880
1027
|
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
881
1028
|
sash.rotation.x = -openAngle;
|
|
882
1029
|
mesh.add(sash);
|
|
1030
|
+
currentWindowSlot = 'frame';
|
|
883
1031
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, innerH - sashFrameThickness / 2, 0);
|
|
884
1032
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashFrameThickness / 2, 0);
|
|
885
1033
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, innerH / 2, 0);
|
|
886
1034
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, innerH / 2, 0);
|
|
1035
|
+
currentWindowSlot = 'glass';
|
|
887
1036
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, innerH / 2, sashDepth * 0.08);
|
|
888
1037
|
// Compact bottom hinge rail, visible even when the sash is closed.
|
|
1038
|
+
currentWindowSlot = 'frame';
|
|
889
1039
|
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
890
1040
|
}
|
|
891
1041
|
if (sill) {
|
|
892
1042
|
const sillW = width + sillDepth * 0.4;
|
|
893
1043
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1044
|
+
currentWindowSlot = 'frame';
|
|
894
1045
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
895
1046
|
}
|
|
896
1047
|
}
|
|
@@ -899,9 +1050,11 @@ function addShapedHopperWindowVisuals(node, mesh) {
|
|
|
899
1050
|
const innerW = width - 2 * frameThickness;
|
|
900
1051
|
const innerH = height - 2 * frameThickness;
|
|
901
1052
|
if (node.openingShape === 'arch') {
|
|
1053
|
+
currentWindowSlot = 'frame';
|
|
902
1054
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
903
1055
|
}
|
|
904
1056
|
else {
|
|
1057
|
+
currentWindowSlot = 'frame';
|
|
905
1058
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
906
1059
|
}
|
|
907
1060
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
@@ -921,37 +1074,45 @@ function addShapedHopperWindowVisuals(node, mesh) {
|
|
|
921
1074
|
mesh.add(sash);
|
|
922
1075
|
if (node.openingShape === 'arch') {
|
|
923
1076
|
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
1077
|
+
currentWindowSlot = 'frame';
|
|
924
1078
|
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
925
1079
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
926
1080
|
if (glassInset > 0.001) {
|
|
927
1081
|
const glassW = innerW - 2 * glassInset;
|
|
928
1082
|
const glassH = innerH - 2 * glassInset;
|
|
1083
|
+
currentWindowSlot = 'glass';
|
|
929
1084
|
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
930
1085
|
}
|
|
931
1086
|
}
|
|
932
1087
|
else {
|
|
933
1088
|
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
1089
|
+
currentWindowSlot = 'frame';
|
|
934
1090
|
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
935
1091
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
936
1092
|
if (glassInset > 0.001) {
|
|
937
1093
|
const glassW = innerW - 2 * glassInset;
|
|
938
1094
|
const glassH = innerH - 2 * glassInset;
|
|
1095
|
+
currentWindowSlot = 'glass';
|
|
939
1096
|
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
940
1097
|
}
|
|
941
1098
|
}
|
|
1099
|
+
currentWindowSlot = 'frame';
|
|
942
1100
|
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
943
1101
|
}
|
|
944
1102
|
if (sill) {
|
|
945
1103
|
const sillW = width + sillDepth * 0.4;
|
|
946
1104
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1105
|
+
currentWindowSlot = 'frame';
|
|
947
1106
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
948
1107
|
}
|
|
949
1108
|
}
|
|
950
1109
|
function addHungSash(parent, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH) {
|
|
1110
|
+
currentWindowSlot = 'frame';
|
|
951
1111
|
addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth * 0.72, 0, panelHeight / 2 - sashFrameThickness / 2, 0);
|
|
952
1112
|
addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth * 0.72, 0, -panelHeight / 2 + sashFrameThickness / 2, 0);
|
|
953
1113
|
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth * 0.72, -panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
954
1114
|
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth * 0.72, panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
1115
|
+
currentWindowSlot = 'glass';
|
|
955
1116
|
addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, 0);
|
|
956
1117
|
}
|
|
957
1118
|
function addSingleHungWindowVisuals(node, mesh) {
|
|
@@ -959,6 +1120,7 @@ function addSingleHungWindowVisuals(node, mesh) {
|
|
|
959
1120
|
const innerW = width - 2 * frameThickness;
|
|
960
1121
|
const innerH = height - 2 * frameThickness;
|
|
961
1122
|
// Fixed outer frame.
|
|
1123
|
+
currentWindowSlot = 'frame';
|
|
962
1124
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
963
1125
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
964
1126
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -984,6 +1146,7 @@ function addSingleHungWindowVisuals(node, mesh) {
|
|
|
984
1146
|
activeSash.position.set(0, bottomPanelY, bottomZ);
|
|
985
1147
|
mesh.add(activeSash);
|
|
986
1148
|
// Side tracks show the lower sash is the moving element.
|
|
1149
|
+
currentWindowSlot = 'frame';
|
|
987
1150
|
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, -innerW / 2 + trackThickness / 2, 0, 0);
|
|
988
1151
|
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, innerW / 2 - trackThickness / 2, 0, 0);
|
|
989
1152
|
const topSash = new THREE.Group();
|
|
@@ -992,12 +1155,14 @@ function addSingleHungWindowVisuals(node, mesh) {
|
|
|
992
1155
|
addHungSash(topSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
993
1156
|
addHungSash(activeSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
994
1157
|
// Meeting rails: top sash fixed, bottom sash moves upward over it.
|
|
1158
|
+
currentWindowSlot = 'frame';
|
|
995
1159
|
addBox(mesh, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, topPanelY - panelHeight / 2 + railThickness / 2, topZ);
|
|
996
1160
|
addBox(activeSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, panelHeight / 2 - railThickness / 2, 0);
|
|
997
1161
|
}
|
|
998
1162
|
if (sill) {
|
|
999
1163
|
const sillW = width + sillDepth * 0.4;
|
|
1000
1164
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1165
|
+
currentWindowSlot = 'frame';
|
|
1001
1166
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1002
1167
|
}
|
|
1003
1168
|
}
|
|
@@ -1006,6 +1171,7 @@ function addDoubleHungWindowVisuals(node, mesh) {
|
|
|
1006
1171
|
const innerW = width - 2 * frameThickness;
|
|
1007
1172
|
const innerH = height - 2 * frameThickness;
|
|
1008
1173
|
// Fixed outer frame.
|
|
1174
|
+
currentWindowSlot = 'frame';
|
|
1009
1175
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1010
1176
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1011
1177
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -1035,17 +1201,20 @@ function addDoubleHungWindowVisuals(node, mesh) {
|
|
|
1035
1201
|
bottomSash.position.set(0, bottomPanelY, bottomZ);
|
|
1036
1202
|
mesh.add(bottomSash);
|
|
1037
1203
|
// Side tracks show both sashes move vertically.
|
|
1204
|
+
currentWindowSlot = 'frame';
|
|
1038
1205
|
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, -innerW / 2 + trackThickness / 2, 0, 0);
|
|
1039
1206
|
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, innerW / 2 - trackThickness / 2, 0, 0);
|
|
1040
1207
|
addHungSash(topSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
1041
1208
|
addHungSash(bottomSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
1042
1209
|
// Opposing meeting rails: top sash descends while bottom sash rises.
|
|
1210
|
+
currentWindowSlot = 'frame';
|
|
1043
1211
|
addBox(topSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, -panelHeight / 2 + railThickness / 2, 0);
|
|
1044
1212
|
addBox(bottomSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, panelHeight / 2 - railThickness / 2, 0);
|
|
1045
1213
|
}
|
|
1046
1214
|
if (sill) {
|
|
1047
1215
|
const sillW = width + sillDepth * 0.4;
|
|
1048
1216
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1217
|
+
currentWindowSlot = 'frame';
|
|
1049
1218
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1050
1219
|
}
|
|
1051
1220
|
}
|
|
@@ -1053,6 +1222,7 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1053
1222
|
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
1054
1223
|
const innerW = width - 2 * frameThickness;
|
|
1055
1224
|
const innerH = height - 2 * frameThickness;
|
|
1225
|
+
currentWindowSlot = 'frame';
|
|
1056
1226
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1057
1227
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1058
1228
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -1075,13 +1245,16 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1075
1245
|
const addBayPanel = (parent, panelW) => {
|
|
1076
1246
|
const glassW = Math.max(panelW - 2 * sashFrameThickness, 0.01);
|
|
1077
1247
|
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
1248
|
+
currentWindowSlot = 'frame';
|
|
1078
1249
|
addBox(parent, baseMaterial, panelW, sashFrameThickness, panelDepth, 0, innerH / 2 - sashFrameThickness / 2, 0);
|
|
1079
1250
|
addBox(parent, baseMaterial, panelW, sashFrameThickness, panelDepth, 0, -innerH / 2 + sashFrameThickness / 2, 0);
|
|
1080
1251
|
addBox(parent, baseMaterial, sashFrameThickness, innerH, panelDepth, -panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
1081
1252
|
addBox(parent, baseMaterial, sashFrameThickness, innerH, panelDepth, panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
1253
|
+
currentWindowSlot = 'glass';
|
|
1082
1254
|
addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, panelDepth * 0.08);
|
|
1083
1255
|
};
|
|
1084
1256
|
const addBayCap = (centerY) => {
|
|
1257
|
+
currentWindowSlot = 'frame';
|
|
1085
1258
|
const halfThickness = frameThickness / 2;
|
|
1086
1259
|
const vertices = [];
|
|
1087
1260
|
const indices = [];
|
|
@@ -1096,7 +1269,7 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1096
1269
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
1097
1270
|
geometry.setIndex(indices);
|
|
1098
1271
|
geometry.computeVertexNormals();
|
|
1099
|
-
mesh.add(new THREE.Mesh(geometry, baseMaterial));
|
|
1272
|
+
mesh.add(tagWindowSlot(new THREE.Mesh(geometry, baseMaterial)));
|
|
1100
1273
|
};
|
|
1101
1274
|
const center = new THREE.Group();
|
|
1102
1275
|
center.position.set(0, 0, projectionDepth);
|
|
@@ -1118,6 +1291,7 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1118
1291
|
if (sill) {
|
|
1119
1292
|
const sillW = width + sillDepth * 0.4;
|
|
1120
1293
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1294
|
+
currentWindowSlot = 'frame';
|
|
1121
1295
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1122
1296
|
}
|
|
1123
1297
|
}
|
|
@@ -1125,6 +1299,7 @@ function addBowWindowVisuals(node, mesh) {
|
|
|
1125
1299
|
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
1126
1300
|
const innerW = width - 2 * frameThickness;
|
|
1127
1301
|
const innerH = height - 2 * frameThickness;
|
|
1302
|
+
currentWindowSlot = 'frame';
|
|
1128
1303
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1129
1304
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1130
1305
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -1180,13 +1355,17 @@ function addBowWindowVisuals(node, mesh) {
|
|
|
1180
1355
|
return geometry;
|
|
1181
1356
|
};
|
|
1182
1357
|
const addCurvedMesh = (material, geometry) => {
|
|
1183
|
-
mesh.add(new THREE.Mesh(geometry, material));
|
|
1358
|
+
mesh.add(tagWindowSlot(new THREE.Mesh(geometry, material)));
|
|
1184
1359
|
};
|
|
1360
|
+
currentWindowSlot = 'frame';
|
|
1185
1361
|
addCurvedMesh(baseMaterial, createCurvedVerticalBand(glassTop, innerH / 2));
|
|
1186
1362
|
addCurvedMesh(baseMaterial, createCurvedVerticalBand(-innerH / 2, glassBottom));
|
|
1363
|
+
currentWindowSlot = 'glass';
|
|
1187
1364
|
addCurvedMesh(glassMaterial, createCurvedVerticalBand(glassBottom, glassTop, frameDepth * 0.04));
|
|
1365
|
+
currentWindowSlot = 'frame';
|
|
1188
1366
|
addCurvedMesh(baseMaterial, createCurvedCap(slabYTop, frameThickness));
|
|
1189
1367
|
addCurvedMesh(baseMaterial, createCurvedCap(slabYBottom, frameThickness));
|
|
1368
|
+
currentWindowSlot = 'frame';
|
|
1190
1369
|
for (let index = 0; index <= mullionCount; index += 1) {
|
|
1191
1370
|
const x = -halfSpan + (innerW * index) / mullionCount;
|
|
1192
1371
|
addBox(mesh, baseMaterial, sashFrameThickness, innerH, frameDepth * 0.72, x, 0, arcZAt(x));
|
|
@@ -1195,6 +1374,7 @@ function addBowWindowVisuals(node, mesh) {
|
|
|
1195
1374
|
if (sill) {
|
|
1196
1375
|
const sillW = width + sillDepth * 0.4;
|
|
1197
1376
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1377
|
+
currentWindowSlot = 'frame';
|
|
1198
1378
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1199
1379
|
}
|
|
1200
1380
|
}
|
|
@@ -1206,6 +1386,7 @@ function addLouveredWindowVisuals(node, mesh) {
|
|
|
1206
1386
|
}
|
|
1207
1387
|
const innerW = width - 2 * frameThickness;
|
|
1208
1388
|
const innerH = height - 2 * frameThickness;
|
|
1389
|
+
currentWindowSlot = 'frame';
|
|
1209
1390
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1210
1391
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1211
1392
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -1221,8 +1402,10 @@ function addLouveredWindowVisuals(node, mesh) {
|
|
|
1221
1402
|
const slats = new THREE.Group();
|
|
1222
1403
|
slats.name = LOUVERED_WINDOW_SLATS_NAME;
|
|
1223
1404
|
mesh.add(slats);
|
|
1405
|
+
currentWindowSlot = 'frame';
|
|
1224
1406
|
addBox(mesh, baseMaterial, railThickness, innerH, frameDepth * 0.95, -innerW / 2 + railThickness / 2, 0, 0);
|
|
1225
1407
|
addBox(mesh, baseMaterial, railThickness, innerH, frameDepth * 0.95, innerW / 2 - railThickness / 2, 0, 0);
|
|
1408
|
+
currentWindowSlot = 'glass';
|
|
1226
1409
|
for (let index = 0; index < slatCount; index += 1) {
|
|
1227
1410
|
const y = innerH / 2 - slatGap * (index + 0.5);
|
|
1228
1411
|
const slat = new THREE.Group();
|
|
@@ -1235,6 +1418,7 @@ function addLouveredWindowVisuals(node, mesh) {
|
|
|
1235
1418
|
if (sill) {
|
|
1236
1419
|
const sillW = width + sillDepth * 0.4;
|
|
1237
1420
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1421
|
+
currentWindowSlot = 'frame';
|
|
1238
1422
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1239
1423
|
}
|
|
1240
1424
|
}
|
|
@@ -1251,9 +1435,11 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1251
1435
|
const innerW = innerRight - innerLeft;
|
|
1252
1436
|
const innerH = innerTop - innerBottom;
|
|
1253
1437
|
if (node.openingShape === 'arch') {
|
|
1438
|
+
currentWindowSlot = 'frame';
|
|
1254
1439
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
1255
1440
|
}
|
|
1256
1441
|
else {
|
|
1442
|
+
currentWindowSlot = 'frame';
|
|
1257
1443
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
1258
1444
|
}
|
|
1259
1445
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
@@ -1283,6 +1469,7 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1283
1469
|
return (y) => getRoundedHorizontalBoundsAtY(y, innerLeft, innerRight, innerTop, innerRadii);
|
|
1284
1470
|
})();
|
|
1285
1471
|
const addVerticalRail = (x) => {
|
|
1472
|
+
currentWindowSlot = 'frame';
|
|
1286
1473
|
const railX1 = x;
|
|
1287
1474
|
const railX2 = x + (x < 0 ? railThickness : -railThickness);
|
|
1288
1475
|
const sampleX = x < 0 ? Math.max(railX1, railX2) : Math.min(railX1, railX2);
|
|
@@ -1293,6 +1480,7 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1293
1480
|
};
|
|
1294
1481
|
addVerticalRail(innerLeft);
|
|
1295
1482
|
addVerticalRail(innerRight);
|
|
1483
|
+
currentWindowSlot = 'glass';
|
|
1296
1484
|
for (let index = 0; index < slatCount; index += 1) {
|
|
1297
1485
|
const y = innerTop - slatGap * (index + 0.5);
|
|
1298
1486
|
const topBounds = getBoundsAtY(Math.min(y + slatHeight / 2, innerTop));
|
|
@@ -1312,10 +1500,12 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1312
1500
|
if (sill) {
|
|
1313
1501
|
const sillW = width + sillDepth * 0.4;
|
|
1314
1502
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1503
|
+
currentWindowSlot = 'frame';
|
|
1315
1504
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1316
1505
|
}
|
|
1317
1506
|
}
|
|
1318
1507
|
function updateWindowMesh(node, mesh) {
|
|
1508
|
+
currentWindowSlot = undefined;
|
|
1319
1509
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
1320
1510
|
mesh.geometry.dispose();
|
|
1321
1511
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
@@ -1330,6 +1520,11 @@ function updateWindowMesh(node, mesh) {
|
|
|
1330
1520
|
disposeObjectGeometry(child);
|
|
1331
1521
|
mesh.remove(child);
|
|
1332
1522
|
}
|
|
1523
|
+
// Point the builder-facing frame/glass materials at this window's slot
|
|
1524
|
+
// overrides for the duration of its build (recomputed per node, so the next
|
|
1525
|
+
// window resets cleanly without a restore).
|
|
1526
|
+
baseMaterial = resolveWindowSlotMaterial(node, 'frame');
|
|
1527
|
+
glassMaterial = resolveWindowSlotMaterial(node, 'glass');
|
|
1333
1528
|
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, openingKind, openingShape, windowType, } = node;
|
|
1334
1529
|
if (openingKind === 'opening') {
|
|
1335
1530
|
syncWindowCutout(node, mesh);
|
|
@@ -1394,6 +1589,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1394
1589
|
const innerH = height - 2 * frameThickness;
|
|
1395
1590
|
// ── Frame members ──
|
|
1396
1591
|
// Top / bottom — full width
|
|
1592
|
+
currentWindowSlot = 'frame';
|
|
1397
1593
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1398
1594
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1399
1595
|
// Left / right — inner height to avoid corner overlap
|
|
@@ -1428,6 +1624,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1428
1624
|
}
|
|
1429
1625
|
// Column dividers — full inner height
|
|
1430
1626
|
cx = -innerW / 2;
|
|
1627
|
+
currentWindowSlot = 'frame';
|
|
1431
1628
|
for (let c = 0; c < numCols - 1; c++) {
|
|
1432
1629
|
cx += colWidths[c];
|
|
1433
1630
|
addBox(mesh, baseMaterial, columnDividerThickness, innerH, frameDepth, cx + columnDividerThickness / 2, 0, 0);
|
|
@@ -1435,6 +1632,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1435
1632
|
}
|
|
1436
1633
|
// Row dividers — per column width, so they don't overlap column dividers (top to bottom)
|
|
1437
1634
|
cy = innerH / 2;
|
|
1635
|
+
currentWindowSlot = 'frame';
|
|
1438
1636
|
for (let r = 0; r < numRows - 1; r++) {
|
|
1439
1637
|
cy -= rowHeights[r];
|
|
1440
1638
|
const divY = cy - rowDividerThickness / 2;
|
|
@@ -1445,6 +1643,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1445
1643
|
}
|
|
1446
1644
|
// Glass panes
|
|
1447
1645
|
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
1646
|
+
currentWindowSlot = 'glass';
|
|
1448
1647
|
for (let c = 0; c < numCols; c++) {
|
|
1449
1648
|
for (let r = 0; r < numRows; r++) {
|
|
1450
1649
|
addBox(mesh, glassMaterial, colWidths[c], rowHeights[r], glassDepth, colXCenters[c], rowYCenters[r], 0);
|
|
@@ -1455,40 +1654,67 @@ function updateWindowMesh(node, mesh) {
|
|
|
1455
1654
|
const sillW = width + sillDepth * 0.4; // slightly wider than frame
|
|
1456
1655
|
// Protrudes from the front face of the frame (+Z)
|
|
1457
1656
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1657
|
+
currentWindowSlot = 'frame';
|
|
1458
1658
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1459
1659
|
}
|
|
1460
1660
|
syncWindowCutout(node, mesh);
|
|
1461
1661
|
}
|
|
1462
1662
|
function syncWindowCutout(node, mesh) {
|
|
1463
|
-
// ── Cutout
|
|
1663
|
+
// ── Cutout: invisible raycast hit target for the whole opening ──
|
|
1464
1664
|
let cutout = mesh.getObjectByName('cutout');
|
|
1465
1665
|
if (!cutout) {
|
|
1466
1666
|
cutout = new THREE.Mesh();
|
|
1467
1667
|
cutout.name = 'cutout';
|
|
1668
|
+
// The cutout (invisible) is proud of the wall on both faces, so it wins the
|
|
1669
|
+
// scene raycast over the wall in front of the recessed window — making it
|
|
1670
|
+
// the selection AND paint hit target for the whole opening. The paint
|
|
1671
|
+
// capability then re-raycasts the window's parts to find the slot. Its depth
|
|
1672
|
+
// is snug to the wall (not 1m) so it no longer blankets the room floor in a
|
|
1673
|
+
// top-down view; the wall CSG ignores this depth (see getOpeningCutoutProxyDepth).
|
|
1468
1674
|
mesh.add(cutout);
|
|
1469
1675
|
}
|
|
1470
1676
|
cutout.geometry.dispose();
|
|
1677
|
+
const depth = resolveOpeningCutoutProxyDepth(node);
|
|
1471
1678
|
if (isRectangleOnlyWindowType(node)) {
|
|
1472
|
-
cutout.geometry = new THREE.BoxGeometry(node.width, node.height,
|
|
1679
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, depth);
|
|
1473
1680
|
}
|
|
1474
1681
|
else if (node.openingShape === 'arch') {
|
|
1475
1682
|
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)), {
|
|
1476
|
-
depth
|
|
1683
|
+
depth,
|
|
1477
1684
|
bevelEnabled: false,
|
|
1478
1685
|
curveSegments: 24,
|
|
1479
1686
|
});
|
|
1480
|
-
cutout.geometry.translate(0, 0, -
|
|
1687
|
+
cutout.geometry.translate(0, 0, -depth / 2);
|
|
1481
1688
|
}
|
|
1482
1689
|
else if (node.openingShape === 'rounded') {
|
|
1483
1690
|
cutout.geometry = new THREE.ExtrudeGeometry(createRoundedShape(-node.width / 2, node.width / 2, -node.height / 2, node.height / 2, getWindowRoundedRadii(node, node.width, node.height)), {
|
|
1484
|
-
depth
|
|
1691
|
+
depth,
|
|
1485
1692
|
bevelEnabled: false,
|
|
1486
1693
|
curveSegments: 24,
|
|
1487
1694
|
});
|
|
1488
|
-
cutout.geometry.translate(0, 0, -
|
|
1695
|
+
cutout.geometry.translate(0, 0, -depth / 2);
|
|
1489
1696
|
}
|
|
1490
1697
|
else {
|
|
1491
|
-
cutout.geometry = new THREE.BoxGeometry(node.width, node.height,
|
|
1698
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, depth);
|
|
1492
1699
|
}
|
|
1493
1700
|
cutout.visible = false;
|
|
1494
1701
|
}
|
|
1702
|
+
// Resolve the cutout proxy depth from the opening's parent wall thickness so
|
|
1703
|
+
// the proxy stays proud of both wall faces (front/back selection) without the
|
|
1704
|
+
// old 1m depth that blanketed the floor. Falls back to the default thickness
|
|
1705
|
+
// when the parent wall isn't a resolvable wall node.
|
|
1706
|
+
function resolveOpeningCutoutProxyDepth(node) {
|
|
1707
|
+
const parentId = node.parentId;
|
|
1708
|
+
const parent = parentId ? useScene.getState().nodes[parentId] : undefined;
|
|
1709
|
+
const wallThickness = parent?.type === 'wall' ? getWallThickness(parent) : DEFAULT_WALL_THICKNESS;
|
|
1710
|
+
return getOpeningCutoutProxyDepth(wallThickness);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Build a fresh window mesh for preview/ghost rendering.
|
|
1714
|
+
* Returns a mesh with an invisible hitbox root and visible children (frame, glass, sash, hardware).
|
|
1715
|
+
*/
|
|
1716
|
+
export function buildWindowPreviewMesh(node) {
|
|
1717
|
+
const mesh = new THREE.Mesh();
|
|
1718
|
+
updateWindowMesh(node, mesh);
|
|
1719
|
+
return mesh;
|
|
1720
|
+
}
|