@pascal-app/viewer 1.0.1 → 1.0.4
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.map +1 -1
- package/dist/components/renderers/node-renderer.js +8 -3
- package/dist/components/viewer/frame-limiter.d.ts +1 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +22 -3
- package/dist/components/viewer/glb-interactive.d.ts +25 -1
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +252 -85
- package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
- package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
- package/dist/components/viewer/glb-mechanisms.js +19 -0
- package/dist/components/viewer/glb-scene.d.ts +45 -27
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +420 -506
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +12 -1
- package/dist/components/viewer/index.d.ts +26 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +108 -29
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +6 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +6 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +8 -3
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +16 -2
- package/dist/components/viewer/render-error.d.ts +18 -0
- package/dist/components/viewer/render-error.d.ts.map +1 -0
- package/dist/components/viewer/render-error.js +29 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
- package/dist/components/viewer/scene-ground-replacement.js +15 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +10 -0
- package/dist/components/viewer/viewer-camera.d.ts +19 -1
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +53 -5
- package/dist/index.d.ts +28 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -18
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -1
- package/dist/lib/csg-utils.d.ts +1 -1
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +1 -1
- package/dist/lib/display-state.d.ts +25 -0
- package/dist/lib/display-state.d.ts.map +1 -0
- package/dist/lib/display-state.js +1 -0
- package/dist/lib/hero-pose.d.ts +12 -2
- package/dist/lib/hero-pose.d.ts.map +1 -1
- package/dist/lib/hero-pose.js +72 -5
- package/dist/lib/ink-edges.d.ts +4 -2
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +17 -7
- package/dist/lib/key-light-override.d.ts +11 -0
- package/dist/lib/key-light-override.d.ts.map +1 -0
- package/dist/lib/key-light-override.js +14 -0
- package/dist/lib/live-frame-hold.d.ts +14 -0
- package/dist/lib/live-frame-hold.d.ts.map +1 -0
- package/dist/lib/live-frame-hold.js +28 -0
- package/dist/lib/materials.d.ts +14 -7
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +31 -5
- package/dist/lib/node-top-surface-height.d.ts +6 -0
- package/dist/lib/node-top-surface-height.d.ts.map +1 -0
- package/dist/lib/node-top-surface-height.js +74 -0
- package/dist/lib/perf-probe-state.d.ts +39 -0
- package/dist/lib/perf-probe-state.d.ts.map +1 -0
- package/dist/lib/perf-probe-state.js +17 -0
- package/dist/lib/pointer-events.d.ts +8 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +27 -4
- package/dist/lib/procedural-emission.d.ts +7 -0
- package/dist/lib/procedural-emission.d.ts.map +1 -0
- package/dist/lib/procedural-emission.js +54 -0
- package/dist/lib/renderer-capability.d.ts +2 -1
- package/dist/lib/renderer-capability.d.ts.map +1 -1
- package/dist/lib/renderer-capability.js +2 -2
- package/dist/lib/scene-support-height.d.ts +4 -0
- package/dist/lib/scene-support-height.d.ts.map +1 -0
- package/dist/lib/scene-support-height.js +34 -0
- package/dist/lib/scene-visibility.d.ts +6 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +15 -3
- package/dist/lib/scripted-opening.d.ts +11 -0
- package/dist/lib/scripted-opening.d.ts.map +1 -0
- package/dist/lib/scripted-opening.js +20 -0
- package/dist/lib/snapshot-pipeline.d.ts +32 -2
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +215 -105
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
- package/dist/lib/terrain-perimeter-fill.js +22 -14
- package/dist/lib/zone-geometry.d.ts +11 -0
- package/dist/lib/zone-geometry.d.ts.map +1 -0
- package/dist/lib/zone-geometry.js +46 -0
- package/dist/store/use-item-light-pool.d.ts +14 -10
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +52 -26
- package/dist/store/use-viewer.d.ts +11 -0
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +13 -6
- package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +86 -6
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +73 -22
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +148 -5
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +71 -11
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +40 -15
- package/dist/systems/interactive/procedural-controls.d.ts +13 -0
- package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
- package/dist/systems/interactive/procedural-controls.js +17 -0
- package/dist/systems/interactive/scripted-clips.d.ts +19 -0
- package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
- package/dist/systems/interactive/scripted-clips.js +134 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +9 -3
- package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
- package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
- package/dist/systems/item/object-cut-invalidation.js +68 -0
- 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 +47 -52
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +17 -19
- package/dist/systems/level/level-utils.d.ts +28 -1
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +36 -4
- package/dist/systems/level/shadow-only-levels.d.ts +9 -0
- package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
- package/dist/systems/level/shadow-only-levels.js +7 -0
- package/dist/systems/roof/roof-materials.d.ts +14 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +54 -15
- package/dist/systems/roof/roof-system.d.ts +13 -3
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +188 -13
- package/dist/systems/slab/slab-system.d.ts +1 -1
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +49 -19
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +133 -354
- package/dist/systems/wall/wall-cutout-cache.d.ts +11 -1
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +58 -20
- package/dist/systems/wall/wall-cutout.d.ts +9 -1
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +46 -32
- package/dist/systems/wall/wall-finish-data.d.ts +22 -0
- package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
- package/dist/systems/wall/wall-finish-data.js +17 -0
- package/dist/systems/wall/wall-materials.d.ts +17 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +109 -156
- package/dist/systems/wall/wall-system.d.ts +43 -4
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +601 -163
- package/dist/systems/window/window-system.d.ts +1 -1
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +44 -24
- package/dist/xr/presentation-context.d.ts +7 -0
- package/dist/xr/presentation-context.d.ts.map +1 -0
- package/dist/xr/presentation-context.js +10 -0
- package/package.json +6 -6
- package/dist/systems/fence/fence-system.d.ts +0 -22
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -357
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getEffectiveRoofSurfaceMaterial, } from '@pascal-app/core';
|
|
2
|
-
import { createMaterial,
|
|
1
|
+
import { getEffectiveRoofSurfaceMaterial, parseMaterialRef, wallAssemblyFinishRef, } from '@pascal-app/core';
|
|
2
|
+
import { createMaterial, createSurfaceRoleMaterial, resolveMaterialRef, resolveSlotDefaultMaterial, } from '../../lib/materials';
|
|
3
3
|
// Declared catalog defaults for an unpainted roof, per the 4-slot layout
|
|
4
4
|
// (0 wall/trim · 1 deck · 2 interior soffit · 3 shingle top). The wall/trim
|
|
5
5
|
// band mirrors the wall kind's default (WALL_SLOT_DEFAULT = concrete-drywall)
|
|
@@ -11,22 +11,59 @@ const ROOF_DEFAULT_REFS = [
|
|
|
11
11
|
'library:roof-terracottatiles',
|
|
12
12
|
];
|
|
13
13
|
const roofMaterialArrayCache = new Map();
|
|
14
|
-
function getSurfaceMaterialSignature(spec) {
|
|
14
|
+
function getSurfaceMaterialSignature(spec, sceneMaterials) {
|
|
15
|
+
const ref = parseMaterialRef(spec.materialPreset);
|
|
15
16
|
return JSON.stringify({
|
|
16
17
|
material: spec.material ?? null,
|
|
17
18
|
materialPreset: spec.materialPreset ?? null,
|
|
19
|
+
sceneMaterial: ref?.kind === 'scene'
|
|
20
|
+
? (sceneMaterials?.[ref.id]?.material ?? null)
|
|
21
|
+
: null,
|
|
18
22
|
});
|
|
19
23
|
}
|
|
20
|
-
function createResolvedMaterial(material, materialPreset, shading) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
function createResolvedMaterial(material, materialPreset, sceneMaterials, shading) {
|
|
25
|
+
// An unknown or dangling ref resolves to nothing: fall through to the
|
|
26
|
+
// colour, then the slot default, as the segment renderer does.
|
|
27
|
+
const preset = resolveMaterialRef(materialPreset, sceneMaterials, shading);
|
|
28
|
+
if (preset)
|
|
29
|
+
return preset;
|
|
24
30
|
if (material) {
|
|
25
31
|
return createMaterial(material, shading);
|
|
26
32
|
}
|
|
27
33
|
return null;
|
|
28
34
|
}
|
|
29
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The cladding the walls under a roof declare through their ASSEMBLIES
|
|
37
|
+
* (`wall.assembly.exterior.finish` → catalog ref), so the roof's gable band —
|
|
38
|
+
* the triangle of wall above the plate that the roof kind builds — is skinned
|
|
39
|
+
* like the walls it sits on instead of the bare drywall default. The most
|
|
40
|
+
* common ref among the level's walls wins; null when none declares one.
|
|
41
|
+
*/
|
|
42
|
+
export function levelWallCladdingRef(nodes, roof) {
|
|
43
|
+
const levelId = roof.parentId;
|
|
44
|
+
if (!levelId)
|
|
45
|
+
return null;
|
|
46
|
+
const counts = new Map();
|
|
47
|
+
for (const node of Object.values(nodes)) {
|
|
48
|
+
if (node?.type !== 'wall' || node.parentId !== levelId)
|
|
49
|
+
continue;
|
|
50
|
+
const ref = wallAssemblyFinishRef(node);
|
|
51
|
+
if (ref)
|
|
52
|
+
counts.set(ref, (counts.get(ref) ?? 0) + 1);
|
|
53
|
+
}
|
|
54
|
+
let best = null;
|
|
55
|
+
let bestCount = 0;
|
|
56
|
+
for (const [ref, count] of counts) {
|
|
57
|
+
if (count > bestCount) {
|
|
58
|
+
best = ref;
|
|
59
|
+
bestCount = count;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return best;
|
|
63
|
+
}
|
|
64
|
+
export function getRoofMaterialArray(node, shading = 'rendered', textures = true, colorPreset = 'clay', sceneTheme,
|
|
65
|
+
/** Catalog ref for the gable/trim band when unpainted — see `levelWallCladdingRef`. */
|
|
66
|
+
wallCladdingRef = null, sceneMaterials) {
|
|
30
67
|
const top = getEffectiveRoofSurfaceMaterial(node, 'top');
|
|
31
68
|
const edge = getEffectiveRoofSurfaceMaterial(node, 'edge');
|
|
32
69
|
const wall = getEffectiveRoofSurfaceMaterial(node, 'wall');
|
|
@@ -35,9 +72,10 @@ export function getRoofMaterialArray(node, shading = 'rendered', textures = true
|
|
|
35
72
|
textures,
|
|
36
73
|
colorPreset,
|
|
37
74
|
sceneTheme,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
wallCladdingRef,
|
|
76
|
+
top: getSurfaceMaterialSignature(top, sceneMaterials),
|
|
77
|
+
edge: getSurfaceMaterialSignature(edge, sceneMaterials),
|
|
78
|
+
wall: getSurfaceMaterialSignature(wall, sceneMaterials),
|
|
41
79
|
});
|
|
42
80
|
const cached = roofMaterialArrayCache.get(cacheKey);
|
|
43
81
|
if (cached)
|
|
@@ -63,14 +101,15 @@ export function getRoofMaterialArray(node, shading = 'rendered', textures = true
|
|
|
63
101
|
// shingle, soft-white deck/soffit, wall-coloured trim). Used both when the
|
|
64
102
|
// roof is unpainted and to fill any individual unpainted slot below.
|
|
65
103
|
const defaultArray = [
|
|
66
|
-
resolveSlotDefaultMaterial(ROOF_DEFAULT_REFS[0], shading),
|
|
104
|
+
resolveSlotDefaultMaterial(wallCladdingRef ?? ROOF_DEFAULT_REFS[0], shading),
|
|
67
105
|
resolveSlotDefaultMaterial(ROOF_DEFAULT_REFS[1], shading),
|
|
68
106
|
resolveSlotDefaultMaterial(ROOF_DEFAULT_REFS[2], shading),
|
|
69
107
|
resolveSlotDefaultMaterial(ROOF_DEFAULT_REFS[3], shading),
|
|
70
108
|
];
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
109
|
+
const resolve = (spec) => createResolvedMaterial(spec.material, spec.materialPreset, sceneMaterials, shading);
|
|
110
|
+
const topMaterial = resolve(top);
|
|
111
|
+
const edgeMaterial = resolve(edge);
|
|
112
|
+
const wallMaterial = resolve(wall);
|
|
74
113
|
if (!(topMaterial || edgeMaterial || wallMaterial)) {
|
|
75
114
|
roofMaterialArrayCache.set(cacheKey, defaultArray);
|
|
76
115
|
return defaultArray;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { type AnyNode, type RoofSegmentNode } from '@pascal-app/core';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import { Brush } from 'three-bvh-csg';
|
|
4
|
+
/**
|
|
5
|
+
* Roofs whose merged shell is still to build: until it is, the roof shows its segments apart (or
|
|
6
|
+
* an old shell), and a capture would show that. Only roofs on screen count, a first merge (a
|
|
7
|
+
* mounted group with no shell yet) included: one never mounted stays queued and would hold every
|
|
8
|
+
* capture.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getPendingRoofMergeCount(): number;
|
|
11
|
+
export declare function countPendingRoofShells(pending: Iterable<string>, mounted: (id: string) => THREE.Object3D | undefined): number;
|
|
4
12
|
export declare const RoofSystem: () => null;
|
|
5
13
|
/**
|
|
6
14
|
* Four dummy materials used as identity placeholders during CSG. Shared
|
|
@@ -36,10 +44,12 @@ export declare function mapRoofGroupMaterialIndex(groupMaterialIndex: number | u
|
|
|
36
44
|
*/
|
|
37
45
|
export declare function clipGeometryBySegmentTrim(geometry: THREE.BufferGeometry | null, segment: RoofSegmentNode): THREE.BufferGeometry | null;
|
|
38
46
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
47
|
+
* `fascia`: build the segment's fascia boards into its deck. Only the roof
|
|
48
|
+
* renderer asks; a dormer, a chimney or a sibling's clip takes the bare shell.
|
|
41
49
|
*/
|
|
42
|
-
export declare function getRoofSegmentBrushes(node: RoofSegmentNode
|
|
50
|
+
export declare function getRoofSegmentBrushes(node: RoofSegmentNode, options?: {
|
|
51
|
+
fascia?: boolean;
|
|
52
|
+
}): RoofSegmentBrushSet | null;
|
|
43
53
|
export declare function generateRoofSegmentGeometry(node: RoofSegmentNode, nodes?: Record<string, AnyNode>): THREE.BufferGeometry;
|
|
44
54
|
export declare function remapRoofShellFaces(geometry: THREE.BufferGeometry, node: RoofSegmentNode): void;
|
|
45
55
|
export type SurfaceFrame = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAoBZ,KAAK,eAAe,EAQrB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAoBZ,KAAK,eAAe,EAQrB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAqIvE;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAIjD;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,EACzB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,KAAK,CAAC,QAAQ,GAAG,SAAS,GAClD,MAAM,CAIR;AA+DD,eAAO,MAAM,UAAU,YAwItB,CAAA;AAgbD;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE;IAC7B,KAAK,CAAC,iBAAiB;IACvB,KAAK,CAAC,iBAAiB;IACvB,KAAK,CAAC,iBAAiB;IACvB,KAAK,CAAC,iBAAiB;CAMxB,CAAA;AAID,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,KAAK,CAAA;IACf,QAAQ,EAAE,KAAK,CAAA;IACf,SAAS,EAAE,KAAK,CAAA;IAChB,UAAU,EAAE,KAAK,CAAA;IACjB,UAAU,EAAE,KAAK,CAAC,cAAc,GAAG,IAAI,CAAA;CACxC,CAAA;AA4FD,wBAAgB,yBAAyB,CACvC,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,YAAY,EAAE,KAAK,CAAC,QAAQ,EAAE,EAC9B,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,GACtC,MAAM,CAuBR;AAuXD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,KAAK,CAAC,cAAc,GAAG,IAAI,EACrC,OAAO,EAAE,eAAe,GACvB,KAAK,CAAC,cAAc,GAAG,IAAI,CAyB7B;AAoHD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,EACrB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,mBAAmB,GAAG,IAAI,CAyV5B;AAED,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,eAAe,EACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,KAAK,CAAC,cAAc,CA6GtB;AAuyCD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,eAAe,QA0ExF;AAirBD,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAA;IACpB,MAAM,EAAE,KAAK,CAAC,OAAO,CAAA;CACtB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,eAAe,EACxB,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,GACT,YAAY,CAqLd"}
|
|
@@ -112,6 +112,22 @@ const previousRoofPlanBounds = new Map();
|
|
|
112
112
|
const warnedMergedRoofNaNIds = new Set();
|
|
113
113
|
const MAX_ROOFS_PER_FRAME = 1;
|
|
114
114
|
const MAX_SEGMENTS_PER_FRAME = 3;
|
|
115
|
+
/**
|
|
116
|
+
* Roofs whose merged shell is still to build: until it is, the roof shows its segments apart (or
|
|
117
|
+
* an old shell), and a capture would show that. Only roofs on screen count, a first merge (a
|
|
118
|
+
* mounted group with no shell yet) included: one never mounted stays queued and would hold every
|
|
119
|
+
* capture.
|
|
120
|
+
*/
|
|
121
|
+
export function getPendingRoofMergeCount() {
|
|
122
|
+
return countPendingRoofShells(pendingRoofUpdates, (id) => sceneRegistry.nodes.get(id));
|
|
123
|
+
}
|
|
124
|
+
export function countPendingRoofShells(pending, mounted) {
|
|
125
|
+
let count = 0;
|
|
126
|
+
for (const id of pending)
|
|
127
|
+
if (mounted(id))
|
|
128
|
+
count++;
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
115
131
|
function queueSiblingRoofUpdates(roofId, nodes) {
|
|
116
132
|
pendingRoofUpdates.add(roofId);
|
|
117
133
|
const roof = nodes[roofId]?.type === 'roof' ? getEffectiveNode(nodes[roofId]) : undefined;
|
|
@@ -400,7 +416,7 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes) {
|
|
|
400
416
|
if (cached?.key === cacheKey) {
|
|
401
417
|
return cloneCachedMergedRoofSegmentBrushes(cached);
|
|
402
418
|
}
|
|
403
|
-
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment));
|
|
419
|
+
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, { fascia: wearsFascia(segment, roofNode) }));
|
|
404
420
|
if (!brushes) {
|
|
405
421
|
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
406
422
|
mergedRoofSegmentGeometryCache.delete(segmentId);
|
|
@@ -734,6 +750,8 @@ function getMergedRoofSegmentCacheKey(roofNode, segment, nodes) {
|
|
|
734
750
|
return JSON.stringify({
|
|
735
751
|
roofPosition: roofNode.position ?? [0, 0, 0],
|
|
736
752
|
roofRotation: roofNode.rotation ?? 0,
|
|
753
|
+
// a shed's fascia depends on whether its roof is a composite of pieces
|
|
754
|
+
composite: (roofNode.children?.length ?? 0) > 1,
|
|
737
755
|
segment,
|
|
738
756
|
accessories: getMergedRoofAccessoryCachePayload(segment, nodes),
|
|
739
757
|
});
|
|
@@ -819,6 +837,22 @@ const TRIM_CUT_EPSILON = 0.002;
|
|
|
819
837
|
const ROOF_EDGE_MATERIAL_INDEX = 0;
|
|
820
838
|
const ROOF_INSET_WALL_MATERIAL_INDEX = 2;
|
|
821
839
|
const DUTCH_RAKE_SIDE_MATERIAL_INDEX = 1;
|
|
840
|
+
/**
|
|
841
|
+
* A lean-to's shed segment (the lean-to assembly writes the side-infill
|
|
842
|
+
* fields) has no wall band of its own: its high side is the host wall and
|
|
843
|
+
* its ends are the inset infill panels. Every other shed — a house's
|
|
844
|
+
* mono-pitch roof, hand-placed or generated — carries the same hollow wall
|
|
845
|
+
* band a gable does (the high wall and the raked side walls above the
|
|
846
|
+
* plate), sided in the wall/trim slot like a gable's pediment.
|
|
847
|
+
*/
|
|
848
|
+
function isLeanToShedSegment(node) {
|
|
849
|
+
return (node.roofType === 'shed' &&
|
|
850
|
+
(node.shedSideInfillSpan !== undefined ||
|
|
851
|
+
node.shedSideInfillMinX !== undefined ||
|
|
852
|
+
node.shedSideInfillMaxX !== undefined ||
|
|
853
|
+
node.shedFootprintPieces !== undefined ||
|
|
854
|
+
node.shedOpenEndSides !== undefined));
|
|
855
|
+
}
|
|
822
856
|
const DUTCH_RAKE_TOP_MATERIAL_INDEX = 3;
|
|
823
857
|
const DUTCH_RAKE_SLOPE_SEAT_OFFSET = 0.0002;
|
|
824
858
|
function pushDoubleSidedFace(targetFaces, face) {
|
|
@@ -1093,7 +1127,119 @@ export function clipGeometryBySegmentTrim(geometry, segment) {
|
|
|
1093
1127
|
* Generate complete hollow-shell geometry for a roof segment.
|
|
1094
1128
|
* Ports the prototype's CSG approach using three-bvh-csg.
|
|
1095
1129
|
*/
|
|
1096
|
-
|
|
1130
|
+
/** A 1x8 fascia / rake board: actual 3/4 × 7-1/4 in. */
|
|
1131
|
+
const FASCIA_BOARD = { thickness: 0.019, height: 0.184 };
|
|
1132
|
+
/** A board's back sits this far inside the deck edge, so the union is one solid with no coplanar faces. */
|
|
1133
|
+
const FASCIA_EMBED = 0.002;
|
|
1134
|
+
/**
|
|
1135
|
+
* Slot 2 takes the trim finish like the rake faces, and the shell remap
|
|
1136
|
+
* leaves it alone: the remap would send a plumb eave board to the wall band
|
|
1137
|
+
* and its top edge to the shingles.
|
|
1138
|
+
*/
|
|
1139
|
+
const FASCIA_MATERIAL_INDEX = 2;
|
|
1140
|
+
/**
|
|
1141
|
+
* The fascia and rake boards a segment wears, in its own frame: a plumb 1x8
|
|
1142
|
+
* on every eave with its top on the deck's top edge, and one up every gable
|
|
1143
|
+
* or shed rake, from the eave line the deck itself is built on. Being part
|
|
1144
|
+
* of the segment, they follow its pitch, size, overhang and type, and the
|
|
1145
|
+
* trims, cuts and sibling roofs clip them like the deck. Gable, hip, shed
|
|
1146
|
+
* and flat; null for the other forms.
|
|
1147
|
+
*/
|
|
1148
|
+
function buildFasciaBoards(node, deckExt, eaveTopY, tanTheta) {
|
|
1149
|
+
const { roofType, width, depth } = node;
|
|
1150
|
+
const { thickness: bt, height: bh } = FASCIA_BOARD;
|
|
1151
|
+
const out = bt / 2 - FASCIA_EMBED;
|
|
1152
|
+
const halfD = depth / 2 + deckExt;
|
|
1153
|
+
const zE = halfD + out;
|
|
1154
|
+
const xE = width / 2 + deckExt + out;
|
|
1155
|
+
// the eave boards run through the corners to the rake / side boards'
|
|
1156
|
+
// outer faces (a lapped return, no notch at the corner)
|
|
1157
|
+
const xL = xE + bt / 2;
|
|
1158
|
+
const faces = [];
|
|
1159
|
+
const board = (a, b, across) => {
|
|
1160
|
+
const ax = (across[0] * bt) / 2;
|
|
1161
|
+
const az = (across[1] * bt) / 2;
|
|
1162
|
+
const p = [
|
|
1163
|
+
[a[0] - ax, a[1] - bh, a[2] - az],
|
|
1164
|
+
[b[0] - ax, b[1] - bh, b[2] - az],
|
|
1165
|
+
[b[0] + ax, b[1] - bh, b[2] + az],
|
|
1166
|
+
[a[0] + ax, a[1] - bh, a[2] + az],
|
|
1167
|
+
[a[0] - ax, a[1], a[2] - az],
|
|
1168
|
+
[b[0] - ax, b[1], b[2] - az],
|
|
1169
|
+
[b[0] + ax, b[1], b[2] + az],
|
|
1170
|
+
[a[0] + ax, a[1], a[2] + az],
|
|
1171
|
+
].map(([x, y, z]) => new THREE.Vector3(x, y, z));
|
|
1172
|
+
const centre = p.reduce((sum, v) => sum.add(v), new THREE.Vector3()).multiplyScalar(1 / 8);
|
|
1173
|
+
const quads = [
|
|
1174
|
+
[0, 1, 2, 3],
|
|
1175
|
+
[4, 5, 6, 7],
|
|
1176
|
+
[0, 4, 5, 1],
|
|
1177
|
+
[1, 5, 6, 2],
|
|
1178
|
+
[2, 6, 7, 3],
|
|
1179
|
+
[3, 7, 4, 0],
|
|
1180
|
+
];
|
|
1181
|
+
for (const quad of quads) {
|
|
1182
|
+
const face = quad.map((k) => p[k].clone());
|
|
1183
|
+
const normal = new THREE.Vector3()
|
|
1184
|
+
.subVectors(face[1], face[0])
|
|
1185
|
+
.cross(new THREE.Vector3().subVectors(face[2], face[0]));
|
|
1186
|
+
const faceCentre = face
|
|
1187
|
+
.reduce((sum, v) => sum.add(v), new THREE.Vector3())
|
|
1188
|
+
.multiplyScalar(1 / 4);
|
|
1189
|
+
if (normal.dot(faceCentre.sub(centre)) < 0)
|
|
1190
|
+
face.reverse();
|
|
1191
|
+
faces.push(face);
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
if (roofType === 'gable') {
|
|
1195
|
+
const ridgeY = eaveTopY + halfD * tanTheta;
|
|
1196
|
+
board([-xL, eaveTopY, zE], [xL, eaveTopY, zE], [0, 1]);
|
|
1197
|
+
board([-xL, eaveTopY, -zE], [xL, eaveTopY, -zE], [0, 1]);
|
|
1198
|
+
for (const sx of [-1, 1]) {
|
|
1199
|
+
board([sx * xE, eaveTopY, zE], [sx * xE, ridgeY, 0], [1, 0]);
|
|
1200
|
+
board([sx * xE, ridgeY, 0], [sx * xE, eaveTopY, -zE], [1, 0]);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
else if (roofType === 'hip' || roofType === 'flat') {
|
|
1204
|
+
for (const sz of [-1, 1])
|
|
1205
|
+
board([-xL, eaveTopY, sz * zE], [xL, eaveTopY, sz * zE], [0, 1]);
|
|
1206
|
+
// the side boards butt the eave boards' backs
|
|
1207
|
+
for (const sx of [-1, 1])
|
|
1208
|
+
board([sx * xE, eaveTopY, -zE + bt / 2], [sx * xE, eaveTopY, zE - bt / 2], [1, 0]);
|
|
1209
|
+
}
|
|
1210
|
+
else if (roofType === 'shed') {
|
|
1211
|
+
// the low eave at +z, the slope rising to −z
|
|
1212
|
+
const highY = eaveTopY + 2 * halfD * tanTheta;
|
|
1213
|
+
board([-xL, eaveTopY, zE], [xL, eaveTopY, zE], [0, 1]);
|
|
1214
|
+
if (node.fasciaHighEdge ?? true)
|
|
1215
|
+
board([-xL, highY, -zE], [xL, highY, -zE], [0, 1]);
|
|
1216
|
+
for (const sx of [-1, 1])
|
|
1217
|
+
board([sx * xE, eaveTopY, zE], [sx * xE, highY, -zE], [1, 0]);
|
|
1218
|
+
}
|
|
1219
|
+
else
|
|
1220
|
+
return null;
|
|
1221
|
+
return createGeometryFromFaces(faces, FASCIA_MATERIAL_INDEX);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Whether a drawn segment wears fascia boards. A lean-to carries its own
|
|
1225
|
+
* trim, and a legacy composite shed roof stacks overlapping shed pieces as
|
|
1226
|
+
* one deck (the same pieces whose wall shells are left out), so boards would
|
|
1227
|
+
* show between them.
|
|
1228
|
+
*/
|
|
1229
|
+
function wearsFascia(node, parentRoof) {
|
|
1230
|
+
if (node.fascia !== true)
|
|
1231
|
+
return false;
|
|
1232
|
+
if (node.roofType !== 'shed')
|
|
1233
|
+
return true;
|
|
1234
|
+
if (isLeanToShedSegment(node))
|
|
1235
|
+
return false;
|
|
1236
|
+
return !parentRoof || (parentRoof.children?.length ?? 0) <= 1;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* `fascia`: build the segment's fascia boards into its deck. Only the roof
|
|
1240
|
+
* renderer asks; a dormer, a chimney or a sibling's clip takes the bare shell.
|
|
1241
|
+
*/
|
|
1242
|
+
export function getRoofSegmentBrushes(node, options = {}) {
|
|
1097
1243
|
const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = node;
|
|
1098
1244
|
const conicalCoverage = getConicalRoofCoverage(node);
|
|
1099
1245
|
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node);
|
|
@@ -1110,20 +1256,27 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1110
1256
|
// the 2D floorplan and the slope frame (which derives activeRh from the
|
|
1111
1257
|
// same ratio). A hardcoded 0.25 desyncs the gablet from the parameter.
|
|
1112
1258
|
const baseI = Math.min(width, depth) * node.dutchHipWidthRatio;
|
|
1113
|
-
const
|
|
1259
|
+
const plateSeated = wallHeight <= 0;
|
|
1260
|
+
const getVol = (wExt, vOffset, baseY, matIndex, isVoid, materialRule, onPlate = false) => {
|
|
1114
1261
|
const wV = Math.max(0.01, width + 2 * wExt);
|
|
1115
1262
|
const dV = Math.max(0.01, depth + 2 * wExt);
|
|
1116
1263
|
const autoDrop = wExt * tanTheta;
|
|
1117
1264
|
// Floor every prism at 5 cm so CSG never sees a degenerate volume — by
|
|
1118
1265
|
// raising the top, never by sinking the base (the base is the wall top).
|
|
1119
1266
|
// One floor for all volumes keeps each cutter level with the shell it carves.
|
|
1120
|
-
|
|
1267
|
+
// A plate-seated roof (wallHeight 0) is the exception for its sloped deck
|
|
1268
|
+
// volumes: their eave hangs below the plate, and raising it lifted the
|
|
1269
|
+
// whole roof off its walls, so their base sinks instead.
|
|
1270
|
+
const eaveY = wallHeight - autoDrop + vOffset;
|
|
1271
|
+
const sinkBase = plateSeated && !onPlate && autoDrop !== 0;
|
|
1272
|
+
const whV = sinkBase ? eaveY : Math.max(0.05, eaveY);
|
|
1121
1273
|
let rhV = activeRh;
|
|
1122
1274
|
if (activeRh > 0) {
|
|
1123
1275
|
rhV = activeRh + autoDrop;
|
|
1124
1276
|
if (roofType === 'shed')
|
|
1125
1277
|
rhV = activeRh + 2 * autoDrop;
|
|
1126
1278
|
}
|
|
1279
|
+
const safeBaseY = sinkBase ? Math.min(baseY, whV - 0.05) : baseY;
|
|
1127
1280
|
let structuralI = baseI;
|
|
1128
1281
|
if (isVoid) {
|
|
1129
1282
|
structuralI += deckThickness;
|
|
@@ -1134,7 +1287,7 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1134
1287
|
d: dV,
|
|
1135
1288
|
wh: whV,
|
|
1136
1289
|
rh: rhV,
|
|
1137
|
-
baseY,
|
|
1290
|
+
baseY: safeBaseY,
|
|
1138
1291
|
insets: { dutchI: structuralI },
|
|
1139
1292
|
baseW: width,
|
|
1140
1293
|
baseD: depth,
|
|
@@ -1146,15 +1299,24 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1146
1299
|
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
1147
1300
|
return createGeometryFromFaces(faces, materialRule ?? matIndex);
|
|
1148
1301
|
};
|
|
1149
|
-
const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false);
|
|
1150
|
-
const innerGeo = getVol(-wallThickness / 2, 0, -5, 2, false);
|
|
1302
|
+
const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false, undefined, true);
|
|
1303
|
+
const innerGeo = getVol(-wallThickness / 2, 0, -5, 2, false, undefined, true);
|
|
1151
1304
|
const horizontalOverhang = overhang * cosTheta;
|
|
1152
1305
|
const deckExt = wallThickness / 2 + horizontalOverhang;
|
|
1153
|
-
|
|
1154
|
-
|
|
1306
|
+
// A shed's deck and shingle edges take the wall/trim slot, unless it wears
|
|
1307
|
+
// fascia boards: then they are fascia like a gable's, in the deck slot.
|
|
1308
|
+
const shedEdgesInTrimSlot = roofType === 'shed' && !options.fascia;
|
|
1309
|
+
const shedRoofSideMaterialRule = shedEdgesInTrimSlot
|
|
1310
|
+
? (normal) => (normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX)
|
|
1155
1311
|
: undefined;
|
|
1156
1312
|
const deckTopGeo = getVol(deckExt, verticalRt, 0, 1, false, shedRoofSideMaterialRule);
|
|
1157
1313
|
const deckBotGeo = getVol(deckExt, 0, -5, 0, true);
|
|
1314
|
+
// the deck's top edge at its outer plane, as `getVol` puts it
|
|
1315
|
+
const deckEaveDrop = deckExt * tanTheta;
|
|
1316
|
+
const deckEaveTopY = plateSeated && deckEaveDrop > 0
|
|
1317
|
+
? wallHeight - deckEaveDrop + verticalRt
|
|
1318
|
+
: Math.max(0.05, wallHeight - deckEaveDrop + verticalRt);
|
|
1319
|
+
const fasciaGeo = options.fascia ? buildFasciaBoards(node, deckExt, deckEaveTopY, tanTheta) : null;
|
|
1158
1320
|
const stSin = shingleThickness * sinTheta;
|
|
1159
1321
|
const stCos = shingleThickness * cosTheta;
|
|
1160
1322
|
const shinBotW = Math.max(0.01, width + 2 * deckExt);
|
|
@@ -1254,7 +1416,7 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1254
1416
|
if (roofType === 'dutch' && insetsTop.dutchI !== undefined) {
|
|
1255
1417
|
rakeBoards = buildDutchRakeBoards(shinTopW, shinTopD, shinTopWh, shinTopRh, insetsTop.dutchI, shapeRatios, node.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake, node.dutchTopRakeThickness ?? ROOF_SHAPE_DEFAULTS.dutchTopRakeThickness);
|
|
1256
1418
|
}
|
|
1257
|
-
const shedRoofSideMaterialIndex =
|
|
1419
|
+
const shedRoofSideMaterialIndex = shedEdgesInTrimSlot ? ROOF_EDGE_MATERIAL_INDEX : 1;
|
|
1258
1420
|
const shinBotGeo = createGeometryFromFaces(botFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : shedRoofSideMaterialIndex);
|
|
1259
1421
|
const shinTopGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : shedRoofSideMaterialIndex);
|
|
1260
1422
|
if (transZ !== 0) {
|
|
@@ -1298,6 +1460,9 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1298
1460
|
}
|
|
1299
1461
|
const shinTopBrush = toBrush(shinTopGeo);
|
|
1300
1462
|
const shinBotBrush = toBrush(shinBotGeo);
|
|
1463
|
+
const fasciaBrush = fasciaGeo ? toBrush(fasciaGeo) : null;
|
|
1464
|
+
if (fasciaGeo && !fasciaBrush)
|
|
1465
|
+
fasciaGeo.dispose();
|
|
1301
1466
|
if (shinBotBrush) {
|
|
1302
1467
|
const wV = shinBotW;
|
|
1303
1468
|
const dV = shinBotD;
|
|
@@ -1312,8 +1477,15 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1312
1477
|
shinBotGeo.dispose();
|
|
1313
1478
|
if (deckTopBrush && deckBotBrush && wallBrush && innerBrush && shinTopBrush && shinBotBrush) {
|
|
1314
1479
|
try {
|
|
1315
|
-
|
|
1480
|
+
let deckSlab = csgEvaluator.evaluate(deckTopBrush, deckBotBrush, SUBTRACTION);
|
|
1316
1481
|
prepareBrushForCSG(deckSlab);
|
|
1482
|
+
if (fasciaBrush) {
|
|
1483
|
+
const withFascia = csgEvaluator.evaluate(deckSlab, fasciaBrush, ADDITION);
|
|
1484
|
+
prepareBrushForCSG(withFascia);
|
|
1485
|
+
deckSlab.geometry.dispose();
|
|
1486
|
+
fasciaBrush.geometry.dispose();
|
|
1487
|
+
deckSlab = withFascia;
|
|
1488
|
+
}
|
|
1317
1489
|
const shinSlab = csgEvaluator.evaluate(shinTopBrush, shinBotBrush, SUBTRACTION);
|
|
1318
1490
|
prepareBrushForCSG(shinSlab);
|
|
1319
1491
|
deckTopBrush.geometry.dispose();
|
|
@@ -1356,6 +1528,7 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1356
1528
|
wallBrush.geometry.dispose();
|
|
1357
1529
|
if (innerBrush)
|
|
1358
1530
|
innerBrush.geometry.dispose();
|
|
1531
|
+
fasciaBrush?.geometry.dispose();
|
|
1359
1532
|
rakeBoards?.dispose();
|
|
1360
1533
|
return null;
|
|
1361
1534
|
}
|
|
@@ -1375,7 +1548,7 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1375
1548
|
ensureRenderableGeometryAttributes(result);
|
|
1376
1549
|
return result;
|
|
1377
1550
|
}
|
|
1378
|
-
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node));
|
|
1551
|
+
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, { fascia: wearsFascia(node, parentRoof) }));
|
|
1379
1552
|
if (!brushes) {
|
|
1380
1553
|
// Fallback: simple box
|
|
1381
1554
|
return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
|
|
@@ -2757,7 +2930,9 @@ function buildDutchRakeBoards(W, D, wh, rh, i, shapeRatios, rake, thickness) {
|
|
|
2757
2930
|
return merged;
|
|
2758
2931
|
}
|
|
2759
2932
|
function createShedInsetEndPanelGeometry(node) {
|
|
2760
|
-
|
|
2933
|
+
// a house shed has real end walls in its wall band; only a lean-to
|
|
2934
|
+
// needs the inset infill panels
|
|
2935
|
+
if (!isLeanToShedSegment(node))
|
|
2761
2936
|
return null;
|
|
2762
2937
|
const trim = normalizeRoofSegmentTrim(node);
|
|
2763
2938
|
const openEndSides = readShedOpenEndSides(node);
|
|
@@ -7,5 +7,5 @@ import * as THREE from 'three';
|
|
|
7
7
|
* a recessed slab is an open shell (pool), everything else a solid occupying
|
|
8
8
|
* `[elevation − thickness, elevation]`.
|
|
9
9
|
*/
|
|
10
|
-
export declare function generateSlabGeometry(slabNode: SlabNode, context: SlabPolygonContext): THREE.BufferGeometry;
|
|
10
|
+
export declare function generateSlabGeometry(slabNode: SlabNode, context: SlabPolygonContext, platformBaseAt?: (x: number, z: number) => number): THREE.BufferGeometry;
|
|
11
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,EAKL,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAExB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAO9B;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,kBAAkB,
|
|
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,EAExB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAO9B;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,kBAAkB,EAC3B,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAChD,KAAK,CAAC,cAAc,CAItB"}
|
|
@@ -11,10 +11,29 @@ import { mergeSurfaceHolePolygons } from '../surface-hole-geometry';
|
|
|
11
11
|
* a recessed slab is an open shell (pool), everything else a solid occupying
|
|
12
12
|
* `[elevation − thickness, elevation]`.
|
|
13
13
|
*/
|
|
14
|
-
export function generateSlabGeometry(slabNode, context) {
|
|
14
|
+
export function generateSlabGeometry(slabNode, context, platformBaseAt) {
|
|
15
15
|
return slabNode.recessed
|
|
16
16
|
? generatePoolGeometry(slabNode, context)
|
|
17
|
-
: generateSolidSlabGeometry(slabNode, context);
|
|
17
|
+
: generateSolidSlabGeometry(slabNode, context, platformBaseAt);
|
|
18
|
+
}
|
|
19
|
+
/** Rings below this area (m²) have collapsed: a sliver cap would still grow full-height side walls. */
|
|
20
|
+
const MIN_SLAB_REGION_AREA = 1e-6;
|
|
21
|
+
function signedArea2(polygon) {
|
|
22
|
+
let area2 = 0;
|
|
23
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
24
|
+
const j = (i + 1) % polygon.length;
|
|
25
|
+
area2 += polygon[i][0] * polygon[j][1] - polygon[j][0] * polygon[i][1];
|
|
26
|
+
}
|
|
27
|
+
return area2;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A renderable polygon collapses when snapping its edges to wall faces or
|
|
31
|
+
* sibling seams leaves fewer than three points or no area (a threshold strip
|
|
32
|
+
* inside a wall band). Such a slab builds an empty geometry instead of a sliver;
|
|
33
|
+
* a hole or cut region that collapses the same way is skipped.
|
|
34
|
+
*/
|
|
35
|
+
function isCollapsedPolygon(polygon) {
|
|
36
|
+
return polygon.length < 3 || Math.abs(signedArea2(polygon)) / 2 < MIN_SLAB_REGION_AREA;
|
|
18
37
|
}
|
|
19
38
|
// Earcut normalizes cap triangulation regardless of input winding, but the side
|
|
20
39
|
// walls below assume a CCW contour (the unflipped quad's right-hand normal faces
|
|
@@ -22,12 +41,7 @@ export function generateSlabGeometry(slabNode, context) {
|
|
|
22
41
|
// winding, so a CW-drawn slab gets inward-facing walls that FrontSide culls and
|
|
23
42
|
// the slab reads as see-through from the front. Normalize to CCW first.
|
|
24
43
|
function ensureCounterClockwisePolygon(polygon) {
|
|
25
|
-
|
|
26
|
-
for (let i = 0; i < polygon.length; i++) {
|
|
27
|
-
const j = (i + 1) % polygon.length;
|
|
28
|
-
area2 += polygon[i][0] * polygon[j][1] - polygon[j][0] * polygon[i][1];
|
|
29
|
-
}
|
|
30
|
-
return area2 < 0 ? [...polygon].reverse() : polygon;
|
|
44
|
+
return signedArea2(polygon) < 0 ? [...polygon].reverse() : polygon;
|
|
31
45
|
}
|
|
32
46
|
function isStrictInteriorHole(contour, hole) {
|
|
33
47
|
return (hole.every((point) => pointInPolygon2D(point, contour, { includeBoundary: false })) &&
|
|
@@ -42,7 +56,7 @@ function buildSlabRegions(contour, holes) {
|
|
|
42
56
|
const containedHoles = [];
|
|
43
57
|
const edgeCutouts = [];
|
|
44
58
|
for (const hole of holes) {
|
|
45
|
-
if (hole
|
|
59
|
+
if (isCollapsedPolygon(hole))
|
|
46
60
|
continue;
|
|
47
61
|
if (isStrictInteriorHole(contour, hole))
|
|
48
62
|
containedHoles.push(hole);
|
|
@@ -68,13 +82,24 @@ function buildSlabRegions(contour, holes) {
|
|
|
68
82
|
* thickness visible from any angle: the two coincident triangles never z-fight
|
|
69
83
|
* because exactly one faces the camera under FrontSide culling.
|
|
70
84
|
*/
|
|
71
|
-
function generateSolidSlabGeometry(slabNode, context) {
|
|
85
|
+
function generateSolidSlabGeometry(slabNode, context, platformBaseAt) {
|
|
72
86
|
const polygon = ensureCounterClockwisePolygon(getRenderableSlabPolygon(slabNode, context));
|
|
73
87
|
const elevation = slabNode.elevation ?? 0.05;
|
|
74
88
|
const thickness = slabNode.thickness ?? 0.05;
|
|
75
89
|
const bottom = elevation - thickness;
|
|
90
|
+
const bottomAt = (x, z) => platformBaseAt ? Math.min(bottom, platformBaseAt(x, z)) : bottom;
|
|
91
|
+
const sampleRing = (ring) => !platformBaseAt
|
|
92
|
+
? ring
|
|
93
|
+
: ring.flatMap((a, i) => {
|
|
94
|
+
const b = ring[(i + 1) % ring.length];
|
|
95
|
+
const count = Math.max(1, Math.ceil(Math.hypot(b[0] - a[0], b[1] - a[1]) / 0.25));
|
|
96
|
+
return Array.from({ length: count }, (_, j) => [
|
|
97
|
+
a[0] + ((b[0] - a[0]) * j) / count,
|
|
98
|
+
a[1] + ((b[1] - a[1]) * j) / count,
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
76
101
|
const holePolygons = mergeSurfaceHolePolygons(slabNode.holes ?? []);
|
|
77
|
-
if (polygon
|
|
102
|
+
if (isCollapsedPolygon(polygon))
|
|
78
103
|
return new THREE.BufferGeometry();
|
|
79
104
|
const positions = [];
|
|
80
105
|
const uvs = [];
|
|
@@ -86,10 +111,10 @@ function generateSolidSlabGeometry(slabNode, context) {
|
|
|
86
111
|
const addWall = (a, b, flipped) => {
|
|
87
112
|
const base = positions.length / 3;
|
|
88
113
|
const len = Math.max(Math.hypot(b.x - a.x, b.y - a.y), 0.001);
|
|
89
|
-
positions.push(a.x,
|
|
90
|
-
uvs.push(0,
|
|
91
|
-
positions.push(b.x,
|
|
92
|
-
uvs.push(len,
|
|
114
|
+
positions.push(a.x, bottomAt(a.x, a.y), a.y);
|
|
115
|
+
uvs.push(0, bottomAt(a.x, a.y) - bottom);
|
|
116
|
+
positions.push(b.x, bottomAt(b.x, b.y), b.y);
|
|
117
|
+
uvs.push(len, bottomAt(b.x, b.y) - bottom);
|
|
93
118
|
positions.push(b.x, elevation, b.y);
|
|
94
119
|
uvs.push(len, thickness);
|
|
95
120
|
positions.push(a.x, elevation, a.y);
|
|
@@ -104,10 +129,12 @@ function generateSolidSlabGeometry(slabNode, context) {
|
|
|
104
129
|
}
|
|
105
130
|
};
|
|
106
131
|
for (const region of buildSlabRegions(polygon, holePolygons)) {
|
|
107
|
-
|
|
132
|
+
if (isCollapsedPolygon(region.contour))
|
|
133
|
+
continue;
|
|
134
|
+
const contour2d = sampleRing(ensureCounterClockwisePolygon(region.contour)).map(([x, z]) => new THREE.Vector2(x, z));
|
|
108
135
|
const holes2d = region.holes
|
|
109
136
|
.filter((h) => h.length >= 3)
|
|
110
|
-
.map((h) => h.map(([x, z]) => new THREE.Vector2(x, z)));
|
|
137
|
+
.map((h) => sampleRing(h).map(([x, z]) => new THREE.Vector2(x, z)));
|
|
111
138
|
// --- Top & bottom caps ---
|
|
112
139
|
// capPoints order (contour then holes) matches triangulateShape's index space.
|
|
113
140
|
// UVs reproduce ExtrudeGeometry's WorldUVGenerator mapping (shape-space x,-z)
|
|
@@ -128,7 +155,8 @@ function generateSolidSlabGeometry(slabNode, context) {
|
|
|
128
155
|
const [a, b, c] = [tri[0], tri[1], tri[2]];
|
|
129
156
|
// Reversed winding → +Y normal on top; standard winding → -Y on bottom.
|
|
130
157
|
indices.push(topBase + a, topBase + c, topBase + b);
|
|
131
|
-
|
|
158
|
+
if (!platformBaseAt)
|
|
159
|
+
indices.push(bottomBase + a, bottomBase + b, bottomBase + c);
|
|
132
160
|
}
|
|
133
161
|
for (let i = 0; i < contour2d.length; i++) {
|
|
134
162
|
addWall(contour2d[i], contour2d[(i + 1) % contour2d.length], false);
|
|
@@ -163,7 +191,7 @@ function generatePoolGeometry(slabNode, context) {
|
|
|
163
191
|
const floor = slabNode.elevation ?? 0.05;
|
|
164
192
|
const depth = Math.max(0, (slabNode.recessedRimElevation ?? 0) - floor);
|
|
165
193
|
const holePolygons = mergeSurfaceHolePolygons(slabNode.holes ?? []);
|
|
166
|
-
if (polygon
|
|
194
|
+
if (isCollapsedPolygon(polygon))
|
|
167
195
|
return new THREE.BufferGeometry();
|
|
168
196
|
const positions = [];
|
|
169
197
|
const uvs = [];
|
|
@@ -179,6 +207,8 @@ function generatePoolGeometry(slabNode, context) {
|
|
|
179
207
|
uvs.push(u, v);
|
|
180
208
|
};
|
|
181
209
|
for (const region of buildSlabRegions(polygon, holePolygons)) {
|
|
210
|
+
if (isCollapsedPolygon(region.contour))
|
|
211
|
+
continue;
|
|
182
212
|
const contour = ensureCounterClockwisePolygon(region.contour);
|
|
183
213
|
const floorBase = positions.length / 3;
|
|
184
214
|
// --- Floor at Y=0 ---
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stair-system.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stair-system.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-system.tsx"],"names":[],"mappings":"AAiCA,eAAO,MAAM,WAAW,YAuGvB,CAAA"}
|