@pascal-app/viewer 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
- package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/daylight-atmosphere.js +315 -0
- package/dist/components/viewer/drone-controller.d.ts +15 -0
- package/dist/components/viewer/drone-controller.d.ts.map +1 -0
- package/dist/components/viewer/drone-controller.js +194 -0
- package/dist/components/viewer/glb-interactive.d.ts +4 -18
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +32 -268
- package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
- package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene-motion.js +57 -0
- package/dist/components/viewer/glb-scene.d.ts +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +289 -49
- package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
- package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/glb-selection-callout.js +54 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
- package/dist/components/viewer/index.d.ts +5 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +30 -6
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +192 -5
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +150 -94
- package/dist/components/viewer/scene-environment.js +4 -5
- package/dist/components/viewer/selection-callout.d.ts +51 -0
- package/dist/components/viewer/selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/selection-callout.js +197 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
- package/dist/components/viewer/use-daylight-rooms.js +236 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/index.d.ts +21 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -7
- package/dist/lib/daylight/ground.d.ts +21 -0
- package/dist/lib/daylight/ground.d.ts.map +1 -0
- package/dist/lib/daylight/ground.js +53 -0
- package/dist/lib/daylight/location.d.ts +32 -0
- package/dist/lib/daylight/location.d.ts.map +1 -0
- package/dist/lib/daylight/location.js +74 -0
- package/dist/lib/daylight/look.d.ts +33 -0
- package/dist/lib/daylight/look.d.ts.map +1 -0
- package/dist/lib/daylight/look.js +13 -0
- package/dist/lib/daylight/material-hooks.d.ts +2 -0
- package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
- package/dist/lib/daylight/material-hooks.js +150 -0
- package/dist/lib/daylight/room-context.d.ts +12 -0
- package/dist/lib/daylight/room-context.d.ts.map +1 -0
- package/dist/lib/daylight/room-context.js +53 -0
- package/dist/lib/daylight/room-scene.d.ts +54 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +356 -0
- package/dist/lib/daylight/rooms.d.ts +262 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1223 -0
- package/dist/lib/daylight/runtime.d.ts +33 -0
- package/dist/lib/daylight/runtime.d.ts.map +1 -0
- package/dist/lib/daylight/runtime.js +42 -0
- package/dist/lib/daylight/sky.d.ts +26 -0
- package/dist/lib/daylight/sky.d.ts.map +1 -0
- package/dist/lib/daylight/sky.js +217 -0
- package/dist/lib/daylight/solar.d.ts +46 -0
- package/dist/lib/daylight/solar.d.ts.map +1 -0
- package/dist/lib/daylight/solar.js +156 -0
- package/dist/lib/device-tier.d.ts +29 -0
- package/dist/lib/device-tier.d.ts.map +1 -0
- package/dist/lib/device-tier.js +71 -0
- package/dist/lib/dispose-object3d.d.ts.map +1 -1
- package/dist/lib/dispose-object3d.js +4 -2
- package/dist/lib/drone-flight.d.ts +44 -0
- package/dist/lib/drone-flight.d.ts.map +1 -0
- package/dist/lib/drone-flight.js +121 -0
- package/dist/lib/editor-highlight.d.ts +16 -0
- package/dist/lib/editor-highlight.d.ts.map +1 -0
- package/dist/lib/editor-highlight.js +33 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -1
- package/dist/lib/foundation-materials.js +2 -4
- package/dist/lib/glb-merge.d.ts +23 -0
- package/dist/lib/glb-merge.d.ts.map +1 -0
- package/dist/lib/glb-merge.js +184 -0
- package/dist/lib/hardware-geometry.d.ts +21 -0
- package/dist/lib/hardware-geometry.d.ts.map +1 -0
- package/dist/lib/hardware-geometry.js +283 -0
- package/dist/lib/layer-pass.d.ts +2 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layer-pass.js +20 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +50 -23
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -1
- package/dist/lib/occlusion-culler.d.ts +34 -0
- package/dist/lib/occlusion-culler.d.ts.map +1 -0
- package/dist/lib/occlusion-culler.js +698 -0
- package/dist/lib/post-processing-resources.d.ts +4 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/post-processing-resources.js +5 -0
- package/dist/lib/readback-rows.d.ts +14 -0
- package/dist/lib/readback-rows.d.ts.map +1 -0
- package/dist/lib/readback-rows.js +31 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -1
- package/dist/lib/reveal-pose.js +3 -0
- package/dist/lib/scene-matrices.d.ts +12 -0
- package/dist/lib/scene-matrices.d.ts.map +1 -0
- package/dist/lib/scene-matrices.js +287 -0
- package/dist/lib/scene-pass-mrt.d.ts +5 -2
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
- package/dist/lib/scene-pass-mrt.js +7 -2
- package/dist/lib/scene-themes.d.ts +0 -2
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +0 -2
- package/dist/lib/scene-visibility.d.ts +2 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +12 -3
- package/dist/lib/site-surfaces.d.ts +1 -8
- package/dist/lib/site-surfaces.d.ts.map +1 -1
- package/dist/lib/site-surfaces.js +0 -19
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +24 -40
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +10 -8
- package/dist/lib/underground-ambiance.d.ts +0 -2
- package/dist/lib/underground-ambiance.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.js +0 -2
- package/dist/store/use-item-light-pool.d.ts +3 -6
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +12 -0
- package/dist/store/use-viewer.d.ts +72 -15
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +103 -18
- package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +11 -1
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +501 -145
- package/dist/systems/door/leaf-profile.d.ts +11 -0
- package/dist/systems/door/leaf-profile.d.ts.map +1 -0
- package/dist/systems/door/leaf-profile.js +140 -0
- package/dist/systems/interactive/callout-actions.d.ts +72 -0
- package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
- package/dist/systems/interactive/callout-actions.js +212 -0
- package/dist/systems/interactive/interactive-system.d.ts +5 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +44 -121
- package/dist/systems/interactive/opening-settled.d.ts +16 -0
- package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
- package/dist/systems/interactive/opening-settled.js +53 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +22 -214
- package/dist/systems/item-light/pooled-lights.d.ts +70 -0
- package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
- package/dist/systems/item-light/pooled-lights.js +449 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -1
- package/dist/systems/roof/roof-material-slots.d.ts +14 -0
- package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
- package/dist/systems/roof/roof-material-slots.js +13 -0
- package/dist/systems/roof/roof-materials.d.ts +25 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +85 -26
- package/dist/systems/roof/roof-system.d.ts +34 -1
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +477 -105
- package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
- package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-casing-geometry.js +102 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
- package/dist/systems/wall/opening-cutout-geometry.js +7 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +28 -35
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +34 -19
- package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
- package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
- package/dist/systems/wall/wall-t-junctions.js +149 -0
- package/dist/systems/window/window-animation-system.d.ts +3 -12
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +37 -122
- package/dist/systems/window/window-pose.d.ts +31 -0
- package/dist/systems/window/window-pose.d.ts.map +1 -0
- package/dist/systems/window/window-pose.js +152 -0
- package/dist/systems/window/window-roller-shutter.d.ts +8 -0
- package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
- package/dist/systems/window/window-roller-shutter.js +64 -0
- package/dist/systems/window/window-system.d.ts +2 -11
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +470 -303
- package/package.json +6 -6
- package/dist/systems/interactive/control-widget.d.ts +0 -10
- package/dist/systems/interactive/control-widget.d.ts.map +0 -1
- package/dist/systems/interactive/control-widget.js +0 -35
- package/dist/systems/interactive/procedural-controls.d.ts +0 -13
- package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
- package/dist/systems/interactive/procedural-controls.js +0 -17
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { clampDoorOperationState, DEFAULT_WALL_THICKNESS, DoorNode as DoorNodeSchema, getDoorRenderOpenAmount, getEffectiveNode, getOpeningWallPlacement, getWallThickness, sceneRegistry, scriptedSize, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { clampDoorOperationState, DEFAULT_WALL_THICKNESS, DoorNode as DoorNodeSchema, doorHandleStyle, getDoorOpeningSize, getDoorRenderOpenAmount, getEffectiveNode, getOpeningWallPlacement, getWallThickness, nodeRegistry, resolveOpeningCasingHost, resolveOpeningFrameDepth, sceneRegistry, scriptedSize, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
2
|
+
import { doorStyleLook, doorStylesOf, isLegacyDefaultDoorLook } from '@pascal-app/core/building';
|
|
2
3
|
import { useFrame } from '@react-three/fiber';
|
|
3
4
|
import { useEffect, useRef } from 'react';
|
|
4
5
|
import * as THREE from 'three';
|
|
5
6
|
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
7
|
+
import { flushPull, hingeJambPart, hingeLeafPart, knobOnRose, leverOnPlate, leverOnRose, leverOnRoseCurved, pullBar, roller, roundedBox, sphere, tube, } from '../../lib/hardware-geometry';
|
|
6
8
|
import { createDefaultMaterial, createMaterial, createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
|
|
7
9
|
import { timeSpan } from '../../lib/perf-tracks';
|
|
8
10
|
import { settleScriptedOpening } from '../../lib/scripted-opening';
|
|
9
11
|
import useViewer from '../../store/use-viewer';
|
|
12
|
+
import { buildOpeningCasingMeshes } from '../wall/opening-casing-geometry';
|
|
10
13
|
import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
|
|
14
|
+
import { buildPanelField, panelProfile } from './leaf-profile';
|
|
11
15
|
// Invisible material for root mesh — used as selection hitbox only
|
|
12
16
|
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
13
17
|
const defaultRevealMaterial = new THREE.MeshBasicMaterial({ color: '#7f766c' });
|
|
@@ -16,8 +20,8 @@ const defaultRevealMaterial = new THREE.MeshBasicMaterial({ color: '#7f766c' });
|
|
|
16
20
|
// only a fallback if the catalog ref ever fails to resolve.
|
|
17
21
|
const HARDWARE_DEFAULT_REF = 'library:metal-chrome';
|
|
18
22
|
// Door body defaults to a catalog colour (generic approach). Glass keeps the
|
|
19
|
-
// built-in FrontSide glass material
|
|
20
|
-
//
|
|
23
|
+
// built-in FrontSide glass material. Glass overrides must also stay FrontSide:
|
|
24
|
+
// DoubleSide glass poisons the WebGPU MRT scene pass.
|
|
21
25
|
const PANEL_DEFAULT_REF = 'library:preset-softwhite';
|
|
22
26
|
const FRAME_DEFAULT_REF = 'library:preset-softwhite';
|
|
23
27
|
const GLASS_DEFAULT_REF = 'library:preset-glass';
|
|
@@ -228,20 +232,38 @@ function resolveDoorSlotMaterial(node, slotId) {
|
|
|
228
232
|
}
|
|
229
233
|
return fallback;
|
|
230
234
|
}
|
|
231
|
-
function addBox(parent, material, w, h, d, x, y, z) {
|
|
232
|
-
const geometry =
|
|
233
|
-
|
|
235
|
+
function addBox(parent, material, w, h, d, x, y, z, customGeometry) {
|
|
236
|
+
const geometry = customGeometry ??
|
|
237
|
+
(currentDoorSlot === 'hardware' ? roundedBox(w, h, d) : new THREE.BoxGeometry(w, h, d));
|
|
238
|
+
if (geometry instanceof THREE.BoxGeometry && !geometry.userData.shared) {
|
|
239
|
+
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
240
|
+
}
|
|
234
241
|
const m = new THREE.Mesh(geometry, material);
|
|
235
242
|
m.position.set(x, y, z);
|
|
236
243
|
tagDoorSlot(m);
|
|
237
244
|
parent.add(m);
|
|
245
|
+
return m;
|
|
246
|
+
}
|
|
247
|
+
function addHardware(parent, geometry, x, y, z) {
|
|
248
|
+
const part = new THREE.Mesh(geometry, hardwareMaterial);
|
|
249
|
+
part.userData.slotId = 'hardware';
|
|
250
|
+
part.position.set(x, y, z);
|
|
251
|
+
parent.add(part);
|
|
252
|
+
return part;
|
|
238
253
|
}
|
|
239
|
-
function
|
|
254
|
+
function addPulls(parent, x, y, faceZ, centerZ = 0, recessed = false) {
|
|
255
|
+
for (const face of [-1, 1]) {
|
|
256
|
+
const pull = addHardware(parent, recessed ? flushPull() : pullBar(), x, y, centerZ + face * faceZ);
|
|
257
|
+
if (face < 0)
|
|
258
|
+
pull.rotateY(Math.PI);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function addRotatedBox(parent, material, w, h, d, x, y, z, rotationZ) {
|
|
240
262
|
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
241
263
|
applyWorldScaleBoxUVs(geometry, w, h, d);
|
|
242
264
|
const m = new THREE.Mesh(geometry, material);
|
|
243
265
|
m.position.set(x, y, z);
|
|
244
|
-
m.rotation.
|
|
266
|
+
m.rotation.z = rotationZ;
|
|
245
267
|
tagDoorSlot(m);
|
|
246
268
|
parent.add(m);
|
|
247
269
|
}
|
|
@@ -556,11 +578,46 @@ function createTopClippedRectShape(left, right, bottom, top, getBoundaryY) {
|
|
|
556
578
|
}
|
|
557
579
|
function disposeObject(object) {
|
|
558
580
|
object.traverse((child) => {
|
|
559
|
-
if (child instanceof THREE.Mesh)
|
|
581
|
+
if (child instanceof THREE.Mesh && !child.geometry.userData.shared)
|
|
560
582
|
child.geometry.dispose();
|
|
561
583
|
});
|
|
562
584
|
}
|
|
563
|
-
function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, renderPerimeterFrame = true, openingShape = 'rectangle', openingTopRadii, archHeight, archOuterSide, }) {
|
|
585
|
+
function addLeafSegmentContent({ addLeafBox: buildLeafBox, addLeafShape, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, flushPullAt, renderPerimeterFrame = true, openingShape = 'rectangle', openingTopRadii, archHeight, archOuterSide, }) {
|
|
586
|
+
leafWidth = Math.max(0.001, leafWidth - 0.006);
|
|
587
|
+
leafHeight = Math.max(0.001, leafHeight - 0.006);
|
|
588
|
+
let pullFaceZ = leafDepth / 2;
|
|
589
|
+
const addLeafBox = (material, w, h, d, x, y, z, geometry) => {
|
|
590
|
+
if (geometry) {
|
|
591
|
+
if (flushPullAt) {
|
|
592
|
+
geometry.computeBoundingBox();
|
|
593
|
+
pullFaceZ = Math.max(pullFaceZ, geometry.boundingBox.max.z);
|
|
594
|
+
}
|
|
595
|
+
return buildLeafBox(material, w, h, d, x, y, z, geometry);
|
|
596
|
+
}
|
|
597
|
+
if (!flushPullAt)
|
|
598
|
+
return buildLeafBox(material, w, h, d, x, y, z);
|
|
599
|
+
const left = x - w / 2;
|
|
600
|
+
const right = x + w / 2;
|
|
601
|
+
const bottom = y - h / 2;
|
|
602
|
+
const top = y + h / 2;
|
|
603
|
+
const cutLeft = Math.max(left, flushPullAt.x - 0.02);
|
|
604
|
+
const cutRight = Math.min(right, flushPullAt.x + 0.02);
|
|
605
|
+
const cutBottom = Math.max(bottom, flushPullAt.y - 0.06);
|
|
606
|
+
const cutTop = Math.min(top, flushPullAt.y + 0.06);
|
|
607
|
+
if (cutLeft >= cutRight || cutBottom >= cutTop) {
|
|
608
|
+
return buildLeafBox(material, w, h, d, x, y, z);
|
|
609
|
+
}
|
|
610
|
+
pullFaceZ = Math.max(pullFaceZ, Math.abs(z) + d / 2);
|
|
611
|
+
for (const [l, r, b, t] of [
|
|
612
|
+
[left, cutLeft, bottom, top],
|
|
613
|
+
[cutRight, right, bottom, top],
|
|
614
|
+
[cutLeft, cutRight, bottom, cutBottom],
|
|
615
|
+
[cutLeft, cutRight, cutTop, top],
|
|
616
|
+
]) {
|
|
617
|
+
if (r > l && t > b)
|
|
618
|
+
buildLeafBox(material, r - l, t - b, d, (l + r) / 2, (b + t) / 2, z);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
564
621
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
|
|
565
622
|
const shouldRenderFrame = hasLeafContent || keepFrameWhenEmpty;
|
|
566
623
|
const cpX = contentPadding[0];
|
|
@@ -626,14 +683,15 @@ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight
|
|
|
626
683
|
cx += colWidths[c];
|
|
627
684
|
const dividerLeft = cx;
|
|
628
685
|
const dividerRight = cx + seg.dividerThickness;
|
|
686
|
+
const dividerDepth = seg.type === 'panel' ? leafDepth : leafDepth + 0.001;
|
|
629
687
|
const dividerShape = hasShapedTop && addLeafShape
|
|
630
688
|
? createTopClippedRectShape(dividerLeft, dividerRight, segBottom, segTop, topBoundaryAtX)
|
|
631
689
|
: null;
|
|
632
690
|
if (dividerShape && addLeafShape) {
|
|
633
|
-
addLeafShape(baseMaterial, dividerShape,
|
|
691
|
+
addLeafShape(baseMaterial, dividerShape, dividerDepth);
|
|
634
692
|
}
|
|
635
693
|
else {
|
|
636
|
-
addLeafBox(baseMaterial, seg.dividerThickness, segH,
|
|
694
|
+
addLeafBox(baseMaterial, seg.dividerThickness, segH, dividerDepth, cx + seg.dividerThickness / 2, segCenterY, 0);
|
|
637
695
|
}
|
|
638
696
|
cx += seg.dividerThickness;
|
|
639
697
|
}
|
|
@@ -655,6 +713,23 @@ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight
|
|
|
655
713
|
else {
|
|
656
714
|
addLeafBox(glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0);
|
|
657
715
|
}
|
|
716
|
+
currentDoorSlot = 'panel';
|
|
717
|
+
const beadWidth = Math.min(0.012, colW / 4, segH / 4);
|
|
718
|
+
if (glassShape && addLeafShape) {
|
|
719
|
+
const bead = glassShape.clone();
|
|
720
|
+
const hole = createTopClippedRectShape(segmentLeft + beadWidth, segmentRight - beadWidth, segBottom + beadWidth, segTop - beadWidth, (x) => topBoundaryAtX(x) - beadWidth);
|
|
721
|
+
if (hole)
|
|
722
|
+
bead.holes.push(shapeToReversedPath(hole));
|
|
723
|
+
addLeafShape(baseMaterial, bead, leafDepth + 0.012);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
for (const face of [-1, 1])
|
|
727
|
+
for (const side of [-1, 1]) {
|
|
728
|
+
const z = face * (leafDepth / 2 + 0.003);
|
|
729
|
+
addLeafBox(baseMaterial, colW, beadWidth, 0.006, colX, segCenterY + (side * (segH - beadWidth)) / 2, z);
|
|
730
|
+
addLeafBox(baseMaterial, beadWidth, segH - 2 * beadWidth, 0.006, colX + (side * (colW - beadWidth)) / 2, segCenterY, z);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
658
733
|
}
|
|
659
734
|
else if (seg.type === 'panel') {
|
|
660
735
|
currentDoorSlot = 'panel';
|
|
@@ -663,81 +738,84 @@ function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight
|
|
|
663
738
|
const outerPanelShape = hasShapedTop && addLeafShape
|
|
664
739
|
? createTopClippedRectShape(segmentLeft, segmentRight, segBottom, segTop, topBoundaryAtX)
|
|
665
740
|
: null;
|
|
741
|
+
const panelW = colW - 2 * seg.panelInset;
|
|
742
|
+
const panelH = segH - 2 * seg.panelInset;
|
|
743
|
+
if (panelW <= 0.01 || panelH <= 0.01 || seg.panelDepth === 0) {
|
|
744
|
+
if (outerPanelShape && addLeafShape)
|
|
745
|
+
addLeafShape(baseMaterial, outerPanelShape, leafDepth);
|
|
746
|
+
else
|
|
747
|
+
addLeafBox(baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0);
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const fieldShape = createTopClippedRectShape(colX - panelW / 2, colX + panelW / 2, segBottom + seg.panelInset, segTop - seg.panelInset, hasShapedTop ? topBoundaryAtX : () => segTop);
|
|
751
|
+
if (!fieldShape)
|
|
752
|
+
continue;
|
|
666
753
|
if (outerPanelShape && addLeafShape) {
|
|
754
|
+
outerPanelShape.holes.push(shapeToReversedPath(fieldShape));
|
|
667
755
|
addLeafShape(baseMaterial, outerPanelShape, leafDepth);
|
|
668
756
|
}
|
|
669
757
|
else {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const panelH = segH - 2 * seg.panelInset;
|
|
674
|
-
if (panelW > 0.01 && panelH > 0.01) {
|
|
675
|
-
const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth);
|
|
676
|
-
const panelZ = leafDepth / 2 + effectiveDepth / 2;
|
|
677
|
-
const insetLeft = colX - panelW / 2;
|
|
678
|
-
const insetRight = colX + panelW / 2;
|
|
679
|
-
const insetTop = segTop - seg.panelInset;
|
|
680
|
-
const insetBottom = segBottom + seg.panelInset;
|
|
681
|
-
const innerPanelShape = hasShapedTop && addLeafShape
|
|
682
|
-
? createTopClippedRectShape(insetLeft, insetRight, insetBottom, insetTop, topBoundaryAtX)
|
|
683
|
-
: null;
|
|
684
|
-
if (innerPanelShape && addLeafShape) {
|
|
685
|
-
addLeafShape(baseMaterial, innerPanelShape, effectiveDepth);
|
|
686
|
-
}
|
|
687
|
-
else {
|
|
688
|
-
// A leaf is paneled on both faces, so the person sees the style from either side.
|
|
689
|
-
addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ);
|
|
690
|
-
addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, -panelZ);
|
|
758
|
+
for (const side of [-1, 1]) {
|
|
759
|
+
addLeafBox(baseMaterial, colW, seg.panelInset, leafDepth, colX, segCenterY + (side * (segH - seg.panelInset)) / 2, 0);
|
|
760
|
+
addLeafBox(baseMaterial, seg.panelInset, panelH, leafDepth, colX + (side * (colW - seg.panelInset)) / 2, segCenterY, 0);
|
|
691
761
|
}
|
|
692
762
|
}
|
|
763
|
+
addLeafBox(baseMaterial, panelW, panelH, leafDepth, 0, 0, 0, buildPanelField(fieldShape.getPoints(), leafDepth, seg.panelDepth, seg.panelInset, flushPullAt));
|
|
693
764
|
}
|
|
694
765
|
}
|
|
695
766
|
segY -= segH;
|
|
696
767
|
}
|
|
768
|
+
return pullFaceZ;
|
|
697
769
|
}
|
|
698
|
-
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, }) {
|
|
770
|
+
function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, frameDepth, hingeX, hingeSide, swingRotation, openRotationY, segments, contentPadding, handle, handleStyle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarStyle, panicBarRods, panicBarHeight, doorHeight, openingShape, openingTopRadii, archHeight, roundedBoundary, archedBoundary, archOuterSide, }) {
|
|
699
771
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
|
|
700
772
|
const leafGroup = new THREE.Group();
|
|
701
|
-
|
|
773
|
+
const swingFace = Math.sign(openRotationY) * (hingeSide === 'right' ? 1 : -1);
|
|
774
|
+
const pivotZ = (swingFace * leafDepth) / 2;
|
|
775
|
+
leafGroup.position.set(hingeX, 0, pivotZ);
|
|
702
776
|
leafGroup.rotation.y = swingRotation;
|
|
703
777
|
// Marks this group as the swing leaf and records its fully-open angle so the
|
|
704
778
|
// GLB exporter can bake an open/close animation clip from a single pose. The
|
|
705
779
|
// exporter strips this marker before writing the file.
|
|
706
780
|
leafGroup.userData.pascalSwingLeaf = { axis: 'y', openRotationY };
|
|
707
781
|
mesh.add(leafGroup);
|
|
708
|
-
const addLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z);
|
|
709
|
-
const addLeafShape = (material, shape, depth) => addShapeAt(leafGroup, material, shape, depth, -hingeX, 0,
|
|
782
|
+
const addLeafBox = (material, w, h, d, x, y, z, geometry) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z - pivotZ, geometry);
|
|
783
|
+
const addLeafShape = (material, shape, depth) => addShapeAt(leafGroup, material, shape, depth, -hingeX, 0, -pivotZ);
|
|
710
784
|
const localLeafCenterX = leafCenterX - hingeX;
|
|
711
785
|
const leafBottom = leafCenterY - leafHeight / 2;
|
|
712
786
|
const leafTop = leafCenterY + leafHeight / 2;
|
|
713
787
|
const usesShapedLeafFrame = openingShape === 'rounded' || openingShape === 'arch';
|
|
788
|
+
const clearLeafWidth = Math.max(0.001, leafWidth - 0.006);
|
|
789
|
+
const clearLeafHeight = Math.max(0.001, leafHeight - 0.006);
|
|
714
790
|
if (usesShapedLeafFrame && hasLeafContent) {
|
|
791
|
+
const clearLeafBottom = leafCenterY - clearLeafHeight / 2;
|
|
792
|
+
const clearLeafTop = leafCenterY + clearLeafHeight / 2;
|
|
715
793
|
currentDoorSlot = 'panel';
|
|
716
794
|
if (openingShape === 'rounded') {
|
|
717
795
|
const roundedLeafShape = roundedBoundary
|
|
718
|
-
? createRoundedClippedLeafFrameShape(leafCenterX -
|
|
719
|
-
: createRoundedLeafFrameShape(
|
|
796
|
+
? createRoundedClippedLeafFrameShape(leafCenterX - clearLeafWidth / 2, leafCenterX + clearLeafWidth / 2, clearLeafBottom, clearLeafTop, roundedBoundary.fullLeft, roundedBoundary.fullRight, roundedBoundary.radii, contentPadding[0], contentPadding[1])
|
|
797
|
+
: createRoundedLeafFrameShape(clearLeafWidth, clearLeafBottom, clearLeafTop, openingTopRadii, contentPadding[0], contentPadding[1]);
|
|
720
798
|
if (roundedLeafShape) {
|
|
721
799
|
if (roundedBoundary) {
|
|
722
800
|
addLeafShape(baseMaterial, roundedLeafShape, leafDepth);
|
|
723
801
|
}
|
|
724
802
|
else {
|
|
725
|
-
addShapeAt(leafGroup, baseMaterial, roundedLeafShape, leafDepth, localLeafCenterX, 0,
|
|
803
|
+
addShapeAt(leafGroup, baseMaterial, roundedLeafShape, leafDepth, localLeafCenterX, 0, -pivotZ);
|
|
726
804
|
}
|
|
727
805
|
}
|
|
728
806
|
}
|
|
729
807
|
else if (openingShape === 'arch') {
|
|
730
808
|
const archedLeafShape = archOuterSide
|
|
731
|
-
? createOneSidedArchLeafFrameShape(leafCenterX -
|
|
809
|
+
? createOneSidedArchLeafFrameShape(leafCenterX - clearLeafWidth / 2, leafCenterX + clearLeafWidth / 2, clearLeafBottom, clearLeafTop, archHeight, contentPadding[0], contentPadding[1], archOuterSide)
|
|
732
810
|
: archedBoundary
|
|
733
|
-
? createArchedClippedLeafFrameShape(leafCenterX -
|
|
734
|
-
: createArchedLeafFrameShape(
|
|
811
|
+
? createArchedClippedLeafFrameShape(leafCenterX - clearLeafWidth / 2, leafCenterX + clearLeafWidth / 2, clearLeafBottom, clearLeafTop, archedBoundary.fullLeft, archedBoundary.fullRight, archedBoundary.archHeight, contentPadding[0], contentPadding[1])
|
|
812
|
+
: createArchedLeafFrameShape(clearLeafWidth, clearLeafBottom, clearLeafTop, getClampedArchHeight(clearLeafWidth, clearLeafHeight, archHeight), contentPadding[0], contentPadding[1]);
|
|
735
813
|
if (archedLeafShape) {
|
|
736
814
|
if (archOuterSide || archedBoundary) {
|
|
737
815
|
addLeafShape(baseMaterial, archedLeafShape, leafDepth);
|
|
738
816
|
}
|
|
739
817
|
else {
|
|
740
|
-
addShapeAt(leafGroup, baseMaterial, archedLeafShape, leafDepth, localLeafCenterX, 0,
|
|
818
|
+
addShapeAt(leafGroup, baseMaterial, archedLeafShape, leafDepth, localLeafCenterX, 0, -pivotZ);
|
|
741
819
|
}
|
|
742
820
|
}
|
|
743
821
|
}
|
|
@@ -761,34 +839,158 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
|
|
|
761
839
|
if (hasLeafContent && handle) {
|
|
762
840
|
currentDoorSlot = 'hardware';
|
|
763
841
|
const handleY = handleHeight - doorHeight / 2;
|
|
764
|
-
const faceZ = leafDepth / 2;
|
|
765
842
|
const handleX = handleSide === 'right'
|
|
766
|
-
? leafCenterX + leafWidth / 2 - 0.
|
|
767
|
-
: leafCenterX - leafWidth / 2 + 0.
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
843
|
+
? leafCenterX + leafWidth / 2 - 0.06
|
|
844
|
+
: leafCenterX - leafWidth / 2 + 0.06;
|
|
845
|
+
const contentH = clearLeafHeight - 2 * contentPadding[1];
|
|
846
|
+
const totalRatio = segments.reduce((sum, segment) => sum + segment.heightRatio, 0);
|
|
847
|
+
let segmentTop = leafCenterY + contentH / 2;
|
|
848
|
+
let panelDepth = 0;
|
|
849
|
+
for (const segment of segments) {
|
|
850
|
+
const segmentBottom = segmentTop - (contentH * segment.heightRatio) / totalRatio;
|
|
851
|
+
if (handleY >= segmentBottom && handleY <= segmentTop && segment.type === 'panel') {
|
|
852
|
+
const contentW = clearLeafWidth - 2 * contentPadding[0];
|
|
853
|
+
const sum = segment.columnRatios.reduce((a, b) => a + b, 0);
|
|
854
|
+
const usableW = contentW - (segment.columnRatios.length - 1) * segment.dividerThickness;
|
|
855
|
+
let columnLeft = leafCenterX - contentW / 2;
|
|
856
|
+
for (const ratio of segment.columnRatios) {
|
|
857
|
+
const columnW = (usableW * ratio) / sum;
|
|
858
|
+
const fieldW = columnW - 2 * segment.panelInset;
|
|
859
|
+
const fieldH = segmentTop - segmentBottom - 2 * segment.panelInset;
|
|
860
|
+
const edgeDistance = Math.min(fieldW / 2 - Math.abs(handleX - columnLeft - columnW / 2), fieldH / 2 - Math.abs(handleY - (segmentTop + segmentBottom) / 2));
|
|
861
|
+
if (edgeDistance > 0 && fieldW > 0.01 && fieldH > 0.01 && segment.panelDepth !== 0) {
|
|
862
|
+
const profile = panelProfile(leafDepth, segment.panelDepth, segment.panelInset, fieldW, fieldH);
|
|
863
|
+
panelDepth = profile[profile.length - 1].depth;
|
|
864
|
+
for (let i = 1; i < profile.length; i++) {
|
|
865
|
+
const outside = profile[i - 1];
|
|
866
|
+
const inside = profile[i];
|
|
867
|
+
if (edgeDistance <= inside.inset) {
|
|
868
|
+
const t = (edgeDistance - outside.inset) / (inside.inset - outside.inset);
|
|
869
|
+
panelDepth = outside.depth + t * (inside.depth - outside.depth);
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
columnLeft += columnW + segment.dividerThickness;
|
|
876
|
+
}
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
segmentTop = segmentBottom;
|
|
880
|
+
}
|
|
881
|
+
const faceZ = leafDepth / 2 + panelDepth;
|
|
882
|
+
// Panic hardware owns the push face (+z): no lever beside the bar there.
|
|
883
|
+
const leverFaces = (handleBothSides ? [1, -1] : [1]).filter((face) => !(panicBar && face > 0));
|
|
884
|
+
for (const face of leverFaces) {
|
|
885
|
+
const lever = addHardware(leafGroup, handleStyle === 'knob'
|
|
886
|
+
? knobOnRose()
|
|
887
|
+
: handleStyle === 'bar'
|
|
888
|
+
? pullBar(true)
|
|
889
|
+
: handleStyle === 'plate'
|
|
890
|
+
? leverOnPlate()
|
|
891
|
+
: handleStyle === 'lever-curved'
|
|
892
|
+
? leverOnRoseCurved()
|
|
893
|
+
: leverOnRose(), handleX - hingeX, handleY, face * faceZ - pivotZ);
|
|
894
|
+
if (face < 0)
|
|
895
|
+
lever.rotateY(Math.PI);
|
|
896
|
+
const gripDirection = face * (hingeSide === 'left' ? -1 : 1);
|
|
897
|
+
if (gripDirection < 0 && handleStyle !== 'bar' && handleStyle !== 'knob')
|
|
898
|
+
lever.rotateZ(Math.PI);
|
|
773
899
|
}
|
|
774
900
|
}
|
|
775
901
|
if (hasLeafContent && doorCloser) {
|
|
776
902
|
currentDoorSlot = 'hardware';
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
903
|
+
const latchSign = hingeSide === 'left' ? 1 : -1;
|
|
904
|
+
const offsetScale = Math.min(1, leafWidth / 0.4);
|
|
905
|
+
const closerY = leafTop - 0.04;
|
|
906
|
+
const bodyX = hingeX + latchSign * 0.16 * offsetScale;
|
|
907
|
+
const pullFace = -swingFace;
|
|
908
|
+
const bodyZ = pullFace * (leafDepth / 2 + 0.03);
|
|
909
|
+
addLeafBox(hardwareMaterial, 0.28, 0.055, 0.055, bodyX, closerY, bodyZ);
|
|
910
|
+
const armY = closerY + 0.0275;
|
|
911
|
+
// The latch-end spindle recedes from the head shoe as the leaf opens.
|
|
912
|
+
const closedPivot = new THREE.Vector3(hingeX + latchSign * 0.27 * offsetScale, armY, bodyZ);
|
|
913
|
+
const closedElbow = new THREE.Vector3(hingeX + latchSign * 0.18 * offsetScale, armY, pullFace * (frameDepth / 2 + 0.19));
|
|
914
|
+
const shoe = new THREE.Vector3(hingeX + latchSign * 0.34 * offsetScale, armY, pullFace * (frameDepth / 2 + 0.015));
|
|
915
|
+
// The shoe overlaps the head face; its underside shares the body pivot's plane below the soffit.
|
|
916
|
+
addHardware(mesh, roundedBox(0.09, 0.025, 0.03), shoe.x, armY + 0.0125, shoe.z);
|
|
917
|
+
const pivotOffset = new THREE.Vector3(hingeX, 0, pivotZ);
|
|
918
|
+
const axis = new THREE.Vector3(0, 1, 0);
|
|
919
|
+
const pivot = closedPivot
|
|
920
|
+
.clone()
|
|
921
|
+
.sub(pivotOffset)
|
|
922
|
+
.applyAxisAngle(axis, swingRotation)
|
|
923
|
+
.add(pivotOffset);
|
|
924
|
+
// Use the cached tubes' millimetre lengths as the actual linkage lengths.
|
|
925
|
+
const l1 = Math.round(closedPivot.distanceTo(closedElbow) * 1000) / 1000;
|
|
926
|
+
const l2 = Math.round(shoe.distanceTo(closedElbow) * 1000) / 1000;
|
|
927
|
+
const delta = shoe.clone().sub(pivot);
|
|
928
|
+
const distance = delta.length();
|
|
929
|
+
const direction = distance > 1e-9 ? delta.divideScalar(distance) : new THREE.Vector3(latchSign, 0, 0);
|
|
930
|
+
// Unreachable anchors keep fixed-length links collinear rather than stretching them.
|
|
931
|
+
const reach = Math.max(Math.abs(l1 - l2), Math.min(l1 + l2, distance));
|
|
932
|
+
const along = reach > 1e-9 ? (l1 * l1 - l2 * l2 + reach * reach) / (2 * reach) : 0;
|
|
933
|
+
const bow = Math.sqrt(Math.max(0, l1 * l1 - along * along));
|
|
934
|
+
const normal = new THREE.Vector3(-direction.z, 0, direction.x);
|
|
935
|
+
if (normal.z * pullFace < 0)
|
|
936
|
+
normal.negate();
|
|
937
|
+
const elbow = pivot.clone().addScaledVector(direction, along).addScaledVector(normal, bow);
|
|
938
|
+
for (const [parent, radius, start, end] of [
|
|
939
|
+
[leafGroup, 0.0075, pivot, elbow],
|
|
940
|
+
[mesh, 0.006, shoe, elbow],
|
|
941
|
+
]) {
|
|
942
|
+
const localStart = start.clone();
|
|
943
|
+
const localEnd = end.clone();
|
|
944
|
+
if (parent === leafGroup) {
|
|
945
|
+
localStart.sub(pivotOffset).applyAxisAngle(axis, -swingRotation);
|
|
946
|
+
localEnd.sub(pivotOffset).applyAxisAngle(axis, -swingRotation);
|
|
947
|
+
}
|
|
948
|
+
const vector = localEnd.clone().sub(localStart);
|
|
949
|
+
const length = parent === leafGroup ? l1 : l2;
|
|
950
|
+
const center = localStart.clone().addScaledVector(vector.clone().normalize(), length / 2);
|
|
951
|
+
const arm = addHardware(parent, tube(radius, length), center.x, center.y, center.z);
|
|
952
|
+
arm.quaternion.setFromUnitVectors(axis, vector.normalize());
|
|
953
|
+
}
|
|
954
|
+
addHardware(mesh, sphere(0.009), elbow.x, elbow.y, elbow.z);
|
|
780
955
|
}
|
|
781
956
|
if (hasLeafContent && panicBar) {
|
|
782
|
-
currentDoorSlot = 'hardware';
|
|
783
957
|
const barY = panicBarHeight - doorHeight / 2;
|
|
784
|
-
|
|
958
|
+
const latchSign = hingeSide === 'left' ? 1 : -1;
|
|
959
|
+
const latchX = leafCenterX + latchSign * (leafWidth / 2 - 0.055);
|
|
960
|
+
const hingeEndX = leafCenterX - latchSign * (leafWidth / 2 - 0.04);
|
|
961
|
+
const faceZ = (swingFace * leafDepth) / 2;
|
|
962
|
+
const device = (geometry, x, y, offFace) => {
|
|
963
|
+
const part = addHardware(leafGroup, geometry, x - hingeX, y, faceZ + swingFace * offFace - pivotZ);
|
|
964
|
+
if (swingFace < 0)
|
|
965
|
+
part.rotateY(Math.PI);
|
|
966
|
+
return part;
|
|
967
|
+
};
|
|
968
|
+
const touchbar = panicBarStyle === 'touchbar';
|
|
969
|
+
const caseHeight = touchbar ? 0.1 : 0.07;
|
|
970
|
+
device(roundedBox(touchbar ? 0.09 : 0.075, caseHeight, touchbar ? 0.055 : 0.06), latchX, barY, touchbar ? 0.0275 : 0.03);
|
|
971
|
+
device(roundedBox(touchbar ? 0.035 : 0.04, 0.07, 0.05), hingeEndX, barY, 0.025);
|
|
972
|
+
const barLength = Math.abs(latchX - hingeEndX);
|
|
973
|
+
const bar = device(touchbar ? roundedBox(barLength, 0.06, 0.03) : tube(0.0125, barLength), (latchX + hingeEndX) / 2, barY, touchbar ? 0.035 : 0.045);
|
|
974
|
+
if (!touchbar)
|
|
975
|
+
bar.rotateZ(Math.PI / 2);
|
|
976
|
+
if (panicBarRods) {
|
|
977
|
+
for (const end of [-1, 1]) {
|
|
978
|
+
const latchY = end > 0 ? leafTop - 0.05 : leafBottom + 0.05;
|
|
979
|
+
device(roundedBox(0.035, 0.06, 0.04), latchX, latchY, 0.02);
|
|
980
|
+
const startY = barY + (end * caseHeight) / 2;
|
|
981
|
+
const endY = latchY - end * 0.03;
|
|
982
|
+
const rodY = (startY + endY) / 2;
|
|
983
|
+
device(tube(0.006, Math.abs(endY - startY)), latchX, rodY, 0.025);
|
|
984
|
+
device(roundedBox(0.03, 0.02, 0.03), latchX, rodY, 0.015);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
785
987
|
}
|
|
786
988
|
if (hasLeafContent) {
|
|
787
989
|
currentDoorSlot = 'hardware';
|
|
788
|
-
const
|
|
990
|
+
const leafHinges = new THREE.Group();
|
|
991
|
+
leafHinges.name = 'door-leaf-hinges';
|
|
992
|
+
leafGroup.add(leafHinges);
|
|
789
993
|
const hingeH = 0.1;
|
|
790
|
-
const hingeW = 0.024;
|
|
791
|
-
const hingeD = leafDepth + 0.016;
|
|
792
994
|
const hingeRadii = roundedBoundary?.radii ?? openingTopRadii;
|
|
793
995
|
const cornerDrop = openingShape === 'rounded'
|
|
794
996
|
? hingeSide === 'left'
|
|
@@ -802,7 +1004,14 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
|
|
|
802
1004
|
for (const hingeY of [leafBottom + 0.25, (leafBottom + leafTop) / 2, leafTop - 0.25]) {
|
|
803
1005
|
if (hingeY + hingeH / 2 > straightEdgeTop + 1e-6)
|
|
804
1006
|
continue;
|
|
805
|
-
|
|
1007
|
+
const jambPart = addHardware(mesh, hingeJambPart(), hingeX, hingeY, pivotZ);
|
|
1008
|
+
const leafPart = addHardware(leafHinges, hingeLeafPart(), 0, hingeY, 0);
|
|
1009
|
+
for (const part of [jambPart, leafPart]) {
|
|
1010
|
+
if (hingeSide === 'right')
|
|
1011
|
+
part.rotateZ(Math.PI);
|
|
1012
|
+
if (swingFace < 0)
|
|
1013
|
+
part.rotateX(Math.PI);
|
|
1014
|
+
}
|
|
806
1015
|
}
|
|
807
1016
|
}
|
|
808
1017
|
// When the leaf is swung open it projects into the room and would otherwise
|
|
@@ -821,6 +1030,34 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
|
|
|
821
1030
|
// registered mesh, and the GLB exporter poses an export clone to sample the
|
|
822
1031
|
// open/close keyframes for a baked animation clip.
|
|
823
1032
|
const SLIDING_ACTIVE_PANEL_NAME = 'door-sliding-active';
|
|
1033
|
+
function slidingPanels(node, width) {
|
|
1034
|
+
const count = Math.max(2, Math.min(4, node.leafCount ?? 2));
|
|
1035
|
+
const sign = node.slideDirection === 'right' ? 1 : -1;
|
|
1036
|
+
const pitch = width / count;
|
|
1037
|
+
return Array.from({ length: count }, (_, index) => {
|
|
1038
|
+
const front = node.slidingStyle === 'bypass'
|
|
1039
|
+
? index % 2 === (count === 3 || sign < 0 ? 1 : 0)
|
|
1040
|
+
: count === 4
|
|
1041
|
+
? index === 1 || index === 2
|
|
1042
|
+
: count === 3
|
|
1043
|
+
? index === 1
|
|
1044
|
+
: index === (sign < 0 ? 1 : 0);
|
|
1045
|
+
const travel = front
|
|
1046
|
+
? (count === 4 && node.slidingStyle !== 'bypass' ? (index === 1 ? -1 : 1) : sign) *
|
|
1047
|
+
(count === 2 ? width * 0.44 : pitch)
|
|
1048
|
+
: 0;
|
|
1049
|
+
return {
|
|
1050
|
+
index,
|
|
1051
|
+
front,
|
|
1052
|
+
travel,
|
|
1053
|
+
x: count === 2 ? (index === 0 ? -1 : 1) * width * 0.23 : -width / 2 + pitch * (index + 0.5),
|
|
1054
|
+
width: count === 2 ? width * 0.54 : pitch + 0.02,
|
|
1055
|
+
};
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
function slidingPanelName(index, first) {
|
|
1059
|
+
return index === first ? SLIDING_ACTIVE_PANEL_NAME : `${SLIDING_ACTIVE_PANEL_NAME}-${index}`;
|
|
1060
|
+
}
|
|
824
1061
|
const POCKET_LEAF_NAME = 'door-pocket-leaf';
|
|
825
1062
|
const BARN_LEAF_NAME = 'door-barn-leaf';
|
|
826
1063
|
const TILTUP_LEAF_NAME = 'door-tiltup-leaf';
|
|
@@ -857,12 +1094,17 @@ export function poseDoorMovingParts(node, mesh, value) {
|
|
|
857
1094
|
const leafCenterY = -frameThickness / 2;
|
|
858
1095
|
switch (node.doorType) {
|
|
859
1096
|
case 'sliding': {
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
1097
|
+
const panels = slidingPanels(node, insideWidth);
|
|
1098
|
+
const first = panels.find((panel) => panel.front).index;
|
|
1099
|
+
let posed = false;
|
|
1100
|
+
for (const panel of panels) {
|
|
1101
|
+
const group = mesh.getObjectByName(slidingPanelName(panel.index, first));
|
|
1102
|
+
if (!group)
|
|
1103
|
+
continue;
|
|
1104
|
+
group.position.x = panel.travel * t;
|
|
1105
|
+
posed = true;
|
|
1106
|
+
}
|
|
1107
|
+
return posed;
|
|
866
1108
|
}
|
|
867
1109
|
case 'pocket': {
|
|
868
1110
|
const group = mesh.getObjectByName(POCKET_LEAF_NAME);
|
|
@@ -894,7 +1136,7 @@ export function poseDoorMovingParts(node, mesh, value) {
|
|
|
894
1136
|
return true;
|
|
895
1137
|
}
|
|
896
1138
|
case 'folding': {
|
|
897
|
-
const panelCount = node.leafCount === 2 ? 2 : 4;
|
|
1139
|
+
const panelCount = node.leafCount >= 6 ? node.leafCount : node.leafCount === 2 ? 2 : 4;
|
|
898
1140
|
const foldAngle = Math.PI * 0.44 * t;
|
|
899
1141
|
// Each panel group is parented to the previous, so its rotation is the
|
|
900
1142
|
// joint angle (the change in absolute segment direction), not the absolute
|
|
@@ -904,7 +1146,7 @@ export function poseDoorMovingParts(node, mesh, value) {
|
|
|
904
1146
|
let prevDirection = 0;
|
|
905
1147
|
for (let index = 0; index < panelCount; index++) {
|
|
906
1148
|
const group = mesh.getObjectByName(`${FOLDING_PANEL_NAME}${index}`);
|
|
907
|
-
const direction = index % 2 === 0 ? -1 : 1;
|
|
1149
|
+
const direction = (index % 2 === 0 ? -1 : 1) * (panelCount > 4 && node.slideDirection === 'right' ? -1 : 1);
|
|
908
1150
|
if (group) {
|
|
909
1151
|
posed = true;
|
|
910
1152
|
// Set the full triple (not just `.y`): when the export clones and
|
|
@@ -963,8 +1205,9 @@ export function poseDoorMovingParts(node, mesh, value) {
|
|
|
963
1205
|
return false;
|
|
964
1206
|
}
|
|
965
1207
|
}
|
|
966
|
-
function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, leafCount, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
967
|
-
const panelCount = leafCount === 2 ? 2 : 4;
|
|
1208
|
+
function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, leafCount, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
1209
|
+
const panelCount = leafCount >= 6 ? leafCount : leafCount === 2 ? 2 : 4;
|
|
1210
|
+
const sign = panelCount > 4 && slideDirection === 'right' ? -1 : 1;
|
|
968
1211
|
const panelLength = insideWidth / panelCount;
|
|
969
1212
|
currentDoorSlot = 'hardware';
|
|
970
1213
|
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);
|
|
@@ -973,17 +1216,20 @@ function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
973
1216
|
// x), starting from the left jamb. At rest every joint is straight, so the
|
|
974
1217
|
// panels lie flat across the opening; `poseDoorMovingParts` folds the joints.
|
|
975
1218
|
let parent = mesh;
|
|
1219
|
+
let pullFaceZ = leafDepth / 2;
|
|
1220
|
+
const handleY = handleHeight - doorHeight / 2;
|
|
976
1221
|
for (let index = 0; index < panelCount; index++) {
|
|
977
1222
|
const group = new THREE.Group();
|
|
978
1223
|
group.name = `${FOLDING_PANEL_NAME}${index}`;
|
|
979
|
-
group.position.set(index === 0 ? -insideWidth / 2 : panelLength, 0, 0);
|
|
1224
|
+
group.position.set(index === 0 ? (-sign * insideWidth) / 2 : sign * panelLength, 0, 0);
|
|
980
1225
|
parent.add(group);
|
|
981
1226
|
// Panel content runs from local x=0 to x=panelLength, so it is centred at
|
|
982
1227
|
// panelLength/2 within the group (the segment helper centres on leafCenterX).
|
|
983
1228
|
currentDoorSlot = undefined;
|
|
984
|
-
const addFoldingLeafBox = (material, w, h, d, x, y, z) => addBox(group, material, w, h, d, panelLength / 2 + x, y, z);
|
|
985
|
-
addLeafSegmentContent({
|
|
1229
|
+
const addFoldingLeafBox = (material, w, h, d, x, y, z, geometry) => addBox(group, material, w, h, d, sign * (panelLength / 2 + x), y, z, geometry);
|
|
1230
|
+
pullFaceZ = addLeafSegmentContent({
|
|
986
1231
|
addLeafBox: addFoldingLeafBox,
|
|
1232
|
+
flushPullAt: index === panelCount - 1 ? { x: panelLength / 2 - 0.035, y: handleY } : undefined,
|
|
987
1233
|
leafWidth: Math.max(0.08, panelLength),
|
|
988
1234
|
leafHeight,
|
|
989
1235
|
leafCenterX: 0,
|
|
@@ -996,15 +1242,13 @@ function addFoldingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
996
1242
|
// Reveal posts at the panel's hinge edges (local x=0 and x=panelLength).
|
|
997
1243
|
currentDoorSlot = undefined;
|
|
998
1244
|
for (const px of [0, panelLength]) {
|
|
999
|
-
addBox(group, revealMaterial, 0.018, leafHeight * 0.92, leafDepth + 0.016, px, leafCenterY, 0);
|
|
1245
|
+
addBox(group, revealMaterial, 0.018, leafHeight * 0.92, leafDepth + 0.016, sign * px, leafCenterY, 0);
|
|
1000
1246
|
}
|
|
1001
1247
|
parent = group;
|
|
1002
1248
|
}
|
|
1003
1249
|
// Handle on the free end of the last panel (its local x=panelLength edge).
|
|
1004
|
-
const handleY = handleHeight - doorHeight / 2;
|
|
1005
1250
|
currentDoorSlot = 'hardware';
|
|
1006
|
-
|
|
1007
|
-
addBox(parent, hardwareMaterial, 0.035, 0.16, leafDepth + 0.035, panelLength - 0.035, handleY, -0.045);
|
|
1251
|
+
addPulls(parent, sign * (panelLength - 0.035), handleY, pullFaceZ, 0, true);
|
|
1008
1252
|
}
|
|
1009
1253
|
function addPocketDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
1010
1254
|
const slideSign = slideDirection === 'right' ? 1 : -1;
|
|
@@ -1023,9 +1267,10 @@ function addPocketDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
1023
1267
|
const leafGroup = new THREE.Group();
|
|
1024
1268
|
leafGroup.name = POCKET_LEAF_NAME;
|
|
1025
1269
|
mesh.add(leafGroup);
|
|
1026
|
-
const addPocketLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x, y, z);
|
|
1027
|
-
addLeafSegmentContent({
|
|
1270
|
+
const addPocketLeafBox = (material, w, h, d, x, y, z, geometry) => addBox(leafGroup, material, w, h, d, x, y, z, geometry);
|
|
1271
|
+
const pullFaceZ = addLeafSegmentContent({
|
|
1028
1272
|
addLeafBox: addPocketLeafBox,
|
|
1273
|
+
flushPullAt: { x: handleX, y: handleY },
|
|
1029
1274
|
leafWidth,
|
|
1030
1275
|
leafHeight,
|
|
1031
1276
|
leafCenterX: 0,
|
|
@@ -1035,8 +1280,7 @@ function addPocketDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth,
|
|
|
1035
1280
|
contentPadding,
|
|
1036
1281
|
});
|
|
1037
1282
|
currentDoorSlot = 'hardware';
|
|
1038
|
-
|
|
1039
|
-
addBox(leafGroup, hardwareMaterial, 0.03, 0.18, leafDepth + 0.03, handleX, handleY, -leafDepth / 2 - 0.02);
|
|
1283
|
+
addPulls(leafGroup, handleX, handleY, pullFaceZ, 0, true);
|
|
1040
1284
|
}
|
|
1041
1285
|
function addBarnDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
1042
1286
|
const slideSign = slideDirection === 'right' ? 1 : -1;
|
|
@@ -1050,15 +1294,16 @@ function addBarnDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, fr
|
|
|
1050
1294
|
// in the opening); `poseDoorMovingParts` slides the group along the rail. The
|
|
1051
1295
|
// rail and end stops stay static on the mesh.
|
|
1052
1296
|
const handleX = -slideSign * (leafWidth / 2 - 0.075);
|
|
1053
|
-
const wheelY = trackY
|
|
1297
|
+
const wheelY = trackY + 0.035 / 2 + 0.0425;
|
|
1054
1298
|
currentDoorSlot = 'hardware';
|
|
1055
1299
|
addBox(mesh, hardwareMaterial, railLength, 0.035, 0.035, railCenterX, trackY, faceZ + 0.01);
|
|
1056
|
-
|
|
1057
|
-
|
|
1300
|
+
for (const end of [-1, 1]) {
|
|
1301
|
+
addHardware(mesh, roller(0.02, 0.03), railCenterX + (end * railLength) / 2, trackY, faceZ + 0.01);
|
|
1302
|
+
}
|
|
1058
1303
|
const leafGroup = new THREE.Group();
|
|
1059
1304
|
leafGroup.name = BARN_LEAF_NAME;
|
|
1060
1305
|
mesh.add(leafGroup);
|
|
1061
|
-
const addBarnLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x, y, faceZ + z);
|
|
1306
|
+
const addBarnLeafBox = (material, w, h, d, x, y, z, geometry) => addBox(leafGroup, material, w, h, d, x, y, faceZ + z, geometry);
|
|
1062
1307
|
addLeafSegmentContent({
|
|
1063
1308
|
addLeafBox: addBarnLeafBox,
|
|
1064
1309
|
leafWidth,
|
|
@@ -1075,62 +1320,49 @@ function addBarnDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, fr
|
|
|
1075
1320
|
addRotatedBox(leafGroup, revealMaterial, 0.018, leafHeight * 0.86, 0.012, 0, leafCenterY, faceZ + leafDepth / 2 + 0.014, 0.52);
|
|
1076
1321
|
currentDoorSlot = 'hardware';
|
|
1077
1322
|
for (const offset of [-leafWidth * 0.28, leafWidth * 0.28]) {
|
|
1078
|
-
|
|
1079
|
-
addBox(leafGroup, hardwareMaterial, 0.026, 0.16, 0.026, offset, wheelY - 0.075, faceZ + 0.
|
|
1323
|
+
addHardware(leafGroup, roller(0.0425, 0.02), offset, wheelY, faceZ + 0.01);
|
|
1324
|
+
addBox(leafGroup, hardwareMaterial, 0.026, 0.16, 0.026, offset, wheelY - 0.075, faceZ + 0.033);
|
|
1080
1325
|
}
|
|
1081
1326
|
currentDoorSlot = 'hardware';
|
|
1082
|
-
|
|
1083
|
-
|
|
1327
|
+
for (const face of [-1, 1]) {
|
|
1328
|
+
const pull = addHardware(leafGroup, pullBar(), handleX, handleY, faceZ + face * (leafDepth / 2 + 0.023));
|
|
1329
|
+
if (face < 0)
|
|
1330
|
+
pull.rotateY(Math.PI);
|
|
1331
|
+
}
|
|
1084
1332
|
}
|
|
1085
|
-
function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
1086
|
-
const
|
|
1087
|
-
const
|
|
1088
|
-
const activeSign = activeOnRight ? 1 : -1;
|
|
1089
|
-
const panelWidth = insideWidth * 0.54;
|
|
1090
|
-
const panelHeight = leafHeight;
|
|
1091
|
-
const closedActiveX = activeSign * insideWidth * 0.23;
|
|
1092
|
-
const fixedX = fixedSign * insideWidth * 0.23;
|
|
1093
|
-
const frontZ = leafDepth / 2 + 0.016;
|
|
1094
|
-
const backZ = -leafDepth / 2 - 0.006;
|
|
1095
|
-
const railY = leafCenterY + panelHeight / 2 - Math.min(frameThickness * 0.35, 0.02);
|
|
1333
|
+
function addSlidingDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, slideDirection, leafCount, slidingStyle, doorHeight, handleHeight, segments, contentPadding, }) {
|
|
1334
|
+
const panels = slidingPanels({ leafCount, slideDirection, slidingStyle }, insideWidth);
|
|
1335
|
+
const first = panels.find((panel) => panel.front).index;
|
|
1096
1336
|
const handleY = handleHeight - doorHeight / 2;
|
|
1097
|
-
// Active panel + handle ride the SLIDING_ACTIVE_PANEL group, built at the
|
|
1098
|
-
// closed position; `poseDoorMovingParts` slides the group behind the fixed
|
|
1099
|
-
// panel. The fixed panel and rails stay static on the mesh.
|
|
1100
|
-
const handleX = closedActiveX + activeSign * (panelWidth / 2 - 0.06);
|
|
1101
1337
|
currentDoorSlot = 'hardware';
|
|
1102
|
-
addBox(mesh, hardwareMaterial, insideWidth, 0.024, Math.max(frameDepth * 0.32, 0.026), 0,
|
|
1338
|
+
addBox(mesh, hardwareMaterial, insideWidth, 0.024, Math.max(frameDepth * 0.32, 0.026), 0, leafCenterY + leafHeight / 2 - Math.min(frameThickness * 0.35, 0.02), 0);
|
|
1103
1339
|
addBox(mesh, hardwareMaterial, insideWidth, 0.018, Math.max(frameDepth * 0.28, 0.022), 0, -leafHeight / 2 + 0.04, 0);
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
});
|
|
1131
|
-
currentDoorSlot = 'hardware';
|
|
1132
|
-
addBox(activePanelGroup, hardwareMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ + leafDepth / 2 + 0.01);
|
|
1133
|
-
addBox(activePanelGroup, hardwareMaterial, 0.032, 0.24, 0.016, handleX, handleY, frontZ - leafDepth / 2 - 0.01);
|
|
1340
|
+
for (const panel of panels) {
|
|
1341
|
+
const group = new THREE.Group();
|
|
1342
|
+
group.name = slidingPanelName(panel.index, first);
|
|
1343
|
+
mesh.add(group);
|
|
1344
|
+
const z = panel.front ? leafDepth / 2 + 0.016 : -leafDepth / 2 - 0.006;
|
|
1345
|
+
const hasPull = panel.front || slidingStyle === 'bypass';
|
|
1346
|
+
const handleX = (slideDirection === 'left' ? 1 : -1) * (panel.width / 2 - 0.06);
|
|
1347
|
+
const addPanelBox = (material, w, h, d, x, y, depth, geometry) => addBox(group, material, w, h, d, x + panel.x, y, depth + z, geometry);
|
|
1348
|
+
currentDoorSlot = undefined;
|
|
1349
|
+
const pullFaceZ = addLeafSegmentContent({
|
|
1350
|
+
addLeafBox: addPanelBox,
|
|
1351
|
+
...(hasPull ? { flushPullAt: { x: handleX, y: handleY } } : {}),
|
|
1352
|
+
leafWidth: panel.width,
|
|
1353
|
+
leafHeight,
|
|
1354
|
+
leafCenterX: 0,
|
|
1355
|
+
leafCenterY,
|
|
1356
|
+
leafDepth,
|
|
1357
|
+
segments,
|
|
1358
|
+
contentPadding,
|
|
1359
|
+
keepFrameWhenEmpty: true,
|
|
1360
|
+
});
|
|
1361
|
+
if (hasPull) {
|
|
1362
|
+
currentDoorSlot = 'hardware';
|
|
1363
|
+
addPulls(group, panel.x + handleX, handleY, pullFaceZ, z, true);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1134
1366
|
}
|
|
1135
1367
|
function addGarageSectionalDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafDepth, frameThickness, frameDepth, garagePanelCount, }) {
|
|
1136
1368
|
const panelCount = Math.max(3, Math.min(12, Math.round(garagePanelCount)));
|
|
@@ -1235,7 +1467,10 @@ function addGarageTiltupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafD
|
|
|
1235
1467
|
addBox(mesh, hardwareMaterial, insideWidth, 0.026, Math.max(frameDepth * 0.4, 0.035), 0, hingeY, 0);
|
|
1236
1468
|
}
|
|
1237
1469
|
function getEffectiveOpeningShape(node) {
|
|
1238
|
-
|
|
1470
|
+
const size = getDoorOpeningSize(node);
|
|
1471
|
+
return size.width !== node.width ||
|
|
1472
|
+
size.height !== node.height ||
|
|
1473
|
+
node.doorType === 'folding' ||
|
|
1239
1474
|
node.doorType === 'pocket' ||
|
|
1240
1475
|
node.doorType === 'barn' ||
|
|
1241
1476
|
node.doorType === 'sliding'
|
|
@@ -1243,16 +1478,27 @@ function getEffectiveOpeningShape(node) {
|
|
|
1243
1478
|
: (node.openingShape ?? 'rectangle');
|
|
1244
1479
|
}
|
|
1245
1480
|
function updateDoorMesh(rawNode, mesh) {
|
|
1246
|
-
const
|
|
1481
|
+
const normalized = normalizeDoorNodeForRender(rawNode);
|
|
1247
1482
|
currentDoorSlot = undefined;
|
|
1483
|
+
// The frame runs through its host wall: an auto depth is the wall's thickness
|
|
1484
|
+
// exactly, so every builder below reads one resolved number.
|
|
1485
|
+
const parent = normalized.parentId
|
|
1486
|
+
? useScene.getState().nodes[normalized.parentId]
|
|
1487
|
+
: undefined;
|
|
1488
|
+
const wall = parent?.type === 'wall' ? getEffectiveNode(parent) : undefined;
|
|
1489
|
+
const node = {
|
|
1490
|
+
...normalized,
|
|
1491
|
+
frameDepth: resolveOpeningFrameDepth(normalized, wall ? getWallThickness(wall) : DEFAULT_WALL_THICKNESS),
|
|
1492
|
+
};
|
|
1248
1493
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
1249
1494
|
mesh.geometry.dispose();
|
|
1250
|
-
|
|
1495
|
+
const openingSize = getDoorOpeningSize(node);
|
|
1496
|
+
mesh.geometry = new THREE.BoxGeometry(openingSize.width, openingSize.height, node.frameDepth);
|
|
1497
|
+
mesh.geometry.translate(-openingSize.leafOffsetX, (openingSize.height - node.height) / 2, 0);
|
|
1251
1498
|
mesh.material = hitboxMaterial;
|
|
1252
1499
|
// Sync transform from node (React may lag behind the system by a frame during drag)
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
? getOpeningWallPlacement(getEffectiveNode(parent), node, useScene.getState().nodes)
|
|
1500
|
+
const placement = wall && !node.roofSegmentId
|
|
1501
|
+
? getOpeningWallPlacement(wall, node, useScene.getState().nodes)
|
|
1256
1502
|
: node;
|
|
1257
1503
|
mesh.position.set(...placement.position);
|
|
1258
1504
|
mesh.rotation.set(...placement.rotation);
|
|
@@ -1277,13 +1523,24 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1277
1523
|
frameMaterial = resolveDoorSlotMaterial(node, 'frame');
|
|
1278
1524
|
glassMaterial = resolveDoorSlotMaterial(node, 'glass');
|
|
1279
1525
|
hardwareMaterial = resolveDoorSlotMaterial(node, 'hardware');
|
|
1280
|
-
|
|
1526
|
+
// The casing sits on the wall faces around the opening, whatever fills it.
|
|
1527
|
+
addDoorCasing(node, mesh);
|
|
1528
|
+
const { width, height, openingKind, openingShape: rawOpeningShape, frameThickness, frameDepth, threshold, thresholdHeight, segments: storedSegments, handle, handleHeight, handleSide, doorCloser, panicBar, panicBarStyle, panicBarRods, panicBarHeight, contentPadding: storedPadding, hingesSide, swingDirection, swingAngle: nodeSwingAngle = 0, doorType = 'hinged', operationState: nodeOperationState = 0, leafCount = 1, slideDirection = 'left', slidingStyle, garagePanelCount = 4, } = node;
|
|
1529
|
+
const { segments, contentPadding } = isLegacyDefaultDoorLook({
|
|
1530
|
+
segments: storedSegments,
|
|
1531
|
+
contentPadding: storedPadding,
|
|
1532
|
+
})
|
|
1533
|
+
? doorStyleLook('panel')
|
|
1534
|
+
: { segments: storedSegments, contentPadding: storedPadding };
|
|
1281
1535
|
const openingShape = getEffectiveOpeningShape(node) ?? rawOpeningShape;
|
|
1282
1536
|
const runtimeDoorState = useInteractive.getState().doors[node.id];
|
|
1283
1537
|
const swingAngle = runtimeDoorState?.swingAngle ?? nodeSwingAngle;
|
|
1284
1538
|
const operationState = runtimeDoorState?.operationState ?? nodeOperationState;
|
|
1285
1539
|
const clampedSwingAngle = Math.max(0, Math.min(Math.PI / 2, swingAngle));
|
|
1286
|
-
|
|
1540
|
+
const renderContent = parent
|
|
1541
|
+
? nodeRegistry.get(parent.type)?.capabilities.surfaces?.renderOpeningContent?.(parent)
|
|
1542
|
+
: undefined;
|
|
1543
|
+
if (openingKind === 'opening' || renderContent === false) {
|
|
1287
1544
|
syncDoorCutout(node, mesh);
|
|
1288
1545
|
return true;
|
|
1289
1546
|
}
|
|
@@ -1321,6 +1578,14 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1321
1578
|
// Head (top bar) — full width
|
|
1322
1579
|
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
1323
1580
|
}
|
|
1581
|
+
// Curved heads need a matching curved rebate; arched/rounded frames have no stop.
|
|
1582
|
+
if (openingShape === 'rectangle' && ['hinged', 'double', 'french'].includes(doorType)) {
|
|
1583
|
+
const stopZ = -swingDirectionSign * (leafDepth / 2 + 0.016);
|
|
1584
|
+
for (const side of [-1, 1]) {
|
|
1585
|
+
addBox(mesh, frameMaterial, 0.012, leafH, 0.032, side * (insideWidth / 2 - 0.006), leafCenterY, stopZ).name = 'door-jamb-stop';
|
|
1586
|
+
}
|
|
1587
|
+
addBox(mesh, frameMaterial, insideWidth - 0.024, 0.012, 0.032, 0, height / 2 - frameThickness - 0.006, stopZ).name = 'door-jamb-stop';
|
|
1588
|
+
}
|
|
1324
1589
|
// ── Threshold (inside the frame) ──
|
|
1325
1590
|
if (threshold) {
|
|
1326
1591
|
currentDoorSlot = 'frame';
|
|
@@ -1367,6 +1632,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1367
1632
|
frameThickness,
|
|
1368
1633
|
frameDepth,
|
|
1369
1634
|
leafCount,
|
|
1635
|
+
slideDirection,
|
|
1370
1636
|
doorHeight: height,
|
|
1371
1637
|
handleHeight,
|
|
1372
1638
|
segments,
|
|
@@ -1412,6 +1678,8 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1412
1678
|
frameThickness,
|
|
1413
1679
|
frameDepth,
|
|
1414
1680
|
slideDirection,
|
|
1681
|
+
leafCount,
|
|
1682
|
+
slidingStyle,
|
|
1415
1683
|
doorHeight: height,
|
|
1416
1684
|
handleHeight,
|
|
1417
1685
|
segments,
|
|
@@ -1434,6 +1702,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1434
1702
|
leafCenterX: -insideWidth / 4,
|
|
1435
1703
|
leafCenterY,
|
|
1436
1704
|
leafDepth,
|
|
1705
|
+
frameDepth,
|
|
1437
1706
|
hingeX: -insideWidth / 2,
|
|
1438
1707
|
hingeSide: 'left',
|
|
1439
1708
|
swingRotation: -clampedSwingAngle * swingDirectionSign,
|
|
@@ -1441,11 +1710,14 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1441
1710
|
segments,
|
|
1442
1711
|
contentPadding,
|
|
1443
1712
|
handle,
|
|
1713
|
+
handleStyle: doorHandleStyle(node),
|
|
1444
1714
|
handleBothSides: doorType === 'double' || doorType === 'french',
|
|
1445
1715
|
handleHeight,
|
|
1446
1716
|
handleSide: 'right',
|
|
1447
1717
|
doorCloser,
|
|
1448
1718
|
panicBar,
|
|
1719
|
+
panicBarStyle,
|
|
1720
|
+
panicBarRods,
|
|
1449
1721
|
panicBarHeight,
|
|
1450
1722
|
doorHeight: height,
|
|
1451
1723
|
openingShape,
|
|
@@ -1462,6 +1734,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1462
1734
|
leafCenterX: insideWidth / 4,
|
|
1463
1735
|
leafCenterY,
|
|
1464
1736
|
leafDepth,
|
|
1737
|
+
frameDepth,
|
|
1465
1738
|
hingeX: insideWidth / 2,
|
|
1466
1739
|
hingeSide: 'right',
|
|
1467
1740
|
swingRotation: clampedSwingAngle * swingDirectionSign,
|
|
@@ -1469,11 +1742,14 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1469
1742
|
segments,
|
|
1470
1743
|
contentPadding,
|
|
1471
1744
|
handle,
|
|
1745
|
+
handleStyle: doorHandleStyle(node),
|
|
1472
1746
|
handleBothSides: doorType === 'double' || doorType === 'french',
|
|
1473
1747
|
handleHeight,
|
|
1474
1748
|
handleSide: 'left',
|
|
1475
1749
|
doorCloser: false,
|
|
1476
1750
|
panicBar,
|
|
1751
|
+
panicBarStyle,
|
|
1752
|
+
panicBarRods,
|
|
1477
1753
|
panicBarHeight,
|
|
1478
1754
|
doorHeight: height,
|
|
1479
1755
|
openingShape,
|
|
@@ -1494,6 +1770,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1494
1770
|
leafCenterX: 0,
|
|
1495
1771
|
leafCenterY,
|
|
1496
1772
|
leafDepth,
|
|
1773
|
+
frameDepth,
|
|
1497
1774
|
hingeX,
|
|
1498
1775
|
hingeSide: hingesSide,
|
|
1499
1776
|
swingRotation: clampedSwingAngle * swingDirectionSign * hingeDirectionSign,
|
|
@@ -1501,11 +1778,14 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1501
1778
|
segments,
|
|
1502
1779
|
contentPadding,
|
|
1503
1780
|
handle,
|
|
1781
|
+
handleStyle: doorHandleStyle(node),
|
|
1504
1782
|
handleBothSides: doorType === 'hinged',
|
|
1505
1783
|
handleHeight,
|
|
1506
1784
|
handleSide,
|
|
1507
1785
|
doorCloser,
|
|
1508
1786
|
panicBar,
|
|
1787
|
+
panicBarStyle,
|
|
1788
|
+
panicBarRods,
|
|
1509
1789
|
panicBarHeight,
|
|
1510
1790
|
doorHeight: height,
|
|
1511
1791
|
openingShape,
|
|
@@ -1520,6 +1800,7 @@ function updateDoorMesh(rawNode, mesh) {
|
|
|
1520
1800
|
if (doorType !== 'garage-rollup') {
|
|
1521
1801
|
poseDoorMovingParts(node, mesh, operationState);
|
|
1522
1802
|
}
|
|
1803
|
+
addDoorFixedLights(node, mesh);
|
|
1523
1804
|
syncDoorCutout(node, mesh);
|
|
1524
1805
|
// Guard: some degenerate door configs can leave a child mesh with an
|
|
1525
1806
|
// empty (0-vertex) geometry — e.g. a zero-area extruded leaf frame.
|
|
@@ -1539,6 +1820,79 @@ function hideEmptyGeometryMeshes(root) {
|
|
|
1539
1820
|
child.visible = false;
|
|
1540
1821
|
});
|
|
1541
1822
|
}
|
|
1823
|
+
/**
|
|
1824
|
+
* The door's casing (head and legs) on the faces of its wall (or gable) around
|
|
1825
|
+
* the cutout, inheriting the frame until each side is painted. Read from the live host, so it stays on
|
|
1826
|
+
* the faces while the wall's thickness is dragged.
|
|
1827
|
+
*/
|
|
1828
|
+
function addDoorCasing(node, mesh) {
|
|
1829
|
+
const nodes = useScene.getState().nodes;
|
|
1830
|
+
const hostId = node.roofSegmentId ?? node.parentId;
|
|
1831
|
+
const hostNode = hostId ? nodes[hostId] : undefined;
|
|
1832
|
+
const host = resolveOpeningCasingHost(node, nodes, hostNode ? getEffectiveNode(hostNode) : undefined);
|
|
1833
|
+
if (!host)
|
|
1834
|
+
return;
|
|
1835
|
+
for (const casing of buildOpeningCasingMeshes(node, host, frameMaterial)) {
|
|
1836
|
+
const ref = node.slots?.[casing.userData.slotId];
|
|
1837
|
+
casing.material =
|
|
1838
|
+
(currentTextures && ref
|
|
1839
|
+
? resolveMaterialRef(ref, currentSceneMaterials, currentShading)
|
|
1840
|
+
: null) ?? frameMaterial;
|
|
1841
|
+
mesh.add(casing);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function addDoorFixedLights(node, mesh) {
|
|
1845
|
+
const size = getDoorOpeningSize(node);
|
|
1846
|
+
if (size.width === node.width && size.height === node.height)
|
|
1847
|
+
return;
|
|
1848
|
+
const group = new THREE.Group();
|
|
1849
|
+
group.name = 'door-fixed-lights';
|
|
1850
|
+
mesh.add(group);
|
|
1851
|
+
const f = node.frameThickness;
|
|
1852
|
+
const bottom = -node.height / 2;
|
|
1853
|
+
const top = bottom + size.height;
|
|
1854
|
+
const left = -size.width / 2 - size.leafOffsetX;
|
|
1855
|
+
const right = left + size.width;
|
|
1856
|
+
const box = (slot, w, h, x, y) => {
|
|
1857
|
+
if (w <= 0 || h <= 0)
|
|
1858
|
+
return;
|
|
1859
|
+
currentDoorSlot = slot;
|
|
1860
|
+
addBox(group, slot === 'glass' ? glassMaterial : slot === 'panel' ? baseMaterial : frameMaterial, w, h, slot === 'glass' ? 0.008 : node.frameDepth, x, y, 0);
|
|
1861
|
+
};
|
|
1862
|
+
const paneled = doorStylesOf(node).includes('paneled-glass');
|
|
1863
|
+
for (const [side, extra] of [
|
|
1864
|
+
[-1, node.sidelightLeft ?? 0],
|
|
1865
|
+
[1, node.sidelightRight ?? 0],
|
|
1866
|
+
]) {
|
|
1867
|
+
if (!extra)
|
|
1868
|
+
continue;
|
|
1869
|
+
const outerX = side < 0 ? left + f / 2 : right - f / 2;
|
|
1870
|
+
box('frame', f, node.height, outerX, 0);
|
|
1871
|
+
const x = side * (node.width / 2 + extra / 2 - f / 2);
|
|
1872
|
+
const w = extra - f;
|
|
1873
|
+
const glassBottom = bottom + (paneled ? 0.3 : f);
|
|
1874
|
+
const glassTop = node.height / 2 - f;
|
|
1875
|
+
box('frame', extra, f, side * (node.width / 2 + extra / 2), node.height / 2 - f / 2);
|
|
1876
|
+
box(paneled ? 'panel' : 'frame', w, paneled ? 0.3 : f, x, bottom + (paneled ? 0.15 : f / 2));
|
|
1877
|
+
const mid = node.height > 1.2 ? (glassBottom + glassTop) / 2 : undefined;
|
|
1878
|
+
if (mid === undefined)
|
|
1879
|
+
box('glass', w, glassTop - glassBottom, x, (glassTop + glassBottom) / 2);
|
|
1880
|
+
else {
|
|
1881
|
+
box('frame', w, f, x, mid);
|
|
1882
|
+
box('glass', w, mid - f / 2 - glassBottom, x, (glassBottom + mid - f / 2) / 2);
|
|
1883
|
+
box('glass', w, glassTop - mid - f / 2, x, (glassTop + mid + f / 2) / 2);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const transom = size.height - node.height;
|
|
1887
|
+
if (transom > 0) {
|
|
1888
|
+
box('frame', size.width, f, -size.leafOffsetX, node.height / 2 - f / 2);
|
|
1889
|
+
box('frame', size.width, f, -size.leafOffsetX, top - f / 2);
|
|
1890
|
+
box('frame', f, transom, left + f / 2, node.height / 2 + transom / 2);
|
|
1891
|
+
box('frame', f, transom, right - f / 2, node.height / 2 + transom / 2);
|
|
1892
|
+
box('glass', size.width - 2 * f, transom - f, -size.leafOffsetX, node.height / 2 + (transom - f) / 2);
|
|
1893
|
+
}
|
|
1894
|
+
currentDoorSlot = undefined;
|
|
1895
|
+
}
|
|
1542
1896
|
function syncDoorCutout(node, mesh) {
|
|
1543
1897
|
// ── Cutout: invisible raycast hit target for the whole opening ──
|
|
1544
1898
|
let cutout = mesh.getObjectByName('cutout');
|
|
@@ -1563,6 +1917,7 @@ function syncDoorCutout(node, mesh) {
|
|
|
1563
1917
|
.set(center[0] - mesh.position.x, 0, center[2] - mesh.position.z)
|
|
1564
1918
|
.applyQuaternion(mesh.quaternion.clone().invert());
|
|
1565
1919
|
}
|
|
1920
|
+
const { width, height, leafOffsetX } = getDoorOpeningSize(node);
|
|
1566
1921
|
const openingShape = getEffectiveOpeningShape(node);
|
|
1567
1922
|
if (openingShape === 'arch') {
|
|
1568
1923
|
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)), {
|
|
@@ -1581,8 +1936,9 @@ function syncDoorCutout(node, mesh) {
|
|
|
1581
1936
|
cutout.geometry.translate(0, 0, -depth / 2);
|
|
1582
1937
|
}
|
|
1583
1938
|
else {
|
|
1584
|
-
cutout.geometry = new THREE.BoxGeometry(
|
|
1939
|
+
cutout.geometry = new THREE.BoxGeometry(width, height, depth);
|
|
1585
1940
|
}
|
|
1941
|
+
cutout.geometry.translate(-leafOffsetX, (height - node.height) / 2, 0);
|
|
1586
1942
|
cutout.visible = false;
|
|
1587
1943
|
}
|
|
1588
1944
|
// Resolve the cutout proxy depth from the opening's parent wall thickness so
|