@pascal-app/viewer 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/renderers/node-renderer.d.ts +1 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +38 -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 +34 -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/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +5 -1
- package/dist/components/viewer/ground-occluder.d.ts +1 -1
- package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
- package/dist/components/viewer/ground-occluder.js +12 -24
- package/dist/components/viewer/index.d.ts +38 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +183 -60
- 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 +170 -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 +99 -11
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +217 -61
- 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 +47 -0
- package/dist/components/viewer/scene-bvh.d.ts +17 -0
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.js +94 -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 +20 -5
- 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 +1 -1
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-node-events.d.ts +5 -73
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +19 -7
- package/dist/index.d.ts +36 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -3
- package/dist/lib/csg-utils.d.ts +16 -0
- package/dist/lib/csg-utils.d.ts.map +1 -0
- package/dist/lib/csg-utils.js +33 -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 +3 -0
- package/dist/lib/edge-style.d.ts.map +1 -0
- package/dist/lib/edge-style.js +11 -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 +43 -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/layers.d.ts +18 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +18 -0
- package/dist/lib/materials.d.ts +25 -15
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +277 -63
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +11 -11
- 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 +29 -0
- package/dist/lib/scene-themes.d.ts.map +1 -0
- package/dist/lib/scene-themes.js +193 -0
- package/dist/store/use-viewer.d.ts +42 -4
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +26 -3
- 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 +62 -12
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +373 -22
- 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 +3 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +10 -6
- 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 +34 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.js +279 -0
- package/dist/systems/guide/guide-system.d.ts.map +1 -1
- package/dist/systems/guide/guide-system.js +1 -26
- package/dist/systems/interactive/interactive-system.d.ts +1 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- 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 +8 -11
- 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/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 +38 -14
- package/dist/systems/roof/roof-system.d.ts +27 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +411 -71
- package/dist/systems/scan/scan-system.d.ts.map +1 -1
- package/dist/systems/scan/scan-system.js +1 -26
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +167 -49
- 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 +62 -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 +58 -71
- 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/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +18 -2
- package/dist/systems/wall/wall-materials.d.ts +4 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +52 -28
- package/dist/systems/wall/wall-system.d.ts +1 -1
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +179 -31
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +71 -7
- package/package.json +4 -5
- 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 -594
- 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 -29
- 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 -32
- 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/lib/suppress-three-clock-warning.d.ts +0 -25
- package/dist/lib/suppress-three-clock-warning.d.ts.map +0 -1
- package/dist/lib/suppress-three-clock-warning.js +0 -51
- package/dist/systems/export/export-system.d.ts +0 -2
- package/dist/systems/export/export-system.d.ts.map +0 -1
- package/dist/systems/export/export-system.js +0 -60
- package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts +0 -14
- package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts.map +0 -1
- package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.js +0 -155
- package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts +0 -6
- package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts.map +0 -1
- package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.js +0 -75
- package/dist/systems/item-mesh-metadata/sync-request.d.ts +0 -7
- package/dist/systems/item-mesh-metadata/sync-request.d.ts.map +0 -1
- package/dist/systems/item-mesh-metadata/sync-request.js +0 -25
|
@@ -1,31 +1,109 @@
|
|
|
1
|
-
import { clampDoorOperationState, getDoorRenderOpenAmount, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { clampDoorOperationState, DoorNode as DoorNodeSchema, getDoorRenderOpenAmount, getEffectiveNode, 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 { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, } from '../../lib/materials';
|
|
6
|
+
import useViewer from '../../store/use-viewer';
|
|
5
7
|
// Invisible material for root mesh — used as selection hitbox only
|
|
6
8
|
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
7
|
-
const
|
|
9
|
+
const defaultRevealMaterial = new THREE.MeshBasicMaterial({ color: '#7f766c' });
|
|
10
|
+
let baseMaterial = getBaseMaterial();
|
|
11
|
+
let revealMaterial = defaultRevealMaterial;
|
|
12
|
+
let glassMaterial = defaultGlassMaterial;
|
|
13
|
+
const DOOR_RENDER_DEFAULTS = DoorNodeSchema.parse({ id: 'door_render_default' });
|
|
14
|
+
const MAX_DOOR_REBUILDS_PER_FRAME = 16;
|
|
15
|
+
const DOOR_PROGRESSIVE_DIRTY_THRESHOLD = MAX_DOOR_REBUILDS_PER_FRAME;
|
|
16
|
+
const DOOR_PROGRESSIVE_TIME_BUDGET_MS = 8;
|
|
17
|
+
// Legacy/unparsed door nodes can miss schema-defaulted fields (segments,
|
|
18
|
+
// columnRatios, dividerThickness, …) and crash the geometry build. Re-apply the
|
|
19
|
+
// Zod defaults; if the node is structurally invalid (e.g. a segment missing a
|
|
20
|
+
// required field) drop the bad segments, then fall back to defaults entirely.
|
|
21
|
+
function normalizeDoorNodeForRender(node) {
|
|
22
|
+
const parsed = DoorNodeSchema.safeParse(node);
|
|
23
|
+
if (parsed.success)
|
|
24
|
+
return parsed.data;
|
|
25
|
+
const retry = DoorNodeSchema.safeParse({ ...node, segments: undefined });
|
|
26
|
+
if (retry.success)
|
|
27
|
+
return retry.data;
|
|
28
|
+
return { ...DOOR_RENDER_DEFAULTS, id: node.id, parentId: node.parentId };
|
|
29
|
+
}
|
|
8
30
|
export const DoorSystem = () => {
|
|
9
31
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
10
32
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
33
|
+
const shading = useViewer((state) => state.shading);
|
|
34
|
+
const textures = useViewer((state) => state.textures);
|
|
35
|
+
const colorPreset = useViewer((state) => state.colorPreset);
|
|
36
|
+
const materialRevisionRef = useRef(null);
|
|
37
|
+
// Subscribe so an override-only update (no scene write) still re-runs
|
|
38
|
+
// the component, letting the gate below pick up the latest dirtyNodes
|
|
39
|
+
// set from the same render pass that received the override-publishing
|
|
40
|
+
// `markDirty` call. Mirrors WallSystem.
|
|
41
|
+
useLiveNodeOverrides((s) => s.overrides);
|
|
42
|
+
const joineryMaterial = createSurfaceRoleMaterial('joinery', colorPreset);
|
|
43
|
+
baseMaterial = textures ? getBaseMaterial(shading) : joineryMaterial;
|
|
44
|
+
revealMaterial = textures ? defaultRevealMaterial : joineryMaterial;
|
|
45
|
+
glassMaterial = textures ? defaultGlassMaterial : joineryMaterial;
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`;
|
|
48
|
+
if (materialRevisionRef.current === materialRevision)
|
|
49
|
+
return;
|
|
50
|
+
materialRevisionRef.current = materialRevision;
|
|
51
|
+
const nodes = useScene.getState().nodes;
|
|
52
|
+
for (const node of Object.values(nodes)) {
|
|
53
|
+
if (node?.type === 'door') {
|
|
54
|
+
useScene.getState().dirtyNodes.add(node.id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
11
58
|
useFrame(() => {
|
|
12
59
|
if (dirtyNodes.size === 0)
|
|
13
60
|
return;
|
|
61
|
+
const frameJoineryMaterial = createSurfaceRoleMaterial('joinery', colorPreset);
|
|
62
|
+
baseMaterial = textures ? getBaseMaterial(shading) : frameJoineryMaterial;
|
|
63
|
+
revealMaterial = textures ? defaultRevealMaterial : frameJoineryMaterial;
|
|
64
|
+
glassMaterial = textures ? defaultGlassMaterial : frameJoineryMaterial;
|
|
14
65
|
const nodes = useScene.getState().nodes;
|
|
66
|
+
const dirtyDoorIds = [];
|
|
15
67
|
dirtyNodes.forEach((id) => {
|
|
16
68
|
const node = nodes[id];
|
|
17
69
|
if (!node || node.type !== 'door')
|
|
18
70
|
return;
|
|
71
|
+
dirtyDoorIds.push(id);
|
|
72
|
+
});
|
|
73
|
+
const useProgressiveDoorRebuilds = dirtyDoorIds.length > DOOR_PROGRESSIVE_DIRTY_THRESHOLD;
|
|
74
|
+
const frameStartedAt = performance.now();
|
|
75
|
+
let rebuiltDoorsThisFrame = 0;
|
|
76
|
+
for (const id of dirtyDoorIds) {
|
|
77
|
+
if (useProgressiveDoorRebuilds) {
|
|
78
|
+
if (rebuiltDoorsThisFrame >= MAX_DOOR_REBUILDS_PER_FRAME) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (rebuiltDoorsThisFrame > 0 &&
|
|
82
|
+
performance.now() - frameStartedAt >= DOOR_PROGRESSIVE_TIME_BUDGET_MS) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const node = nodes[id];
|
|
87
|
+
if (!node || node.type !== 'door')
|
|
88
|
+
continue;
|
|
19
89
|
const mesh = sceneRegistry.nodes.get(id);
|
|
20
90
|
if (!mesh)
|
|
21
|
-
|
|
22
|
-
|
|
91
|
+
continue; // Keep dirty until mesh mounts
|
|
92
|
+
// Merge any live override (width / height / position) so the mesh
|
|
93
|
+
// rebuild reflects the in-flight drag without zustand churn. When
|
|
94
|
+
// no override is set this returns the scene node unchanged.
|
|
95
|
+
const effectiveNode = getEffectiveNode(node);
|
|
96
|
+
updateDoorMesh(effectiveNode, mesh);
|
|
23
97
|
clearDirty(id);
|
|
98
|
+
rebuiltDoorsThisFrame += 1;
|
|
24
99
|
// Rebuild the parent wall so its cutout reflects the updated door geometry
|
|
25
|
-
|
|
26
|
-
|
|
100
|
+
// Avoid triggering expensive wall CSG rebuilds while the door is being interactively moved/duplicated.
|
|
101
|
+
// The editor tools will request a final wall rebuild on commit.
|
|
102
|
+
const isTransient = !!node.metadata?.isTransient;
|
|
103
|
+
if (!isTransient && effectiveNode.parentId) {
|
|
104
|
+
useScene.getState().dirtyNodes.add(effectiveNode.parentId);
|
|
27
105
|
}
|
|
28
|
-
}
|
|
106
|
+
}
|
|
29
107
|
}, 3);
|
|
30
108
|
return null;
|
|
31
109
|
};
|
|
@@ -56,6 +134,16 @@ function addShape(parent, material, shape, depth) {
|
|
|
56
134
|
const mesh = new THREE.Mesh(geometry, material);
|
|
57
135
|
parent.add(mesh);
|
|
58
136
|
}
|
|
137
|
+
function addShapeAt(parent, material, shape, depth, x, y, z) {
|
|
138
|
+
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
139
|
+
depth,
|
|
140
|
+
bevelEnabled: false,
|
|
141
|
+
curveSegments: 24,
|
|
142
|
+
});
|
|
143
|
+
geometry.translate(x, y, z - depth / 2);
|
|
144
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
145
|
+
parent.add(mesh);
|
|
146
|
+
}
|
|
59
147
|
function getClampedArchHeight(width, height, archHeight) {
|
|
60
148
|
return Math.min(Math.max(archHeight ?? width / 2, 0.01), Math.max(height, 0.01));
|
|
61
149
|
}
|
|
@@ -83,6 +171,32 @@ function getArchBoundaryY(x, halfWidth, springY, archHeight) {
|
|
|
83
171
|
const t = Math.min(Math.abs(x) / halfWidth, 1);
|
|
84
172
|
return springY + archHeight * Math.sqrt(Math.max(1 - t * t, 0));
|
|
85
173
|
}
|
|
174
|
+
function getOneSidedArchBoundaryYAtX(x, left, right, top, archHeight, curvedSide) {
|
|
175
|
+
const width = right - left;
|
|
176
|
+
if (width <= 1e-6)
|
|
177
|
+
return top;
|
|
178
|
+
const clampedArchHeight = getClampedArchHeight(width, Number.MAX_SAFE_INTEGER, archHeight);
|
|
179
|
+
const springY = top - clampedArchHeight;
|
|
180
|
+
const distanceFromApex = curvedSide === 'left'
|
|
181
|
+
? Math.max(0, Math.min((right - x) / width, 1))
|
|
182
|
+
: Math.max(0, Math.min((x - left) / width, 1));
|
|
183
|
+
return (springY + clampedArchHeight * Math.sqrt(Math.max(1 - distanceFromApex * distanceFromApex, 0)));
|
|
184
|
+
}
|
|
185
|
+
function getRoundedBoundaryYAtX(x, left, right, top, radii) {
|
|
186
|
+
if (radii.topLeft > 1e-6 && x < left + radii.topLeft) {
|
|
187
|
+
const centerX = left + radii.topLeft;
|
|
188
|
+
const centerY = top - radii.topLeft;
|
|
189
|
+
const dx = x - centerX;
|
|
190
|
+
return centerY + Math.sqrt(Math.max(radii.topLeft * radii.topLeft - dx * dx, 0));
|
|
191
|
+
}
|
|
192
|
+
if (radii.topRight > 1e-6 && x > right - radii.topRight) {
|
|
193
|
+
const centerX = right - radii.topRight;
|
|
194
|
+
const centerY = top - radii.topRight;
|
|
195
|
+
const dx = x - centerX;
|
|
196
|
+
return centerY + Math.sqrt(Math.max(radii.topRight * radii.topRight - dx * dx, 0));
|
|
197
|
+
}
|
|
198
|
+
return top;
|
|
199
|
+
}
|
|
86
200
|
function createArchBandShape(width, outerSpringY, outerTopY, innerSpringY, innerTopY, insetX) {
|
|
87
201
|
const halfWidth = width / 2;
|
|
88
202
|
const innerHalfWidth = Math.max(halfWidth - insetX, 0);
|
|
@@ -215,6 +329,87 @@ function createRoundedLeafFrameShape(width, bottom, top, radii, insetX, insetY)
|
|
|
215
329
|
outer.holes.push(shapeToReversedPath(createRoundedTopShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii)));
|
|
216
330
|
return outer;
|
|
217
331
|
}
|
|
332
|
+
function createRoundedClippedLeafFrameShape(left, right, bottom, top, fullLeft, fullRight, radii, insetX, insetY) {
|
|
333
|
+
const outerRadii = normalizeTopCornerRadii(radii, fullRight - fullLeft, top - bottom);
|
|
334
|
+
const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getRoundedBoundaryYAtX(x, fullLeft, fullRight, top, {
|
|
335
|
+
topLeft: outerRadii.topLeft,
|
|
336
|
+
topRight: outerRadii.topRight,
|
|
337
|
+
}));
|
|
338
|
+
if (!outer)
|
|
339
|
+
return null;
|
|
340
|
+
const innerLeft = left + insetX;
|
|
341
|
+
const innerRight = right - insetX;
|
|
342
|
+
const innerBottom = bottom + insetY;
|
|
343
|
+
const innerTop = top - insetY;
|
|
344
|
+
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
|
|
345
|
+
return outer;
|
|
346
|
+
const innerFullLeft = fullLeft + insetX;
|
|
347
|
+
const innerFullRight = fullRight - insetX;
|
|
348
|
+
const innerRadii = normalizeTopCornerRadii({
|
|
349
|
+
topLeft: Math.max(outerRadii.topLeft - Math.max(insetX, insetY), 0),
|
|
350
|
+
topRight: Math.max(outerRadii.topRight - Math.max(insetX, insetY), 0),
|
|
351
|
+
}, innerFullRight - innerFullLeft, innerTop - innerBottom);
|
|
352
|
+
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getRoundedBoundaryYAtX(x, innerFullLeft, innerFullRight, innerTop, {
|
|
353
|
+
topLeft: innerRadii.topLeft,
|
|
354
|
+
topRight: innerRadii.topRight,
|
|
355
|
+
}));
|
|
356
|
+
if (holeShape)
|
|
357
|
+
outer.holes.push(shapeToReversedPath(holeShape));
|
|
358
|
+
return outer;
|
|
359
|
+
}
|
|
360
|
+
function createArchedLeafFrameShape(width, bottom, top, archHeight, insetX, insetY) {
|
|
361
|
+
const halfWidth = width / 2;
|
|
362
|
+
const outer = createArchShape(-halfWidth, halfWidth, bottom, top, archHeight);
|
|
363
|
+
const innerLeft = -halfWidth + insetX;
|
|
364
|
+
const innerRight = halfWidth - insetX;
|
|
365
|
+
const innerBottom = bottom + insetY;
|
|
366
|
+
const innerTop = top - insetY;
|
|
367
|
+
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
|
|
368
|
+
return outer;
|
|
369
|
+
const innerArchHeight = getClampedArchHeight(innerRight - innerLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
|
|
370
|
+
outer.holes.push(shapeToReversedPath(createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight)));
|
|
371
|
+
return outer;
|
|
372
|
+
}
|
|
373
|
+
function createArchedClippedLeafFrameShape(left, right, bottom, top, fullLeft, fullRight, archHeight, insetX, insetY) {
|
|
374
|
+
const fullCenterX = (fullLeft + fullRight) / 2;
|
|
375
|
+
const fullHalfWidth = (fullRight - fullLeft) / 2;
|
|
376
|
+
const springY = top - archHeight;
|
|
377
|
+
const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getArchBoundaryY(x - fullCenterX, fullHalfWidth, springY, archHeight));
|
|
378
|
+
if (!outer)
|
|
379
|
+
return null;
|
|
380
|
+
const innerLeft = left + insetX;
|
|
381
|
+
const innerRight = right - insetX;
|
|
382
|
+
const innerBottom = bottom + insetY;
|
|
383
|
+
const innerTop = top - insetY;
|
|
384
|
+
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
|
|
385
|
+
return outer;
|
|
386
|
+
const innerFullLeft = fullLeft + insetX;
|
|
387
|
+
const innerFullRight = fullRight - insetX;
|
|
388
|
+
const innerArchHeight = getClampedArchHeight(innerFullRight - innerFullLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
|
|
389
|
+
const innerFullCenterX = (innerFullLeft + innerFullRight) / 2;
|
|
390
|
+
const innerFullHalfWidth = (innerFullRight - innerFullLeft) / 2;
|
|
391
|
+
const innerSpringY = innerTop - innerArchHeight;
|
|
392
|
+
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getArchBoundaryY(x - innerFullCenterX, innerFullHalfWidth, innerSpringY, innerArchHeight));
|
|
393
|
+
if (holeShape)
|
|
394
|
+
outer.holes.push(shapeToReversedPath(holeShape));
|
|
395
|
+
return outer;
|
|
396
|
+
}
|
|
397
|
+
function createOneSidedArchLeafFrameShape(left, right, bottom, top, archHeight, insetX, insetY, curvedSide) {
|
|
398
|
+
const outer = createTopClippedRectShape(left, right, bottom, top, (x) => getOneSidedArchBoundaryYAtX(x, left, right, top, archHeight, curvedSide));
|
|
399
|
+
if (!outer)
|
|
400
|
+
return null;
|
|
401
|
+
const innerLeft = left + insetX;
|
|
402
|
+
const innerRight = right - insetX;
|
|
403
|
+
const innerBottom = bottom + insetY;
|
|
404
|
+
const innerTop = top - insetY;
|
|
405
|
+
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01)
|
|
406
|
+
return outer;
|
|
407
|
+
const innerArchHeight = getClampedArchHeight(innerRight - innerLeft, innerTop - innerBottom, Math.max(archHeight - insetY, 0.01));
|
|
408
|
+
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) => getOneSidedArchBoundaryYAtX(x, innerLeft, innerRight, innerTop, innerArchHeight, curvedSide));
|
|
409
|
+
if (holeShape)
|
|
410
|
+
outer.holes.push(shapeToReversedPath(holeShape));
|
|
411
|
+
return outer;
|
|
412
|
+
}
|
|
218
413
|
function createTopClippedRectShape(left, right, bottom, top, getBoundaryY) {
|
|
219
414
|
const segments = 20;
|
|
220
415
|
const points = [];
|
|
@@ -242,16 +437,16 @@ function disposeObject(object) {
|
|
|
242
437
|
child.geometry.dispose();
|
|
243
438
|
});
|
|
244
439
|
}
|
|
245
|
-
function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, }) {
|
|
440
|
+
function addLeafSegmentContent({ addLeafBox, addLeafShape, leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, segments, contentPadding, keepFrameWhenEmpty = false, renderPerimeterFrame = true, openingShape = 'rectangle', openingTopRadii, archHeight, archOuterSide, }) {
|
|
246
441
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
|
|
247
442
|
const shouldRenderFrame = hasLeafContent || keepFrameWhenEmpty;
|
|
248
443
|
const cpX = contentPadding[0];
|
|
249
444
|
const cpY = contentPadding[1];
|
|
250
|
-
if (shouldRenderFrame && cpY > 0) {
|
|
445
|
+
if (renderPerimeterFrame && shouldRenderFrame && cpY > 0) {
|
|
251
446
|
addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY + leafHeight / 2 - cpY / 2, 0);
|
|
252
447
|
addLeafBox(baseMaterial, leafWidth, cpY, leafDepth, leafCenterX, leafCenterY - leafHeight / 2 + cpY / 2, 0);
|
|
253
448
|
}
|
|
254
|
-
if (shouldRenderFrame && cpX > 0) {
|
|
449
|
+
if (renderPerimeterFrame && shouldRenderFrame && cpX > 0) {
|
|
255
450
|
const innerH = leafHeight - 2 * cpY;
|
|
256
451
|
addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX - leafWidth / 2 + cpX / 2, leafCenterY, 0);
|
|
257
452
|
addLeafBox(baseMaterial, cpX, innerH, leafDepth, leafCenterX + leafWidth / 2 - cpX / 2, leafCenterY, 0);
|
|
@@ -261,9 +456,32 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
|
|
|
261
456
|
const totalRatio = segments.reduce((sum, s) => sum + s.heightRatio, 0);
|
|
262
457
|
const contentTop = leafCenterY + contentH / 2;
|
|
263
458
|
let segY = contentTop;
|
|
459
|
+
const leafLeft = leafCenterX - leafWidth / 2;
|
|
460
|
+
const leafRight = leafCenterX + leafWidth / 2;
|
|
461
|
+
const leafTop = leafCenterY + leafHeight / 2;
|
|
462
|
+
const hasShapedTop = openingShape === 'rounded' || openingShape === 'arch';
|
|
463
|
+
const clampedLeafArchHeight = openingShape === 'arch' ? getClampedArchHeight(leafWidth, leafHeight, archHeight) : 0;
|
|
464
|
+
const leafSpringY = leafTop - clampedLeafArchHeight;
|
|
465
|
+
const topBoundaryAtX = (x) => {
|
|
466
|
+
if (openingShape === 'rounded' && openingTopRadii) {
|
|
467
|
+
return getRoundedBoundaryYAtX(x, leafLeft, leafRight, leafTop, {
|
|
468
|
+
topLeft: openingTopRadii.topLeft,
|
|
469
|
+
topRight: openingTopRadii.topRight,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
if (openingShape === 'arch') {
|
|
473
|
+
if (archOuterSide) {
|
|
474
|
+
return getOneSidedArchBoundaryYAtX(x, leafLeft, leafRight, leafTop, clampedLeafArchHeight, archOuterSide);
|
|
475
|
+
}
|
|
476
|
+
return getArchBoundaryY(x - leafCenterX, leafWidth / 2, leafSpringY, clampedLeafArchHeight);
|
|
477
|
+
}
|
|
478
|
+
return leafTop;
|
|
479
|
+
};
|
|
264
480
|
for (const seg of segments) {
|
|
265
481
|
const segH = (seg.heightRatio / totalRatio) * contentH;
|
|
266
482
|
const segCenterY = segY - segH / 2;
|
|
483
|
+
const segTop = segY;
|
|
484
|
+
const segBottom = segY - segH;
|
|
267
485
|
const numCols = seg.columnRatios.length;
|
|
268
486
|
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0);
|
|
269
487
|
const usableW = contentW - (numCols - 1) * seg.dividerThickness;
|
|
@@ -280,7 +498,17 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
|
|
|
280
498
|
cx = leafCenterX - contentW / 2;
|
|
281
499
|
for (let c = 0; c < numCols - 1; c++) {
|
|
282
500
|
cx += colWidths[c];
|
|
283
|
-
|
|
501
|
+
const dividerLeft = cx;
|
|
502
|
+
const dividerRight = cx + seg.dividerThickness;
|
|
503
|
+
const dividerShape = hasShapedTop && addLeafShape
|
|
504
|
+
? createTopClippedRectShape(dividerLeft, dividerRight, segBottom, segTop, topBoundaryAtX)
|
|
505
|
+
: null;
|
|
506
|
+
if (dividerShape && addLeafShape) {
|
|
507
|
+
addLeafShape(baseMaterial, dividerShape, leafDepth + 0.001);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
addLeafBox(baseMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0);
|
|
511
|
+
}
|
|
284
512
|
cx += seg.dividerThickness;
|
|
285
513
|
}
|
|
286
514
|
}
|
|
@@ -289,31 +517,99 @@ function addLeafSegmentContent({ addLeafBox, leafWidth, leafHeight, leafCenterX,
|
|
|
289
517
|
const colX = colXCenters[c];
|
|
290
518
|
if (seg.type === 'glass') {
|
|
291
519
|
const glassDepth = Math.max(0.004, leafDepth * 0.15);
|
|
292
|
-
|
|
520
|
+
const segmentLeft = colX - colW / 2;
|
|
521
|
+
const segmentRight = colX + colW / 2;
|
|
522
|
+
const glassShape = hasShapedTop && addLeafShape
|
|
523
|
+
? createTopClippedRectShape(segmentLeft, segmentRight, segBottom, segTop, topBoundaryAtX)
|
|
524
|
+
: null;
|
|
525
|
+
if (glassShape && addLeafShape) {
|
|
526
|
+
addLeafShape(glassMaterial, glassShape, glassDepth);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
addLeafBox(glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0);
|
|
530
|
+
}
|
|
293
531
|
}
|
|
294
532
|
else if (seg.type === 'panel') {
|
|
295
|
-
|
|
533
|
+
const segmentLeft = colX - colW / 2;
|
|
534
|
+
const segmentRight = colX + colW / 2;
|
|
535
|
+
const outerPanelShape = hasShapedTop && addLeafShape
|
|
536
|
+
? createTopClippedRectShape(segmentLeft, segmentRight, segBottom, segTop, topBoundaryAtX)
|
|
537
|
+
: null;
|
|
538
|
+
if (outerPanelShape && addLeafShape) {
|
|
539
|
+
addLeafShape(baseMaterial, outerPanelShape, leafDepth);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
addLeafBox(baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0);
|
|
543
|
+
}
|
|
296
544
|
const panelW = colW - 2 * seg.panelInset;
|
|
297
545
|
const panelH = segH - 2 * seg.panelInset;
|
|
298
546
|
if (panelW > 0.01 && panelH > 0.01) {
|
|
299
547
|
const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth);
|
|
300
548
|
const panelZ = leafDepth / 2 + effectiveDepth / 2;
|
|
301
|
-
|
|
549
|
+
const insetLeft = colX - panelW / 2;
|
|
550
|
+
const insetRight = colX + panelW / 2;
|
|
551
|
+
const insetTop = segTop - seg.panelInset;
|
|
552
|
+
const insetBottom = segBottom + seg.panelInset;
|
|
553
|
+
const innerPanelShape = hasShapedTop && addLeafShape
|
|
554
|
+
? createTopClippedRectShape(insetLeft, insetRight, insetBottom, insetTop, topBoundaryAtX)
|
|
555
|
+
: null;
|
|
556
|
+
if (innerPanelShape && addLeafShape) {
|
|
557
|
+
addLeafShape(baseMaterial, innerPanelShape, effectiveDepth);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ);
|
|
561
|
+
}
|
|
302
562
|
}
|
|
303
563
|
}
|
|
304
564
|
}
|
|
305
565
|
segY -= segH;
|
|
306
566
|
}
|
|
307
567
|
}
|
|
308
|
-
function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, hingeX, hingeSide, swingRotation, segments, contentPadding, handle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, doorHeight, }) {
|
|
568
|
+
function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, leafDepth, hingeX, hingeSide, swingRotation, segments, contentPadding, handle, handleBothSides = false, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, doorHeight, openingShape, openingTopRadii, archHeight, roundedBoundary, archedBoundary, archOuterSide, }) {
|
|
309
569
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty');
|
|
310
570
|
const leafGroup = new THREE.Group();
|
|
311
571
|
leafGroup.position.set(hingeX, 0, 0);
|
|
312
572
|
leafGroup.rotation.y = swingRotation;
|
|
313
573
|
mesh.add(leafGroup);
|
|
314
574
|
const addLeafBox = (material, w, h, d, x, y, z) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z);
|
|
575
|
+
const addLeafShape = (material, shape, depth) => addShapeAt(leafGroup, material, shape, depth, -hingeX, 0, 0);
|
|
576
|
+
const localLeafCenterX = leafCenterX - hingeX;
|
|
577
|
+
const leafBottom = leafCenterY - leafHeight / 2;
|
|
578
|
+
const leafTop = leafCenterY + leafHeight / 2;
|
|
579
|
+
const usesShapedLeafFrame = openingShape === 'rounded' || openingShape === 'arch';
|
|
580
|
+
if (usesShapedLeafFrame && hasLeafContent) {
|
|
581
|
+
if (openingShape === 'rounded') {
|
|
582
|
+
const roundedLeafShape = roundedBoundary
|
|
583
|
+
? createRoundedClippedLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, roundedBoundary.fullLeft, roundedBoundary.fullRight, roundedBoundary.radii, contentPadding[0], contentPadding[1])
|
|
584
|
+
: createRoundedLeafFrameShape(leafWidth, leafBottom, leafTop, openingTopRadii, contentPadding[0], contentPadding[1]);
|
|
585
|
+
if (roundedLeafShape) {
|
|
586
|
+
if (roundedBoundary) {
|
|
587
|
+
addLeafShape(baseMaterial, roundedLeafShape, leafDepth);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
addShapeAt(leafGroup, baseMaterial, roundedLeafShape, leafDepth, localLeafCenterX, 0, 0);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else if (openingShape === 'arch') {
|
|
595
|
+
const archedLeafShape = archOuterSide
|
|
596
|
+
? createOneSidedArchLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, archHeight, contentPadding[0], contentPadding[1], archOuterSide)
|
|
597
|
+
: archedBoundary
|
|
598
|
+
? createArchedClippedLeafFrameShape(leafCenterX - leafWidth / 2, leafCenterX + leafWidth / 2, leafBottom, leafTop, archedBoundary.fullLeft, archedBoundary.fullRight, archedBoundary.archHeight, contentPadding[0], contentPadding[1])
|
|
599
|
+
: createArchedLeafFrameShape(leafWidth, leafBottom, leafTop, getClampedArchHeight(leafWidth, leafHeight, archHeight), contentPadding[0], contentPadding[1]);
|
|
600
|
+
if (archedLeafShape) {
|
|
601
|
+
if (archOuterSide || archedBoundary) {
|
|
602
|
+
addLeafShape(baseMaterial, archedLeafShape, leafDepth);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
addShapeAt(leafGroup, baseMaterial, archedLeafShape, leafDepth, localLeafCenterX, 0, 0);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
315
610
|
addLeafSegmentContent({
|
|
316
611
|
addLeafBox,
|
|
612
|
+
addLeafShape,
|
|
317
613
|
leafWidth,
|
|
318
614
|
leafHeight,
|
|
319
615
|
leafCenterX,
|
|
@@ -321,6 +617,11 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
|
|
|
321
617
|
leafDepth,
|
|
322
618
|
segments,
|
|
323
619
|
contentPadding,
|
|
620
|
+
renderPerimeterFrame: !usesShapedLeafFrame,
|
|
621
|
+
openingShape,
|
|
622
|
+
openingTopRadii,
|
|
623
|
+
archHeight,
|
|
624
|
+
archOuterSide,
|
|
324
625
|
});
|
|
325
626
|
if (hasLeafContent && handle) {
|
|
326
627
|
const handleY = handleHeight - doorHeight / 2;
|
|
@@ -349,8 +650,6 @@ function addDoorLeaf(mesh, { leafWidth, leafHeight, leafCenterX, leafCenterY, le
|
|
|
349
650
|
const hingeH = 0.1;
|
|
350
651
|
const hingeW = 0.024;
|
|
351
652
|
const hingeD = leafDepth + 0.016;
|
|
352
|
-
const leafBottom = leafCenterY - leafHeight / 2;
|
|
353
|
-
const leafTop = leafCenterY + leafHeight / 2;
|
|
354
653
|
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafBottom + 0.25, 0);
|
|
355
654
|
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, (leafBottom + leafTop) / 2, 0);
|
|
356
655
|
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafTop - 0.25, 0);
|
|
@@ -607,7 +906,16 @@ function addGarageTiltupDoor(mesh, { insideWidth, leafHeight, leafCenterY, leafD
|
|
|
607
906
|
addTiltupTrim(insetWidth / 2, 0, 0.018, insetHeight);
|
|
608
907
|
addBox(mesh, revealMaterial, insideWidth, 0.026, Math.max(frameDepth * 0.4, 0.035), 0, hingeY, 0);
|
|
609
908
|
}
|
|
610
|
-
function
|
|
909
|
+
function getEffectiveOpeningShape(node) {
|
|
910
|
+
return node.doorType === 'folding' ||
|
|
911
|
+
node.doorType === 'pocket' ||
|
|
912
|
+
node.doorType === 'barn' ||
|
|
913
|
+
node.doorType === 'sliding'
|
|
914
|
+
? 'rectangle'
|
|
915
|
+
: (node.openingShape ?? 'rectangle');
|
|
916
|
+
}
|
|
917
|
+
function updateDoorMesh(rawNode, mesh) {
|
|
918
|
+
const node = normalizeDoorNodeForRender(rawNode);
|
|
611
919
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
612
920
|
mesh.geometry.dispose();
|
|
613
921
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
@@ -622,7 +930,8 @@ function updateDoorMesh(node, mesh) {
|
|
|
622
930
|
disposeObject(child);
|
|
623
931
|
mesh.remove(child);
|
|
624
932
|
}
|
|
625
|
-
const { width, height, openingKind, openingShape, 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;
|
|
933
|
+
const { width, height, openingKind, openingShape: rawOpeningShape, frameThickness, frameDepth, threshold, thresholdHeight, segments, handle, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, contentPadding, hingesSide, swingDirection, swingAngle: nodeSwingAngle = 0, doorType = 'hinged', operationState: nodeOperationState = 0, leafCount = 1, slideDirection = 'left', garagePanelCount = 4, } = node;
|
|
934
|
+
const openingShape = getEffectiveOpeningShape(node) ?? rawOpeningShape;
|
|
626
935
|
const runtimeDoorState = useInteractive.getState().doors[node.id];
|
|
627
936
|
const swingAngle = runtimeDoorState?.swingAngle ?? nodeSwingAngle;
|
|
628
937
|
const operationState = runtimeDoorState?.operationState ?? nodeOperationState;
|
|
@@ -768,6 +1077,14 @@ function updateDoorMesh(node, mesh) {
|
|
|
768
1077
|
}
|
|
769
1078
|
else if (doorType === 'double' || doorType === 'french') {
|
|
770
1079
|
const doubleLeafW = insideWidth / 2;
|
|
1080
|
+
const fullLeafTopRadii = getDoorTopRadii(node, insideWidth, leafH);
|
|
1081
|
+
const roundedBoundary = openingShape === 'rounded'
|
|
1082
|
+
? {
|
|
1083
|
+
fullLeft: -insideWidth / 2,
|
|
1084
|
+
fullRight: insideWidth / 2,
|
|
1085
|
+
radii: fullLeafTopRadii,
|
|
1086
|
+
}
|
|
1087
|
+
: undefined;
|
|
771
1088
|
addDoorLeaf(mesh, {
|
|
772
1089
|
leafWidth: doubleLeafW,
|
|
773
1090
|
leafHeight: leafH,
|
|
@@ -787,6 +1104,13 @@ function updateDoorMesh(node, mesh) {
|
|
|
787
1104
|
panicBar,
|
|
788
1105
|
panicBarHeight,
|
|
789
1106
|
doorHeight: height,
|
|
1107
|
+
openingShape,
|
|
1108
|
+
openingTopRadii: openingShape === 'rounded'
|
|
1109
|
+
? { topLeft: fullLeafTopRadii.topLeft, topRight: 0 }
|
|
1110
|
+
: fullLeafTopRadii,
|
|
1111
|
+
archHeight: node.archHeight ?? 0.45,
|
|
1112
|
+
roundedBoundary,
|
|
1113
|
+
archOuterSide: openingShape === 'arch' ? 'left' : undefined,
|
|
790
1114
|
});
|
|
791
1115
|
addDoorLeaf(mesh, {
|
|
792
1116
|
leafWidth: doubleLeafW,
|
|
@@ -807,6 +1131,13 @@ function updateDoorMesh(node, mesh) {
|
|
|
807
1131
|
panicBar,
|
|
808
1132
|
panicBarHeight,
|
|
809
1133
|
doorHeight: height,
|
|
1134
|
+
openingShape,
|
|
1135
|
+
openingTopRadii: openingShape === 'rounded'
|
|
1136
|
+
? { topLeft: 0, topRight: fullLeafTopRadii.topRight }
|
|
1137
|
+
: fullLeafTopRadii,
|
|
1138
|
+
archHeight: node.archHeight ?? 0.45,
|
|
1139
|
+
roundedBoundary,
|
|
1140
|
+
archOuterSide: openingShape === 'arch' ? 'right' : undefined,
|
|
810
1141
|
});
|
|
811
1142
|
}
|
|
812
1143
|
else {
|
|
@@ -831,9 +1162,28 @@ function updateDoorMesh(node, mesh) {
|
|
|
831
1162
|
panicBar,
|
|
832
1163
|
panicBarHeight,
|
|
833
1164
|
doorHeight: height,
|
|
1165
|
+
openingShape,
|
|
1166
|
+
openingTopRadii: getDoorTopRadii(node, insideWidth, leafH),
|
|
1167
|
+
archHeight: node.archHeight ?? 0.45,
|
|
834
1168
|
});
|
|
835
1169
|
}
|
|
836
1170
|
syncDoorCutout(node, mesh);
|
|
1171
|
+
// Guard: some degenerate door configs can leave a child mesh with an
|
|
1172
|
+
// empty (0-vertex) geometry — e.g. a zero-area extruded leaf frame.
|
|
1173
|
+
// Submitting such a mesh trips a WebGPU error ("Vertex buffer slot 0
|
|
1174
|
+
// … was not set" on a Draw(0, …)). Hide any empty mesh so it is never
|
|
1175
|
+
// drawn (it would render nothing anyway).
|
|
1176
|
+
hideEmptyGeometryMeshes(mesh);
|
|
1177
|
+
}
|
|
1178
|
+
function hideEmptyGeometryMeshes(root) {
|
|
1179
|
+
root.traverse((obj) => {
|
|
1180
|
+
const child = obj;
|
|
1181
|
+
if (!child.isMesh || !child.geometry)
|
|
1182
|
+
return;
|
|
1183
|
+
const position = child.geometry.getAttribute('position');
|
|
1184
|
+
if (!position || position.count === 0)
|
|
1185
|
+
child.visible = false;
|
|
1186
|
+
});
|
|
837
1187
|
}
|
|
838
1188
|
function syncDoorCutout(node, mesh) {
|
|
839
1189
|
// ── Cutout (for wall CSG) — always full door dimensions, 1m deep ──
|
|
@@ -844,7 +1194,8 @@ function syncDoorCutout(node, mesh) {
|
|
|
844
1194
|
mesh.add(cutout);
|
|
845
1195
|
}
|
|
846
1196
|
cutout.geometry.dispose();
|
|
847
|
-
|
|
1197
|
+
const openingShape = getEffectiveOpeningShape(node);
|
|
1198
|
+
if (openingShape === 'arch') {
|
|
848
1199
|
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
1200
|
depth: 1,
|
|
850
1201
|
bevelEnabled: false,
|
|
@@ -852,7 +1203,7 @@ function syncDoorCutout(node, mesh) {
|
|
|
852
1203
|
});
|
|
853
1204
|
cutout.geometry.translate(0, 0, -0.5);
|
|
854
1205
|
}
|
|
855
|
-
else if (
|
|
1206
|
+
else if (openingShape === 'rounded') {
|
|
856
1207
|
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
1208
|
depth: 1,
|
|
858
1209
|
bevelEnabled: false,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elevator-interaction-system.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-interaction-system.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,yBAAyB,SA0DxC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { openElevatorDoor, requestElevatorLevel, resolveElevatorDispatchTarget, useInteractive, useScene, } from '@pascal-app/core';
|
|
2
|
+
import { useThree } from '@react-three/fiber';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { Raycaster, Vector2 } from 'three';
|
|
5
|
+
function getElevatorButtonData(object) {
|
|
6
|
+
let current = object;
|
|
7
|
+
while (current) {
|
|
8
|
+
const data = current.userData.elevatorButton;
|
|
9
|
+
if (data)
|
|
10
|
+
return data;
|
|
11
|
+
current = current.parent;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export function ElevatorInteractionSystem() {
|
|
16
|
+
const camera = useThree((state) => state.camera);
|
|
17
|
+
const gl = useThree((state) => state.gl);
|
|
18
|
+
const scene = useThree((state) => state.scene);
|
|
19
|
+
const raycasterRef = useRef(new Raycaster());
|
|
20
|
+
const pointerRef = useRef(new Vector2());
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const canvas = gl.domElement;
|
|
23
|
+
const handlePointerDown = (event) => {
|
|
24
|
+
if (event.button !== 0)
|
|
25
|
+
return;
|
|
26
|
+
const rect = canvas.getBoundingClientRect();
|
|
27
|
+
const pointer = pointerRef.current;
|
|
28
|
+
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
29
|
+
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
30
|
+
const raycaster = raycasterRef.current;
|
|
31
|
+
raycaster.setFromCamera(pointer, camera);
|
|
32
|
+
const button = raycaster
|
|
33
|
+
.intersectObjects(scene.children, true)
|
|
34
|
+
.map((intersection) => getElevatorButtonData(intersection.object))
|
|
35
|
+
.find((data) => data !== null);
|
|
36
|
+
if (!button || button.disabled)
|
|
37
|
+
return;
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
event.stopPropagation();
|
|
40
|
+
if (button.action === 'open-door') {
|
|
41
|
+
openElevatorDoor(button.elevatorId);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!button.levelId)
|
|
45
|
+
return;
|
|
46
|
+
const targetElevatorId = button.kind === 'landing'
|
|
47
|
+
? resolveElevatorDispatchTarget({
|
|
48
|
+
elevators: useInteractive.getState().elevators,
|
|
49
|
+
levelId: button.levelId,
|
|
50
|
+
nodes: useScene.getState().nodes,
|
|
51
|
+
requestedElevatorId: button.elevatorId,
|
|
52
|
+
})
|
|
53
|
+
: button.elevatorId;
|
|
54
|
+
requestElevatorLevel(targetElevatorId, button.levelId);
|
|
55
|
+
};
|
|
56
|
+
canvas.addEventListener('pointerdown', handlePointerDown, true);
|
|
57
|
+
return () => {
|
|
58
|
+
canvas.removeEventListener('pointerdown', handlePointerDown, true);
|
|
59
|
+
};
|
|
60
|
+
}, [camera, gl, scene]);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
import { type FenceNode } from '@pascal-app/core';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
export declare function generateFenceGeometry(fence: FenceNode): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
|
|
1
4
|
export declare const FenceSystem: () => null;
|
|
2
5
|
//# sourceMappingURL=fence-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA2O9B,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,oFAcrD;AAgBD,eAAO,MAAM,WAAW,YAiBvB,CAAA"}
|