@pascal-app/viewer 0.9.1 → 1.0.0-beta.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/README.md +20 -5
- package/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +5 -2
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/parametric-node-renderer.js +7 -4
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +59 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
- package/dist/components/viewer/index.d.ts +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +113 -12
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +36 -5
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +17 -2
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +180 -24
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +6 -4
- package/dist/components/viewer/scene-bvh.d.ts +2 -1
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +3 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +23 -12
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -24
- package/dist/hooks/use-node-events.d.ts +1 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +22 -14
- package/dist/index.d.ts +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -13
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +3 -9
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +65 -5
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/edge-style.d.ts +1 -0
- package/dist/lib/edge-style.d.ts.map +1 -1
- package/dist/lib/edge-style.js +26 -6
- package/dist/lib/hero-pose.d.ts +47 -0
- package/dist/lib/hero-pose.d.ts.map +1 -0
- package/dist/lib/hero-pose.js +227 -0
- package/dist/lib/ink-edges.d.ts +1 -1
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +13 -7
- package/dist/lib/ktx2-loader.d.ts +46 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +142 -0
- package/dist/lib/layers.d.ts +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +16 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +158 -34
- package/dist/lib/materials.test.d.ts +2 -0
- package/dist/lib/materials.test.d.ts.map +1 -0
- package/dist/lib/materials.test.js +25 -0
- package/dist/lib/merged-outline-node.test.d.ts +2 -0
- package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
- package/dist/lib/merged-outline-node.test.js +22 -0
- package/dist/lib/scene-themes.d.ts +1 -0
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +25 -16
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/snapshot-pipeline.d.ts +42 -0
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
- package/dist/lib/snapshot-pipeline.js +254 -0
- package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
- package/dist/lib/terrain-perimeter-fill.js +75 -0
- package/dist/lib/texture-node-guard.d.ts +13 -0
- package/dist/lib/texture-node-guard.d.ts.map +1 -0
- package/dist/lib/texture-node-guard.js +39 -0
- package/dist/lib/texture-reference.d.ts +31 -0
- package/dist/lib/texture-reference.d.ts.map +1 -0
- package/dist/lib/texture-reference.js +139 -0
- package/dist/lib/texture-reference.test.d.ts +2 -0
- package/dist/lib/texture-reference.test.d.ts.map +1 -0
- package/dist/lib/texture-reference.test.js +86 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +73 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +191 -4
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +61 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +12 -5
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +494 -161
- package/dist/systems/fence/fence-system.d.ts +17 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +221 -53
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
- package/dist/systems/geometry/geometry-system.d.ts +7 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +95 -26
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +20 -6
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +34 -9
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +7 -45
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +29 -8
- package/dist/systems/roof/roof-system.d.ts +21 -8
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1079 -221
- package/dist/systems/slab/slab-system.d.ts +7 -4
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +31 -83
- package/dist/systems/slab/slab-system.test.js +54 -14
- package/dist/systems/stair/stair-system.js +7 -4
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +49 -17
- package/dist/systems/wall/wall-materials.d.ts +11 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +244 -35
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +116 -0
- package/dist/systems/wall/wall-system.d.ts +4 -8
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +397 -177
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +240 -14
- package/package.json +4 -4
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- package/dist/components/viewer/ground-occluder.js +0 -67
- package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
- package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
- package/dist/components/viewer/walkthrough-controls.js +0 -131
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { type SlabNode } from '@pascal-app/core';
|
|
1
|
+
import { type SlabNode, type SlabPolygonContext } from '@pascal-app/core';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
|
-
export declare const SlabSystem: () => null;
|
|
4
3
|
/**
|
|
5
|
-
* Generates
|
|
4
|
+
* Generates slab geometry from polygon. `context` carries the slab's level
|
|
5
|
+
* neighbourhood (walls + sibling slabs) driving the per-edge render offsets —
|
|
6
|
+
* see `getRenderableSlabPolygon`. Branches on the explicit `recessed` intent:
|
|
7
|
+
* a recessed slab is an open shell (pool), everything else a solid occupying
|
|
8
|
+
* `[elevation − thickness, elevation]`.
|
|
6
9
|
*/
|
|
7
|
-
export declare function generateSlabGeometry(slabNode: SlabNode): THREE.BufferGeometry;
|
|
10
|
+
export declare function generateSlabGeometry(slabNode: SlabNode, context: SlabPolygonContext): THREE.BufferGeometry;
|
|
8
11
|
//# sourceMappingURL=slab-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACxB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,kBAAkB,GAC1B,KAAK,CAAC,cAAc,CAItB"}
|
|
@@ -1,68 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useFrame } from '@react-three/fiber';
|
|
3
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { getRenderableSlabPolygon, pointInPolygon2D, polygonsIntersect, } from '@pascal-app/core';
|
|
4
2
|
import * as THREE from 'three';
|
|
5
3
|
import { subtractPolygonsFromPolygon } from '../../lib/polygon-union';
|
|
6
4
|
import { mergeSurfaceHolePolygons } from '../surface-hole-geometry';
|
|
7
|
-
function ensureUv2Attribute(geometry) {
|
|
8
|
-
const uv = geometry.getAttribute('uv');
|
|
9
|
-
if (!uv)
|
|
10
|
-
return;
|
|
11
|
-
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
|
|
12
|
-
}
|
|
13
5
|
// ============================================================================
|
|
14
|
-
// SLAB
|
|
6
|
+
// SLAB GEOMETRY GENERATORS
|
|
15
7
|
// ============================================================================
|
|
16
|
-
export const SlabSystem = () => {
|
|
17
|
-
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
18
|
-
const clearDirty = useScene((state) => state.clearDirty);
|
|
19
|
-
const markDirty = useScene((state) => state.markDirty);
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const nodes = useScene.getState().nodes;
|
|
22
|
-
for (const node of Object.values(nodes)) {
|
|
23
|
-
if (node.type === 'slab') {
|
|
24
|
-
markDirty(node.id);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}, [markDirty]);
|
|
28
|
-
useFrame(() => {
|
|
29
|
-
if (dirtyNodes.size === 0)
|
|
30
|
-
return;
|
|
31
|
-
const nodes = useScene.getState().nodes;
|
|
32
|
-
// Process dirty slabs
|
|
33
|
-
dirtyNodes.forEach((id) => {
|
|
34
|
-
const node = nodes[id];
|
|
35
|
-
if (!node || node.type !== 'slab')
|
|
36
|
-
return;
|
|
37
|
-
const mesh = sceneRegistry.nodes.get(id);
|
|
38
|
-
if (mesh) {
|
|
39
|
-
updateSlabGeometry(getEffectiveNode(node), mesh);
|
|
40
|
-
clearDirty(id);
|
|
41
|
-
}
|
|
42
|
-
// If mesh not found, keep it dirty for next frame
|
|
43
|
-
});
|
|
44
|
-
}, 1);
|
|
45
|
-
return null;
|
|
46
|
-
};
|
|
47
8
|
/**
|
|
48
|
-
*
|
|
9
|
+
* Generates slab geometry from polygon. `context` carries the slab's level
|
|
10
|
+
* neighbourhood (walls + sibling slabs) driving the per-edge render offsets —
|
|
11
|
+
* see `getRenderableSlabPolygon`. Branches on the explicit `recessed` intent:
|
|
12
|
+
* a recessed slab is an open shell (pool), everything else a solid occupying
|
|
13
|
+
* `[elevation − thickness, elevation]`.
|
|
49
14
|
*/
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
mesh.geometry = newGeo;
|
|
55
|
-
// For negative elevation, shift the mesh down so the top face sits at Y=elevation
|
|
56
|
-
// rather than at Y=0. Positive elevation stays at Y=0 (slab sits at floor level).
|
|
57
|
-
const elevation = node.elevation ?? 0.05;
|
|
58
|
-
mesh.position.y = elevation < 0 ? elevation : 0;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Generates extruded slab geometry from polygon
|
|
62
|
-
*/
|
|
63
|
-
export function generateSlabGeometry(slabNode) {
|
|
64
|
-
const elevation = slabNode.elevation ?? 0.05;
|
|
65
|
-
return elevation < 0 ? generatePoolGeometry(slabNode) : generatePositiveSlabGeometry(slabNode);
|
|
15
|
+
export function generateSlabGeometry(slabNode, context) {
|
|
16
|
+
return slabNode.recessed
|
|
17
|
+
? generatePoolGeometry(slabNode, context)
|
|
18
|
+
: generateSolidSlabGeometry(slabNode, context);
|
|
66
19
|
}
|
|
67
20
|
// Earcut normalizes cap triangulation regardless of input winding, but the side
|
|
68
21
|
// walls below assume a CCW contour (the unflipped quad's right-hand normal faces
|
|
@@ -104,7 +57,8 @@ function buildSlabRegions(contour, holes) {
|
|
|
104
57
|
}));
|
|
105
58
|
}
|
|
106
59
|
/**
|
|
107
|
-
*
|
|
60
|
+
* Solid slab occupying `[elevation − thickness, elevation]`: the top cap is
|
|
61
|
+
* the walking surface at `elevation`, the body grows downward by `thickness`.
|
|
108
62
|
*
|
|
109
63
|
* Built directly in 3D (Y-up) rather than via ExtrudeGeometry so the hole side
|
|
110
64
|
* walls can be emitted double-sided. The slab material is forced to FrontSide
|
|
@@ -115,9 +69,11 @@ function buildSlabRegions(contour, holes) {
|
|
|
115
69
|
* thickness visible from any angle: the two coincident triangles never z-fight
|
|
116
70
|
* because exactly one faces the camera under FrontSide culling.
|
|
117
71
|
*/
|
|
118
|
-
function
|
|
119
|
-
const polygon = ensureCounterClockwisePolygon(getRenderableSlabPolygon(slabNode));
|
|
72
|
+
function generateSolidSlabGeometry(slabNode, context) {
|
|
73
|
+
const polygon = ensureCounterClockwisePolygon(getRenderableSlabPolygon(slabNode, context));
|
|
120
74
|
const elevation = slabNode.elevation ?? 0.05;
|
|
75
|
+
const thickness = slabNode.thickness ?? 0.05;
|
|
76
|
+
const bottom = elevation - thickness;
|
|
121
77
|
const holePolygons = mergeSurfaceHolePolygons(slabNode.holes ?? []);
|
|
122
78
|
if (polygon.length < 3)
|
|
123
79
|
return new THREE.BufferGeometry();
|
|
@@ -131,14 +87,14 @@ function generatePositiveSlabGeometry(slabNode) {
|
|
|
131
87
|
const addWall = (a, b, flipped) => {
|
|
132
88
|
const base = positions.length / 3;
|
|
133
89
|
const len = Math.max(Math.hypot(b.x - a.x, b.y - a.y), 0.001);
|
|
134
|
-
positions.push(a.x,
|
|
90
|
+
positions.push(a.x, bottom, a.y);
|
|
135
91
|
uvs.push(0, 0);
|
|
136
|
-
positions.push(b.x,
|
|
92
|
+
positions.push(b.x, bottom, b.y);
|
|
137
93
|
uvs.push(len, 0);
|
|
138
94
|
positions.push(b.x, elevation, b.y);
|
|
139
|
-
uvs.push(len,
|
|
95
|
+
uvs.push(len, thickness);
|
|
140
96
|
positions.push(a.x, elevation, a.y);
|
|
141
|
-
uvs.push(0,
|
|
97
|
+
uvs.push(0, thickness);
|
|
142
98
|
// Standard winding on a CCW polygon gives inward-facing normals (see pool
|
|
143
99
|
// path), so the unflipped quad faces outward; flipped is its back face.
|
|
144
100
|
if (!flipped) {
|
|
@@ -165,7 +121,7 @@ function generatePositiveSlabGeometry(slabNode) {
|
|
|
165
121
|
}
|
|
166
122
|
const bottomBase = positions.length / 3;
|
|
167
123
|
for (const p of capPoints) {
|
|
168
|
-
positions.push(p.x,
|
|
124
|
+
positions.push(p.x, bottom, p.y);
|
|
169
125
|
uvs.push(p.x, -p.y);
|
|
170
126
|
}
|
|
171
127
|
const capTris = THREE.ShapeUtils.triangulateShape(contour2d, holes2d);
|
|
@@ -195,37 +151,29 @@ function generatePositiveSlabGeometry(slabNode) {
|
|
|
195
151
|
return geometry;
|
|
196
152
|
}
|
|
197
153
|
/**
|
|
198
|
-
* Pool / recessed slab: floor cap at Y=0 (local) + inner walls up to
|
|
199
|
-
* No top cap — the opening at
|
|
154
|
+
* Pool / recessed slab: floor cap at Y=0 (local) + inner walls up to the rim.
|
|
155
|
+
* No top cap — the opening at the rim is handled by the ground occluder hole.
|
|
200
156
|
* mesh.position.y must be set to elevation so the floor sits at the correct world Y.
|
|
201
157
|
*
|
|
202
158
|
* Geometry is built directly in 3D (Y-up) to avoid rotation confusion:
|
|
203
159
|
* - floor in XZ plane at Y=0, normals pointing +Y (visible when looking down into pool)
|
|
204
160
|
* - walls from Y=0 to Y=depth, inward-facing normals (visible from inside pool)
|
|
205
161
|
*/
|
|
206
|
-
function generatePoolGeometry(slabNode) {
|
|
207
|
-
const polygon = ensureCounterClockwisePolygon(getRenderableSlabPolygon(slabNode));
|
|
208
|
-
const
|
|
162
|
+
function generatePoolGeometry(slabNode, context) {
|
|
163
|
+
const polygon = ensureCounterClockwisePolygon(getRenderableSlabPolygon(slabNode, context));
|
|
164
|
+
const floor = slabNode.elevation ?? 0.05;
|
|
165
|
+
const depth = Math.max(0, (slabNode.recessedRimElevation ?? 0) - floor);
|
|
209
166
|
const holePolygons = mergeSurfaceHolePolygons(slabNode.holes ?? []);
|
|
210
167
|
if (polygon.length < 3)
|
|
211
168
|
return new THREE.BufferGeometry();
|
|
212
169
|
const positions = [];
|
|
213
170
|
const uvs = [];
|
|
214
171
|
const indices = [];
|
|
215
|
-
const bounds = new THREE.Box2();
|
|
216
|
-
for (const [x, z] of polygon) {
|
|
217
|
-
bounds.expandByPoint(new THREE.Vector2(x, z));
|
|
218
|
-
}
|
|
219
|
-
for (const hole of holePolygons) {
|
|
220
|
-
for (const [x, z] of hole) {
|
|
221
|
-
bounds.expandByPoint(new THREE.Vector2(x, z));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const floorWidth = Math.max(bounds.max.x - bounds.min.x, 0.001);
|
|
225
|
-
const floorHeight = Math.max(bounds.max.y - bounds.min.y, 0.001);
|
|
226
172
|
const pushFloorVertex = (x, y, z) => {
|
|
227
173
|
positions.push(x, y, z);
|
|
228
|
-
|
|
174
|
+
// Floor UVs in metres (shape-space x, -z), matching generateSolidSlabGeometry's
|
|
175
|
+
// cap mapping so a finish tiles at the same world scale on every surface.
|
|
176
|
+
uvs.push(x, -z);
|
|
229
177
|
};
|
|
230
178
|
const pushWallVertex = (x, y, z, u, v) => {
|
|
231
179
|
positions.push(x, y, z);
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
import { describe, expect, test } from 'bun:test';
|
|
4
4
|
import { SlabNode } from '@pascal-app/core';
|
|
5
5
|
import { generateSlabGeometry } from './slab-system';
|
|
6
|
+
const EMPTY_CONTEXT = { walls: [], siblingSlabs: [] };
|
|
7
|
+
const SQUARE = [
|
|
8
|
+
[0, 0],
|
|
9
|
+
[4, 0],
|
|
10
|
+
[4, 3],
|
|
11
|
+
[0, 3],
|
|
12
|
+
];
|
|
6
13
|
function hasVertexAt(geometry, x, z) {
|
|
7
14
|
const positions = geometry.getAttribute('position');
|
|
8
15
|
for (let index = 0; index < positions.count; index += 1) {
|
|
@@ -12,16 +19,19 @@ function hasVertexAt(geometry, x, z) {
|
|
|
12
19
|
}
|
|
13
20
|
return false;
|
|
14
21
|
}
|
|
22
|
+
function uniqueSortedYs(geometry) {
|
|
23
|
+
const positions = geometry.getAttribute('position');
|
|
24
|
+
const ys = new Set();
|
|
25
|
+
for (let index = 0; index < positions.count; index += 1) {
|
|
26
|
+
ys.add(Math.round(positions.getY(index) * 1e4) / 1e4);
|
|
27
|
+
}
|
|
28
|
+
return [...ys].sort((a, b) => a - b);
|
|
29
|
+
}
|
|
15
30
|
describe('generateSlabGeometry', () => {
|
|
16
31
|
test('renders a boundary-overlapping hole as an open indentation', () => {
|
|
17
32
|
const slab = SlabNode.parse({
|
|
18
33
|
elevation: 0.05,
|
|
19
|
-
polygon:
|
|
20
|
-
[0, 0],
|
|
21
|
-
[4, 0],
|
|
22
|
-
[4, 3],
|
|
23
|
-
[0, 3],
|
|
24
|
-
],
|
|
34
|
+
polygon: SQUARE,
|
|
25
35
|
holes: [
|
|
26
36
|
[
|
|
27
37
|
[1, -0.5],
|
|
@@ -31,7 +41,7 @@ describe('generateSlabGeometry', () => {
|
|
|
31
41
|
],
|
|
32
42
|
],
|
|
33
43
|
});
|
|
34
|
-
const geometry = generateSlabGeometry(slab);
|
|
44
|
+
const geometry = generateSlabGeometry(slab, EMPTY_CONTEXT);
|
|
35
45
|
expect((geometry.index?.count ?? 0) / 3).toBeGreaterThan(0);
|
|
36
46
|
expect(hasVertexAt(geometry, 1, 1)).toBe(true);
|
|
37
47
|
expect(hasVertexAt(geometry, 3, 1)).toBe(true);
|
|
@@ -39,12 +49,8 @@ describe('generateSlabGeometry', () => {
|
|
|
39
49
|
test('renders a boundary-overlapping hole as an open indentation on recessed slabs', () => {
|
|
40
50
|
const slab = SlabNode.parse({
|
|
41
51
|
elevation: -0.2,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
[4, 0],
|
|
45
|
-
[4, 3],
|
|
46
|
-
[0, 3],
|
|
47
|
-
],
|
|
52
|
+
recessed: true,
|
|
53
|
+
polygon: SQUARE,
|
|
48
54
|
holes: [
|
|
49
55
|
[
|
|
50
56
|
[1, -0.5],
|
|
@@ -54,9 +60,43 @@ describe('generateSlabGeometry', () => {
|
|
|
54
60
|
],
|
|
55
61
|
],
|
|
56
62
|
});
|
|
57
|
-
const geometry = generateSlabGeometry(slab);
|
|
63
|
+
const geometry = generateSlabGeometry(slab, EMPTY_CONTEXT);
|
|
58
64
|
expect((geometry.index?.count ?? 0) / 3).toBeGreaterThan(0);
|
|
59
65
|
expect(hasVertexAt(geometry, 1, 1)).toBe(true);
|
|
60
66
|
expect(hasVertexAt(geometry, 3, 1)).toBe(true);
|
|
61
67
|
});
|
|
68
|
+
test('solid slab occupies [elevation − thickness, elevation]', () => {
|
|
69
|
+
const slab = SlabNode.parse({ elevation: 0.3, thickness: 0.1, polygon: SQUARE });
|
|
70
|
+
const ys = uniqueSortedYs(generateSlabGeometry(slab, EMPTY_CONTEXT));
|
|
71
|
+
expect(ys).toEqual([0.2, 0.3]);
|
|
72
|
+
});
|
|
73
|
+
test('migrated legacy slab (thickness = elevation) reproduces the [0, elevation] extrusion', () => {
|
|
74
|
+
const slab = SlabNode.parse({ elevation: 0.05, thickness: 0.05, polygon: SQUARE });
|
|
75
|
+
const geometry = generateSlabGeometry(slab, EMPTY_CONTEXT);
|
|
76
|
+
// Old-style expectations: extrude-from-zero put the bottom cap at 0 and
|
|
77
|
+
// the top cap at `elevation`, with 8 cap verts + 4 side quads (16 verts)
|
|
78
|
+
// and 12 triangles for a plain quad slab.
|
|
79
|
+
expect(uniqueSortedYs(geometry)).toEqual([0, 0.05]);
|
|
80
|
+
expect(geometry.getAttribute('position').count).toBe(24);
|
|
81
|
+
expect((geometry.index?.count ?? 0) / 3).toBe(12);
|
|
82
|
+
for (const [x, z] of SQUARE) {
|
|
83
|
+
expect(hasVertexAt(geometry, x, z)).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
test('recess is keyed by the flag, not the elevation sign', () => {
|
|
87
|
+
const belowPlaneSolid = SlabNode.parse({ elevation: -0.2, thickness: 0.05, polygon: SQUARE });
|
|
88
|
+
// Without `recessed`, a negative elevation is just a solid placed below
|
|
89
|
+
// the level plane: closed body at [-0.25, -0.2], world-space Y baked in.
|
|
90
|
+
expect(uniqueSortedYs(generateSlabGeometry(belowPlaneSolid, EMPTY_CONTEXT))).toEqual([
|
|
91
|
+
-0.25, -0.2,
|
|
92
|
+
]);
|
|
93
|
+
// The recessed shell is authored at local Y=0 (floor) up to |elevation|
|
|
94
|
+
// (rim), with no top cap: 4 floor verts + 4 wall quads = 20 verts,
|
|
95
|
+
// 2 floor + 8 wall triangles.
|
|
96
|
+
const pool = SlabNode.parse({ elevation: -0.2, recessed: true, polygon: SQUARE });
|
|
97
|
+
const poolGeometry = generateSlabGeometry(pool, EMPTY_CONTEXT);
|
|
98
|
+
expect(uniqueSortedYs(poolGeometry)).toEqual([0, 0.2]);
|
|
99
|
+
expect(poolGeometry.getAttribute('position').count).toBe(20);
|
|
100
|
+
expect((poolGeometry.index?.count ?? 0) / 3).toBe(10);
|
|
101
|
+
});
|
|
62
102
|
});
|
|
@@ -76,7 +76,7 @@ export const StairSystem = () => {
|
|
|
76
76
|
// --- Pass 1b: Sync chained transforms to individual segment meshes (edit mode) ---
|
|
77
77
|
for (const stairId of parentsNeedingSegmentSync) {
|
|
78
78
|
const baseStairNode = nodes[stairId];
|
|
79
|
-
if (
|
|
79
|
+
if (baseStairNode?.type !== 'stair')
|
|
80
80
|
continue;
|
|
81
81
|
// Merge any in-flight drag override (e.g. parent-stair rotate handle)
|
|
82
82
|
// so slab-elevation spatial queries match where the segments are
|
|
@@ -98,7 +98,7 @@ export const StairSystem = () => {
|
|
|
98
98
|
if (stairsProcessed >= MAX_STAIRS_PER_FRAME)
|
|
99
99
|
break;
|
|
100
100
|
const node = nodes[id];
|
|
101
|
-
if (
|
|
101
|
+
if (node?.type !== 'stair') {
|
|
102
102
|
pendingStairUpdates.delete(id);
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
@@ -106,7 +106,7 @@ export const StairSystem = () => {
|
|
|
106
106
|
if (group) {
|
|
107
107
|
const mergedMesh = group.getObjectByName('merged-stair');
|
|
108
108
|
if (mergedMesh?.visible !== false) {
|
|
109
|
-
updateMergedStairGeometry(node, group, nodes);
|
|
109
|
+
updateMergedStairGeometry(getEffectiveNode(node), group, nodes);
|
|
110
110
|
stairsProcessed++;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -434,6 +434,9 @@ function createEmptyGeometry() {
|
|
|
434
434
|
// unbound and the draw is rejected ("Vertex buffer slot 0 … was not set"),
|
|
435
435
|
// poisoning the command encoder. The count-0 groups keep nothing drawn.
|
|
436
436
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(9), 3));
|
|
437
|
+
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Float32Array(9), 3));
|
|
438
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array(6), 2));
|
|
439
|
+
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array(6), 2));
|
|
437
440
|
geometry.addGroup(0, 0, STAIR_TREAD_MATERIAL_INDEX);
|
|
438
441
|
geometry.addGroup(0, 0, STAIR_SIDE_MATERIAL_INDEX);
|
|
439
442
|
return geometry;
|
|
@@ -745,7 +748,7 @@ function computeAbsoluteHeight(node) {
|
|
|
745
748
|
if (!node.parentId)
|
|
746
749
|
return 0;
|
|
747
750
|
const parent = nodes[node.parentId];
|
|
748
|
-
if (
|
|
751
|
+
if (parent?.type !== 'stair')
|
|
749
752
|
return 0;
|
|
750
753
|
const stair = parent;
|
|
751
754
|
const segments = (stair.children ?? [])
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DoorNode, WindowNode } from '@pascal-app/core';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
export type OpeningCutoutNode = DoorNode | WindowNode;
|
|
4
|
+
export type OpeningCutoutRect = {
|
|
5
|
+
left: number;
|
|
6
|
+
right: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
top: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function getOpeningCutoutProxyDepth(wallThickness: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Pure cutout profile for a shaped door / window opening. `rect` is in
|
|
13
|
+
* the caller's coordinate frame — the wall CSG pipeline passes wall-local
|
|
14
|
+
* coords, the roof-wall pipeline an origin-centered rect — so the same
|
|
15
|
+
* radii / arch math serves both hosts.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildOpeningCutoutShape(opening: OpeningCutoutNode, rect: OpeningCutoutRect): THREE.Shape;
|
|
18
|
+
export declare function buildOpeningCutoutGeometry(opening: OpeningCutoutNode, rect: OpeningCutoutRect, depth: number, wallThickness: number): THREE.BufferGeometry;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the cutout profile's bottom edge is a flat chord. Cuts whose
|
|
21
|
+
* bottom sits coplanar with the host wall base get extended slightly
|
|
22
|
+
* downward to keep CSG away from coplanar faces — but only a flat chord
|
|
23
|
+
* may extend; shifting a rounded bottom would distort the profile.
|
|
24
|
+
*/
|
|
25
|
+
export declare function hasFlatOpeningCutoutBottom(opening: OpeningCutoutNode): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Extends floor-level flat cutouts below the host wall so CSG never has to
|
|
28
|
+
* subtract a face exactly coplanar with the wall base.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getOpeningCutoutBottomPadding(opening: OpeningCutoutNode, bottom: number): number;
|
|
31
|
+
//# sourceMappingURL=opening-cutout-geometry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opening-cutout-geometry.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/opening-cutout-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,UAAU,CAAA;AAErD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAYD,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAExE;AASD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,iBAAiB,GACtB,KAAK,CAAC,KAAK,CAuCb;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,KAAK,CAAC,cAAc,CAqBtB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAW9E;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEhG"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
// The cutout proxy doubles as the invisible raycast hit target for an opening:
|
|
3
|
+
// centered on the wall and extending past both faces so it wins the scene
|
|
4
|
+
// raycast over the recessed door/window body for front AND back selection +
|
|
5
|
+
// paint. Wall CSG rebuilds opening cuts directly from node data, so this proxy
|
|
6
|
+
// only needs to clear the wall thickness plus a small proud margin. A snug depth
|
|
7
|
+
// keeps the hit target useful without blanketing the room floor in a top-down
|
|
8
|
+
// view (the bug a 1m-deep proxy caused in narrow hallways).
|
|
9
|
+
const OPENING_CUTOUT_PROXY_PROUD_MARGIN = 0.08;
|
|
10
|
+
const OPENING_CUTOUT_BOTTOM_PADDING = 0.02;
|
|
11
|
+
export function getOpeningCutoutProxyDepth(wallThickness) {
|
|
12
|
+
return Math.max(wallThickness, 0) + OPENING_CUTOUT_PROXY_PROUD_MARGIN;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Pure cutout profile for a shaped door / window opening. `rect` is in
|
|
16
|
+
* the caller's coordinate frame — the wall CSG pipeline passes wall-local
|
|
17
|
+
* coords, the roof-wall pipeline an origin-centered rect — so the same
|
|
18
|
+
* radii / arch math serves both hosts.
|
|
19
|
+
*/
|
|
20
|
+
export function buildOpeningCutoutShape(opening, rect) {
|
|
21
|
+
const { left, right, bottom, top } = rect;
|
|
22
|
+
const width = Math.max(right - left, 1e-6);
|
|
23
|
+
const height = Math.max(top - bottom, 1e-6);
|
|
24
|
+
const shape = new THREE.Shape();
|
|
25
|
+
if (opening.openingShape === 'arch') {
|
|
26
|
+
const halfWidth = width / 2;
|
|
27
|
+
const centerX = (left + right) / 2;
|
|
28
|
+
const archHeight = Math.min(Math.max(opening.archHeight ?? width / 2, 0.01), height);
|
|
29
|
+
const springY = top - archHeight;
|
|
30
|
+
const segments = 32;
|
|
31
|
+
shape.moveTo(left, bottom);
|
|
32
|
+
shape.lineTo(right, bottom);
|
|
33
|
+
shape.lineTo(right, springY);
|
|
34
|
+
for (let index = 1; index <= segments; index += 1) {
|
|
35
|
+
const x = right + (left - right) * (index / segments);
|
|
36
|
+
const normalizedX = Math.min(Math.abs((x - centerX) / halfWidth), 1);
|
|
37
|
+
const y = springY + archHeight * Math.sqrt(Math.max(1 - normalizedX * normalizedX, 0));
|
|
38
|
+
shape.lineTo(x, y);
|
|
39
|
+
}
|
|
40
|
+
shape.lineTo(left, bottom);
|
|
41
|
+
shape.closePath();
|
|
42
|
+
return shape;
|
|
43
|
+
}
|
|
44
|
+
if (opening.openingShape === 'rounded') {
|
|
45
|
+
const radii = getRoundedOpeningRadii(opening, width, height);
|
|
46
|
+
applyRoundedOpeningShape(shape, left, right, bottom, top, radii);
|
|
47
|
+
return shape;
|
|
48
|
+
}
|
|
49
|
+
shape.moveTo(left, bottom);
|
|
50
|
+
shape.lineTo(right, bottom);
|
|
51
|
+
shape.lineTo(right, top);
|
|
52
|
+
shape.lineTo(left, top);
|
|
53
|
+
shape.closePath();
|
|
54
|
+
return shape;
|
|
55
|
+
}
|
|
56
|
+
export function buildOpeningCutoutGeometry(opening, rect, depth, wallThickness) {
|
|
57
|
+
const shape = buildOpeningCutoutShape(opening, rect);
|
|
58
|
+
const bevelSize = opening.openingShape === 'rounded'
|
|
59
|
+
? Math.min(Math.max(opening.openingRevealRadius ?? 0.025, 0), Math.max(wallThickness * 0.45, 0.001), Math.max((opening.cornerRadius ?? 0.15) * 0.45, 0.001))
|
|
60
|
+
: 0;
|
|
61
|
+
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
62
|
+
depth,
|
|
63
|
+
bevelEnabled: bevelSize > 0,
|
|
64
|
+
bevelSegments: bevelSize > 0 ? 8 : 0,
|
|
65
|
+
bevelSize,
|
|
66
|
+
bevelThickness: bevelSize,
|
|
67
|
+
curveSegments: 24,
|
|
68
|
+
});
|
|
69
|
+
geometry.translate(0, 0, -depth / 2);
|
|
70
|
+
return geometry;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether the cutout profile's bottom edge is a flat chord. Cuts whose
|
|
74
|
+
* bottom sits coplanar with the host wall base get extended slightly
|
|
75
|
+
* downward to keep CSG away from coplanar faces — but only a flat chord
|
|
76
|
+
* may extend; shifting a rounded bottom would distort the profile.
|
|
77
|
+
*/
|
|
78
|
+
export function hasFlatOpeningCutoutBottom(opening) {
|
|
79
|
+
if (opening.openingShape !== 'rounded' || opening.type !== 'window')
|
|
80
|
+
return true;
|
|
81
|
+
if (opening.openingRadiusMode === 'individual') {
|
|
82
|
+
const [, , bottomRight = 0, bottomLeft = 0] = opening.openingCornerRadii ?? [
|
|
83
|
+
0.15, 0.15, 0.15, 0.15,
|
|
84
|
+
];
|
|
85
|
+
return bottomRight <= 1e-6 && bottomLeft <= 1e-6;
|
|
86
|
+
}
|
|
87
|
+
return Math.max(opening.cornerRadius ?? 0.15, 0) <= 1e-6;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extends floor-level flat cutouts below the host wall so CSG never has to
|
|
91
|
+
* subtract a face exactly coplanar with the wall base.
|
|
92
|
+
*/
|
|
93
|
+
export function getOpeningCutoutBottomPadding(opening, bottom) {
|
|
94
|
+
return bottom < 0.005 && hasFlatOpeningCutoutBottom(opening) ? OPENING_CUTOUT_BOTTOM_PADDING : 0;
|
|
95
|
+
}
|
|
96
|
+
function getRoundedOpeningRadii(opening, width, height) {
|
|
97
|
+
if (opening.type !== 'window') {
|
|
98
|
+
if (opening.openingRadiusMode === 'individual') {
|
|
99
|
+
const [topLeft = 0, topRight = 0] = opening.openingTopRadii ?? [0.15, 0.15];
|
|
100
|
+
return normalizeCornerRadii({
|
|
101
|
+
topLeft: Math.max(topLeft, 0),
|
|
102
|
+
topRight: Math.max(topRight, 0),
|
|
103
|
+
bottomRight: 0,
|
|
104
|
+
bottomLeft: 0,
|
|
105
|
+
}, width, height);
|
|
106
|
+
}
|
|
107
|
+
const maxRadius = Math.min(width / 2, height);
|
|
108
|
+
const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
|
|
109
|
+
return { topLeft: radius, topRight: radius, bottomRight: 0, bottomLeft: 0 };
|
|
110
|
+
}
|
|
111
|
+
if (opening.openingRadiusMode === 'individual') {
|
|
112
|
+
const [topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0] = opening.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15];
|
|
113
|
+
return normalizeCornerRadii({
|
|
114
|
+
topLeft: Math.max(topLeft, 0),
|
|
115
|
+
topRight: Math.max(topRight, 0),
|
|
116
|
+
bottomRight: Math.max(bottomRight, 0),
|
|
117
|
+
bottomLeft: Math.max(bottomLeft, 0),
|
|
118
|
+
}, width, height);
|
|
119
|
+
}
|
|
120
|
+
const maxRadius = Math.min(width / 2, height / 2);
|
|
121
|
+
const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
|
|
122
|
+
return { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius };
|
|
123
|
+
}
|
|
124
|
+
function normalizeCornerRadii(radii, width, height) {
|
|
125
|
+
const next = { ...radii };
|
|
126
|
+
const maxScale = Math.min(1, width / Math.max(next.topLeft + next.topRight, 1e-6), width / Math.max(next.bottomLeft + next.bottomRight, 1e-6), height / Math.max(next.topLeft + next.bottomLeft, 1e-6), height / Math.max(next.topRight + next.bottomRight, 1e-6));
|
|
127
|
+
if (maxScale < 1) {
|
|
128
|
+
next.topLeft *= maxScale;
|
|
129
|
+
next.topRight *= maxScale;
|
|
130
|
+
next.bottomRight *= maxScale;
|
|
131
|
+
next.bottomLeft *= maxScale;
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
}
|
|
135
|
+
function applyRoundedOpeningShape(shape, left, right, bottom, top, radii) {
|
|
136
|
+
const { topLeft, topRight, bottomRight, bottomLeft } = radii;
|
|
137
|
+
shape.moveTo(left + bottomLeft, bottom);
|
|
138
|
+
shape.lineTo(right - bottomRight, bottom);
|
|
139
|
+
if (bottomRight > 1e-6) {
|
|
140
|
+
shape.absarc(right - bottomRight, bottom + bottomRight, bottomRight, -Math.PI / 2, 0, false);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
shape.lineTo(right, bottom);
|
|
144
|
+
}
|
|
145
|
+
shape.lineTo(right, top - topRight);
|
|
146
|
+
if (topRight > 1e-6) {
|
|
147
|
+
shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
shape.lineTo(right, top);
|
|
151
|
+
}
|
|
152
|
+
shape.lineTo(left + topLeft, top);
|
|
153
|
+
if (topLeft > 1e-6) {
|
|
154
|
+
shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
shape.lineTo(left, top);
|
|
158
|
+
}
|
|
159
|
+
shape.lineTo(left, bottom + bottomLeft);
|
|
160
|
+
if (bottomLeft > 1e-6) {
|
|
161
|
+
shape.absarc(left + bottomLeft, bottom + bottomLeft, bottomLeft, Math.PI, Math.PI * 1.5, false);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
shape.lineTo(left, bottom);
|
|
165
|
+
}
|
|
166
|
+
shape.closePath();
|
|
167
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opening-cutout-geometry.test.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/opening-cutout-geometry.test.ts"],"names":[],"mappings":""}
|