@pascal-app/viewer 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/error-boundary.d.ts +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 +6 -2
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +41 -22
- package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
- package/dist/components/renderers/parametric-node-renderer.js +37 -0
- package/dist/components/renderers/scene-renderer.d.ts +1 -1
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
- 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 +39 -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 +12 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
- package/dist/components/viewer/index.d.ts +56 -3
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +280 -65
- package/dist/components/viewer/lights.d.ts +1 -1
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +201 -43
- package/dist/components/viewer/perf-monitor.d.ts +1 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +115 -12
- 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 +395 -63
- package/dist/components/viewer/registered-systems.d.ts +12 -0
- package/dist/components/viewer/registered-systems.d.ts.map +1 -0
- package/dist/components/viewer/registered-systems.js +49 -0
- package/dist/components/viewer/scene-bvh.d.ts +18 -0
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.js +97 -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 +1 -1
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +37 -14
- package/dist/components/viewer/viewer-camera.d.ts +1 -1
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.d.ts +2 -1
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.js +20 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +3 -22
- package/dist/hooks/use-node-events.d.ts +6 -74
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +28 -8
- package/dist/index.d.ts +48 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -4
- 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 +10 -0
- package/dist/lib/csg-utils.d.ts.map +1 -0
- package/dist/lib/csg-utils.js +93 -0
- 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/drawable-geometry.d.ts +21 -0
- package/dist/lib/drawable-geometry.d.ts.map +1 -0
- package/dist/lib/drawable-geometry.js +22 -0
- package/dist/lib/edge-style.d.ts +4 -0
- package/dist/lib/edge-style.d.ts.map +1 -0
- package/dist/lib/edge-style.js +31 -0
- package/dist/lib/gpu-perf.d.ts +4 -0
- package/dist/lib/gpu-perf.d.ts.map +1 -0
- package/dist/lib/gpu-perf.js +23 -0
- package/dist/lib/ink-edges.d.ts +9 -0
- package/dist/lib/ink-edges.d.ts.map +1 -0
- package/dist/lib/ink-edges.js +49 -0
- package/dist/lib/isolation.d.ts +39 -0
- package/dist/lib/isolation.d.ts.map +1 -0
- package/dist/lib/isolation.js +103 -0
- package/dist/lib/ktx2-loader.d.ts +43 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +136 -0
- package/dist/lib/layers.d.ts +28 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +28 -0
- package/dist/lib/materials.d.ts +38 -15
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +364 -72
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -0
- package/dist/lib/polygon-union.d.ts +4 -0
- package/dist/lib/polygon-union.d.ts.map +1 -0
- package/dist/lib/polygon-union.js +289 -0
- package/dist/lib/polygon-union.test.d.ts +2 -0
- package/dist/lib/polygon-union.test.d.ts.map +1 -0
- package/dist/lib/polygon-union.test.js +105 -0
- package/dist/lib/scene-themes.d.ts +30 -0
- package/dist/lib/scene-themes.d.ts.map +1 -0
- package/dist/lib/scene-themes.js +202 -0
- 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/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 +92 -12
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +201 -6
- 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 +42 -0
- package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +66 -13
- 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 +864 -180
- package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-interaction-system.js +62 -0
- package/dist/systems/fence/fence-system.d.ts +20 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +229 -57
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
- package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
- package/dist/systems/geometry/geometry-system.d.ts +41 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.js +348 -0
- 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 +1 -1
- 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 +7 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +28 -17
- package/dist/systems/item-light/item-light-system.d.ts +1 -1
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +18 -5
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +28 -4
- 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 +2 -41
- package/dist/systems/roof/roof-materials.d.ts +3 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +59 -14
- package/dist/systems/roof/roof-system.d.ts +46 -6
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1365 -170
- package/dist/systems/slab/slab-system.d.ts +5 -3
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +204 -68
- package/dist/systems/slab/slab-system.test.d.ts +2 -0
- package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
- package/dist/systems/slab/slab-system.test.js +63 -0
- package/dist/systems/stair/stair-materials.d.ts +4 -3
- package/dist/systems/stair/stair-materials.d.ts.map +1 -1
- package/dist/systems/stair/stair-materials.js +30 -15
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +55 -63
- package/dist/systems/surface-hole-geometry.d.ts +3 -0
- package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
- package/dist/systems/surface-hole-geometry.js +4 -0
- 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 +62 -17
- package/dist/systems/wall/wall-materials.d.ts +12 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +279 -46
- 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 +48 -0
- package/dist/systems/wall/wall-system.d.ts +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +493 -193
- 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 +308 -18
- package/package.json +5 -6
- package/dist/components/renderers/building/building-renderer.d.ts +0 -5
- package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
- package/dist/components/renderers/building/building-renderer.js +0 -11
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
- package/dist/components/renderers/column/column-renderer.d.ts +0 -5
- package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
- package/dist/components/renderers/column/column-renderer.js +0 -597
- package/dist/components/renderers/door/door-renderer.d.ts +0 -5
- package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
- package/dist/components/renderers/door/door-renderer.js +0 -16
- package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
- package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
- package/dist/components/renderers/fence/fence-renderer.js +0 -15
- package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
- package/dist/components/renderers/guide/guide-renderer.js +0 -38
- package/dist/components/renderers/item/item-renderer.d.ts +0 -5
- package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
- package/dist/components/renderers/item/item-renderer.js +0 -172
- package/dist/components/renderers/level/level-renderer.d.ts +0 -5
- package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
- package/dist/components/renderers/level/level-renderer.js +0 -11
- package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
- package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-materials.js +0 -16
- package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-renderer.js +0 -32
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
- package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
- package/dist/components/renderers/scan/scan-renderer.js +0 -54
- package/dist/components/renderers/site/site-renderer.d.ts +0 -5
- package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
- package/dist/components/renderers/site/site-renderer.js +0 -106
- package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
- package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
- package/dist/components/renderers/slab/slab-renderer.js +0 -58
- package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
- package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
- package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
- package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
- package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair/stair-renderer.js +0 -603
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
- package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
- package/dist/components/renderers/wall/wall-renderer.js +0 -16
- package/dist/components/renderers/window/window-renderer.d.ts +0 -5
- package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
- package/dist/components/renderers/window/window-renderer.js +0 -21
- package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
- package/dist/components/renderers/zone/zone-renderer.js +0 -181
- 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 -79
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { sceneRegistry, useInteractive, 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
|
+
import { useEffect, useRef } from 'react';
|
|
3
4
|
import * as THREE from 'three';
|
|
4
|
-
import {
|
|
5
|
+
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
6
|
+
import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
|
|
7
|
+
import useViewer from '../../store/use-viewer';
|
|
8
|
+
import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
|
|
5
9
|
// Invisible material for root mesh — used as selection hitbox only
|
|
6
10
|
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
11
|
+
let baseMaterial = getBaseMaterial();
|
|
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;
|
|
7
20
|
export const CASEMENT_WINDOW_SASH_NAME = 'casement-window-sash';
|
|
8
21
|
export const FRENCH_CASEMENT_LEFT_SASH_NAME = 'french-casement-left-sash';
|
|
9
22
|
export const FRENCH_CASEMENT_RIGHT_SASH_NAME = 'french-casement-right-sash';
|
|
@@ -14,33 +27,173 @@ export const DOUBLE_HUNG_BOTTOM_SASH_NAME = 'double-hung-bottom-sash';
|
|
|
14
27
|
export const LOUVERED_WINDOW_SLATS_NAME = 'louvered-window-slats';
|
|
15
28
|
export const AWNING_WINDOW_SASH_NAME = 'awning-window-sash';
|
|
16
29
|
export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash';
|
|
30
|
+
const MAX_WINDOW_REBUILDS_PER_FRAME = 16;
|
|
31
|
+
const WINDOW_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WINDOW_REBUILDS_PER_FRAME;
|
|
32
|
+
const WINDOW_PROGRESSIVE_TIME_BUDGET_MS = 8;
|
|
17
33
|
export const WindowSystem = () => {
|
|
18
34
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
19
35
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
36
|
+
const shading = useViewer((state) => state.shading);
|
|
37
|
+
const textures = useViewer((state) => state.textures);
|
|
38
|
+
const colorPreset = useViewer((state) => state.colorPreset);
|
|
39
|
+
const sceneMaterials = useScene((state) => state.materials);
|
|
40
|
+
const materialRevisionRef = useRef(null);
|
|
41
|
+
// Subscribe so override-only updates re-run this component. Mirrors
|
|
42
|
+
// WallSystem + DoorSystem.
|
|
43
|
+
useLiveNodeOverrides((s) => s.overrides);
|
|
44
|
+
baseMaterial = textures
|
|
45
|
+
? getBaseMaterial(shading)
|
|
46
|
+
: createSurfaceRoleMaterial('joinery', colorPreset);
|
|
47
|
+
glassMaterial = textures
|
|
48
|
+
? defaultGlassMaterial
|
|
49
|
+
: createSurfaceRoleMaterial('glazing', colorPreset);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`;
|
|
52
|
+
if (materialRevisionRef.current === materialRevision)
|
|
53
|
+
return;
|
|
54
|
+
materialRevisionRef.current = materialRevision;
|
|
55
|
+
const nodes = useScene.getState().nodes;
|
|
56
|
+
for (const node of Object.values(nodes)) {
|
|
57
|
+
if (node?.type === 'window') {
|
|
58
|
+
useScene.getState().dirtyNodes.add(node.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
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]);
|
|
20
76
|
useFrame(() => {
|
|
21
77
|
if (dirtyNodes.size === 0)
|
|
22
78
|
return;
|
|
79
|
+
baseMaterial = textures
|
|
80
|
+
? getBaseMaterial(shading)
|
|
81
|
+
: createSurfaceRoleMaterial('joinery', colorPreset);
|
|
82
|
+
glassMaterial = textures
|
|
83
|
+
? defaultGlassMaterial
|
|
84
|
+
: createSurfaceRoleMaterial('glazing', colorPreset);
|
|
85
|
+
currentShading = shading;
|
|
86
|
+
currentTextures = textures;
|
|
87
|
+
currentColorPreset = colorPreset;
|
|
88
|
+
currentSceneMaterials = sceneMaterials;
|
|
23
89
|
const nodes = useScene.getState().nodes;
|
|
90
|
+
const dirtyWindowIds = [];
|
|
24
91
|
dirtyNodes.forEach((id) => {
|
|
25
92
|
const node = nodes[id];
|
|
26
|
-
if (
|
|
93
|
+
if (node?.type !== 'window')
|
|
27
94
|
return;
|
|
95
|
+
dirtyWindowIds.push(id);
|
|
96
|
+
});
|
|
97
|
+
const useProgressiveWindowRebuilds = dirtyWindowIds.length > WINDOW_PROGRESSIVE_DIRTY_THRESHOLD;
|
|
98
|
+
const frameStartedAt = performance.now();
|
|
99
|
+
let rebuiltWindowsThisFrame = 0;
|
|
100
|
+
for (const id of dirtyWindowIds) {
|
|
101
|
+
if (useProgressiveWindowRebuilds) {
|
|
102
|
+
if (rebuiltWindowsThisFrame >= MAX_WINDOW_REBUILDS_PER_FRAME) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if (rebuiltWindowsThisFrame > 0 &&
|
|
106
|
+
performance.now() - frameStartedAt >= WINDOW_PROGRESSIVE_TIME_BUDGET_MS) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const node = nodes[id];
|
|
111
|
+
if (node?.type !== 'window')
|
|
112
|
+
continue;
|
|
28
113
|
const mesh = sceneRegistry.nodes.get(id);
|
|
29
114
|
if (!mesh)
|
|
30
|
-
|
|
31
|
-
|
|
115
|
+
continue; // Keep dirty until mesh mounts
|
|
116
|
+
// Merge any live override (width / height / position) so the mesh
|
|
117
|
+
// rebuild reflects the in-flight drag without zustand churn.
|
|
118
|
+
const effectiveNode = getEffectiveNode(node);
|
|
119
|
+
updateWindowMesh(effectiveNode, mesh);
|
|
32
120
|
clearDirty(id);
|
|
121
|
+
rebuiltWindowsThisFrame += 1;
|
|
33
122
|
// Rebuild the parent wall so its cutout reflects the updated window geometry
|
|
34
|
-
|
|
35
|
-
|
|
123
|
+
// Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
|
|
124
|
+
// The editor tools will request a final wall rebuild on commit.
|
|
125
|
+
const isTransient = !!node.metadata?.isTransient;
|
|
126
|
+
if (!isTransient && effectiveNode.parentId) {
|
|
127
|
+
useScene.getState().dirtyNodes.add(effectiveNode.parentId);
|
|
36
128
|
}
|
|
37
|
-
}
|
|
129
|
+
}
|
|
38
130
|
}, 3);
|
|
39
131
|
return null;
|
|
40
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
|
+
}
|
|
41
191
|
function addBox(parent, material, w, h, d, x, y, z) {
|
|
42
|
-
const
|
|
192
|
+
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
193
|
+
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
194
|
+
const m = new THREE.Mesh(geometry, material);
|
|
43
195
|
m.position.set(x, y, z);
|
|
196
|
+
tagWindowSlot(m);
|
|
44
197
|
parent.add(m);
|
|
45
198
|
}
|
|
46
199
|
function addShape(parent, material, shape, depth, z = 0) {
|
|
@@ -51,6 +204,7 @@ function addShape(parent, material, shape, depth, z = 0) {
|
|
|
51
204
|
});
|
|
52
205
|
geometry.translate(0, 0, -depth / 2 + z);
|
|
53
206
|
const mesh = new THREE.Mesh(geometry, material);
|
|
207
|
+
tagWindowSlot(mesh);
|
|
54
208
|
parent.add(mesh);
|
|
55
209
|
}
|
|
56
210
|
function disposeObjectGeometry(object) {
|
|
@@ -267,9 +421,11 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
267
421
|
const innerW = innerRight - innerLeft;
|
|
268
422
|
const innerH = innerTop - innerBottom;
|
|
269
423
|
const innerRadii = insetCornerRadii(outerRadii, inset, innerW, innerH);
|
|
424
|
+
currentWindowSlot = 'frame';
|
|
270
425
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, inset, outerRadii), frameDepth);
|
|
271
426
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
272
427
|
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
428
|
+
currentWindowSlot = 'glass';
|
|
273
429
|
addShape(mesh, glassMaterial, createRoundedShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii), glassDepth);
|
|
274
430
|
const numCols = columnRatios.length;
|
|
275
431
|
const numRows = rowRatios.length;
|
|
@@ -280,6 +436,7 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
280
436
|
const colWidths = columnRatios.map((r) => (r / colSum) * usableW);
|
|
281
437
|
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
|
|
282
438
|
let x = innerLeft;
|
|
439
|
+
currentWindowSlot = 'frame';
|
|
283
440
|
for (let c = 0; c < numCols - 1; c++) {
|
|
284
441
|
x += colWidths[c];
|
|
285
442
|
const x1 = x;
|
|
@@ -291,6 +448,7 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
291
448
|
x += columnDividerThickness;
|
|
292
449
|
}
|
|
293
450
|
let y = innerTop;
|
|
451
|
+
currentWindowSlot = 'frame';
|
|
294
452
|
for (let r = 0; r < numRows - 1; r++) {
|
|
295
453
|
y -= rowHeights[r];
|
|
296
454
|
const yTop = y;
|
|
@@ -305,6 +463,7 @@ function addRoundedWindowVisuals(node, mesh) {
|
|
|
305
463
|
if (sill) {
|
|
306
464
|
const sillW = width + sillDepth * 0.4;
|
|
307
465
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
466
|
+
currentWindowSlot = 'frame';
|
|
308
467
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
309
468
|
}
|
|
310
469
|
}
|
|
@@ -323,9 +482,11 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
323
482
|
const innerH = innerTop - innerBottom;
|
|
324
483
|
const innerArchHeight = getClampedArchHeight(innerW, innerH, archHeight - inset);
|
|
325
484
|
const innerSpringY = innerTop - innerArchHeight;
|
|
485
|
+
currentWindowSlot = 'frame';
|
|
326
486
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, archHeight, inset), frameDepth);
|
|
327
487
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
328
488
|
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
489
|
+
currentWindowSlot = 'glass';
|
|
329
490
|
addShape(mesh, glassMaterial, createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight), glassDepth);
|
|
330
491
|
const numCols = columnRatios.length;
|
|
331
492
|
const numRows = rowRatios.length;
|
|
@@ -337,6 +498,7 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
337
498
|
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
|
|
338
499
|
const innerHalfWidth = innerW / 2;
|
|
339
500
|
let x = innerLeft;
|
|
501
|
+
currentWindowSlot = 'frame';
|
|
340
502
|
for (let c = 0; c < numCols - 1; c++) {
|
|
341
503
|
x += colWidths[c];
|
|
342
504
|
const x1 = x;
|
|
@@ -348,6 +510,7 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
348
510
|
x += columnDividerThickness;
|
|
349
511
|
}
|
|
350
512
|
let y = innerTop;
|
|
513
|
+
currentWindowSlot = 'frame';
|
|
351
514
|
for (let r = 0; r < numRows - 1; r++) {
|
|
352
515
|
y -= rowHeights[r];
|
|
353
516
|
const yTop = y;
|
|
@@ -364,6 +527,7 @@ function addArchedWindowVisuals(node, mesh) {
|
|
|
364
527
|
if (sill) {
|
|
365
528
|
const sillW = width + sillDepth * 0.4;
|
|
366
529
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
530
|
+
currentWindowSlot = 'frame';
|
|
367
531
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
368
532
|
}
|
|
369
533
|
}
|
|
@@ -386,6 +550,7 @@ function addSlidingWindowVisuals(node, mesh) {
|
|
|
386
550
|
const innerW = width - 2 * frameThickness;
|
|
387
551
|
const innerH = height - 2 * frameThickness;
|
|
388
552
|
// Outer frame.
|
|
553
|
+
currentWindowSlot = 'frame';
|
|
389
554
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
390
555
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
391
556
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -409,11 +574,14 @@ function addSlidingWindowVisuals(node, mesh) {
|
|
|
409
574
|
activePanel.position.set(leftPanelX, 0, leftZ);
|
|
410
575
|
mesh.add(activePanel);
|
|
411
576
|
// Twin tracks signal the sliding operation without adding editor-only state.
|
|
577
|
+
currentWindowSlot = 'frame';
|
|
412
578
|
addBox(mesh, baseMaterial, innerW, trackThickness, frameDepth, 0, innerH / 2 - trackThickness / 2, 0);
|
|
413
579
|
addBox(mesh, baseMaterial, innerW, trackThickness, frameDepth, 0, -innerH / 2 + trackThickness / 2, 0);
|
|
580
|
+
currentWindowSlot = 'glass';
|
|
414
581
|
addBox(activePanel, glassMaterial, panelWidth, panelH, glassDepth, 0, 0, 0);
|
|
415
582
|
addBox(mesh, glassMaterial, panelWidth, panelH, glassDepth, rightPanelX, 0, rightZ);
|
|
416
583
|
// The right sash stays fixed. The left sash is the active panel that slides across it.
|
|
584
|
+
currentWindowSlot = 'frame';
|
|
417
585
|
addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth * 0.72, -panelWidth / 2 + railThickness / 2, 0, 0);
|
|
418
586
|
addBox(mesh, baseMaterial, railThickness, panelH, frameDepth * 0.72, rightPanelX + panelWidth / 2 - railThickness / 2, 0, rightZ);
|
|
419
587
|
addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth * 0.78, panelWidth / 2 - railThickness / 2, 0, 0);
|
|
@@ -422,6 +590,7 @@ function addSlidingWindowVisuals(node, mesh) {
|
|
|
422
590
|
if (sill) {
|
|
423
591
|
const sillW = width + sillDepth * 0.4;
|
|
424
592
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
593
|
+
currentWindowSlot = 'frame';
|
|
425
594
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
426
595
|
}
|
|
427
596
|
}
|
|
@@ -436,15 +605,19 @@ function addRectCasementSash(parent, name, panelW, panelH, frameThickness, frame
|
|
|
436
605
|
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
437
606
|
sash.rotation.y = rotationY;
|
|
438
607
|
parent.add(sash);
|
|
608
|
+
currentWindowSlot = 'frame';
|
|
439
609
|
addBox(sash, baseMaterial, panelW, sashFrameThickness, sashDepth, sashCenterX, panelH / 2 - sashFrameThickness / 2, 0);
|
|
440
610
|
addBox(sash, baseMaterial, panelW, sashFrameThickness, sashDepth, sashCenterX, -panelH / 2 + sashFrameThickness / 2, 0);
|
|
441
611
|
addBox(sash, baseMaterial, sashFrameThickness, panelH, sashDepth, sashCenterX - panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
442
612
|
addBox(sash, baseMaterial, sashFrameThickness, panelH, sashDepth, sashCenterX + panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
613
|
+
currentWindowSlot = 'glass';
|
|
443
614
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
|
|
615
|
+
disableSubtreeRaycastIfSwung(sash, rotationY);
|
|
444
616
|
}
|
|
445
617
|
function addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth) {
|
|
446
618
|
const markerW = Math.max(frameThickness * 0.38, 0.018);
|
|
447
619
|
const markerH = innerH * 0.24;
|
|
620
|
+
currentWindowSlot = 'frame';
|
|
448
621
|
for (const pivotX of [-innerW / 2, innerW / 2]) {
|
|
449
622
|
addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, innerH * 0.25, frameDepth * 0.08);
|
|
450
623
|
addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, -innerH * 0.25, frameDepth * 0.08);
|
|
@@ -535,25 +708,32 @@ function addShapedFrenchCasementSash(parent, node, name, side, fullW, leafW, lea
|
|
|
535
708
|
const outerArchHeight = getClampedArchHeight(node.width, node.height, node.archHeight);
|
|
536
709
|
const sashArchHeight = getClampedArchHeight(fullW, leafH, outerArchHeight - frameThickness);
|
|
537
710
|
const sashSpringY = node.height / 2 - outerArchHeight;
|
|
711
|
+
currentWindowSlot = 'frame';
|
|
538
712
|
addShape(sashVisual, baseMaterial, createFrenchArchLeafFrameShape(leafW, leafH, fullW, sashSpringY, sashArchHeight, sashFrameThickness, side), sashDepth);
|
|
539
713
|
const glassInset = Math.min(sashFrameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
|
|
540
714
|
if (glassInset > 0.001) {
|
|
715
|
+
currentWindowSlot = 'glass';
|
|
541
716
|
addShape(sashVisual, glassMaterial, createFrenchArchLeafShape(leafW, leafH, fullW, sashSpringY, sashArchHeight, side, glassInset), glassDepth, sashDepth * 0.08);
|
|
542
717
|
}
|
|
718
|
+
disableSubtreeRaycastIfSwung(sash, rotationY);
|
|
543
719
|
return;
|
|
544
720
|
}
|
|
545
721
|
const frameRadii = insetCornerRadii(getWindowRoundedRadii(node, node.width, node.height), frameThickness, fullW, leafH);
|
|
722
|
+
currentWindowSlot = 'frame';
|
|
546
723
|
addShape(sashVisual, baseMaterial, createFrenchRoundedLeafFrameShape(leafW, leafH, fullW, frameRadii, sashFrameThickness, side), sashDepth);
|
|
547
724
|
const glassInset = Math.min(sashFrameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
|
|
548
725
|
if (glassInset > 0.001) {
|
|
726
|
+
currentWindowSlot = 'glass';
|
|
549
727
|
addShape(sashVisual, glassMaterial, createFrenchRoundedLeafShape(leafW, leafH, fullW, frameRadii, side, glassInset), glassDepth, sashDepth * 0.08);
|
|
550
728
|
}
|
|
729
|
+
disableSubtreeRaycastIfSwung(sash, rotationY);
|
|
551
730
|
}
|
|
552
731
|
function addFrenchCasementWindowVisuals(node, mesh) {
|
|
553
732
|
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
554
733
|
const innerW = width - 2 * frameThickness;
|
|
555
734
|
const innerH = height - 2 * frameThickness;
|
|
556
735
|
// Fixed outer frame.
|
|
736
|
+
currentWindowSlot = 'frame';
|
|
557
737
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
558
738
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
559
739
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -568,6 +748,7 @@ function addFrenchCasementWindowVisuals(node, mesh) {
|
|
|
568
748
|
if (sill) {
|
|
569
749
|
const sillW = width + sillDepth * 0.4;
|
|
570
750
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
751
|
+
currentWindowSlot = 'frame';
|
|
571
752
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
572
753
|
}
|
|
573
754
|
}
|
|
@@ -576,9 +757,11 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
576
757
|
const innerW = width - 2 * frameThickness;
|
|
577
758
|
const innerH = height - 2 * frameThickness;
|
|
578
759
|
if (node.openingShape === 'arch') {
|
|
760
|
+
currentWindowSlot = 'frame';
|
|
579
761
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
580
762
|
}
|
|
581
763
|
else {
|
|
764
|
+
currentWindowSlot = 'frame';
|
|
582
765
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
583
766
|
}
|
|
584
767
|
if ((node.casementStyle ?? 'single') === 'french') {
|
|
@@ -592,6 +775,7 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
592
775
|
if (sill) {
|
|
593
776
|
const sillW = width + sillDepth * 0.4;
|
|
594
777
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
778
|
+
currentWindowSlot = 'frame';
|
|
595
779
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
596
780
|
}
|
|
597
781
|
return;
|
|
@@ -616,30 +800,37 @@ function addShapedCasementWindowVisuals(node, mesh) {
|
|
|
616
800
|
mesh.add(sash);
|
|
617
801
|
if (node.openingShape === 'arch') {
|
|
618
802
|
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
803
|
+
currentWindowSlot = 'frame';
|
|
619
804
|
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
620
805
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
621
806
|
if (glassInset > 0.001) {
|
|
622
807
|
const glassW = innerW - 2 * glassInset;
|
|
623
808
|
const glassH = innerH - 2 * glassInset;
|
|
809
|
+
currentWindowSlot = 'glass';
|
|
624
810
|
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
625
811
|
}
|
|
626
812
|
}
|
|
627
813
|
else {
|
|
628
814
|
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
815
|
+
currentWindowSlot = 'frame';
|
|
629
816
|
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
630
817
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
631
818
|
if (glassInset > 0.001) {
|
|
632
819
|
const glassW = innerW - 2 * glassInset;
|
|
633
820
|
const glassH = innerH - 2 * glassInset;
|
|
821
|
+
currentWindowSlot = 'glass';
|
|
634
822
|
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
635
823
|
}
|
|
636
824
|
}
|
|
825
|
+
disableSubtreeRaycastIfSwung(sash, sash.rotation.y);
|
|
826
|
+
currentWindowSlot = 'frame';
|
|
637
827
|
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
|
|
638
828
|
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
|
|
639
829
|
}
|
|
640
830
|
if (sill) {
|
|
641
831
|
const sillW = width + sillDepth * 0.4;
|
|
642
832
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
833
|
+
currentWindowSlot = 'frame';
|
|
643
834
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
644
835
|
}
|
|
645
836
|
}
|
|
@@ -656,6 +847,7 @@ function addCasementWindowVisuals(node, mesh) {
|
|
|
656
847
|
const innerW = width - 2 * frameThickness;
|
|
657
848
|
const innerH = height - 2 * frameThickness;
|
|
658
849
|
// Fixed outer frame.
|
|
850
|
+
currentWindowSlot = 'frame';
|
|
659
851
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
660
852
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
661
853
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -677,18 +869,23 @@ function addCasementWindowVisuals(node, mesh) {
|
|
|
677
869
|
sash.position.set(pivotX, 0, frameDepth * 0.06);
|
|
678
870
|
sash.rotation.y = hingeSign * openAngle;
|
|
679
871
|
mesh.add(sash);
|
|
872
|
+
currentWindowSlot = 'frame';
|
|
680
873
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, sashCenterX, innerH / 2 - sashFrameThickness / 2, 0);
|
|
681
874
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, sashCenterX, -innerH / 2 + sashFrameThickness / 2, 0);
|
|
682
875
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, sashCenterX - innerW / 2 + sashFrameThickness / 2, 0, 0);
|
|
683
876
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, sashCenterX + innerW / 2 - sashFrameThickness / 2, 0, 0);
|
|
877
|
+
currentWindowSlot = 'glass';
|
|
684
878
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
|
|
879
|
+
disableSubtreeRaycastIfSwung(sash, sash.rotation.y);
|
|
685
880
|
// Small hinge markers make the pivot side legible when the sash is closed.
|
|
881
|
+
currentWindowSlot = 'frame';
|
|
686
882
|
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
|
|
687
883
|
addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
|
|
688
884
|
}
|
|
689
885
|
if (sill) {
|
|
690
886
|
const sillW = width + sillDepth * 0.4;
|
|
691
887
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
888
|
+
currentWindowSlot = 'frame';
|
|
692
889
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
693
890
|
}
|
|
694
891
|
}
|
|
@@ -701,6 +898,7 @@ function addAwningWindowVisuals(node, mesh) {
|
|
|
701
898
|
const innerW = width - 2 * frameThickness;
|
|
702
899
|
const innerH = height - 2 * frameThickness;
|
|
703
900
|
// Fixed outer frame.
|
|
901
|
+
currentWindowSlot = 'frame';
|
|
704
902
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
705
903
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
706
904
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -721,17 +919,21 @@ function addAwningWindowVisuals(node, mesh) {
|
|
|
721
919
|
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
722
920
|
sash.rotation.x = -openAngle;
|
|
723
921
|
mesh.add(sash);
|
|
922
|
+
currentWindowSlot = 'frame';
|
|
724
923
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashCenterY + innerH / 2 - sashFrameThickness / 2, 0);
|
|
725
924
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashCenterY - innerH / 2 + sashFrameThickness / 2, 0);
|
|
726
925
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, sashCenterY, 0);
|
|
727
926
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, sashCenterY, 0);
|
|
927
|
+
currentWindowSlot = 'glass';
|
|
728
928
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, sashCenterY, sashDepth * 0.08);
|
|
729
929
|
// Compact hinge rail, visible even when the sash is closed.
|
|
930
|
+
currentWindowSlot = 'frame';
|
|
730
931
|
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
731
932
|
}
|
|
732
933
|
if (sill) {
|
|
733
934
|
const sillW = width + sillDepth * 0.4;
|
|
734
935
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
936
|
+
currentWindowSlot = 'frame';
|
|
735
937
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
736
938
|
}
|
|
737
939
|
}
|
|
@@ -740,9 +942,11 @@ function addShapedAwningWindowVisuals(node, mesh) {
|
|
|
740
942
|
const innerW = width - 2 * frameThickness;
|
|
741
943
|
const innerH = height - 2 * frameThickness;
|
|
742
944
|
if (node.openingShape === 'arch') {
|
|
945
|
+
currentWindowSlot = 'frame';
|
|
743
946
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
744
947
|
}
|
|
745
948
|
else {
|
|
949
|
+
currentWindowSlot = 'frame';
|
|
746
950
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
747
951
|
}
|
|
748
952
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
@@ -763,29 +967,35 @@ function addShapedAwningWindowVisuals(node, mesh) {
|
|
|
763
967
|
mesh.add(sash);
|
|
764
968
|
if (node.openingShape === 'arch') {
|
|
765
969
|
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
970
|
+
currentWindowSlot = 'frame';
|
|
766
971
|
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
767
972
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
768
973
|
if (glassInset > 0.001) {
|
|
769
974
|
const glassW = innerW - 2 * glassInset;
|
|
770
975
|
const glassH = innerH - 2 * glassInset;
|
|
976
|
+
currentWindowSlot = 'glass';
|
|
771
977
|
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
772
978
|
}
|
|
773
979
|
}
|
|
774
980
|
else {
|
|
775
981
|
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
982
|
+
currentWindowSlot = 'frame';
|
|
776
983
|
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
777
984
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
778
985
|
if (glassInset > 0.001) {
|
|
779
986
|
const glassW = innerW - 2 * glassInset;
|
|
780
987
|
const glassH = innerH - 2 * glassInset;
|
|
988
|
+
currentWindowSlot = 'glass';
|
|
781
989
|
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
782
990
|
}
|
|
783
991
|
}
|
|
992
|
+
currentWindowSlot = 'frame';
|
|
784
993
|
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
785
994
|
}
|
|
786
995
|
if (sill) {
|
|
787
996
|
const sillW = width + sillDepth * 0.4;
|
|
788
997
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
998
|
+
currentWindowSlot = 'frame';
|
|
789
999
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
790
1000
|
}
|
|
791
1001
|
}
|
|
@@ -798,6 +1008,7 @@ function addHopperWindowVisuals(node, mesh) {
|
|
|
798
1008
|
const innerW = width - 2 * frameThickness;
|
|
799
1009
|
const innerH = height - 2 * frameThickness;
|
|
800
1010
|
// Fixed outer frame.
|
|
1011
|
+
currentWindowSlot = 'frame';
|
|
801
1012
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
802
1013
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
803
1014
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -816,17 +1027,21 @@ function addHopperWindowVisuals(node, mesh) {
|
|
|
816
1027
|
sash.position.set(0, pivotY, frameDepth * 0.06);
|
|
817
1028
|
sash.rotation.x = -openAngle;
|
|
818
1029
|
mesh.add(sash);
|
|
1030
|
+
currentWindowSlot = 'frame';
|
|
819
1031
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, innerH - sashFrameThickness / 2, 0);
|
|
820
1032
|
addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashFrameThickness / 2, 0);
|
|
821
1033
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, innerH / 2, 0);
|
|
822
1034
|
addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, innerH / 2, 0);
|
|
1035
|
+
currentWindowSlot = 'glass';
|
|
823
1036
|
addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, innerH / 2, sashDepth * 0.08);
|
|
824
1037
|
// Compact bottom hinge rail, visible even when the sash is closed.
|
|
1038
|
+
currentWindowSlot = 'frame';
|
|
825
1039
|
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
826
1040
|
}
|
|
827
1041
|
if (sill) {
|
|
828
1042
|
const sillW = width + sillDepth * 0.4;
|
|
829
1043
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1044
|
+
currentWindowSlot = 'frame';
|
|
830
1045
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
831
1046
|
}
|
|
832
1047
|
}
|
|
@@ -835,9 +1050,11 @@ function addShapedHopperWindowVisuals(node, mesh) {
|
|
|
835
1050
|
const innerW = width - 2 * frameThickness;
|
|
836
1051
|
const innerH = height - 2 * frameThickness;
|
|
837
1052
|
if (node.openingShape === 'arch') {
|
|
1053
|
+
currentWindowSlot = 'frame';
|
|
838
1054
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
839
1055
|
}
|
|
840
1056
|
else {
|
|
1057
|
+
currentWindowSlot = 'frame';
|
|
841
1058
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
842
1059
|
}
|
|
843
1060
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
@@ -857,37 +1074,45 @@ function addShapedHopperWindowVisuals(node, mesh) {
|
|
|
857
1074
|
mesh.add(sash);
|
|
858
1075
|
if (node.openingShape === 'arch') {
|
|
859
1076
|
const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
|
|
1077
|
+
currentWindowSlot = 'frame';
|
|
860
1078
|
addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
|
|
861
1079
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
862
1080
|
if (glassInset > 0.001) {
|
|
863
1081
|
const glassW = innerW - 2 * glassInset;
|
|
864
1082
|
const glassH = innerH - 2 * glassInset;
|
|
1083
|
+
currentWindowSlot = 'glass';
|
|
865
1084
|
addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
|
|
866
1085
|
}
|
|
867
1086
|
}
|
|
868
1087
|
else {
|
|
869
1088
|
const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
|
|
1089
|
+
currentWindowSlot = 'frame';
|
|
870
1090
|
addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
|
|
871
1091
|
const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
|
|
872
1092
|
if (glassInset > 0.001) {
|
|
873
1093
|
const glassW = innerW - 2 * glassInset;
|
|
874
1094
|
const glassH = innerH - 2 * glassInset;
|
|
1095
|
+
currentWindowSlot = 'glass';
|
|
875
1096
|
addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
|
|
876
1097
|
}
|
|
877
1098
|
}
|
|
1099
|
+
currentWindowSlot = 'frame';
|
|
878
1100
|
addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
|
|
879
1101
|
}
|
|
880
1102
|
if (sill) {
|
|
881
1103
|
const sillW = width + sillDepth * 0.4;
|
|
882
1104
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1105
|
+
currentWindowSlot = 'frame';
|
|
883
1106
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
884
1107
|
}
|
|
885
1108
|
}
|
|
886
1109
|
function addHungSash(parent, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH) {
|
|
1110
|
+
currentWindowSlot = 'frame';
|
|
887
1111
|
addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth * 0.72, 0, panelHeight / 2 - sashFrameThickness / 2, 0);
|
|
888
1112
|
addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth * 0.72, 0, -panelHeight / 2 + sashFrameThickness / 2, 0);
|
|
889
1113
|
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth * 0.72, -panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
890
1114
|
addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth * 0.72, panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
1115
|
+
currentWindowSlot = 'glass';
|
|
891
1116
|
addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, 0);
|
|
892
1117
|
}
|
|
893
1118
|
function addSingleHungWindowVisuals(node, mesh) {
|
|
@@ -895,6 +1120,7 @@ function addSingleHungWindowVisuals(node, mesh) {
|
|
|
895
1120
|
const innerW = width - 2 * frameThickness;
|
|
896
1121
|
const innerH = height - 2 * frameThickness;
|
|
897
1122
|
// Fixed outer frame.
|
|
1123
|
+
currentWindowSlot = 'frame';
|
|
898
1124
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
899
1125
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
900
1126
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -920,6 +1146,7 @@ function addSingleHungWindowVisuals(node, mesh) {
|
|
|
920
1146
|
activeSash.position.set(0, bottomPanelY, bottomZ);
|
|
921
1147
|
mesh.add(activeSash);
|
|
922
1148
|
// Side tracks show the lower sash is the moving element.
|
|
1149
|
+
currentWindowSlot = 'frame';
|
|
923
1150
|
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, -innerW / 2 + trackThickness / 2, 0, 0);
|
|
924
1151
|
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, innerW / 2 - trackThickness / 2, 0, 0);
|
|
925
1152
|
const topSash = new THREE.Group();
|
|
@@ -928,12 +1155,14 @@ function addSingleHungWindowVisuals(node, mesh) {
|
|
|
928
1155
|
addHungSash(topSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
929
1156
|
addHungSash(activeSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
930
1157
|
// Meeting rails: top sash fixed, bottom sash moves upward over it.
|
|
1158
|
+
currentWindowSlot = 'frame';
|
|
931
1159
|
addBox(mesh, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, topPanelY - panelHeight / 2 + railThickness / 2, topZ);
|
|
932
1160
|
addBox(activeSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, panelHeight / 2 - railThickness / 2, 0);
|
|
933
1161
|
}
|
|
934
1162
|
if (sill) {
|
|
935
1163
|
const sillW = width + sillDepth * 0.4;
|
|
936
1164
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1165
|
+
currentWindowSlot = 'frame';
|
|
937
1166
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
938
1167
|
}
|
|
939
1168
|
}
|
|
@@ -942,6 +1171,7 @@ function addDoubleHungWindowVisuals(node, mesh) {
|
|
|
942
1171
|
const innerW = width - 2 * frameThickness;
|
|
943
1172
|
const innerH = height - 2 * frameThickness;
|
|
944
1173
|
// Fixed outer frame.
|
|
1174
|
+
currentWindowSlot = 'frame';
|
|
945
1175
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
946
1176
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
947
1177
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -971,17 +1201,20 @@ function addDoubleHungWindowVisuals(node, mesh) {
|
|
|
971
1201
|
bottomSash.position.set(0, bottomPanelY, bottomZ);
|
|
972
1202
|
mesh.add(bottomSash);
|
|
973
1203
|
// Side tracks show both sashes move vertically.
|
|
1204
|
+
currentWindowSlot = 'frame';
|
|
974
1205
|
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, -innerW / 2 + trackThickness / 2, 0, 0);
|
|
975
1206
|
addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, innerW / 2 - trackThickness / 2, 0, 0);
|
|
976
1207
|
addHungSash(topSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
977
1208
|
addHungSash(bottomSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
|
|
978
1209
|
// Opposing meeting rails: top sash descends while bottom sash rises.
|
|
1210
|
+
currentWindowSlot = 'frame';
|
|
979
1211
|
addBox(topSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, -panelHeight / 2 + railThickness / 2, 0);
|
|
980
1212
|
addBox(bottomSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, panelHeight / 2 - railThickness / 2, 0);
|
|
981
1213
|
}
|
|
982
1214
|
if (sill) {
|
|
983
1215
|
const sillW = width + sillDepth * 0.4;
|
|
984
1216
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1217
|
+
currentWindowSlot = 'frame';
|
|
985
1218
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
986
1219
|
}
|
|
987
1220
|
}
|
|
@@ -989,6 +1222,7 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
989
1222
|
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
990
1223
|
const innerW = width - 2 * frameThickness;
|
|
991
1224
|
const innerH = height - 2 * frameThickness;
|
|
1225
|
+
currentWindowSlot = 'frame';
|
|
992
1226
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
993
1227
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
994
1228
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -1011,13 +1245,16 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1011
1245
|
const addBayPanel = (parent, panelW) => {
|
|
1012
1246
|
const glassW = Math.max(panelW - 2 * sashFrameThickness, 0.01);
|
|
1013
1247
|
const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
|
|
1248
|
+
currentWindowSlot = 'frame';
|
|
1014
1249
|
addBox(parent, baseMaterial, panelW, sashFrameThickness, panelDepth, 0, innerH / 2 - sashFrameThickness / 2, 0);
|
|
1015
1250
|
addBox(parent, baseMaterial, panelW, sashFrameThickness, panelDepth, 0, -innerH / 2 + sashFrameThickness / 2, 0);
|
|
1016
1251
|
addBox(parent, baseMaterial, sashFrameThickness, innerH, panelDepth, -panelW / 2 + sashFrameThickness / 2, 0, 0);
|
|
1017
1252
|
addBox(parent, baseMaterial, sashFrameThickness, innerH, panelDepth, panelW / 2 - sashFrameThickness / 2, 0, 0);
|
|
1253
|
+
currentWindowSlot = 'glass';
|
|
1018
1254
|
addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, panelDepth * 0.08);
|
|
1019
1255
|
};
|
|
1020
1256
|
const addBayCap = (centerY) => {
|
|
1257
|
+
currentWindowSlot = 'frame';
|
|
1021
1258
|
const halfThickness = frameThickness / 2;
|
|
1022
1259
|
const vertices = [];
|
|
1023
1260
|
const indices = [];
|
|
@@ -1032,7 +1269,7 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1032
1269
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
1033
1270
|
geometry.setIndex(indices);
|
|
1034
1271
|
geometry.computeVertexNormals();
|
|
1035
|
-
mesh.add(new THREE.Mesh(geometry, baseMaterial));
|
|
1272
|
+
mesh.add(tagWindowSlot(new THREE.Mesh(geometry, baseMaterial)));
|
|
1036
1273
|
};
|
|
1037
1274
|
const center = new THREE.Group();
|
|
1038
1275
|
center.position.set(0, 0, projectionDepth);
|
|
@@ -1054,6 +1291,7 @@ function addBayWindowVisuals(node, mesh) {
|
|
|
1054
1291
|
if (sill) {
|
|
1055
1292
|
const sillW = width + sillDepth * 0.4;
|
|
1056
1293
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1294
|
+
currentWindowSlot = 'frame';
|
|
1057
1295
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1058
1296
|
}
|
|
1059
1297
|
}
|
|
@@ -1061,6 +1299,7 @@ function addBowWindowVisuals(node, mesh) {
|
|
|
1061
1299
|
const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
|
|
1062
1300
|
const innerW = width - 2 * frameThickness;
|
|
1063
1301
|
const innerH = height - 2 * frameThickness;
|
|
1302
|
+
currentWindowSlot = 'frame';
|
|
1064
1303
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1065
1304
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1066
1305
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -1116,13 +1355,17 @@ function addBowWindowVisuals(node, mesh) {
|
|
|
1116
1355
|
return geometry;
|
|
1117
1356
|
};
|
|
1118
1357
|
const addCurvedMesh = (material, geometry) => {
|
|
1119
|
-
mesh.add(new THREE.Mesh(geometry, material));
|
|
1358
|
+
mesh.add(tagWindowSlot(new THREE.Mesh(geometry, material)));
|
|
1120
1359
|
};
|
|
1360
|
+
currentWindowSlot = 'frame';
|
|
1121
1361
|
addCurvedMesh(baseMaterial, createCurvedVerticalBand(glassTop, innerH / 2));
|
|
1122
1362
|
addCurvedMesh(baseMaterial, createCurvedVerticalBand(-innerH / 2, glassBottom));
|
|
1363
|
+
currentWindowSlot = 'glass';
|
|
1123
1364
|
addCurvedMesh(glassMaterial, createCurvedVerticalBand(glassBottom, glassTop, frameDepth * 0.04));
|
|
1365
|
+
currentWindowSlot = 'frame';
|
|
1124
1366
|
addCurvedMesh(baseMaterial, createCurvedCap(slabYTop, frameThickness));
|
|
1125
1367
|
addCurvedMesh(baseMaterial, createCurvedCap(slabYBottom, frameThickness));
|
|
1368
|
+
currentWindowSlot = 'frame';
|
|
1126
1369
|
for (let index = 0; index <= mullionCount; index += 1) {
|
|
1127
1370
|
const x = -halfSpan + (innerW * index) / mullionCount;
|
|
1128
1371
|
addBox(mesh, baseMaterial, sashFrameThickness, innerH, frameDepth * 0.72, x, 0, arcZAt(x));
|
|
@@ -1131,6 +1374,7 @@ function addBowWindowVisuals(node, mesh) {
|
|
|
1131
1374
|
if (sill) {
|
|
1132
1375
|
const sillW = width + sillDepth * 0.4;
|
|
1133
1376
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1377
|
+
currentWindowSlot = 'frame';
|
|
1134
1378
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1135
1379
|
}
|
|
1136
1380
|
}
|
|
@@ -1142,6 +1386,7 @@ function addLouveredWindowVisuals(node, mesh) {
|
|
|
1142
1386
|
}
|
|
1143
1387
|
const innerW = width - 2 * frameThickness;
|
|
1144
1388
|
const innerH = height - 2 * frameThickness;
|
|
1389
|
+
currentWindowSlot = 'frame';
|
|
1145
1390
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1146
1391
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1147
1392
|
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
@@ -1157,8 +1402,10 @@ function addLouveredWindowVisuals(node, mesh) {
|
|
|
1157
1402
|
const slats = new THREE.Group();
|
|
1158
1403
|
slats.name = LOUVERED_WINDOW_SLATS_NAME;
|
|
1159
1404
|
mesh.add(slats);
|
|
1405
|
+
currentWindowSlot = 'frame';
|
|
1160
1406
|
addBox(mesh, baseMaterial, railThickness, innerH, frameDepth * 0.95, -innerW / 2 + railThickness / 2, 0, 0);
|
|
1161
1407
|
addBox(mesh, baseMaterial, railThickness, innerH, frameDepth * 0.95, innerW / 2 - railThickness / 2, 0, 0);
|
|
1408
|
+
currentWindowSlot = 'glass';
|
|
1162
1409
|
for (let index = 0; index < slatCount; index += 1) {
|
|
1163
1410
|
const y = innerH / 2 - slatGap * (index + 0.5);
|
|
1164
1411
|
const slat = new THREE.Group();
|
|
@@ -1171,6 +1418,7 @@ function addLouveredWindowVisuals(node, mesh) {
|
|
|
1171
1418
|
if (sill) {
|
|
1172
1419
|
const sillW = width + sillDepth * 0.4;
|
|
1173
1420
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1421
|
+
currentWindowSlot = 'frame';
|
|
1174
1422
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1175
1423
|
}
|
|
1176
1424
|
}
|
|
@@ -1187,9 +1435,11 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1187
1435
|
const innerW = innerRight - innerLeft;
|
|
1188
1436
|
const innerH = innerTop - innerBottom;
|
|
1189
1437
|
if (node.openingShape === 'arch') {
|
|
1438
|
+
currentWindowSlot = 'frame';
|
|
1190
1439
|
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
|
|
1191
1440
|
}
|
|
1192
1441
|
else {
|
|
1442
|
+
currentWindowSlot = 'frame';
|
|
1193
1443
|
addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
|
|
1194
1444
|
}
|
|
1195
1445
|
if (innerW > 0.01 && innerH > 0.01) {
|
|
@@ -1219,6 +1469,7 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1219
1469
|
return (y) => getRoundedHorizontalBoundsAtY(y, innerLeft, innerRight, innerTop, innerRadii);
|
|
1220
1470
|
})();
|
|
1221
1471
|
const addVerticalRail = (x) => {
|
|
1472
|
+
currentWindowSlot = 'frame';
|
|
1222
1473
|
const railX1 = x;
|
|
1223
1474
|
const railX2 = x + (x < 0 ? railThickness : -railThickness);
|
|
1224
1475
|
const sampleX = x < 0 ? Math.max(railX1, railX2) : Math.min(railX1, railX2);
|
|
@@ -1229,6 +1480,7 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1229
1480
|
};
|
|
1230
1481
|
addVerticalRail(innerLeft);
|
|
1231
1482
|
addVerticalRail(innerRight);
|
|
1483
|
+
currentWindowSlot = 'glass';
|
|
1232
1484
|
for (let index = 0; index < slatCount; index += 1) {
|
|
1233
1485
|
const y = innerTop - slatGap * (index + 0.5);
|
|
1234
1486
|
const topBounds = getBoundsAtY(Math.min(y + slatHeight / 2, innerTop));
|
|
@@ -1248,10 +1500,12 @@ function addShapedLouveredWindowVisuals(node, mesh) {
|
|
|
1248
1500
|
if (sill) {
|
|
1249
1501
|
const sillW = width + sillDepth * 0.4;
|
|
1250
1502
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1503
|
+
currentWindowSlot = 'frame';
|
|
1251
1504
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1252
1505
|
}
|
|
1253
1506
|
}
|
|
1254
1507
|
function updateWindowMesh(node, mesh) {
|
|
1508
|
+
currentWindowSlot = undefined;
|
|
1255
1509
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
1256
1510
|
mesh.geometry.dispose();
|
|
1257
1511
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
@@ -1266,6 +1520,11 @@ function updateWindowMesh(node, mesh) {
|
|
|
1266
1520
|
disposeObjectGeometry(child);
|
|
1267
1521
|
mesh.remove(child);
|
|
1268
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');
|
|
1269
1528
|
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, openingKind, openingShape, windowType, } = node;
|
|
1270
1529
|
if (openingKind === 'opening') {
|
|
1271
1530
|
syncWindowCutout(node, mesh);
|
|
@@ -1330,6 +1589,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1330
1589
|
const innerH = height - 2 * frameThickness;
|
|
1331
1590
|
// ── Frame members ──
|
|
1332
1591
|
// Top / bottom — full width
|
|
1592
|
+
currentWindowSlot = 'frame';
|
|
1333
1593
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1334
1594
|
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
1335
1595
|
// Left / right — inner height to avoid corner overlap
|
|
@@ -1364,6 +1624,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1364
1624
|
}
|
|
1365
1625
|
// Column dividers — full inner height
|
|
1366
1626
|
cx = -innerW / 2;
|
|
1627
|
+
currentWindowSlot = 'frame';
|
|
1367
1628
|
for (let c = 0; c < numCols - 1; c++) {
|
|
1368
1629
|
cx += colWidths[c];
|
|
1369
1630
|
addBox(mesh, baseMaterial, columnDividerThickness, innerH, frameDepth, cx + columnDividerThickness / 2, 0, 0);
|
|
@@ -1371,6 +1632,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1371
1632
|
}
|
|
1372
1633
|
// Row dividers — per column width, so they don't overlap column dividers (top to bottom)
|
|
1373
1634
|
cy = innerH / 2;
|
|
1635
|
+
currentWindowSlot = 'frame';
|
|
1374
1636
|
for (let r = 0; r < numRows - 1; r++) {
|
|
1375
1637
|
cy -= rowHeights[r];
|
|
1376
1638
|
const divY = cy - rowDividerThickness / 2;
|
|
@@ -1381,6 +1643,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1381
1643
|
}
|
|
1382
1644
|
// Glass panes
|
|
1383
1645
|
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
1646
|
+
currentWindowSlot = 'glass';
|
|
1384
1647
|
for (let c = 0; c < numCols; c++) {
|
|
1385
1648
|
for (let r = 0; r < numRows; r++) {
|
|
1386
1649
|
addBox(mesh, glassMaterial, colWidths[c], rowHeights[r], glassDepth, colXCenters[c], rowYCenters[r], 0);
|
|
@@ -1391,40 +1654,67 @@ function updateWindowMesh(node, mesh) {
|
|
|
1391
1654
|
const sillW = width + sillDepth * 0.4; // slightly wider than frame
|
|
1392
1655
|
// Protrudes from the front face of the frame (+Z)
|
|
1393
1656
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
1657
|
+
currentWindowSlot = 'frame';
|
|
1394
1658
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1395
1659
|
}
|
|
1396
1660
|
syncWindowCutout(node, mesh);
|
|
1397
1661
|
}
|
|
1398
1662
|
function syncWindowCutout(node, mesh) {
|
|
1399
|
-
// ── Cutout
|
|
1663
|
+
// ── Cutout: invisible raycast hit target for the whole opening ──
|
|
1400
1664
|
let cutout = mesh.getObjectByName('cutout');
|
|
1401
1665
|
if (!cutout) {
|
|
1402
1666
|
cutout = new THREE.Mesh();
|
|
1403
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).
|
|
1404
1674
|
mesh.add(cutout);
|
|
1405
1675
|
}
|
|
1406
1676
|
cutout.geometry.dispose();
|
|
1677
|
+
const depth = resolveOpeningCutoutProxyDepth(node);
|
|
1407
1678
|
if (isRectangleOnlyWindowType(node)) {
|
|
1408
|
-
cutout.geometry = new THREE.BoxGeometry(node.width, node.height,
|
|
1679
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, depth);
|
|
1409
1680
|
}
|
|
1410
1681
|
else if (node.openingShape === 'arch') {
|
|
1411
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)), {
|
|
1412
|
-
depth
|
|
1683
|
+
depth,
|
|
1413
1684
|
bevelEnabled: false,
|
|
1414
1685
|
curveSegments: 24,
|
|
1415
1686
|
});
|
|
1416
|
-
cutout.geometry.translate(0, 0, -
|
|
1687
|
+
cutout.geometry.translate(0, 0, -depth / 2);
|
|
1417
1688
|
}
|
|
1418
1689
|
else if (node.openingShape === 'rounded') {
|
|
1419
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)), {
|
|
1420
|
-
depth
|
|
1691
|
+
depth,
|
|
1421
1692
|
bevelEnabled: false,
|
|
1422
1693
|
curveSegments: 24,
|
|
1423
1694
|
});
|
|
1424
|
-
cutout.geometry.translate(0, 0, -
|
|
1695
|
+
cutout.geometry.translate(0, 0, -depth / 2);
|
|
1425
1696
|
}
|
|
1426
1697
|
else {
|
|
1427
|
-
cutout.geometry = new THREE.BoxGeometry(node.width, node.height,
|
|
1698
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, depth);
|
|
1428
1699
|
}
|
|
1429
1700
|
cutout.visible = false;
|
|
1430
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
|
+
}
|