@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,49 +1,227 @@
|
|
|
1
|
-
import { clampDoorOperationState, getDoorRenderOpenAmount, sceneRegistry, useInteractive, 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
|
+
import { useEffect, useRef } from 'react';
|
|
3
4
|
import * as THREE from 'three';
|
|
4
|
-
import {
|
|
5
|
+
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
6
|
+
import { createDefaultMaterial, 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 });
|
|
7
|
-
const
|
|
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);
|
|
23
|
+
let baseMaterial = getBaseMaterial();
|
|
24
|
+
let frameMaterial = getBaseMaterial();
|
|
25
|
+
let revealMaterial = defaultRevealMaterial;
|
|
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;
|
|
35
|
+
const DOOR_RENDER_DEFAULTS = DoorNodeSchema.parse({ id: 'door_render_default' });
|
|
36
|
+
const MAX_DOOR_REBUILDS_PER_FRAME = 16;
|
|
37
|
+
const DOOR_PROGRESSIVE_DIRTY_THRESHOLD = MAX_DOOR_REBUILDS_PER_FRAME;
|
|
38
|
+
const DOOR_PROGRESSIVE_TIME_BUDGET_MS = 8;
|
|
39
|
+
// Legacy/unparsed door nodes can miss schema-defaulted fields (segments,
|
|
40
|
+
// columnRatios, dividerThickness, …) and crash the geometry build. Re-apply the
|
|
41
|
+
// Zod defaults; if the node is structurally invalid (e.g. a segment missing a
|
|
42
|
+
// required field) drop the bad segments, then fall back to defaults entirely.
|
|
43
|
+
function normalizeDoorNodeForRender(node) {
|
|
44
|
+
const parsed = DoorNodeSchema.safeParse(node);
|
|
45
|
+
if (parsed.success)
|
|
46
|
+
return parsed.data;
|
|
47
|
+
const retry = DoorNodeSchema.safeParse({ ...node, segments: undefined });
|
|
48
|
+
if (retry.success)
|
|
49
|
+
return retry.data;
|
|
50
|
+
return { ...DOOR_RENDER_DEFAULTS, id: node.id, parentId: node.parentId };
|
|
51
|
+
}
|
|
8
52
|
export const DoorSystem = () => {
|
|
9
53
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
10
54
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
55
|
+
const shading = useViewer((state) => state.shading);
|
|
56
|
+
const textures = useViewer((state) => state.textures);
|
|
57
|
+
const colorPreset = useViewer((state) => state.colorPreset);
|
|
58
|
+
const sceneMaterials = useScene((state) => state.materials);
|
|
59
|
+
const materialRevisionRef = useRef(null);
|
|
60
|
+
// Subscribe so an override-only update (no scene write) still re-runs
|
|
61
|
+
// the component, letting the gate below pick up the latest dirtyNodes
|
|
62
|
+
// set from the same render pass that received the override-publishing
|
|
63
|
+
// `markDirty` call. Mirrors WallSystem.
|
|
64
|
+
useLiveNodeOverrides((s) => s.overrides);
|
|
65
|
+
const joineryMaterial = createSurfaceRoleMaterial('joinery', colorPreset);
|
|
66
|
+
baseMaterial = textures ? getBaseMaterial(shading) : joineryMaterial;
|
|
67
|
+
frameMaterial = textures ? getBaseMaterial(shading) : joineryMaterial;
|
|
68
|
+
revealMaterial = textures ? defaultRevealMaterial : joineryMaterial;
|
|
69
|
+
glassMaterial = textures ? defaultGlassMaterial : joineryMaterial;
|
|
70
|
+
hardwareMaterial = textures ? defaultHardwareMaterial : joineryMaterial;
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`;
|
|
73
|
+
if (materialRevisionRef.current === materialRevision)
|
|
74
|
+
return;
|
|
75
|
+
materialRevisionRef.current = materialRevision;
|
|
76
|
+
const nodes = useScene.getState().nodes;
|
|
77
|
+
for (const node of Object.values(nodes)) {
|
|
78
|
+
if (node?.type === 'door') {
|
|
79
|
+
useScene.getState().dirtyNodes.add(node.id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
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]);
|
|
11
96
|
useFrame(() => {
|
|
12
97
|
if (dirtyNodes.size === 0)
|
|
13
98
|
return;
|
|
99
|
+
const frameJoineryMaterial = createSurfaceRoleMaterial('joinery', colorPreset);
|
|
100
|
+
baseMaterial = textures ? getBaseMaterial(shading) : frameJoineryMaterial;
|
|
101
|
+
frameMaterial = textures ? getBaseMaterial(shading) : frameJoineryMaterial;
|
|
102
|
+
revealMaterial = textures ? defaultRevealMaterial : frameJoineryMaterial;
|
|
103
|
+
glassMaterial = textures ? defaultGlassMaterial : frameJoineryMaterial;
|
|
104
|
+
hardwareMaterial = textures ? defaultHardwareMaterial : frameJoineryMaterial;
|
|
105
|
+
currentShading = shading;
|
|
106
|
+
currentTextures = textures;
|
|
107
|
+
currentColorPreset = colorPreset;
|
|
108
|
+
currentSceneMaterials = sceneMaterials;
|
|
14
109
|
const nodes = useScene.getState().nodes;
|
|
110
|
+
const dirtyDoorIds = [];
|
|
15
111
|
dirtyNodes.forEach((id) => {
|
|
16
112
|
const node = nodes[id];
|
|
17
|
-
if (
|
|
113
|
+
if (node?.type !== 'door')
|
|
18
114
|
return;
|
|
115
|
+
dirtyDoorIds.push(id);
|
|
116
|
+
});
|
|
117
|
+
const useProgressiveDoorRebuilds = dirtyDoorIds.length > DOOR_PROGRESSIVE_DIRTY_THRESHOLD;
|
|
118
|
+
const frameStartedAt = performance.now();
|
|
119
|
+
let rebuiltDoorsThisFrame = 0;
|
|
120
|
+
for (const id of dirtyDoorIds) {
|
|
121
|
+
if (useProgressiveDoorRebuilds) {
|
|
122
|
+
if (rebuiltDoorsThisFrame >= MAX_DOOR_REBUILDS_PER_FRAME) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if (rebuiltDoorsThisFrame > 0 &&
|
|
126
|
+
performance.now() - frameStartedAt >= DOOR_PROGRESSIVE_TIME_BUDGET_MS) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const node = nodes[id];
|
|
131
|
+
if (node?.type !== 'door')
|
|
132
|
+
continue;
|
|
19
133
|
const mesh = sceneRegistry.nodes.get(id);
|
|
20
134
|
if (!mesh)
|
|
21
|
-
|
|
22
|
-
|
|
135
|
+
continue; // Keep dirty until mesh mounts
|
|
136
|
+
// Merge any live override (width / height / position) so the mesh
|
|
137
|
+
// rebuild reflects the in-flight drag without zustand churn. When
|
|
138
|
+
// no override is set this returns the scene node unchanged.
|
|
139
|
+
const effectiveNode = getEffectiveNode(node);
|
|
140
|
+
updateDoorMesh(effectiveNode, mesh);
|
|
23
141
|
clearDirty(id);
|
|
142
|
+
rebuiltDoorsThisFrame += 1;
|
|
24
143
|
// Rebuild the parent wall so its cutout reflects the updated door geometry
|
|
25
|
-
|
|
26
|
-
|
|
144
|
+
// Avoid triggering expensive wall CSG rebuilds while the door is being interactively moved/duplicated.
|
|
145
|
+
// The editor tools will request a final wall rebuild on commit.
|
|
146
|
+
const isTransient = !!node.metadata?.isTransient;
|
|
147
|
+
if (!isTransient && effectiveNode.parentId) {
|
|
148
|
+
useScene.getState().dirtyNodes.add(effectiveNode.parentId);
|
|
27
149
|
}
|
|
28
|
-
}
|
|
150
|
+
}
|
|
29
151
|
}, 3);
|
|
30
152
|
return null;
|
|
31
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
|
+
}
|
|
32
210
|
function addBox(parent, material, w, h, d, x, y, z) {
|
|
33
|
-
const
|
|
211
|
+
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
212
|
+
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
213
|
+
const m = new THREE.Mesh(geometry, material);
|
|
34
214
|
m.position.set(x, y, z);
|
|
215
|
+
tagDoorSlot(m);
|
|
35
216
|
parent.add(m);
|
|
36
217
|
}
|
|
37
218
|
function addRotatedBox(parent, material, w, h, d, x, y, z, rotationY) {
|
|
38
|
-
const
|
|
219
|
+
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
220
|
+
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
221
|
+
const m = new THREE.Mesh(geometry, material);
|
|
39
222
|
m.position.set(x, y, z);
|
|
40
223
|
m.rotation.y = rotationY;
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
function addBoxWithRotation(parent, material, w, h, d, x, y, z, rotation) {
|
|
44
|
-
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
|
|
45
|
-
m.position.set(x, y, z);
|
|
46
|
-
m.rotation.set(rotation[0], rotation[1], rotation[2]);
|
|
224
|
+
tagDoorSlot(m);
|
|
47
225
|
parent.add(m);
|
|
48
226
|
}
|
|
49
227
|
function addShape(parent, material, shape, depth) {
|
|
@@ -54,6 +232,18 @@ function addShape(parent, material, shape, depth) {
|
|
|
54
232
|
});
|
|
55
233
|
geometry.translate(0, 0, -depth / 2);
|
|
56
234
|
const mesh = new THREE.Mesh(geometry, material);
|
|
235
|
+
tagDoorSlot(mesh);
|
|
236
|
+
parent.add(mesh);
|
|
237
|
+
}
|
|
238
|
+
function addShapeAt(parent, material, shape, depth, x, y, z) {
|
|
239
|
+
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
240
|
+
depth,
|
|
241
|
+
bevelEnabled: false,
|
|
242
|
+
curveSegments: 24,
|
|
243
|
+
});
|
|
244
|
+
geometry.translate(x, y, z - depth / 2);
|
|
245
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
246
|
+
tagDoorSlot(mesh);
|
|
57
247
|
parent.add(mesh);
|
|
58
248
|
}
|
|
59
249
|
function getClampedArchHeight(width, height, archHeight) {
|
|
@@ -83,6 +273,32 @@ function getArchBoundaryY(x, halfWidth, springY, archHeight) {
|
|
|
83
273
|
const t = Math.min(Math.abs(x) / halfWidth, 1);
|
|
84
274
|
return springY + archHeight * Math.sqrt(Math.max(1 - t * t, 0));
|
|
85
275
|
}
|
|
276
|
+
function getOneSidedArchBoundaryYAtX(x, left, right, top, archHeight, curvedSide) {
|
|
277
|
+
const width = right - left;
|
|
278
|
+
if (width <= 1e-6)
|
|
279
|
+
return top;
|
|
280
|
+
const clampedArchHeight = getClampedArchHeight(width, Number.MAX_SAFE_INTEGER, archHeight);
|
|
281
|
+
const springY = top - clampedArchHeight;
|
|
282
|
+
const distanceFromApex = curvedSide === 'left'
|
|
283
|
+
? Math.max(0, Math.min((right - x) / width, 1))
|
|
284
|
+
: Math.max(0, Math.min((x - left) / width, 1));
|
|
285
|
+
return (springY + clampedArchHeight * Math.sqrt(Math.max(1 - distanceFromApex * distanceFromApex, 0)));
|
|
286
|
+
}
|
|
287
|
+
function getRoundedBoundaryYAtX(x, left, right, top, radii) {
|
|
288
|
+
if (radii.topLeft > 1e-6 && x < left + radii.topLeft) {
|
|
289
|
+
const centerX = left + radii.topLeft;
|
|
290
|
+
const centerY = top - radii.topLeft;
|
|
291
|
+
const dx = x - centerX;
|
|
292
|
+
return centerY + Math.sqrt(Math.max(radii.topLeft * radii.topLeft - dx * dx, 0));
|
|
293
|
+
}
|
|
294
|
+
if (radii.topRight > 1e-6 && x > right - radii.topRight) {
|
|
295
|
+
const centerX = right - radii.topRight;
|
|
296
|
+
const centerY = top - radii.topRight;
|
|
297
|
+
const dx = x - centerX;
|
|
298
|
+
return centerY + Math.sqrt(Math.max(radii.topRight * radii.topRight - dx * dx, 0));
|
|
299
|
+
}
|
|
300
|
+
return top;
|
|
301
|
+
}
|
|
86
302
|
function createArchBandShape(width, outerSpringY, outerTopY, innerSpringY, innerTopY, insetX) {
|
|
87
303
|
const halfWidth = width / 2;
|
|
88
304
|
const innerHalfWidth = Math.max(halfWidth - insetX, 0);
|
|
@@ -215,6 +431,87 @@ function createRoundedLeafFrameShape(width, bottom, top, radii, insetX, insetY)
|
|
|
215
431
|
outer.holes.push(shapeToReversedPath(createRoundedTopShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii)));
|
|
216
432
|
return outer;
|
|
217
433
|
}
|
|
434
|
+
function createRoundedClippedLeafFrameShape(left, right, bottom, top, fullLeft, fullRight, radii, insetX, insetY) {
|
|
435
|
+
const outerRadii = normalizeTopCornerRadii(radii, fullRight - fullLeft, top - bottom);
|
|
436
|
+
const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getRoundedBoundaryYAtX(x, fullLeft, fullRight, top, {
|
|
437
|
+
topLeft: outerRadii.topLeft,
|
|
438
|
+
topRight: outerRadii.topRight,
|
|
439
|
+
}));
|
|
440
|
+
if (!outer)
|
|
441
|
+
return null;
|
|
442
|
+
const innerLeft = left + insetX;
|
|
443
|
+
const innerRight = right - insetX;
|
|
444
|
+
const innerBottom = bottom + insetY;
|
|
445
|
+
const innerTop = top - insetY;
|
|
446
|
+
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
|
|
447
|
+
return outer;
|
|
448
|
+
const innerFullLeft = fullLeft + insetX;
|
|
449
|
+
const innerFullRight = fullRight - insetX;
|
|
450
|
+
const innerRadii = normalizeTopCornerRadii({
|
|
451
|
+
topLeft: Math.max(outerRadii.topLeft - Math.max(insetX, insetY), 0),
|
|
452
|
+
topRight: Math.max(outerRadii.topRight - Math.max(insetX, insetY), 0),
|
|
453
|
+
}, innerFullRight - innerFullLeft, innerTop - innerBottom);
|
|
454
|
+
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getRoundedBoundaryYAtX(x, innerFullLeft, innerFullRight, innerTop, {
|
|
455
|
+
topLeft: innerRadii.topLeft,
|
|
456
|
+
topRight: innerRadii.topRight,
|
|
457
|
+
}));
|
|
458
|
+
if (holeShape)
|
|
459
|
+
outer.holes.push(shapeToReversedPath(holeShape));
|
|
460
|
+
return outer;
|
|
461
|
+
}
|
|
462
|
+
function createArchedLeafFrameShape(width, bottom, top, archHeight, insetX, insetY) {
|
|
463
|
+
const halfWidth = width / 2;
|
|
464
|
+
const outer = createArchShape(-halfWidth, halfWidth, bottom, top, archHeight);
|
|
465
|
+
const innerLeft = -halfWidth + insetX;
|
|
466
|
+
const innerRight = halfWidth - insetX;
|
|
467
|
+
const innerBottom = bottom + insetY;
|
|
468
|
+
const innerTop = top - insetY;
|
|
469
|
+
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
|
|
470
|
+
return outer;
|
|
471
|
+
const innerArchHeight = getClampedArchHeight(innerRight - innerLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
|
|
472
|
+
outer.holes.push(shapeToReversedPath(createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight)));
|
|
473
|
+
return outer;
|
|
474
|
+
}
|
|
475
|
+
function createArchedClippedLeafFrameShape(left, right, bottom, top, fullLeft, fullRight, archHeight, insetX, insetY) {
|
|
476
|
+
const fullCenterX = (fullLeft + fullRight) / 2;
|
|
477
|
+
const fullHalfWidth = (fullRight - fullLeft) / 2;
|
|
478
|
+
const springY = top - archHeight;
|
|
479
|
+
const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getArchBoundaryY(x - fullCenterX, fullHalfWidth, springY, archHeight));
|
|
480
|
+
if (!outer)
|
|
481
|
+
return null;
|
|
482
|
+
const innerLeft = left + insetX;
|
|
483
|
+
const innerRight = right - insetX;
|
|
484
|
+
const innerBottom = bottom + insetY;
|
|
485
|
+
const innerTop = top - insetY;
|
|
486
|
+
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
|
|
487
|
+
return outer;
|
|
488
|
+
const innerFullLeft = fullLeft + insetX;
|
|
489
|
+
const innerFullRight = fullRight - insetX;
|
|
490
|
+
const innerArchHeight = getClampedArchHeight(innerFullRight - innerFullLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
|
|
491
|
+
const innerFullCenterX = (innerFullLeft + innerFullRight) / 2;
|
|
492
|
+
const innerFullHalfWidth = (innerFullRight - innerFullLeft) / 2;
|
|
493
|
+
const innerSpringY = innerTop - innerArchHeight;
|
|
494
|
+
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getArchBoundaryY(x - innerFullCenterX, innerFullHalfWidth, innerSpringY, innerArchHeight));
|
|
495
|
+
if (holeShape)
|
|
496
|
+
outer.holes.push(shapeToReversedPath(holeShape));
|
|
497
|
+
return outer;
|
|
498
|
+
}
|
|
499
|
+
function createOneSidedArchLeafFrameShape(left, right, bottom, top, archHeight, insetX, insetY, curvedSide) {
|
|
500
|
+
const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getOneSidedArchBoundaryYAtX(x, left, right, top, archHeight, curvedSide));
|
|
501
|
+
if (!outer)
|
|
502
|
+
return null;
|
|
503
|
+
const innerLeft = left + insetX;
|
|
504
|
+
const innerRight = right - insetX;
|
|
505
|
+
const innerBottom = bottom + insetY;
|
|
506
|
+
const innerTop = top - insetY;
|
|
507
|
+
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
|
|
508
|
+
return outer;
|
|
509
|
+
const innerArchHeight = getClampedArchHeight(innerRight - innerLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
|
|
510
|
+
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getOneSidedArchBoundaryYAtX(x, innerLeft, innerRight, innerTop, innerArchHeight, curvedSide));
|
|
511
|
+
if (holeShape)
|
|
512
|
+
outer.holes.push(shapeToReversedPath(holeShape));
|
|
513
|
+
return outer;
|
|
514
|
+
}
|
|
218
515
|
function createTopClippedRectShape(left, right, bottom, top, getBoundaryY) {
|
|
219
516
|
const segments = 20;
|
|
220
517
|
const points = [];
|
|
@@ -242,17 +539,19 @@ function disposeObject(object) {
|
|
|
242
539
|
child.geometry.dispose();
|
|
243
540
|
});
|
|
244
541
|
}
|
|
245
|
-
function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, }) {
|
|
542
|
+
function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, renderPerimeterFrame = true, openingShape = 'rectangle', openingTopRadii, archHeight, archOuterSide, }) {
|
|
246
543
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
|
|
247
544
|
const shouldRenderFrame = hasLeafContent || keepFrameWhenEmpty;
|
|
248
545
|
const cpX = contentPadding[0];
|
|
249
546
|
const cpY = contentPadding[1];
|
|
250
|
-
if (shouldRenderFrame && cpY > 0) {
|
|
547
|
+
if (renderPerimeterFrame && shouldRenderFrame && cpY > 0) {
|
|
548
|
+
currentDoorSlot = 'panel';
|
|
251
549
|
addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY + leafHeight / 2 - cpY / 2, 0);
|
|
252
550
|
addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY - leafHeight / 2 + cpY / 2, 0);
|
|
253
551
|
}
|
|
254
|
-
if (shouldRenderFrame && cpX > 0) {
|
|
552
|
+
if (renderPerimeterFrame && shouldRenderFrame && cpX > 0) {
|
|
255
553
|
const innerH = leafHeight - 2 * cpY;
|
|
554
|
+
currentDoorSlot = 'panel';
|
|
256
555
|
addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX - leafWidth / 2 + cpX / 2, leafCenterY, 0);
|
|
257
556
|
addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX + leafWidth / 2 - cpX / 2, leafCenterY, 0);
|
|
258
557
|
}
|
|
@@ -261,9 +560,32 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
|
|
|
261
560
|
const totalRatio = segments.reduce((sum, s) => sum + s.heightRatio, 0);
|
|
262
561
|
const contentTop = leafCenterY + contentH / 2;
|
|
263
562
|
let segY = contentTop;
|
|
563
|
+
const leafLeft = leafCenterX - leafWidth / 2;
|
|
564
|
+
const leafRight = leafCenterX + leafWidth / 2;
|
|
565
|
+
const leafTop = leafCenterY + leafHeight / 2;
|
|
566
|
+
const hasShapedTop = openingShape === 'rounded' || openingShape === 'arch';
|
|
567
|
+
const clampedLeafArchHeight = openingShape === 'arch' ? getClampedArchHeight(leafWidth, leafHeight, archHeight) : 0;
|
|
568
|
+
const leafSpringY = leafTop - clampedLeafArchHeight;
|
|
569
|
+
const topBoundaryAtX = (x) => {
|
|
570
|
+
if (openingShape === 'rounded' && openingTopRadii) {
|
|
571
|
+
return getRoundedBoundaryYAtX(x, leafLeft, leafRight, leafTop, {
|
|
572
|
+
topLeft: openingTopRadii.topLeft,
|
|
573
|
+
topRight: openingTopRadii.topRight,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (openingShape === 'arch') {
|
|
577
|
+
if (archOuterSide) {
|
|
578
|
+
return getOneSidedArchBoundaryYAtX(x, leafLeft, leafRight, leafTop, clampedLeafArchHeight, archOuterSide);
|
|
579
|
+
}
|
|
580
|
+
return getArchBoundaryY(x - leafCenterX, leafWidth / 2, leafSpringY, clampedLeafArchHeight);
|
|
581
|
+
}
|
|
582
|
+
return leafTop;
|
|
583
|
+
};
|
|
264
584
|
for (const seg of segments) {
|
|
265
585
|
const segH = (seg.heightRatio / totalRatio) * contentH;
|
|
266
586
|
const segCenterY = segY - segH / 2;
|
|
587
|
+
const segTop = segY;
|
|
588
|
+
const segBottom = segY - segH;
|
|
267
589
|
const numCols = seg.columnRatios.length;
|
|
268
590
|
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0);
|
|
269
591
|
const usableW = contentW - (numCols - 1) * seg.dividerThickness;
|
|
@@ -278,9 +600,20 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
|
|
|
278
600
|
}
|
|
279
601
|
if (seg.type !== 'empty') {
|
|
280
602
|
cx = leafCenterX - contentW / 2;
|
|
603
|
+
currentDoorSlot = 'panel';
|
|
281
604
|
for (let c = 0; c < numCols - 1; c++) {
|
|
282
605
|
cx += colWidths[c];
|
|
283
|
-
|
|
606
|
+
const dividerLeft = cx;
|
|
607
|
+
const dividerRight = cx + seg.dividerThickness;
|
|
608
|
+
const dividerShape = hasShapedTop && addLeafShape
|
|
609
|
+
? createTopClippedRectShape(dividerLeft, dividerRight, segBottom, segTop, topBoundaryAtX)
|
|
610
|
+
: null;
|
|
611
|
+
if (dividerShape && addLeafShape) {
|
|
612
|
+
addLeafShape(baseMaterial, dividerShape, leafDepth + 0.001);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
addLeafBox(baseMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0);
|
|
616
|
+
}
|
|
284
617
|
cx += seg.dividerThickness;
|
|
285
618
|
}
|
|
286
619
|
}
|
|
@@ -288,32 +621,107 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
|
|
|
288
621
|
const colW = colWidths[c];
|
|
289
622
|
const colX = colXCenters[c];
|
|
290
623
|
if (seg.type === 'glass') {
|
|
624
|
+
currentDoorSlot = 'glass';
|
|
291
625
|
const glassDepth = Math.max(0.004, leafDepth * 0.15);
|
|
292
|
-
|
|
626
|
+
const segmentLeft = colX - colW / 2;
|
|
627
|
+
const segmentRight = colX + colW / 2;
|
|
628
|
+
const glassShape = hasShapedTop && addLeafShape
|
|
629
|
+
? createTopClippedRectShape(segmentLeft, segmentRight, segBottom, segTop, topBoundaryAtX)
|
|
630
|
+
: null;
|
|
631
|
+
if (glassShape && addLeafShape) {
|
|
632
|
+
addLeafShape(glassMaterial, glassShape, glassDepth);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
addLeafBox(glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0);
|
|
636
|
+
}
|
|
293
637
|
}
|
|
294
638
|
else if (seg.type === 'panel') {
|
|
295
|
-
|
|
639
|
+
currentDoorSlot = 'panel';
|
|
640
|
+
const segmentLeft = colX - colW / 2;
|
|
641
|
+
const segmentRight = colX + colW / 2;
|
|
642
|
+
const outerPanelShape = hasShapedTop && addLeafShape
|
|
643
|
+
? createTopClippedRectShape(segmentLeft, segmentRight, segBottom, segTop, topBoundaryAtX)
|
|
644
|
+
: null;
|
|
645
|
+
if (outerPanelShape && addLeafShape) {
|
|
646
|
+
addLeafShape(baseMaterial, outerPanelShape, leafDepth);
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
addLeafBox(baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0);
|
|
650
|
+
}
|
|
296
651
|
const panelW = colW - 2 * seg.panelInset;
|
|
297
652
|
const panelH = segH - 2 * seg.panelInset;
|
|
298
653
|
if (panelW > 0.01 && panelH > 0.01) {
|
|
299
654
|
const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth);
|
|
300
655
|
const panelZ = leafDepth / 2 + effectiveDepth / 2;
|
|
301
|
-
|
|
656
|
+
const insetLeft = colX - panelW / 2;
|
|
657
|
+
const insetRight = colX + panelW / 2;
|
|
658
|
+
const insetTop = segTop - seg.panelInset;
|
|
659
|
+
const insetBottom = segBottom + seg.panelInset;
|
|
660
|
+
const innerPanelShape = hasShapedTop && addLeafShape
|
|
661
|
+
? createTopClippedRectShape(insetLeft, insetRight, insetBottom, insetTop, topBoundaryAtX)
|
|
662
|
+
: null;
|
|
663
|
+
if (innerPanelShape && addLeafShape) {
|
|
664
|
+
addLeafShape(baseMaterial, innerPanelShape, effectiveDepth);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ);
|
|
668
|
+
}
|
|
302
669
|
}
|
|
303
670
|
}
|
|
304
671
|
}
|
|
305
672
|
segY -= segH;
|
|
306
673
|
}
|
|
307
674
|
}
|
|
308
|
-
function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, hingeX, hingeSide, swingRotation, segments, contentPadding, handle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, doorHeight, }) {
|
|
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, }) {
|
|
309
676
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
|
|
310
677
|
const leafGroup = new THREE.Group();
|
|
311
678
|
leafGroup.position.set(hingeX, 0, 0);
|
|
312
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 };
|
|
313
684
|
mesh.add(leafGroup);
|
|
314
685
|
const addLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z);
|
|
686
|
+
const addLeafShape = (material, shape, depth) => addShapeAt(leafGroup, material, shape, depth, -hingeX, 0, 0);
|
|
687
|
+
const localLeafCenterX = leafCenterX - hingeX;
|
|
688
|
+
const leafBottom = leafCenterY - leafHeight / 2;
|
|
689
|
+
const leafTop = leafCenterY + leafHeight / 2;
|
|
690
|
+
const usesShapedLeafFrame = openingShape === 'rounded' || openingShape === 'arch';
|
|
691
|
+
if (usesShapedLeafFrame && hasLeafContent) {
|
|
692
|
+
currentDoorSlot = 'panel';
|
|
693
|
+
if (openingShape === 'rounded') {
|
|
694
|
+
const roundedLeafShape = roundedBoundary
|
|
695
|
+
? createRoundedClippedLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, roundedBoundary.fullLeft, roundedBoundary.fullRight, roundedBoundary.radii, contentPadding[0], contentPadding[1])
|
|
696
|
+
: createRoundedLeafFrameShape(leafWidth, leafBottom, leafTop, openingTopRadii, contentPadding[0], contentPadding[1]);
|
|
697
|
+
if (roundedLeafShape) {
|
|
698
|
+
if (roundedBoundary) {
|
|
699
|
+
addLeafShape(baseMaterial, roundedLeafShape, leafDepth);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
addShapeAt(leafGroup, baseMaterial, roundedLeafShape, leafDepth, localLeafCenterX, 0, 0);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
else if (openingShape === 'arch') {
|
|
707
|
+
const archedLeafShape = archOuterSide
|
|
708
|
+
? createOneSidedArchLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, archHeight, contentPadding[0], contentPadding[1], archOuterSide)
|
|
709
|
+
: archedBoundary
|
|
710
|
+
? createArchedClippedLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, archedBoundary.fullLeft, archedBoundary.fullRight, archedBoundary.archHeight, contentPadding[0], contentPadding[1])
|
|
711
|
+
: createArchedLeafFrameShape(leafWidth, leafBottom, leafTop, getClampedArchHeight(leafWidth, leafHeight, archHeight), contentPadding[0], contentPadding[1]);
|
|
712
|
+
if (archedLeafShape) {
|
|
713
|
+
if (archOuterSide || archedBoundary) {
|
|
714
|
+
addLeafShape(baseMaterial, archedLeafShape, leafDepth);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
addShapeAt(leafGroup, baseMaterial, archedLeafShape, leafDepth, localLeafCenterX, 0, 0);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
315
722
|
addLeafSegmentContent({
|
|
316
723
|
addLeafBox,
|
|
724
|
+
addLeafShape,
|
|
317
725
|
leafWidth,
|
|
318
726
|
leafHeight,
|
|
319
727
|
leafCenterX,
|
|
@@ -321,72 +729,224 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
|
|
|
321
729
|
leafDepth,
|
|
322
730
|
segments,
|
|
323
731
|
contentPadding,
|
|
732
|
+
renderPerimeterFrame: !usesShapedLeafFrame,
|
|
733
|
+
openingShape,
|
|
734
|
+
openingTopRadii,
|
|
735
|
+
archHeight,
|
|
736
|
+
archOuterSide,
|
|
324
737
|
});
|
|
325
738
|
if (hasLeafContent && handle) {
|
|
739
|
+
currentDoorSlot = 'hardware';
|
|
326
740
|
const handleY = handleHeight - doorHeight / 2;
|
|
327
741
|
const faceZ = leafDepth / 2;
|
|
328
742
|
const handleX = handleSide === 'right'
|
|
329
743
|
? leafCenterX + leafWidth / 2 - 0.045
|
|
330
744
|
: leafCenterX - leafWidth / 2 + 0.045;
|
|
331
|
-
addLeafBox(
|
|
332
|
-
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);
|
|
333
747
|
if (handleBothSides) {
|
|
334
|
-
addLeafBox(
|
|
335
|
-
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);
|
|
336
750
|
}
|
|
337
751
|
}
|
|
338
752
|
if (hasLeafContent && doorCloser) {
|
|
753
|
+
currentDoorSlot = 'hardware';
|
|
339
754
|
const closerY = leafCenterY + leafHeight / 2 - 0.04;
|
|
340
|
-
addLeafBox(
|
|
341
|
-
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);
|
|
342
757
|
}
|
|
343
758
|
if (hasLeafContent && panicBar) {
|
|
759
|
+
currentDoorSlot = 'hardware';
|
|
344
760
|
const barY = panicBarHeight - doorHeight / 2;
|
|
345
|
-
addLeafBox(
|
|
761
|
+
addLeafBox(hardwareMaterial, leafWidth * 0.72, 0.04, 0.055, leafCenterX, barY, leafDepth / 2 + 0.03);
|
|
346
762
|
}
|
|
347
763
|
if (hasLeafContent) {
|
|
764
|
+
currentDoorSlot = 'hardware';
|
|
348
765
|
const hingeMarkerX = hingeSide === 'right' ? hingeX - 0.012 : hingeX + 0.012;
|
|
349
766
|
const hingeH = 0.1;
|
|
350
767
|
const hingeW = 0.024;
|
|
351
768
|
const hingeD = leafDepth + 0.016;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
addBox(mesh,
|
|
355
|
-
|
|
356
|
-
|
|
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);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
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;
|
|
357
929
|
}
|
|
358
930
|
}
|
|
359
|
-
function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
931
|
+
function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, leafCount, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
360
932
|
const panelCount = leafCount === 2 ? 2 : 4;
|
|
361
|
-
const foldAmount = clampDoorOperationState(operationState);
|
|
362
933
|
const panelLength = insideWidth / panelCount;
|
|
363
|
-
|
|
364
|
-
addBox(mesh,
|
|
365
|
-
|
|
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;
|
|
366
941
|
for (let index = 0; index < panelCount; index++) {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
for (let index = 0; index < panelCount; index++) {
|
|
376
|
-
const start = vertices[index];
|
|
377
|
-
const end = vertices[index + 1];
|
|
378
|
-
const dx = end.x - start.x;
|
|
379
|
-
const dz = end.z - start.z;
|
|
380
|
-
const centerX = (start.x + end.x) / 2;
|
|
381
|
-
const centerZ = (start.z + end.z) / 2;
|
|
382
|
-
const rotationY = Math.atan2(-dz, dx);
|
|
383
|
-
const localX = {
|
|
384
|
-
x: Math.cos(rotationY),
|
|
385
|
-
z: -Math.sin(rotationY),
|
|
386
|
-
};
|
|
387
|
-
const addFoldingLeafBox = (material, w, h, d, x, y, z) => {
|
|
388
|
-
addRotatedBox(mesh, material, w, h, d, centerX + localX.x * x + Math.sin(rotationY) * z, y, centerZ + localX.z * x + Math.cos(rotationY) * z, rotationY);
|
|
389
|
-
};
|
|
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);
|
|
390
950
|
addLeafSegmentContent({
|
|
391
951
|
addLeafBox: addFoldingLeafBox,
|
|
392
952
|
leafWidth: Math.max(0.08, panelLength),
|
|
@@ -398,79 +958,96 @@ function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
398
958
|
contentPadding,
|
|
399
959
|
keepFrameWhenEmpty: true,
|
|
400
960
|
});
|
|
401
|
-
|
|
402
|
-
|
|
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);
|
|
403
965
|
}
|
|
966
|
+
parent = group;
|
|
404
967
|
}
|
|
405
|
-
|
|
968
|
+
// Handle on the free end of the last panel (its local x=panelLength edge).
|
|
406
969
|
const handleY = handleHeight - doorHeight / 2;
|
|
407
|
-
|
|
408
|
-
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);
|
|
409
973
|
}
|
|
410
|
-
function addPocketDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
411
|
-
const openAmount = clampDoorOperationState(operationState);
|
|
974
|
+
function addPocketDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
412
975
|
const slideSign = slideDirection === 'right' ? 1 : -1;
|
|
413
976
|
const leafWidth = insideWidth;
|
|
414
|
-
const leafCenterX = slideSign * insideWidth * openAmount;
|
|
415
977
|
const topY = leafCenterY + leafHeight / 2;
|
|
416
978
|
const pocketCenterX = slideSign * insideWidth;
|
|
417
979
|
const handleY = handleHeight - doorHeight / 2;
|
|
418
|
-
|
|
419
|
-
|
|
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;
|
|
420
986
|
addBox(mesh, revealMaterial, insideWidth * 0.9, 0.018, Math.max(frameDepth * 0.32, 0.026), pocketCenterX, topY - 0.055, 0);
|
|
421
987
|
addBox(mesh, revealMaterial, 0.018, leafHeight * 0.94, leafDepth + 0.014, slideSign * insideWidth * 0.5, leafCenterY, 0);
|
|
422
|
-
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);
|
|
423
992
|
addLeafSegmentContent({
|
|
424
993
|
addLeafBox: addPocketLeafBox,
|
|
425
994
|
leafWidth,
|
|
426
995
|
leafHeight,
|
|
427
|
-
leafCenterX,
|
|
996
|
+
leafCenterX: 0,
|
|
428
997
|
leafCenterY,
|
|
429
998
|
leafDepth,
|
|
430
999
|
segments,
|
|
431
1000
|
contentPadding,
|
|
432
1001
|
});
|
|
433
|
-
|
|
434
|
-
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);
|
|
435
1005
|
}
|
|
436
|
-
function addBarnDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
437
|
-
const openAmount = clampDoorOperationState(operationState);
|
|
1006
|
+
function addBarnDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
438
1007
|
const slideSign = slideDirection === 'right' ? 1 : -1;
|
|
439
1008
|
const leafWidth = insideWidth * 1.06;
|
|
440
|
-
const leafCenterX = slideSign * insideWidth * openAmount;
|
|
441
1009
|
const faceZ = frameDepth / 2 + leafDepth / 2 + 0.028;
|
|
442
1010
|
const trackY = leafCenterY + leafHeight / 2 + Math.max(frameThickness * 0.55, 0.045);
|
|
443
1011
|
const railLength = insideWidth * 2.25;
|
|
444
1012
|
const railCenterX = slideSign * (insideWidth * 0.56);
|
|
445
1013
|
const handleY = handleHeight - doorHeight / 2;
|
|
446
|
-
|
|
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);
|
|
447
1018
|
const wheelY = trackY - 0.075;
|
|
448
|
-
|
|
449
|
-
addBox(mesh,
|
|
450
|
-
addBox(mesh,
|
|
451
|
-
|
|
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);
|
|
452
1027
|
addLeafSegmentContent({
|
|
453
1028
|
addLeafBox: addBarnLeafBox,
|
|
454
1029
|
leafWidth,
|
|
455
1030
|
leafHeight,
|
|
456
|
-
leafCenterX,
|
|
1031
|
+
leafCenterX: 0,
|
|
457
1032
|
leafCenterY,
|
|
458
1033
|
leafDepth,
|
|
459
1034
|
segments,
|
|
460
1035
|
contentPadding,
|
|
461
1036
|
keepFrameWhenEmpty: true,
|
|
462
1037
|
});
|
|
463
|
-
|
|
464
|
-
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';
|
|
465
1042
|
for (const offset of [-leafWidth * 0.28, leafWidth * 0.28]) {
|
|
466
|
-
addBox(
|
|
467
|
-
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);
|
|
468
1045
|
}
|
|
469
|
-
|
|
470
|
-
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);
|
|
471
1049
|
}
|
|
472
|
-
function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
473
|
-
const openAmount = clampDoorOperationState(operationState);
|
|
1050
|
+
function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
474
1051
|
const activeOnRight = slideDirection === 'left';
|
|
475
1052
|
const fixedSign = activeOnRight ? -1 : 1;
|
|
476
1053
|
const activeSign = activeOnRight ? 1 : -1;
|
|
@@ -478,16 +1055,22 @@ function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
478
1055
|
const panelHeight = leafHeight;
|
|
479
1056
|
const closedActiveX = activeSign * insideWidth * 0.23;
|
|
480
1057
|
const fixedX = fixedSign * insideWidth * 0.23;
|
|
481
|
-
const activeX = closedActiveX - activeSign * insideWidth * 0.44 * openAmount;
|
|
482
1058
|
const frontZ = leafDepth / 2 + 0.016;
|
|
483
1059
|
const backZ = -leafDepth / 2 - 0.006;
|
|
484
1060
|
const railY = leafCenterY + panelHeight / 2 - Math.min(frameThickness * 0.35, 0.02);
|
|
485
1061
|
const handleY = handleHeight - doorHeight / 2;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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);
|
|
489
1069
|
const addFixedPanelBox = (material, w, h, d, x, y, z) => addBox(mesh, material, w, h, d, x + fixedX, y, z + backZ);
|
|
490
|
-
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);
|
|
491
1074
|
addLeafSegmentContent({
|
|
492
1075
|
addLeafBox: addFixedPanelBox,
|
|
493
1076
|
leafWidth: panelWidth,
|
|
@@ -510,51 +1093,39 @@ function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
510
1093
|
contentPadding,
|
|
511
1094
|
keepFrameWhenEmpty: true,
|
|
512
1095
|
});
|
|
513
|
-
|
|
514
|
-
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);
|
|
515
1099
|
}
|
|
516
|
-
function addGarageSectionalDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
517
|
-
const openAmount = getDoorRenderOpenAmount('garage-sectional', operationState);
|
|
1100
|
+
function addGarageSectionalDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, garagePanelCount, }) {
|
|
518
1101
|
const panelCount = Math.max(3, Math.min(12, Math.round(garagePanelCount)));
|
|
519
1102
|
const panelHeight = leafHeight / panelCount;
|
|
520
1103
|
const panelGap = Math.min(0.012, panelHeight * 0.08);
|
|
521
1104
|
const travelDepth = Math.max(leafHeight, 1.4);
|
|
522
|
-
const curveRadius = panelHeight * 0.58;
|
|
523
|
-
const curveLength = (Math.PI / 2) * curveRadius;
|
|
524
|
-
const travel = openAmount * ((panelCount - 1) * panelHeight + curveLength + panelHeight * 0.65);
|
|
525
|
-
const overheadY = leafCenterY + leafHeight / 2 - panelHeight / 2;
|
|
526
1105
|
const railY = leafCenterY + leafHeight / 2 - 0.04;
|
|
527
1106
|
const railZ = -travelDepth / 2;
|
|
528
|
-
|
|
529
|
-
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;
|
|
530
1117
|
for (let index = 0; index < panelCount; index++) {
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
else if (pathPosition > curveLength) {
|
|
543
|
-
rotationX = -Math.PI / 2;
|
|
544
|
-
y = overheadY + curveRadius;
|
|
545
|
-
z = -(curveRadius + pathPosition - curveLength);
|
|
546
|
-
}
|
|
547
|
-
const revealOffset = (panelHeight - panelGap) * 0.22;
|
|
548
|
-
const trimDepth = 0.01;
|
|
549
|
-
const trimFaceOffset = leafDepth / 2 + trimDepth + 0.006;
|
|
550
|
-
const addSectionalTrim = (localY) => {
|
|
551
|
-
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]);
|
|
552
|
-
};
|
|
553
|
-
addBoxWithRotation(mesh, baseMaterial, insideWidth, Math.max(0.04, panelHeight - panelGap), leafDepth, 0, y, z, [rotationX, 0, 0]);
|
|
554
|
-
addSectionalTrim(revealOffset);
|
|
555
|
-
addSectionalTrim(-revealOffset);
|
|
556
|
-
}
|
|
557
|
-
addBox(mesh, revealMaterial, insideWidth, 0.032, Math.max(frameDepth * 0.36, 0.03), 0, railY, 0);
|
|
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);
|
|
1126
|
+
}
|
|
1127
|
+
currentDoorSlot = 'hardware';
|
|
1128
|
+
addBox(mesh, hardwareMaterial, insideWidth, 0.032, Math.max(frameDepth * 0.36, 0.03), 0, railY, 0);
|
|
558
1129
|
}
|
|
559
1130
|
function addGarageRollupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, operationState, }) {
|
|
560
1131
|
const openAmount = clampDoorOperationState(operationState);
|
|
@@ -562,52 +1133,83 @@ function addGarageRollupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafD
|
|
|
562
1133
|
const visibleHeight = leafHeight * (1 - openAmount);
|
|
563
1134
|
const visibleSlatCount = Math.ceil(visibleHeight / slatHeight);
|
|
564
1135
|
const topY = leafCenterY + leafHeight / 2;
|
|
565
|
-
const curtainCenterY = topY - visibleHeight / 2;
|
|
566
1136
|
const drumMaxRadius = Math.max(0.12, Math.min(0.22, leafHeight * 0.075));
|
|
567
1137
|
const drumY = topY + drumMaxRadius * 0.12;
|
|
568
1138
|
const drumZ = -frameDepth / 2 - drumMaxRadius * 0.72;
|
|
569
|
-
|
|
570
|
-
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);
|
|
571
1142
|
if (visibleHeight > 0.01) {
|
|
572
|
-
|
|
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;
|
|
573
1155
|
for (let index = 0; index < visibleSlatCount; index++) {
|
|
574
|
-
const y =
|
|
575
|
-
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);
|
|
576
1158
|
}
|
|
577
|
-
|
|
1159
|
+
const bottomBarHeight = 0.028;
|
|
1160
|
+
addBox(curtain, revealMaterial, insideWidth - 0.04, bottomBarHeight, leafDepth + 0.018, 0, -visibleHeight + bottomBarHeight / 2, leafDepth / 2 + 0.004);
|
|
578
1161
|
}
|
|
1162
|
+
currentDoorSlot = 'panel';
|
|
579
1163
|
const drum = new THREE.Mesh(new THREE.CylinderGeometry(drumMaxRadius, drumMaxRadius, insideWidth + frameThickness, 36), baseMaterial);
|
|
580
1164
|
drum.position.set(0, drumY, drumZ);
|
|
581
1165
|
drum.rotation.z = Math.PI / 2;
|
|
1166
|
+
tagDoorSlot(drum);
|
|
582
1167
|
mesh.add(drum);
|
|
583
|
-
|
|
1168
|
+
currentDoorSlot = 'hardware';
|
|
1169
|
+
addBox(mesh, hardwareMaterial, insideWidth + frameThickness, 0.026, Math.max(frameDepth * 0.52, 0.04), 0, topY + 0.02, 0);
|
|
584
1170
|
}
|
|
585
|
-
function addGarageTiltupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth,
|
|
586
|
-
const openAmount = clampDoorOperationState(operationState);
|
|
587
|
-
const angle = (Math.PI / 2) * openAmount;
|
|
1171
|
+
function addGarageTiltupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, }) {
|
|
588
1172
|
const hingeY = leafCenterY + leafHeight / 2;
|
|
589
|
-
|
|
590
|
-
|
|
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;
|
|
591
1176
|
const railLength = Math.max(leafHeight * 0.72, 1.2);
|
|
592
1177
|
const railY = hingeY - frameThickness * 0.35;
|
|
593
1178
|
const railZ = -railLength / 2;
|
|
594
|
-
|
|
595
|
-
addBox(mesh,
|
|
596
|
-
|
|
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);
|
|
597
1187
|
const insetWidth = Math.max(0.1, insideWidth - 0.22);
|
|
598
1188
|
const insetHeight = Math.max(0.1, leafHeight - 0.28);
|
|
599
1189
|
const trimDepth = 0.012;
|
|
600
1190
|
const trimFaceOffset = leafDepth / 2 + trimDepth + 0.006;
|
|
601
1191
|
const addTiltupTrim = (localX, localY, trimWidth, trimHeight) => {
|
|
602
|
-
|
|
1192
|
+
currentDoorSlot = undefined;
|
|
1193
|
+
addBox(leafGroup, revealMaterial, trimWidth, trimHeight, trimDepth, localX, panelCenterY + localY, trimFaceOffset);
|
|
603
1194
|
};
|
|
604
1195
|
addTiltupTrim(0, insetHeight / 2, insetWidth, 0.018);
|
|
605
1196
|
addTiltupTrim(0, -insetHeight / 2, insetWidth, 0.018);
|
|
606
1197
|
addTiltupTrim(-insetWidth / 2, 0, 0.018, insetHeight);
|
|
607
1198
|
addTiltupTrim(insetWidth / 2, 0, 0.018, insetHeight);
|
|
608
|
-
|
|
1199
|
+
currentDoorSlot = 'hardware';
|
|
1200
|
+
addBox(mesh, hardwareMaterial, insideWidth, 0.026, Math.max(frameDepth * 0.4, 0.035), 0, hingeY, 0);
|
|
1201
|
+
}
|
|
1202
|
+
function getEffectiveOpeningShape(node) {
|
|
1203
|
+
return node.doorType === 'folding' ||
|
|
1204
|
+
node.doorType === 'pocket' ||
|
|
1205
|
+
node.doorType === 'barn' ||
|
|
1206
|
+
node.doorType === 'sliding'
|
|
1207
|
+
? 'rectangle'
|
|
1208
|
+
: (node.openingShape ?? 'rectangle');
|
|
609
1209
|
}
|
|
610
|
-
function updateDoorMesh(
|
|
1210
|
+
function updateDoorMesh(rawNode, mesh) {
|
|
1211
|
+
const node = normalizeDoorNodeForRender(rawNode);
|
|
1212
|
+
currentDoorSlot = undefined;
|
|
611
1213
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
612
1214
|
mesh.geometry.dispose();
|
|
613
1215
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
@@ -622,7 +1224,15 @@ function updateDoorMesh(node, mesh) {
|
|
|
622
1224
|
disposeObject(child);
|
|
623
1225
|
mesh.remove(child);
|
|
624
1226
|
}
|
|
625
|
-
|
|
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');
|
|
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;
|
|
1235
|
+
const openingShape = getEffectiveOpeningShape(node) ?? rawOpeningShape;
|
|
626
1236
|
const runtimeDoorState = useInteractive.getState().doors[node.id];
|
|
627
1237
|
const swingAngle = runtimeDoorState?.swingAngle ?? nodeSwingAngle;
|
|
628
1238
|
const operationState = runtimeDoorState?.operationState ?? nodeOperationState;
|
|
@@ -637,6 +1247,7 @@ function updateDoorMesh(node, mesh) {
|
|
|
637
1247
|
const leafCenterY = -frameThickness / 2;
|
|
638
1248
|
const swingDirectionSign = swingDirection === 'inward' ? 1 : -1;
|
|
639
1249
|
// ── Frame members ──
|
|
1250
|
+
currentDoorSlot = 'frame';
|
|
640
1251
|
if (openingShape === 'arch') {
|
|
641
1252
|
const frameBottom = -height / 2;
|
|
642
1253
|
const frameTop = height / 2;
|
|
@@ -647,26 +1258,27 @@ function updateDoorMesh(node, mesh) {
|
|
|
647
1258
|
const useShallowHeadBar = frameArchHeight <= frameThickness * 2;
|
|
648
1259
|
const frameHeadBottomY = useShallowHeadBar ? frameSpringY - frameThickness : frameSpringY;
|
|
649
1260
|
const postHeight = Math.max(frameHeadBottomY - frameBottom, 0.01);
|
|
650
|
-
addBox(mesh,
|
|
651
|
-
addBox(mesh,
|
|
652
|
-
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
|
|
653
1264
|
? createArchHeadBarShape(width, frameHeadBottomY, frameSpringY, frameTop)
|
|
654
1265
|
: createArchBandShape(width, frameSpringY, frameTop, frameInnerSpringY, frameInnerTopY, frameThickness), frameDepth);
|
|
655
1266
|
}
|
|
656
1267
|
else if (openingShape === 'rounded') {
|
|
657
|
-
addShape(mesh,
|
|
1268
|
+
addShape(mesh, frameMaterial, createRoundedDoorFrameShape(width, height, frameThickness, getDoorTopRadii(node, width, height)), frameDepth);
|
|
658
1269
|
}
|
|
659
1270
|
else {
|
|
660
1271
|
// Left post — full height
|
|
661
|
-
addBox(mesh,
|
|
1272
|
+
addBox(mesh, frameMaterial, frameThickness, height, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
662
1273
|
// Right post — full height
|
|
663
|
-
addBox(mesh,
|
|
1274
|
+
addBox(mesh, frameMaterial, frameThickness, height, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
664
1275
|
// Head (top bar) — full width
|
|
665
|
-
addBox(mesh,
|
|
1276
|
+
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
666
1277
|
}
|
|
667
1278
|
// ── Threshold (inside the frame) ──
|
|
668
1279
|
if (threshold) {
|
|
669
|
-
|
|
1280
|
+
currentDoorSlot = 'frame';
|
|
1281
|
+
addBox(mesh, frameMaterial, insideWidth, thresholdHeight, frameDepth, 0, -height / 2 + thresholdHeight / 2, 0);
|
|
670
1282
|
}
|
|
671
1283
|
if (doorType === 'garage-sectional') {
|
|
672
1284
|
addGarageSectionalDoor(mesh, {
|
|
@@ -676,7 +1288,6 @@ function updateDoorMesh(node, mesh) {
|
|
|
676
1288
|
leafDepth,
|
|
677
1289
|
frameThickness,
|
|
678
1290
|
frameDepth,
|
|
679
|
-
operationState,
|
|
680
1291
|
garagePanelCount,
|
|
681
1292
|
});
|
|
682
1293
|
}
|
|
@@ -699,7 +1310,6 @@ function updateDoorMesh(node, mesh) {
|
|
|
699
1310
|
leafDepth,
|
|
700
1311
|
frameThickness,
|
|
701
1312
|
frameDepth,
|
|
702
|
-
operationState,
|
|
703
1313
|
});
|
|
704
1314
|
}
|
|
705
1315
|
else if (doorType === 'folding') {
|
|
@@ -710,7 +1320,6 @@ function updateDoorMesh(node, mesh) {
|
|
|
710
1320
|
leafDepth,
|
|
711
1321
|
frameThickness,
|
|
712
1322
|
frameDepth,
|
|
713
|
-
operationState,
|
|
714
1323
|
leafCount,
|
|
715
1324
|
doorHeight: height,
|
|
716
1325
|
handleHeight,
|
|
@@ -726,7 +1335,6 @@ function updateDoorMesh(node, mesh) {
|
|
|
726
1335
|
leafDepth,
|
|
727
1336
|
frameThickness,
|
|
728
1337
|
frameDepth,
|
|
729
|
-
operationState,
|
|
730
1338
|
slideDirection,
|
|
731
1339
|
doorHeight: height,
|
|
732
1340
|
handleHeight,
|
|
@@ -742,7 +1350,6 @@ function updateDoorMesh(node, mesh) {
|
|
|
742
1350
|
leafDepth,
|
|
743
1351
|
frameThickness,
|
|
744
1352
|
frameDepth,
|
|
745
|
-
operationState,
|
|
746
1353
|
slideDirection,
|
|
747
1354
|
doorHeight: height,
|
|
748
1355
|
handleHeight,
|
|
@@ -758,7 +1365,6 @@ function updateDoorMesh(node, mesh) {
|
|
|
758
1365
|
leafDepth,
|
|
759
1366
|
frameThickness,
|
|
760
1367
|
frameDepth,
|
|
761
|
-
operationState,
|
|
762
1368
|
slideDirection,
|
|
763
1369
|
doorHeight: height,
|
|
764
1370
|
handleHeight,
|
|
@@ -768,6 +1374,14 @@ function updateDoorMesh(node, mesh) {
|
|
|
768
1374
|
}
|
|
769
1375
|
else if (doorType === 'double' || doorType === 'french') {
|
|
770
1376
|
const doubleLeafW = insideWidth / 2;
|
|
1377
|
+
const fullLeafTopRadii = getDoorTopRadii(node, insideWidth, leafH);
|
|
1378
|
+
const roundedBoundary = openingShape === 'rounded'
|
|
1379
|
+
? {
|
|
1380
|
+
fullLeft: -insideWidth / 2,
|
|
1381
|
+
fullRight: insideWidth / 2,
|
|
1382
|
+
radii: fullLeafTopRadii,
|
|
1383
|
+
}
|
|
1384
|
+
: undefined;
|
|
771
1385
|
addDoorLeaf(mesh, {
|
|
772
1386
|
leafWidth: doubleLeafW,
|
|
773
1387
|
leafHeight: leafH,
|
|
@@ -777,6 +1391,7 @@ function updateDoorMesh(node, mesh) {
|
|
|
777
1391
|
hingeX: -insideWidth / 2,
|
|
778
1392
|
hingeSide: 'left',
|
|
779
1393
|
swingRotation: -clampedSwingAngle * swingDirectionSign,
|
|
1394
|
+
openRotationY: (-Math.PI / 2) * swingDirectionSign,
|
|
780
1395
|
segments,
|
|
781
1396
|
contentPadding,
|
|
782
1397
|
handle,
|
|
@@ -787,6 +1402,13 @@ function updateDoorMesh(node, mesh) {
|
|
|
787
1402
|
panicBar,
|
|
788
1403
|
panicBarHeight,
|
|
789
1404
|
doorHeight: height,
|
|
1405
|
+
openingShape,
|
|
1406
|
+
openingTopRadii: openingShape === 'rounded'
|
|
1407
|
+
? { topLeft: fullLeafTopRadii.topLeft, topRight: 0 }
|
|
1408
|
+
: fullLeafTopRadii,
|
|
1409
|
+
archHeight: node.archHeight ?? 0.45,
|
|
1410
|
+
roundedBoundary,
|
|
1411
|
+
archOuterSide: openingShape === 'arch' ? 'left' : undefined,
|
|
790
1412
|
});
|
|
791
1413
|
addDoorLeaf(mesh, {
|
|
792
1414
|
leafWidth: doubleLeafW,
|
|
@@ -797,6 +1419,7 @@ function updateDoorMesh(node, mesh) {
|
|
|
797
1419
|
hingeX: insideWidth / 2,
|
|
798
1420
|
hingeSide: 'right',
|
|
799
1421
|
swingRotation: clampedSwingAngle * swingDirectionSign,
|
|
1422
|
+
openRotationY: (Math.PI / 2) * swingDirectionSign,
|
|
800
1423
|
segments,
|
|
801
1424
|
contentPadding,
|
|
802
1425
|
handle,
|
|
@@ -807,6 +1430,13 @@ function updateDoorMesh(node, mesh) {
|
|
|
807
1430
|
panicBar,
|
|
808
1431
|
panicBarHeight,
|
|
809
1432
|
doorHeight: height,
|
|
1433
|
+
openingShape,
|
|
1434
|
+
openingTopRadii: openingShape === 'rounded'
|
|
1435
|
+
? { topLeft: 0, topRight: fullLeafTopRadii.topRight }
|
|
1436
|
+
: fullLeafTopRadii,
|
|
1437
|
+
archHeight: node.archHeight ?? 0.45,
|
|
1438
|
+
roundedBoundary,
|
|
1439
|
+
archOuterSide: openingShape === 'arch' ? 'right' : undefined,
|
|
810
1440
|
});
|
|
811
1441
|
}
|
|
812
1442
|
else {
|
|
@@ -821,6 +1451,7 @@ function updateDoorMesh(node, mesh) {
|
|
|
821
1451
|
hingeX,
|
|
822
1452
|
hingeSide: hingesSide,
|
|
823
1453
|
swingRotation: clampedSwingAngle * swingDirectionSign * hingeDirectionSign,
|
|
1454
|
+
openRotationY: (Math.PI / 2) * swingDirectionSign * hingeDirectionSign,
|
|
824
1455
|
segments,
|
|
825
1456
|
contentPadding,
|
|
826
1457
|
handle,
|
|
@@ -831,37 +1462,90 @@ function updateDoorMesh(node, mesh) {
|
|
|
831
1462
|
panicBar,
|
|
832
1463
|
panicBarHeight,
|
|
833
1464
|
doorHeight: height,
|
|
1465
|
+
openingShape,
|
|
1466
|
+
openingTopRadii: getDoorTopRadii(node, insideWidth, leafH),
|
|
1467
|
+
archHeight: node.archHeight ?? 0.45,
|
|
834
1468
|
});
|
|
835
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
|
+
}
|
|
836
1477
|
syncDoorCutout(node, mesh);
|
|
1478
|
+
// Guard: some degenerate door configs can leave a child mesh with an
|
|
1479
|
+
// empty (0-vertex) geometry — e.g. a zero-area extruded leaf frame.
|
|
1480
|
+
// Submitting such a mesh trips a WebGPU error ("Vertex buffer slot 0
|
|
1481
|
+
// … was not set" on a Draw(0, …)). Hide any empty mesh so it is never
|
|
1482
|
+
// drawn (it would render nothing anyway).
|
|
1483
|
+
hideEmptyGeometryMeshes(mesh);
|
|
1484
|
+
}
|
|
1485
|
+
function hideEmptyGeometryMeshes(root) {
|
|
1486
|
+
root.traverse((obj) => {
|
|
1487
|
+
const child = obj;
|
|
1488
|
+
if (!child.isMesh || !child.geometry)
|
|
1489
|
+
return;
|
|
1490
|
+
const position = child.geometry.getAttribute('position');
|
|
1491
|
+
if (!position || position.count === 0)
|
|
1492
|
+
child.visible = false;
|
|
1493
|
+
});
|
|
837
1494
|
}
|
|
838
1495
|
function syncDoorCutout(node, mesh) {
|
|
839
|
-
// ── Cutout
|
|
1496
|
+
// ── Cutout: invisible raycast hit target for the whole opening ──
|
|
840
1497
|
let cutout = mesh.getObjectByName('cutout');
|
|
841
1498
|
if (!cutout) {
|
|
842
1499
|
cutout = new THREE.Mesh();
|
|
843
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).
|
|
844
1507
|
mesh.add(cutout);
|
|
845
1508
|
}
|
|
846
1509
|
cutout.geometry.dispose();
|
|
847
|
-
|
|
1510
|
+
const depth = resolveOpeningCutoutProxyDepth(node);
|
|
1511
|
+
const openingShape = getEffectiveOpeningShape(node);
|
|
1512
|
+
if (openingShape === 'arch') {
|
|
848
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)), {
|
|
849
|
-
depth
|
|
1514
|
+
depth,
|
|
850
1515
|
bevelEnabled: false,
|
|
851
1516
|
curveSegments: 24,
|
|
852
1517
|
});
|
|
853
|
-
cutout.geometry.translate(0, 0, -
|
|
1518
|
+
cutout.geometry.translate(0, 0, -depth / 2);
|
|
854
1519
|
}
|
|
855
|
-
else if (
|
|
1520
|
+
else if (openingShape === 'rounded') {
|
|
856
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)), {
|
|
857
|
-
depth
|
|
1522
|
+
depth,
|
|
858
1523
|
bevelEnabled: false,
|
|
859
1524
|
curveSegments: 24,
|
|
860
1525
|
});
|
|
861
|
-
cutout.geometry.translate(0, 0, -
|
|
1526
|
+
cutout.geometry.translate(0, 0, -depth / 2);
|
|
862
1527
|
}
|
|
863
1528
|
else {
|
|
864
|
-
cutout.geometry = new THREE.BoxGeometry(node.width, node.height,
|
|
1529
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, depth);
|
|
865
1530
|
}
|
|
866
1531
|
cutout.visible = false;
|
|
867
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
|
+
}
|