@pascal-app/viewer 0.9.1 → 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 +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 +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 +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +104 -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 +179 -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 +18 -10
- package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
- 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 +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 +19 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -8
- 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/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 +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 +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +14 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +103 -26
- 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/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 +60 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +175 -3
- 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.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +4 -1
- 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/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 +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.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 +5 -3
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +41 -23
- package/dist/systems/slab/slab-system.test.js +3 -2
- package/dist/systems/stair/stair-system.js +6 -3
- 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 +46 -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 +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 +329 -174
- 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
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
import { clampDoorOperationState, DoorNode as DoorNodeSchema, getDoorRenderOpenAmount, getEffectiveNode, sceneRegistry, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { clampDoorOperationState, DEFAULT_WALL_THICKNESS, DoorNode as DoorNodeSchema, getDoorRenderOpenAmount, 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 { createDefaultMaterial, 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
|
const defaultRevealMaterial = new THREE.MeshBasicMaterial({ color: '#7f766c' });
|
|
12
|
+
// Door hardware (handle / hinges / closer / panic bar) renders a catalog metal
|
|
13
|
+
// finish by default (chrome), separate from the door body. The flat material is
|
|
14
|
+
// only a fallback if the catalog ref ever fails to resolve.
|
|
15
|
+
const HARDWARE_DEFAULT_REF = 'library:metal-chrome';
|
|
16
|
+
// Door body defaults to a catalog colour (generic approach). Glass keeps the
|
|
17
|
+
// built-in FrontSide glass material — the catalog `preset-glass` is DoubleSide,
|
|
18
|
+
// which poisons the WebGPU MRT scene pass.
|
|
19
|
+
const PANEL_DEFAULT_REF = 'library:preset-softwhite';
|
|
20
|
+
const FRAME_DEFAULT_REF = 'library:preset-softwhite';
|
|
21
|
+
const GLASS_DEFAULT_REF = 'library:preset-glass';
|
|
22
|
+
const defaultHardwareMaterial = createDefaultMaterial('#3a3a3a', 0.4);
|
|
10
23
|
let baseMaterial = getBaseMaterial();
|
|
24
|
+
let frameMaterial = getBaseMaterial();
|
|
11
25
|
let revealMaterial = defaultRevealMaterial;
|
|
12
26
|
let glassMaterial = defaultGlassMaterial;
|
|
27
|
+
let hardwareMaterial = defaultHardwareMaterial;
|
|
28
|
+
let currentDoorSlot;
|
|
29
|
+
// Per-frame viewer state, captured so the per-node mesh builder (which runs
|
|
30
|
+
// outside React) can resolve each door's slot materials.
|
|
31
|
+
let currentShading = 'rendered';
|
|
32
|
+
let currentTextures = true;
|
|
33
|
+
let currentColorPreset = 'clay';
|
|
34
|
+
let currentSceneMaterials;
|
|
13
35
|
const DOOR_RENDER_DEFAULTS = DoorNodeSchema.parse({ id: 'door_render_default' });
|
|
14
36
|
const MAX_DOOR_REBUILDS_PER_FRAME = 16;
|
|
15
37
|
const DOOR_PROGRESSIVE_DIRTY_THRESHOLD = MAX_DOOR_REBUILDS_PER_FRAME;
|
|
@@ -33,6 +55,7 @@ export const DoorSystem = () => {
|
|
|
33
55
|
const shading = useViewer((state) => state.shading);
|
|
34
56
|
const textures = useViewer((state) => state.textures);
|
|
35
57
|
const colorPreset = useViewer((state) => state.colorPreset);
|
|
58
|
+
const sceneMaterials = useScene((state) => state.materials);
|
|
36
59
|
const materialRevisionRef = useRef(null);
|
|
37
60
|
// Subscribe so an override-only update (no scene write) still re-runs
|
|
38
61
|
// the component, letting the gate below pick up the latest dirtyNodes
|
|
@@ -41,8 +64,10 @@ export const DoorSystem = () => {
|
|
|
41
64
|
useLiveNodeOverrides((s) => s.overrides);
|
|
42
65
|
const joineryMaterial = createSurfaceRoleMaterial('joinery', colorPreset);
|
|
43
66
|
baseMaterial = textures ? getBaseMaterial(shading) : joineryMaterial;
|
|
67
|
+
frameMaterial = textures ? getBaseMaterial(shading) : joineryMaterial;
|
|
44
68
|
revealMaterial = textures ? defaultRevealMaterial : joineryMaterial;
|
|
45
69
|
glassMaterial = textures ? defaultGlassMaterial : joineryMaterial;
|
|
70
|
+
hardwareMaterial = textures ? defaultHardwareMaterial : joineryMaterial;
|
|
46
71
|
useEffect(() => {
|
|
47
72
|
const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`;
|
|
48
73
|
if (materialRevisionRef.current === materialRevision)
|
|
@@ -55,18 +80,37 @@ export const DoorSystem = () => {
|
|
|
55
80
|
}
|
|
56
81
|
}
|
|
57
82
|
});
|
|
83
|
+
// Editing a scene material a door slot references must rebuild that door
|
|
84
|
+
// (door meshes are built by this system, not <GeometrySystem>).
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
void sceneMaterials;
|
|
87
|
+
const nodes = useScene.getState().nodes;
|
|
88
|
+
for (const node of Object.values(nodes)) {
|
|
89
|
+
if (node?.type !== 'door')
|
|
90
|
+
continue;
|
|
91
|
+
if (!nodeReferencesSceneMaterial(node))
|
|
92
|
+
continue;
|
|
93
|
+
useScene.getState().dirtyNodes.add(node.id);
|
|
94
|
+
}
|
|
95
|
+
}, [sceneMaterials]);
|
|
58
96
|
useFrame(() => {
|
|
59
97
|
if (dirtyNodes.size === 0)
|
|
60
98
|
return;
|
|
61
99
|
const frameJoineryMaterial = createSurfaceRoleMaterial('joinery', colorPreset);
|
|
62
100
|
baseMaterial = textures ? getBaseMaterial(shading) : frameJoineryMaterial;
|
|
101
|
+
frameMaterial = textures ? getBaseMaterial(shading) : frameJoineryMaterial;
|
|
63
102
|
revealMaterial = textures ? defaultRevealMaterial : frameJoineryMaterial;
|
|
64
103
|
glassMaterial = textures ? defaultGlassMaterial : frameJoineryMaterial;
|
|
104
|
+
hardwareMaterial = textures ? defaultHardwareMaterial : frameJoineryMaterial;
|
|
105
|
+
currentShading = shading;
|
|
106
|
+
currentTextures = textures;
|
|
107
|
+
currentColorPreset = colorPreset;
|
|
108
|
+
currentSceneMaterials = sceneMaterials;
|
|
65
109
|
const nodes = useScene.getState().nodes;
|
|
66
110
|
const dirtyDoorIds = [];
|
|
67
111
|
dirtyNodes.forEach((id) => {
|
|
68
112
|
const node = nodes[id];
|
|
69
|
-
if (
|
|
113
|
+
if (node?.type !== 'door')
|
|
70
114
|
return;
|
|
71
115
|
dirtyDoorIds.push(id);
|
|
72
116
|
});
|
|
@@ -84,7 +128,7 @@ export const DoorSystem = () => {
|
|
|
84
128
|
}
|
|
85
129
|
}
|
|
86
130
|
const node = nodes[id];
|
|
87
|
-
if (
|
|
131
|
+
if (node?.type !== 'door')
|
|
88
132
|
continue;
|
|
89
133
|
const mesh = sceneRegistry.nodes.get(id);
|
|
90
134
|
if (!mesh)
|
|
@@ -107,21 +151,77 @@ export const DoorSystem = () => {
|
|
|
107
151
|
}, 3);
|
|
108
152
|
return null;
|
|
109
153
|
};
|
|
154
|
+
function tagDoorSlot(mesh) {
|
|
155
|
+
mesh.userData.slotId = currentDoorSlot;
|
|
156
|
+
return mesh;
|
|
157
|
+
}
|
|
158
|
+
const NO_RAYCAST = () => { };
|
|
159
|
+
// An open door leaf swings perpendicular to the wall, so in a top-down view its
|
|
160
|
+
// flat panel blankets the room interior and wins the selection raycast over the
|
|
161
|
+
// slab/items beneath it. Drop the swung leaf out of the raycast so a click on
|
|
162
|
+
// the floor falls through to what's underneath; the door stays selectable via
|
|
163
|
+
// its proud invisible cutout proxy at the opening (see syncDoorCutout).
|
|
164
|
+
function disableSubtreeRaycast(object) {
|
|
165
|
+
object.traverse((child) => {
|
|
166
|
+
;
|
|
167
|
+
child.raycast = NO_RAYCAST;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function nodeReferencesSceneMaterial(node) {
|
|
171
|
+
const slots = node.slots;
|
|
172
|
+
if (!slots)
|
|
173
|
+
return false;
|
|
174
|
+
for (const ref of Object.values(slots)) {
|
|
175
|
+
if (typeof ref === 'string' && ref.startsWith('scene:'))
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
function doorSlotDefault(slotId) {
|
|
181
|
+
if (!currentTextures)
|
|
182
|
+
return createSurfaceRoleMaterial('joinery', currentColorPreset);
|
|
183
|
+
if (slotId === 'glass') {
|
|
184
|
+
return (resolveMaterialRef(GLASS_DEFAULT_REF, currentSceneMaterials, currentShading) ??
|
|
185
|
+
defaultGlassMaterial);
|
|
186
|
+
}
|
|
187
|
+
if (slotId === 'hardware') {
|
|
188
|
+
return (resolveMaterialRef(HARDWARE_DEFAULT_REF, currentSceneMaterials, currentShading) ??
|
|
189
|
+
defaultHardwareMaterial);
|
|
190
|
+
}
|
|
191
|
+
if (slotId === 'frame') {
|
|
192
|
+
return (resolveMaterialRef(FRAME_DEFAULT_REF, currentSceneMaterials, currentShading) ??
|
|
193
|
+
getBaseMaterial(currentShading));
|
|
194
|
+
}
|
|
195
|
+
return (resolveMaterialRef(PANEL_DEFAULT_REF, currentSceneMaterials, currentShading) ??
|
|
196
|
+
getBaseMaterial(currentShading));
|
|
197
|
+
}
|
|
198
|
+
// Resolve a door's slot to a material: the `node.slots` override (colored mode
|
|
199
|
+
// only) → the body/glass/hardware default. Textures-off ignores overrides — the
|
|
200
|
+
// monochrome escape hatch.
|
|
201
|
+
function resolveDoorSlotMaterial(node, slotId) {
|
|
202
|
+
const fallback = doorSlotDefault(slotId);
|
|
203
|
+
if (!currentTextures)
|
|
204
|
+
return fallback;
|
|
205
|
+
const ref = node.slots?.[slotId];
|
|
206
|
+
if (!ref)
|
|
207
|
+
return fallback;
|
|
208
|
+
return resolveMaterialRef(ref, currentSceneMaterials, currentShading) ?? fallback;
|
|
209
|
+
}
|
|
110
210
|
function addBox(parent, material, w, h, d, x, y, z) {
|
|
111
|
-
const
|
|
211
|
+
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
212
|
+
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
213
|
+
const m = new THREE.Mesh(geometry, material);
|
|
112
214
|
m.position.set(x, y, z);
|
|
215
|
+
tagDoorSlot(m);
|
|
113
216
|
parent.add(m);
|
|
114
217
|
}
|
|
115
218
|
function addRotatedBox(parent, material, w, h, d, x, y, z, rotationY) {
|
|
116
|
-
const
|
|
219
|
+
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
220
|
+
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
221
|
+
const m = new THREE.Mesh(geometry, material);
|
|
117
222
|
m.position.set(x, y, z);
|
|
118
223
|
m.rotation.y = rotationY;
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
function addBoxWithRotation(parent, material, w, h, d, x, y, z, rotation) {
|
|
122
|
-
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
|
|
123
|
-
m.position.set(x, y, z);
|
|
124
|
-
m.rotation.set(rotation[0], rotation[1], rotation[2]);
|
|
224
|
+
tagDoorSlot(m);
|
|
125
225
|
parent.add(m);
|
|
126
226
|
}
|
|
127
227
|
function addShape(parent, material, shape, depth) {
|
|
@@ -132,6 +232,7 @@ function addShape(parent, material, shape, depth) {
|
|
|
132
232
|
});
|
|
133
233
|
geometry.translate(0, 0, -depth / 2);
|
|
134
234
|
const mesh = new THREE.Mesh(geometry, material);
|
|
235
|
+
tagDoorSlot(mesh);
|
|
135
236
|
parent.add(mesh);
|
|
136
237
|
}
|
|
137
238
|
function addShapeAt(parent, material, shape, depth, x, y, z) {
|
|
@@ -142,6 +243,7 @@ function addShapeAt(parent, material, shape, depth, x, y, z) {
|
|
|
142
243
|
});
|
|
143
244
|
geometry.translate(x, y, z - depth / 2);
|
|
144
245
|
const mesh = new THREE.Mesh(geometry, material);
|
|
246
|
+
tagDoorSlot(mesh);
|
|
145
247
|
parent.add(mesh);
|
|
146
248
|
}
|
|
147
249
|
function getClampedArchHeight(width, height, archHeight) {
|
|
@@ -443,11 +545,13 @@ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight
|
|
|
443
545
|
const cpX = contentPadding[0];
|
|
444
546
|
const cpY = contentPadding[1];
|
|
445
547
|
if (renderPerimeterFrame && shouldRenderFrame && cpY > 0) {
|
|
548
|
+
currentDoorSlot = 'panel';
|
|
446
549
|
addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY + leafHeight / 2 - cpY / 2, 0);
|
|
447
550
|
addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY - leafHeight / 2 + cpY / 2, 0);
|
|
448
551
|
}
|
|
449
552
|
if (renderPerimeterFrame && shouldRenderFrame && cpX > 0) {
|
|
450
553
|
const innerH = leafHeight - 2 * cpY;
|
|
554
|
+
currentDoorSlot = 'panel';
|
|
451
555
|
addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX - leafWidth / 2 + cpX / 2, leafCenterY, 0);
|
|
452
556
|
addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX + leafWidth / 2 - cpX / 2, leafCenterY, 0);
|
|
453
557
|
}
|
|
@@ -496,6 +600,7 @@ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight
|
|
|
496
600
|
}
|
|
497
601
|
if (seg.type !== 'empty') {
|
|
498
602
|
cx = leafCenterX - contentW / 2;
|
|
603
|
+
currentDoorSlot = 'panel';
|
|
499
604
|
for (let c = 0; c < numCols - 1; c++) {
|
|
500
605
|
cx += colWidths[c];
|
|
501
606
|
const dividerLeft = cx;
|
|
@@ -516,6 +621,7 @@ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight
|
|
|
516
621
|
const colW = colWidths[c];
|
|
517
622
|
const colX = colXCenters[c];
|
|
518
623
|
if (seg.type === 'glass') {
|
|
624
|
+
currentDoorSlot = 'glass';
|
|
519
625
|
const glassDepth = Math.max(0.004, leafDepth * 0.15);
|
|
520
626
|
const segmentLeft = colX - colW / 2;
|
|
521
627
|
const segmentRight = colX + colW / 2;
|
|
@@ -530,6 +636,7 @@ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight
|
|
|
530
636
|
}
|
|
531
637
|
}
|
|
532
638
|
else if (seg.type === 'panel') {
|
|
639
|
+
currentDoorSlot = 'panel';
|
|
533
640
|
const segmentLeft = colX - colW / 2;
|
|
534
641
|
const segmentRight = colX + colW / 2;
|
|
535
642
|
const outerPanelShape = hasShapedTop && addLeafShape
|
|
@@ -565,11 +672,15 @@ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight
|
|
|
565
672
|
segY -= segH;
|
|
566
673
|
}
|
|
567
674
|
}
|
|
568
|
-
function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, hingeX, hingeSide, swingRotation, segments, contentPadding, handle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, doorHeight, openingShape, openingTopRadii, archHeight, roundedBoundary, archedBoundary, archOuterSide, }) {
|
|
675
|
+
function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, hingeX, hingeSide, swingRotation, openRotationY, segments, contentPadding, handle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, doorHeight, openingShape, openingTopRadii, archHeight, roundedBoundary, archedBoundary, archOuterSide, }) {
|
|
569
676
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
|
|
570
677
|
const leafGroup = new THREE.Group();
|
|
571
678
|
leafGroup.position.set(hingeX, 0, 0);
|
|
572
679
|
leafGroup.rotation.y = swingRotation;
|
|
680
|
+
// Marks this group as the swing leaf and records its fully-open angle so the
|
|
681
|
+
// GLB exporter can bake an open/close animation clip from a single pose. The
|
|
682
|
+
// exporter strips this marker before writing the file.
|
|
683
|
+
leafGroup.userData.pascalSwingLeaf = { axis: 'y', openRotationY };
|
|
573
684
|
mesh.add(leafGroup);
|
|
574
685
|
const addLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z);
|
|
575
686
|
const addLeafShape = (material, shape, depth) => addShapeAt(leafGroup, material, shape, depth, -hingeX, 0, 0);
|
|
@@ -578,6 +689,7 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
|
|
|
578
689
|
const leafTop = leafCenterY + leafHeight / 2;
|
|
579
690
|
const usesShapedLeafFrame = openingShape === 'rounded' || openingShape === 'arch';
|
|
580
691
|
if (usesShapedLeafFrame && hasLeafContent) {
|
|
692
|
+
currentDoorSlot = 'panel';
|
|
581
693
|
if (openingShape === 'rounded') {
|
|
582
694
|
const roundedLeafShape = roundedBoundary
|
|
583
695
|
? createRoundedClippedLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, roundedBoundary.fullLeft, roundedBoundary.fullRight, roundedBoundary.radii, contentPadding[0], contentPadding[1])
|
|
@@ -624,68 +736,217 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
|
|
|
624
736
|
archOuterSide,
|
|
625
737
|
});
|
|
626
738
|
if (hasLeafContent && handle) {
|
|
739
|
+
currentDoorSlot = 'hardware';
|
|
627
740
|
const handleY = handleHeight - doorHeight / 2;
|
|
628
741
|
const faceZ = leafDepth / 2;
|
|
629
742
|
const handleX = handleSide === 'right'
|
|
630
743
|
? leafCenterX + leafWidth / 2 - 0.045
|
|
631
744
|
: leafCenterX - leafWidth / 2 + 0.045;
|
|
632
|
-
addLeafBox(
|
|
633
|
-
addLeafBox(
|
|
745
|
+
addLeafBox(hardwareMaterial, 0.028, 0.14, 0.01, handleX, handleY, faceZ + 0.005);
|
|
746
|
+
addLeafBox(hardwareMaterial, 0.022, 0.1, 0.035, handleX, handleY, faceZ + 0.025);
|
|
634
747
|
if (handleBothSides) {
|
|
635
|
-
addLeafBox(
|
|
636
|
-
addLeafBox(
|
|
748
|
+
addLeafBox(hardwareMaterial, 0.028, 0.14, 0.01, handleX, handleY, -faceZ - 0.005);
|
|
749
|
+
addLeafBox(hardwareMaterial, 0.022, 0.1, 0.035, handleX, handleY, -faceZ - 0.025);
|
|
637
750
|
}
|
|
638
751
|
}
|
|
639
752
|
if (hasLeafContent && doorCloser) {
|
|
753
|
+
currentDoorSlot = 'hardware';
|
|
640
754
|
const closerY = leafCenterY + leafHeight / 2 - 0.04;
|
|
641
|
-
addLeafBox(
|
|
642
|
-
addLeafBox(
|
|
755
|
+
addLeafBox(hardwareMaterial, 0.28, 0.055, 0.055, leafCenterX, closerY, leafDepth / 2 + 0.03);
|
|
756
|
+
addLeafBox(hardwareMaterial, 0.14, 0.015, 0.015, leafCenterX + leafWidth / 4, closerY + 0.025, leafDepth / 2 + 0.015);
|
|
643
757
|
}
|
|
644
758
|
if (hasLeafContent && panicBar) {
|
|
759
|
+
currentDoorSlot = 'hardware';
|
|
645
760
|
const barY = panicBarHeight - doorHeight / 2;
|
|
646
|
-
addLeafBox(
|
|
761
|
+
addLeafBox(hardwareMaterial, leafWidth * 0.72, 0.04, 0.055, leafCenterX, barY, leafDepth / 2 + 0.03);
|
|
647
762
|
}
|
|
648
763
|
if (hasLeafContent) {
|
|
764
|
+
currentDoorSlot = 'hardware';
|
|
649
765
|
const hingeMarkerX = hingeSide === 'right' ? hingeX - 0.012 : hingeX + 0.012;
|
|
650
766
|
const hingeH = 0.1;
|
|
651
767
|
const hingeW = 0.024;
|
|
652
768
|
const hingeD = leafDepth + 0.016;
|
|
653
|
-
addBox(mesh,
|
|
654
|
-
addBox(mesh,
|
|
655
|
-
addBox(mesh,
|
|
769
|
+
addBox(mesh, hardwareMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafBottom + 0.25, 0);
|
|
770
|
+
addBox(mesh, hardwareMaterial, hingeW, hingeH, hingeD, hingeMarkerX, (leafBottom + leafTop) / 2, 0);
|
|
771
|
+
addBox(mesh, hardwareMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafTop - 0.25, 0);
|
|
772
|
+
}
|
|
773
|
+
// When the leaf is swung open it projects into the room and would otherwise
|
|
774
|
+
// win a top-down selection click over the floor beneath it. Drop only the
|
|
775
|
+
// swung leaf out of the raycast; a closed leaf stays in the wall plane and
|
|
776
|
+
// keeps its hit-eligibility (so paint-by-slot still works on it).
|
|
777
|
+
if (Math.abs(swingRotation) > 1e-3) {
|
|
778
|
+
disableSubtreeRaycast(leafGroup);
|
|
656
779
|
}
|
|
657
780
|
}
|
|
658
|
-
|
|
781
|
+
// Names of the re-poseable moving groups each operation-door builder emits. The
|
|
782
|
+
// builder fills them with geometry at the CLOSED pose; `poseDoorMovingParts`
|
|
783
|
+
// then transforms the group to a given open fraction. This is the same
|
|
784
|
+
// build-once + pose-at-t split windows use (`poseWindowMovingParts`), and it is
|
|
785
|
+
// the single source of truth for door kinematics: the live system poses the
|
|
786
|
+
// registered mesh, and the GLB exporter poses an export clone to sample the
|
|
787
|
+
// open/close keyframes for a baked animation clip.
|
|
788
|
+
const SLIDING_ACTIVE_PANEL_NAME = 'door-sliding-active';
|
|
789
|
+
const POCKET_LEAF_NAME = 'door-pocket-leaf';
|
|
790
|
+
const BARN_LEAF_NAME = 'door-barn-leaf';
|
|
791
|
+
const TILTUP_LEAF_NAME = 'door-tiltup-leaf';
|
|
792
|
+
// Folding panels form a hinged chain; each `<name><index>` group is parented to
|
|
793
|
+
// the previous one so a single per-joint rotation reproduces the accordion.
|
|
794
|
+
const FOLDING_PANEL_NAME = 'door-fold-';
|
|
795
|
+
// Sectional panels each ride the overhead curve independently (non-rigid as a
|
|
796
|
+
// set), so each `<name><index>` group is posed on its own.
|
|
797
|
+
const SECTIONAL_PANEL_NAME = 'door-sectional-';
|
|
798
|
+
const ROLLUP_CURTAIN_NAME = 'door-rollup-curtain';
|
|
799
|
+
/**
|
|
800
|
+
* Pose an operation door's moving parts (sliding/pocket/barn leaf, tilt-up and
|
|
801
|
+
* sectional panels, folding chain, roll-up curtain) at `value` (0 = closed,
|
|
802
|
+
* 1 = open) by transforming the named groups its builder emitted at the closed
|
|
803
|
+
* pose. Returns true when the door type has a pose path and its groups exist.
|
|
804
|
+
*
|
|
805
|
+
* Mirrors `poseWindowMovingParts`: the live door system calls it after a
|
|
806
|
+
* (re)build so `operationState` is reflected, and the GLB exporter calls it on a
|
|
807
|
+
* clone to sample keyframes. Swing doors are not handled here — their leaf group
|
|
808
|
+
* carries a `pascalSwingLeaf` marker the exporter reads directly.
|
|
809
|
+
*
|
|
810
|
+
* Roll-up is the one type whose live geometry changes (slats vanish onto a
|
|
811
|
+
* drum), which a glTF clip can't express; the baked approximation scales the
|
|
812
|
+
* curtain up into the lintel. The live roll-up animation keeps its full-detail
|
|
813
|
+
* rebuild and is intentionally NOT routed through this scale.
|
|
814
|
+
*/
|
|
815
|
+
export function poseDoorMovingParts(node, mesh, value) {
|
|
816
|
+
if (!mesh)
|
|
817
|
+
return false;
|
|
818
|
+
const t = clampDoorOperationState(value);
|
|
819
|
+
const frameThickness = node.frameThickness;
|
|
820
|
+
const insideWidth = node.width - 2 * frameThickness;
|
|
821
|
+
const leafHeight = node.height - frameThickness;
|
|
822
|
+
const leafCenterY = -frameThickness / 2;
|
|
823
|
+
switch (node.doorType) {
|
|
824
|
+
case 'sliding': {
|
|
825
|
+
const group = mesh.getObjectByName(SLIDING_ACTIVE_PANEL_NAME);
|
|
826
|
+
if (!group)
|
|
827
|
+
return false;
|
|
828
|
+
const activeSign = node.slideDirection === 'left' ? 1 : -1;
|
|
829
|
+
group.position.x = -activeSign * insideWidth * 0.44 * t;
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
case 'pocket': {
|
|
833
|
+
const group = mesh.getObjectByName(POCKET_LEAF_NAME);
|
|
834
|
+
if (!group)
|
|
835
|
+
return false;
|
|
836
|
+
const slideSign = node.slideDirection === 'right' ? 1 : -1;
|
|
837
|
+
group.position.x = slideSign * insideWidth * t;
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
case 'barn': {
|
|
841
|
+
const group = mesh.getObjectByName(BARN_LEAF_NAME);
|
|
842
|
+
if (!group)
|
|
843
|
+
return false;
|
|
844
|
+
const slideSign = node.slideDirection === 'right' ? 1 : -1;
|
|
845
|
+
group.position.x = slideSign * insideWidth * t;
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
case 'garage-tiltup': {
|
|
849
|
+
const group = mesh.getObjectByName(TILTUP_LEAF_NAME);
|
|
850
|
+
if (!group)
|
|
851
|
+
return false;
|
|
852
|
+
// Rigid hinge: the closed leaf hangs from the lintel; opening rotates it
|
|
853
|
+
// about the top edge. `position` keeps the top edge tracking the hinge as
|
|
854
|
+
// the group rotates about its own origin (see the closed build below).
|
|
855
|
+
const angle = (Math.PI / 2) * t;
|
|
856
|
+
const hingeY = leafCenterY + leafHeight / 2;
|
|
857
|
+
group.rotation.set(-angle, 0, 0);
|
|
858
|
+
group.position.set(0, hingeY * (1 - Math.cos(angle)), Math.sin(angle) * (hingeY - leafHeight));
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
case 'folding': {
|
|
862
|
+
const panelCount = node.leafCount === 2 ? 2 : 4;
|
|
863
|
+
const foldAngle = Math.PI * 0.44 * t;
|
|
864
|
+
// Each panel group is parented to the previous, so its rotation is the
|
|
865
|
+
// joint angle (the change in absolute segment direction), not the absolute
|
|
866
|
+
// angle: alternating panels target ∓foldAngle, so joints fold by ±2·angle.
|
|
867
|
+
// The leading sign folds the leaves toward −z (matching the original rig).
|
|
868
|
+
let posed = false;
|
|
869
|
+
let prevDirection = 0;
|
|
870
|
+
for (let index = 0; index < panelCount; index++) {
|
|
871
|
+
const group = mesh.getObjectByName(`${FOLDING_PANEL_NAME}${index}`);
|
|
872
|
+
const direction = index % 2 === 0 ? -1 : 1;
|
|
873
|
+
if (group) {
|
|
874
|
+
posed = true;
|
|
875
|
+
// Set the full triple (not just `.y`): when the export clones and
|
|
876
|
+
// decomposes the door matrix, a |Y| > π/2 rotation re-derives into a
|
|
877
|
+
// gimbal-flipped euler (x=z=π). Assigning only `.y` would leave that
|
|
878
|
+
// π residue on x/z and bake a flipped rest pose.
|
|
879
|
+
group.rotation.set(0, (prevDirection - direction) * foldAngle, 0);
|
|
880
|
+
}
|
|
881
|
+
prevDirection = direction;
|
|
882
|
+
}
|
|
883
|
+
return posed;
|
|
884
|
+
}
|
|
885
|
+
case 'garage-sectional': {
|
|
886
|
+
const panelCount = Math.max(3, Math.min(12, Math.round(node.garagePanelCount)));
|
|
887
|
+
const panelHeight = leafHeight / panelCount;
|
|
888
|
+
const curveRadius = panelHeight * 0.58;
|
|
889
|
+
const curveLength = (Math.PI / 2) * curveRadius;
|
|
890
|
+
const overheadY = leafCenterY + leafHeight / 2 - panelHeight / 2;
|
|
891
|
+
const openAmount = getDoorRenderOpenAmount('garage-sectional', t);
|
|
892
|
+
const travel = openAmount * ((panelCount - 1) * panelHeight + curveLength + panelHeight * 0.65);
|
|
893
|
+
let posed = false;
|
|
894
|
+
for (let index = 0; index < panelCount; index++) {
|
|
895
|
+
const group = mesh.getObjectByName(`${SECTIONAL_PANEL_NAME}${index}`);
|
|
896
|
+
if (!group)
|
|
897
|
+
continue;
|
|
898
|
+
posed = true;
|
|
899
|
+
const orderFromTop = panelCount - 1 - index;
|
|
900
|
+
const pathPosition = travel - orderFromTop * panelHeight;
|
|
901
|
+
let y = overheadY + pathPosition;
|
|
902
|
+
let z = 0;
|
|
903
|
+
let rotationX = 0;
|
|
904
|
+
if (pathPosition > 0 && pathPosition <= curveLength) {
|
|
905
|
+
const theta = pathPosition / curveRadius;
|
|
906
|
+
rotationX = -theta;
|
|
907
|
+
y = overheadY + curveRadius * Math.sin(theta);
|
|
908
|
+
z = -curveRadius * (1 - Math.cos(theta));
|
|
909
|
+
}
|
|
910
|
+
else if (pathPosition > curveLength) {
|
|
911
|
+
rotationX = -Math.PI / 2;
|
|
912
|
+
y = overheadY + curveRadius;
|
|
913
|
+
z = -(curveRadius + pathPosition - curveLength);
|
|
914
|
+
}
|
|
915
|
+
group.position.set(0, y, z);
|
|
916
|
+
group.rotation.set(rotationX, 0, 0);
|
|
917
|
+
}
|
|
918
|
+
return posed;
|
|
919
|
+
}
|
|
920
|
+
case 'garage-rollup': {
|
|
921
|
+
const group = mesh.getObjectByName(ROLLUP_CURTAIN_NAME);
|
|
922
|
+
if (!group)
|
|
923
|
+
return false;
|
|
924
|
+
group.scale.y = Math.max(0.02, 1 - t);
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
default:
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, leafCount, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
659
932
|
const panelCount = leafCount === 2 ? 2 : 4;
|
|
660
|
-
const foldAmount = clampDoorOperationState(operationState);
|
|
661
933
|
const panelLength = insideWidth / panelCount;
|
|
662
|
-
|
|
663
|
-
addBox(mesh,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
vertices.push({
|
|
670
|
-
x: previous.x + panelLength * Math.cos(angle),
|
|
671
|
-
z: previous.z + panelLength * Math.sin(angle),
|
|
672
|
-
});
|
|
673
|
-
}
|
|
934
|
+
currentDoorSlot = 'hardware';
|
|
935
|
+
addBox(mesh, hardwareMaterial, insideWidth, Math.min(frameThickness * 0.5, 0.025), Math.max(frameDepth * 0.45, 0.035), 0, leafCenterY + leafHeight / 2 - 0.018, 0);
|
|
936
|
+
// Build the panels as a hinged chain at the CLOSED pose: each panel group is
|
|
937
|
+
// parented to the previous one at that panel's end (`panelLength` along local
|
|
938
|
+
// x), starting from the left jamb. At rest every joint is straight, so the
|
|
939
|
+
// panels lie flat across the opening; `poseDoorMovingParts` folds the joints.
|
|
940
|
+
let parent = mesh;
|
|
674
941
|
for (let index = 0; index < panelCount; index++) {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
x: Math.cos(rotationY),
|
|
684
|
-
z: -Math.sin(rotationY),
|
|
685
|
-
};
|
|
686
|
-
const addFoldingLeafBox = (material, w, h, d, x, y, z) => {
|
|
687
|
-
addRotatedBox(mesh, material, w, h, d, centerX + localX.x * x + Math.sin(rotationY) * z, y, centerZ + localX.z * x + Math.cos(rotationY) * z, rotationY);
|
|
688
|
-
};
|
|
942
|
+
const group = new THREE.Group();
|
|
943
|
+
group.name = `${FOLDING_PANEL_NAME}${index}`;
|
|
944
|
+
group.position.set(index === 0 ? -insideWidth / 2 : panelLength, 0, 0);
|
|
945
|
+
parent.add(group);
|
|
946
|
+
// Panel content runs from local x=0 to x=panelLength, so it is centred at
|
|
947
|
+
// panelLength/2 within the group (the segment helper centres on leafCenterX).
|
|
948
|
+
currentDoorSlot = undefined;
|
|
949
|
+
const addFoldingLeafBox = (material, w, h, d, x, y, z) => addBox(group, material, w, h, d, panelLength / 2 + x, y, z);
|
|
689
950
|
addLeafSegmentContent({
|
|
690
951
|
addLeafBox: addFoldingLeafBox,
|
|
691
952
|
leafWidth: Math.max(0.08, panelLength),
|
|
@@ -697,79 +958,96 @@ function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
697
958
|
contentPadding,
|
|
698
959
|
keepFrameWhenEmpty: true,
|
|
699
960
|
});
|
|
700
|
-
|
|
701
|
-
|
|
961
|
+
// Reveal posts at the panel's hinge edges (local x=0 and x=panelLength).
|
|
962
|
+
currentDoorSlot = undefined;
|
|
963
|
+
for (const px of [0, panelLength]) {
|
|
964
|
+
addBox(group, revealMaterial, 0.018, leafHeight * 0.92, leafDepth + 0.016, px, leafCenterY, 0);
|
|
702
965
|
}
|
|
966
|
+
parent = group;
|
|
703
967
|
}
|
|
704
|
-
|
|
968
|
+
// Handle on the free end of the last panel (its local x=panelLength edge).
|
|
705
969
|
const handleY = handleHeight - doorHeight / 2;
|
|
706
|
-
|
|
707
|
-
addBox(
|
|
970
|
+
currentDoorSlot = 'hardware';
|
|
971
|
+
addBox(parent, hardwareMaterial, 0.035, 0.16, leafDepth + 0.035, panelLength - 0.035, handleY, 0.045);
|
|
972
|
+
addBox(parent, hardwareMaterial, 0.035, 0.16, leafDepth + 0.035, panelLength - 0.035, handleY, -0.045);
|
|
708
973
|
}
|
|
709
|
-
function addPocketDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
710
|
-
const openAmount = clampDoorOperationState(operationState);
|
|
974
|
+
function addPocketDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
711
975
|
const slideSign = slideDirection === 'right' ? 1 : -1;
|
|
712
976
|
const leafWidth = insideWidth;
|
|
713
|
-
const leafCenterX = slideSign * insideWidth * openAmount;
|
|
714
977
|
const topY = leafCenterY + leafHeight / 2;
|
|
715
978
|
const pocketCenterX = slideSign * insideWidth;
|
|
716
979
|
const handleY = handleHeight - doorHeight / 2;
|
|
717
|
-
|
|
718
|
-
|
|
980
|
+
// Leaf built closed (centred in the opening); `poseDoorMovingParts` slides the
|
|
981
|
+
// group into the pocket. Handle rides inside the group at its closed offset.
|
|
982
|
+
const handleX = -slideSign * (leafWidth / 2 - 0.055);
|
|
983
|
+
currentDoorSlot = 'hardware';
|
|
984
|
+
addBox(mesh, hardwareMaterial, insideWidth * 2, Math.min(frameThickness * 0.45, 0.024), Math.max(frameDepth * 0.38, 0.03), slideSign * (insideWidth / 2), topY - 0.018, 0);
|
|
985
|
+
currentDoorSlot = undefined;
|
|
719
986
|
addBox(mesh, revealMaterial, insideWidth * 0.9, 0.018, Math.max(frameDepth * 0.32, 0.026), pocketCenterX, topY - 0.055, 0);
|
|
720
987
|
addBox(mesh, revealMaterial, 0.018, leafHeight * 0.94, leafDepth + 0.014, slideSign * insideWidth * 0.5, leafCenterY, 0);
|
|
721
|
-
const
|
|
988
|
+
const leafGroup = new THREE.Group();
|
|
989
|
+
leafGroup.name = POCKET_LEAF_NAME;
|
|
990
|
+
mesh.add(leafGroup);
|
|
991
|
+
const addPocketLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x, y, z);
|
|
722
992
|
addLeafSegmentContent({
|
|
723
993
|
addLeafBox: addPocketLeafBox,
|
|
724
994
|
leafWidth,
|
|
725
995
|
leafHeight,
|
|
726
|
-
leafCenterX,
|
|
996
|
+
leafCenterX: 0,
|
|
727
997
|
leafCenterY,
|
|
728
998
|
leafDepth,
|
|
729
999
|
segments,
|
|
730
1000
|
contentPadding,
|
|
731
1001
|
});
|
|
732
|
-
|
|
733
|
-
addBox(
|
|
1002
|
+
currentDoorSlot = 'hardware';
|
|
1003
|
+
addBox(leafGroup, hardwareMaterial, 0.03, 0.18, leafDepth + 0.03, handleX, handleY, leafDepth / 2 + 0.02);
|
|
1004
|
+
addBox(leafGroup, hardwareMaterial, 0.03, 0.18, leafDepth + 0.03, handleX, handleY, -leafDepth / 2 - 0.02);
|
|
734
1005
|
}
|
|
735
|
-
function addBarnDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
736
|
-
const openAmount = clampDoorOperationState(operationState);
|
|
1006
|
+
function addBarnDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
737
1007
|
const slideSign = slideDirection === 'right' ? 1 : -1;
|
|
738
1008
|
const leafWidth = insideWidth * 1.06;
|
|
739
|
-
const leafCenterX = slideSign * insideWidth * openAmount;
|
|
740
1009
|
const faceZ = frameDepth / 2 + leafDepth / 2 + 0.028;
|
|
741
1010
|
const trackY = leafCenterY + leafHeight / 2 + Math.max(frameThickness * 0.55, 0.045);
|
|
742
1011
|
const railLength = insideWidth * 2.25;
|
|
743
1012
|
const railCenterX = slideSign * (insideWidth * 0.56);
|
|
744
1013
|
const handleY = handleHeight - doorHeight / 2;
|
|
745
|
-
|
|
1014
|
+
// Leaf + wheels + handle ride the BARN_LEAF group, built closed (leaf centred
|
|
1015
|
+
// in the opening); `poseDoorMovingParts` slides the group along the rail. The
|
|
1016
|
+
// rail and end stops stay static on the mesh.
|
|
1017
|
+
const handleX = -slideSign * (leafWidth / 2 - 0.075);
|
|
746
1018
|
const wheelY = trackY - 0.075;
|
|
747
|
-
|
|
748
|
-
addBox(mesh,
|
|
749
|
-
addBox(mesh,
|
|
750
|
-
|
|
1019
|
+
currentDoorSlot = 'hardware';
|
|
1020
|
+
addBox(mesh, hardwareMaterial, railLength, 0.035, 0.035, railCenterX, trackY, faceZ + 0.01);
|
|
1021
|
+
addBox(mesh, hardwareMaterial, 0.05, 0.13, 0.035, -insideWidth / 2, trackY - 0.02, faceZ + 0.01);
|
|
1022
|
+
addBox(mesh, hardwareMaterial, 0.05, 0.13, 0.035, insideWidth / 2, trackY - 0.02, faceZ + 0.01);
|
|
1023
|
+
const leafGroup = new THREE.Group();
|
|
1024
|
+
leafGroup.name = BARN_LEAF_NAME;
|
|
1025
|
+
mesh.add(leafGroup);
|
|
1026
|
+
const addBarnLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x, y, faceZ + z);
|
|
751
1027
|
addLeafSegmentContent({
|
|
752
1028
|
addLeafBox: addBarnLeafBox,
|
|
753
1029
|
leafWidth,
|
|
754
1030
|
leafHeight,
|
|
755
|
-
leafCenterX,
|
|
1031
|
+
leafCenterX: 0,
|
|
756
1032
|
leafCenterY,
|
|
757
1033
|
leafDepth,
|
|
758
1034
|
segments,
|
|
759
1035
|
contentPadding,
|
|
760
1036
|
keepFrameWhenEmpty: true,
|
|
761
1037
|
});
|
|
762
|
-
|
|
763
|
-
addRotatedBox(
|
|
1038
|
+
currentDoorSlot = undefined;
|
|
1039
|
+
addRotatedBox(leafGroup, revealMaterial, 0.018, leafHeight * 0.86, 0.012, 0, leafCenterY, faceZ + leafDepth / 2 + 0.014, -0.52);
|
|
1040
|
+
addRotatedBox(leafGroup, revealMaterial, 0.018, leafHeight * 0.86, 0.012, 0, leafCenterY, faceZ + leafDepth / 2 + 0.014, 0.52);
|
|
1041
|
+
currentDoorSlot = 'hardware';
|
|
764
1042
|
for (const offset of [-leafWidth * 0.28, leafWidth * 0.28]) {
|
|
765
|
-
addBox(
|
|
766
|
-
addBox(
|
|
1043
|
+
addBox(leafGroup, hardwareMaterial, 0.085, 0.085, 0.035, offset, wheelY, faceZ + 0.022);
|
|
1044
|
+
addBox(leafGroup, hardwareMaterial, 0.026, 0.16, 0.026, offset, wheelY - 0.075, faceZ + 0.022);
|
|
767
1045
|
}
|
|
768
|
-
|
|
769
|
-
addBox(
|
|
1046
|
+
currentDoorSlot = 'hardware';
|
|
1047
|
+
addBox(leafGroup, hardwareMaterial, 0.032, 0.22, leafDepth + 0.034, handleX, handleY, faceZ + leafDepth / 2 + 0.02);
|
|
1048
|
+
addBox(leafGroup, hardwareMaterial, 0.032, 0.22, leafDepth + 0.034, handleX, handleY, faceZ - leafDepth / 2 - 0.02);
|
|
770
1049
|
}
|
|
771
|
-
function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
772
|
-
const openAmount = clampDoorOperationState(operationState);
|
|
1050
|
+
function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
773
1051
|
const activeOnRight = slideDirection === 'left';
|
|
774
1052
|
const fixedSign = activeOnRight ? -1 : 1;
|
|
775
1053
|
const activeSign = activeOnRight ? 1 : -1;
|
|
@@ -777,16 +1055,22 @@ function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
777
1055
|
const panelHeight = leafHeight;
|
|
778
1056
|
const closedActiveX = activeSign * insideWidth * 0.23;
|
|
779
1057
|
const fixedX = fixedSign * insideWidth * 0.23;
|
|
780
|
-
const activeX = closedActiveX - activeSign * insideWidth * 0.44 * openAmount;
|
|
781
1058
|
const frontZ = leafDepth / 2 + 0.016;
|
|
782
1059
|
const backZ = -leafDepth / 2 - 0.006;
|
|
783
1060
|
const railY = leafCenterY + panelHeight / 2 - Math.min(frameThickness * 0.35, 0.02);
|
|
784
1061
|
const handleY = handleHeight - doorHeight / 2;
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1062
|
+
// Active panel + handle ride the SLIDING_ACTIVE_PANEL group, built at the
|
|
1063
|
+
// closed position; `poseDoorMovingParts` slides the group behind the fixed
|
|
1064
|
+
// panel. The fixed panel and rails stay static on the mesh.
|
|
1065
|
+
const handleX = closedActiveX + activeSign * (panelWidth / 2 - 0.06);
|
|
1066
|
+
currentDoorSlot = 'hardware';
|
|
1067
|
+
addBox(mesh, hardwareMaterial, insideWidth, 0.024, Math.max(frameDepth * 0.32, 0.026), 0, railY, 0);
|
|
1068
|
+
addBox(mesh, hardwareMaterial, insideWidth, 0.018, Math.max(frameDepth * 0.28, 0.022), 0, -leafHeight / 2 + 0.04, 0);
|
|
788
1069
|
const addFixedPanelBox = (material, w, h, d, x, y, z) => addBox(mesh, material, w, h, d, x + fixedX, y, z + backZ);
|
|
789
|
-
const
|
|
1070
|
+
const activePanelGroup = new THREE.Group();
|
|
1071
|
+
activePanelGroup.name = SLIDING_ACTIVE_PANEL_NAME;
|
|
1072
|
+
mesh.add(activePanelGroup);
|
|
1073
|
+
const addActivePanelBox = (material, w, h, d, x, y, z) => addBox(activePanelGroup, material, w, h, d, x + closedActiveX, y, z + frontZ);
|
|
790
1074
|
addLeafSegmentContent({
|
|
791
1075
|
addLeafBox: addFixedPanelBox,
|
|
792
1076
|
leafWidth: panelWidth,
|
|
@@ -809,51 +1093,39 @@ function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
809
1093
|
contentPadding,
|
|
810
1094
|
keepFrameWhenEmpty: true,
|
|
811
1095
|
});
|
|
812
|
-
|
|
813
|
-
addBox(
|
|
1096
|
+
currentDoorSlot = 'hardware';
|
|
1097
|
+
addBox(activePanelGroup, hardwareMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ + leafDepth / 2 + 0.01);
|
|
1098
|
+
addBox(activePanelGroup, hardwareMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ - leafDepth / 2 - 0.01);
|
|
814
1099
|
}
|
|
815
|
-
function addGarageSectionalDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
816
|
-
const openAmount = getDoorRenderOpenAmount('garage-sectional', operationState);
|
|
1100
|
+
function addGarageSectionalDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, garagePanelCount, }) {
|
|
817
1101
|
const panelCount = Math.max(3, Math.min(12, Math.round(garagePanelCount)));
|
|
818
1102
|
const panelHeight = leafHeight / panelCount;
|
|
819
1103
|
const panelGap = Math.min(0.012, panelHeight * 0.08);
|
|
820
1104
|
const travelDepth = Math.max(leafHeight, 1.4);
|
|
821
|
-
const curveRadius = panelHeight * 0.58;
|
|
822
|
-
const curveLength = (Math.PI / 2) * curveRadius;
|
|
823
|
-
const travel = openAmount * ((panelCount - 1) * panelHeight + curveLength + panelHeight * 0.65);
|
|
824
|
-
const overheadY = leafCenterY + leafHeight / 2 - panelHeight / 2;
|
|
825
1105
|
const railY = leafCenterY + leafHeight / 2 - 0.04;
|
|
826
1106
|
const railZ = -travelDepth / 2;
|
|
827
|
-
|
|
828
|
-
addBox(mesh,
|
|
1107
|
+
currentDoorSlot = 'hardware';
|
|
1108
|
+
addBox(mesh, hardwareMaterial, 0.035, Math.max(0.04, frameThickness * 0.75), travelDepth, -insideWidth / 2 + 0.035, railY, railZ);
|
|
1109
|
+
addBox(mesh, hardwareMaterial, 0.035, Math.max(0.04, frameThickness * 0.75), travelDepth, insideWidth / 2 - 0.035, railY, railZ);
|
|
1110
|
+
// Each panel is built flat (centred at its group origin) and the trims sit on
|
|
1111
|
+
// the panel's front face in local space; `poseDoorMovingParts` rides each
|
|
1112
|
+
// group along the overhead curve. Panel order, geometry and trim placement
|
|
1113
|
+
// match the inline curve math (which the pose function reuses).
|
|
1114
|
+
const revealOffset = (panelHeight - panelGap) * 0.22;
|
|
1115
|
+
const trimDepth = 0.01;
|
|
1116
|
+
const trimFaceOffset = leafDepth / 2 + trimDepth + 0.006;
|
|
829
1117
|
for (let index = 0; index < panelCount; index++) {
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
y = overheadY + curveRadius * Math.sin(theta);
|
|
839
|
-
z = -curveRadius * (1 - Math.cos(theta));
|
|
840
|
-
}
|
|
841
|
-
else if (pathPosition > curveLength) {
|
|
842
|
-
rotationX = -Math.PI / 2;
|
|
843
|
-
y = overheadY + curveRadius;
|
|
844
|
-
z = -(curveRadius + pathPosition - curveLength);
|
|
845
|
-
}
|
|
846
|
-
const revealOffset = (panelHeight - panelGap) * 0.22;
|
|
847
|
-
const trimDepth = 0.01;
|
|
848
|
-
const trimFaceOffset = leafDepth / 2 + trimDepth + 0.006;
|
|
849
|
-
const addSectionalTrim = (localY) => {
|
|
850
|
-
addBoxWithRotation(mesh, revealMaterial, insideWidth - 0.16, 0.012, trimDepth, 0, y + localY * Math.cos(rotationX) - trimFaceOffset * Math.sin(rotationX), z + localY * Math.sin(rotationX) + trimFaceOffset * Math.cos(rotationX), [rotationX, 0, 0]);
|
|
851
|
-
};
|
|
852
|
-
addBoxWithRotation(mesh, baseMaterial, insideWidth, Math.max(0.04, panelHeight - panelGap), leafDepth, 0, y, z, [rotationX, 0, 0]);
|
|
853
|
-
addSectionalTrim(revealOffset);
|
|
854
|
-
addSectionalTrim(-revealOffset);
|
|
1118
|
+
const group = new THREE.Group();
|
|
1119
|
+
group.name = `${SECTIONAL_PANEL_NAME}${index}`;
|
|
1120
|
+
mesh.add(group);
|
|
1121
|
+
currentDoorSlot = 'panel';
|
|
1122
|
+
addBox(group, baseMaterial, insideWidth, Math.max(0.04, panelHeight - panelGap), leafDepth, 0, 0, 0);
|
|
1123
|
+
currentDoorSlot = undefined;
|
|
1124
|
+
addBox(group, revealMaterial, insideWidth - 0.16, 0.012, trimDepth, 0, revealOffset, trimFaceOffset);
|
|
1125
|
+
addBox(group, revealMaterial, insideWidth - 0.16, 0.012, trimDepth, 0, -revealOffset, trimFaceOffset);
|
|
855
1126
|
}
|
|
856
|
-
|
|
1127
|
+
currentDoorSlot = 'hardware';
|
|
1128
|
+
addBox(mesh, hardwareMaterial, insideWidth, 0.032, Math.max(frameDepth * 0.36, 0.03), 0, railY, 0);
|
|
857
1129
|
}
|
|
858
1130
|
function addGarageRollupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, }) {
|
|
859
1131
|
const openAmount = clampDoorOperationState(operationState);
|
|
@@ -861,50 +1133,71 @@ function addGarageRollupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafD
|
|
|
861
1133
|
const visibleHeight = leafHeight * (1 - openAmount);
|
|
862
1134
|
const visibleSlatCount = Math.ceil(visibleHeight / slatHeight);
|
|
863
1135
|
const topY = leafCenterY + leafHeight / 2;
|
|
864
|
-
const curtainCenterY = topY - visibleHeight / 2;
|
|
865
1136
|
const drumMaxRadius = Math.max(0.12, Math.min(0.22, leafHeight * 0.075));
|
|
866
1137
|
const drumY = topY + drumMaxRadius * 0.12;
|
|
867
1138
|
const drumZ = -frameDepth / 2 - drumMaxRadius * 0.72;
|
|
868
|
-
|
|
869
|
-
addBox(mesh,
|
|
1139
|
+
currentDoorSlot = 'hardware';
|
|
1140
|
+
addBox(mesh, hardwareMaterial, 0.032, leafHeight, Math.max(frameDepth * 0.48, 0.035), -insideWidth / 2 + 0.03, leafCenterY, 0);
|
|
1141
|
+
addBox(mesh, hardwareMaterial, 0.032, leafHeight, Math.max(frameDepth * 0.48, 0.035), insideWidth / 2 - 0.03, leafCenterY, 0);
|
|
870
1142
|
if (visibleHeight > 0.01) {
|
|
871
|
-
|
|
1143
|
+
// Wrap the visible curtain in a group pivoted at the lintel (topY). The live
|
|
1144
|
+
// door keeps rebuilding the full slat detail per `operationState` (this group
|
|
1145
|
+
// is just an organisational wrapper at scale 1 — world positions unchanged),
|
|
1146
|
+
// but it gives the GLB exporter a single node to scale up into the header as
|
|
1147
|
+
// an open clip, since the slats can't literally vanish in a glTF animation.
|
|
1148
|
+
const curtain = new THREE.Group();
|
|
1149
|
+
curtain.name = ROLLUP_CURTAIN_NAME;
|
|
1150
|
+
curtain.position.set(0, topY, 0);
|
|
1151
|
+
mesh.add(curtain);
|
|
1152
|
+
currentDoorSlot = 'panel';
|
|
1153
|
+
addBox(curtain, baseMaterial, insideWidth, visibleHeight, leafDepth, 0, -visibleHeight / 2, 0);
|
|
1154
|
+
currentDoorSlot = undefined;
|
|
872
1155
|
for (let index = 0; index < visibleSlatCount; index++) {
|
|
873
|
-
const y =
|
|
874
|
-
addBox(
|
|
1156
|
+
const y = -Math.min(visibleHeight, index * slatHeight);
|
|
1157
|
+
addBox(curtain, revealMaterial, insideWidth - 0.08, 0.01, 0.012, 0, y, leafDepth / 2 + 0.012);
|
|
875
1158
|
}
|
|
876
|
-
|
|
1159
|
+
const bottomBarHeight = 0.028;
|
|
1160
|
+
addBox(curtain, revealMaterial, insideWidth - 0.04, bottomBarHeight, leafDepth + 0.018, 0, -visibleHeight + bottomBarHeight / 2, leafDepth / 2 + 0.004);
|
|
877
1161
|
}
|
|
1162
|
+
currentDoorSlot = 'panel';
|
|
878
1163
|
const drum = new THREE.Mesh(new THREE.CylinderGeometry(drumMaxRadius, drumMaxRadius, insideWidth + frameThickness, 36), baseMaterial);
|
|
879
1164
|
drum.position.set(0, drumY, drumZ);
|
|
880
1165
|
drum.rotation.z = Math.PI / 2;
|
|
1166
|
+
tagDoorSlot(drum);
|
|
881
1167
|
mesh.add(drum);
|
|
882
|
-
|
|
1168
|
+
currentDoorSlot = 'hardware';
|
|
1169
|
+
addBox(mesh, hardwareMaterial, insideWidth + frameThickness, 0.026, Math.max(frameDepth * 0.52, 0.04), 0, topY + 0.02, 0);
|
|
883
1170
|
}
|
|
884
|
-
function addGarageTiltupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
885
|
-
const openAmount = clampDoorOperationState(operationState);
|
|
886
|
-
const angle = (Math.PI / 2) * openAmount;
|
|
1171
|
+
function addGarageTiltupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, }) {
|
|
887
1172
|
const hingeY = leafCenterY + leafHeight / 2;
|
|
888
|
-
|
|
889
|
-
|
|
1173
|
+
// Leaf built closed (hanging from the lintel); `poseDoorMovingParts` rotates
|
|
1174
|
+
// the group about the top hinge to open it. Rails + top bar stay static.
|
|
1175
|
+
const panelCenterY = hingeY - leafHeight / 2;
|
|
890
1176
|
const railLength = Math.max(leafHeight * 0.72, 1.2);
|
|
891
1177
|
const railY = hingeY - frameThickness * 0.35;
|
|
892
1178
|
const railZ = -railLength / 2;
|
|
893
|
-
|
|
894
|
-
addBox(mesh,
|
|
895
|
-
|
|
1179
|
+
currentDoorSlot = 'hardware';
|
|
1180
|
+
addBox(mesh, hardwareMaterial, 0.03, Math.max(frameThickness * 0.7, 0.035), railLength, -insideWidth / 2 + 0.04, railY, railZ);
|
|
1181
|
+
addBox(mesh, hardwareMaterial, 0.03, Math.max(frameThickness * 0.7, 0.035), railLength, insideWidth / 2 - 0.04, railY, railZ);
|
|
1182
|
+
const leafGroup = new THREE.Group();
|
|
1183
|
+
leafGroup.name = TILTUP_LEAF_NAME;
|
|
1184
|
+
mesh.add(leafGroup);
|
|
1185
|
+
currentDoorSlot = 'panel';
|
|
1186
|
+
addBox(leafGroup, baseMaterial, insideWidth, leafHeight, leafDepth, 0, panelCenterY, 0);
|
|
896
1187
|
const insetWidth = Math.max(0.1, insideWidth - 0.22);
|
|
897
1188
|
const insetHeight = Math.max(0.1, leafHeight - 0.28);
|
|
898
1189
|
const trimDepth = 0.012;
|
|
899
1190
|
const trimFaceOffset = leafDepth / 2 + trimDepth + 0.006;
|
|
900
1191
|
const addTiltupTrim = (localX, localY, trimWidth, trimHeight) => {
|
|
901
|
-
|
|
1192
|
+
currentDoorSlot = undefined;
|
|
1193
|
+
addBox(leafGroup, revealMaterial, trimWidth, trimHeight, trimDepth, localX, panelCenterY + localY, trimFaceOffset);
|
|
902
1194
|
};
|
|
903
1195
|
addTiltupTrim(0, insetHeight / 2, insetWidth, 0.018);
|
|
904
1196
|
addTiltupTrim(0, -insetHeight / 2, insetWidth, 0.018);
|
|
905
1197
|
addTiltupTrim(-insetWidth / 2, 0, 0.018, insetHeight);
|
|
906
1198
|
addTiltupTrim(insetWidth / 2, 0, 0.018, insetHeight);
|
|
907
|
-
|
|
1199
|
+
currentDoorSlot = 'hardware';
|
|
1200
|
+
addBox(mesh, hardwareMaterial, insideWidth, 0.026, Math.max(frameDepth * 0.4, 0.035), 0, hingeY, 0);
|
|
908
1201
|
}
|
|
909
1202
|
function getEffectiveOpeningShape(node) {
|
|
910
1203
|
return node.doorType === 'folding' ||
|
|
@@ -916,6 +1209,7 @@ function getEffectiveOpeningShape(node) {
|
|
|
916
1209
|
}
|
|
917
1210
|
function updateDoorMesh(rawNode, mesh) {
|
|
918
1211
|
const node = normalizeDoorNodeForRender(rawNode);
|
|
1212
|
+
currentDoorSlot = undefined;
|
|
919
1213
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
920
1214
|
mesh.geometry.dispose();
|
|
921
1215
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
@@ -930,6 +1224,13 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
930
1224
|
disposeObject(child);
|
|
931
1225
|
mesh.remove(child);
|
|
932
1226
|
}
|
|
1227
|
+
// Point the builder-facing materials at this door's slot overrides for the
|
|
1228
|
+
// duration of its build (recomputed per node, so the next door resets cleanly
|
|
1229
|
+
// without a restore). Reveal keeps its own material.
|
|
1230
|
+
baseMaterial = resolveDoorSlotMaterial(node, 'panel');
|
|
1231
|
+
frameMaterial = resolveDoorSlotMaterial(node, 'frame');
|
|
1232
|
+
glassMaterial = resolveDoorSlotMaterial(node, 'glass');
|
|
1233
|
+
hardwareMaterial = resolveDoorSlotMaterial(node, 'hardware');
|
|
933
1234
|
const { width, height, openingKind, openingShape: rawOpeningShape, frameThickness, frameDepth, threshold, thresholdHeight, segments, handle, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, contentPadding, hingesSide, swingDirection, swingAngle: nodeSwingAngle = 0, doorType = 'hinged', operationState: nodeOperationState = 0, leafCount = 1, slideDirection = 'left', garagePanelCount = 4, } = node;
|
|
934
1235
|
const openingShape = getEffectiveOpeningShape(node) ?? rawOpeningShape;
|
|
935
1236
|
const runtimeDoorState = useInteractive.getState().doors[node.id];
|
|
@@ -946,6 +1247,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
946
1247
|
const leafCenterY = -frameThickness / 2;
|
|
947
1248
|
const swingDirectionSign = swingDirection === 'inward' ? 1 : -1;
|
|
948
1249
|
// ── Frame members ──
|
|
1250
|
+
currentDoorSlot = 'frame';
|
|
949
1251
|
if (openingShape === 'arch') {
|
|
950
1252
|
const frameBottom = -height / 2;
|
|
951
1253
|
const frameTop = height / 2;
|
|
@@ -956,26 +1258,27 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
956
1258
|
const useShallowHeadBar = frameArchHeight <= frameThickness * 2;
|
|
957
1259
|
const frameHeadBottomY = useShallowHeadBar ? frameSpringY - frameThickness : frameSpringY;
|
|
958
1260
|
const postHeight = Math.max(frameHeadBottomY - frameBottom, 0.01);
|
|
959
|
-
addBox(mesh,
|
|
960
|
-
addBox(mesh,
|
|
961
|
-
addShape(mesh,
|
|
1261
|
+
addBox(mesh, frameMaterial, frameThickness, postHeight, frameDepth, -width / 2 + frameThickness / 2, frameBottom + postHeight / 2, 0);
|
|
1262
|
+
addBox(mesh, frameMaterial, frameThickness, postHeight, frameDepth, width / 2 - frameThickness / 2, frameBottom + postHeight / 2, 0);
|
|
1263
|
+
addShape(mesh, frameMaterial, useShallowHeadBar
|
|
962
1264
|
? createArchHeadBarShape(width, frameHeadBottomY, frameSpringY, frameTop)
|
|
963
1265
|
: createArchBandShape(width, frameSpringY, frameTop, frameInnerSpringY, frameInnerTopY, frameThickness), frameDepth);
|
|
964
1266
|
}
|
|
965
1267
|
else if (openingShape === 'rounded') {
|
|
966
|
-
addShape(mesh,
|
|
1268
|
+
addShape(mesh, frameMaterial, createRoundedDoorFrameShape(width, height, frameThickness, getDoorTopRadii(node, width, height)), frameDepth);
|
|
967
1269
|
}
|
|
968
1270
|
else {
|
|
969
1271
|
// Left post — full height
|
|
970
|
-
addBox(mesh,
|
|
1272
|
+
addBox(mesh, frameMaterial, frameThickness, height, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
971
1273
|
// Right post — full height
|
|
972
|
-
addBox(mesh,
|
|
1274
|
+
addBox(mesh, frameMaterial, frameThickness, height, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
973
1275
|
// Head (top bar) — full width
|
|
974
|
-
addBox(mesh,
|
|
1276
|
+
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
975
1277
|
}
|
|
976
1278
|
// ── Threshold (inside the frame) ──
|
|
977
1279
|
if (threshold) {
|
|
978
|
-
|
|
1280
|
+
currentDoorSlot = 'frame';
|
|
1281
|
+
addBox(mesh, frameMaterial, insideWidth, thresholdHeight, frameDepth, 0, -height / 2 + thresholdHeight / 2, 0);
|
|
979
1282
|
}
|
|
980
1283
|
if (doorType === 'garage-sectional') {
|
|
981
1284
|
addGarageSectionalDoor(mesh, {
|
|
@@ -985,7 +1288,6 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
985
1288
|
leafDepth,
|
|
986
1289
|
frameThickness,
|
|
987
1290
|
frameDepth,
|
|
988
|
-
operationState,
|
|
989
1291
|
garagePanelCount,
|
|
990
1292
|
});
|
|
991
1293
|
}
|
|
@@ -1008,7 +1310,6 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1008
1310
|
leafDepth,
|
|
1009
1311
|
frameThickness,
|
|
1010
1312
|
frameDepth,
|
|
1011
|
-
operationState,
|
|
1012
1313
|
});
|
|
1013
1314
|
}
|
|
1014
1315
|
else if (doorType === 'folding') {
|
|
@@ -1019,7 +1320,6 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1019
1320
|
leafDepth,
|
|
1020
1321
|
frameThickness,
|
|
1021
1322
|
frameDepth,
|
|
1022
|
-
operationState,
|
|
1023
1323
|
leafCount,
|
|
1024
1324
|
doorHeight: height,
|
|
1025
1325
|
handleHeight,
|
|
@@ -1035,7 +1335,6 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1035
1335
|
leafDepth,
|
|
1036
1336
|
frameThickness,
|
|
1037
1337
|
frameDepth,
|
|
1038
|
-
operationState,
|
|
1039
1338
|
slideDirection,
|
|
1040
1339
|
doorHeight: height,
|
|
1041
1340
|
handleHeight,
|
|
@@ -1051,7 +1350,6 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1051
1350
|
leafDepth,
|
|
1052
1351
|
frameThickness,
|
|
1053
1352
|
frameDepth,
|
|
1054
|
-
operationState,
|
|
1055
1353
|
slideDirection,
|
|
1056
1354
|
doorHeight: height,
|
|
1057
1355
|
handleHeight,
|
|
@@ -1067,7 +1365,6 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1067
1365
|
leafDepth,
|
|
1068
1366
|
frameThickness,
|
|
1069
1367
|
frameDepth,
|
|
1070
|
-
operationState,
|
|
1071
1368
|
slideDirection,
|
|
1072
1369
|
doorHeight: height,
|
|
1073
1370
|
handleHeight,
|
|
@@ -1094,6 +1391,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1094
1391
|
hingeX: -insideWidth / 2,
|
|
1095
1392
|
hingeSide: 'left',
|
|
1096
1393
|
swingRotation: -clampedSwingAngle * swingDirectionSign,
|
|
1394
|
+
openRotationY: (-Math.PI / 2) * swingDirectionSign,
|
|
1097
1395
|
segments,
|
|
1098
1396
|
contentPadding,
|
|
1099
1397
|
handle,
|
|
@@ -1121,6 +1419,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1121
1419
|
hingeX: insideWidth / 2,
|
|
1122
1420
|
hingeSide: 'right',
|
|
1123
1421
|
swingRotation: clampedSwingAngle * swingDirectionSign,
|
|
1422
|
+
openRotationY: (Math.PI / 2) * swingDirectionSign,
|
|
1124
1423
|
segments,
|
|
1125
1424
|
contentPadding,
|
|
1126
1425
|
handle,
|
|
@@ -1152,6 +1451,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1152
1451
|
hingeX,
|
|
1153
1452
|
hingeSide: hingesSide,
|
|
1154
1453
|
swingRotation: clampedSwingAngle * swingDirectionSign * hingeDirectionSign,
|
|
1454
|
+
openRotationY: (Math.PI / 2) * swingDirectionSign * hingeDirectionSign,
|
|
1155
1455
|
segments,
|
|
1156
1456
|
contentPadding,
|
|
1157
1457
|
handle,
|
|
@@ -1167,6 +1467,13 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1167
1467
|
archHeight: node.archHeight ?? 0.45,
|
|
1168
1468
|
});
|
|
1169
1469
|
}
|
|
1470
|
+
// Operation doors build their moving parts at the closed pose inside named
|
|
1471
|
+
// groups; reflect the door's current `operationState` by posing them. Roll-up
|
|
1472
|
+
// is excluded: its live rebuild already renders the open state at full detail
|
|
1473
|
+
// (the named curtain group is only there for the GLB exporter's scale clip).
|
|
1474
|
+
if (doorType !== 'garage-rollup') {
|
|
1475
|
+
poseDoorMovingParts(node, mesh, operationState);
|
|
1476
|
+
}
|
|
1170
1477
|
syncDoorCutout(node, mesh);
|
|
1171
1478
|
// Guard: some degenerate door configs can leave a child mesh with an
|
|
1172
1479
|
// empty (0-vertex) geometry — e.g. a zero-area extruded leaf frame.
|
|
@@ -1186,33 +1493,59 @@ function hideEmptyGeometryMeshes(root) {
|
|
|
1186
1493
|
});
|
|
1187
1494
|
}
|
|
1188
1495
|
function syncDoorCutout(node, mesh) {
|
|
1189
|
-
// ── Cutout
|
|
1496
|
+
// ── Cutout: invisible raycast hit target for the whole opening ──
|
|
1190
1497
|
let cutout = mesh.getObjectByName('cutout');
|
|
1191
1498
|
if (!cutout) {
|
|
1192
1499
|
cutout = new THREE.Mesh();
|
|
1193
1500
|
cutout.name = 'cutout';
|
|
1501
|
+
// The cutout (invisible) is proud of the wall on both faces, so it wins the
|
|
1502
|
+
// scene raycast over the wall in front of the recessed door body — making it
|
|
1503
|
+
// the selection AND paint hit target for the whole opening. The paint
|
|
1504
|
+
// capability then re-raycasts the door's parts to find the slot. Its depth
|
|
1505
|
+
// is snug to the wall (not 1m) so it no longer blankets the room floor in a
|
|
1506
|
+
// top-down view; the wall CSG ignores this depth (see getOpeningCutoutProxyDepth).
|
|
1194
1507
|
mesh.add(cutout);
|
|
1195
1508
|
}
|
|
1196
1509
|
cutout.geometry.dispose();
|
|
1510
|
+
const depth = resolveOpeningCutoutProxyDepth(node);
|
|
1197
1511
|
const openingShape = getEffectiveOpeningShape(node);
|
|
1198
1512
|
if (openingShape === 'arch') {
|
|
1199
1513
|
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)), {
|
|
1200
|
-
depth
|
|
1514
|
+
depth,
|
|
1201
1515
|
bevelEnabled: false,
|
|
1202
1516
|
curveSegments: 24,
|
|
1203
1517
|
});
|
|
1204
|
-
cutout.geometry.translate(0, 0, -
|
|
1518
|
+
cutout.geometry.translate(0, 0, -depth / 2);
|
|
1205
1519
|
}
|
|
1206
1520
|
else if (openingShape === 'rounded') {
|
|
1207
1521
|
cutout.geometry = new THREE.ExtrudeGeometry(createRoundedTopShape(-node.width / 2, node.width / 2, -node.height / 2, node.height / 2, getDoorTopRadii(node, node.width, node.height)), {
|
|
1208
|
-
depth
|
|
1522
|
+
depth,
|
|
1209
1523
|
bevelEnabled: false,
|
|
1210
1524
|
curveSegments: 24,
|
|
1211
1525
|
});
|
|
1212
|
-
cutout.geometry.translate(0, 0, -
|
|
1526
|
+
cutout.geometry.translate(0, 0, -depth / 2);
|
|
1213
1527
|
}
|
|
1214
1528
|
else {
|
|
1215
|
-
cutout.geometry = new THREE.BoxGeometry(node.width, node.height,
|
|
1529
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, depth);
|
|
1216
1530
|
}
|
|
1217
1531
|
cutout.visible = false;
|
|
1218
1532
|
}
|
|
1533
|
+
// Resolve the cutout proxy depth from the opening's parent wall thickness so
|
|
1534
|
+
// the proxy stays proud of both wall faces (front/back selection) without the
|
|
1535
|
+
// old 1m depth that blanketed the floor. Falls back to the default thickness
|
|
1536
|
+
// when the parent wall isn't a resolvable wall node.
|
|
1537
|
+
function resolveOpeningCutoutProxyDepth(node) {
|
|
1538
|
+
const parentId = node.parentId;
|
|
1539
|
+
const parent = parentId ? useScene.getState().nodes[parentId] : undefined;
|
|
1540
|
+
const wallThickness = parent?.type === 'wall' ? getWallThickness(parent) : DEFAULT_WALL_THICKNESS;
|
|
1541
|
+
return getOpeningCutoutProxyDepth(wallThickness);
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Build a fresh door mesh for preview/ghost rendering.
|
|
1545
|
+
* Returns a mesh with an invisible hitbox root and visible children (frame, panels, hardware).
|
|
1546
|
+
*/
|
|
1547
|
+
export function buildDoorPreviewMesh(node) {
|
|
1548
|
+
const mesh = new THREE.Mesh();
|
|
1549
|
+
updateDoorMesh(node, mesh);
|
|
1550
|
+
return mesh;
|
|
1551
|
+
}
|