@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.
Files changed (173) hide show
  1. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/node-renderer.js +8 -3
  3. package/dist/components/viewer/frame-limiter.d.ts +1 -0
  4. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  5. package/dist/components/viewer/frame-limiter.js +22 -3
  6. package/dist/components/viewer/glb-interactive.d.ts +25 -1
  7. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  8. package/dist/components/viewer/glb-interactive.js +252 -85
  9. package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
  10. package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
  11. package/dist/components/viewer/glb-mechanisms.js +19 -0
  12. package/dist/components/viewer/glb-scene.d.ts +45 -27
  13. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  14. package/dist/components/viewer/glb-scene.js +420 -506
  15. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  16. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  17. package/dist/components/viewer/glb-walkthrough-controller.js +12 -1
  18. package/dist/components/viewer/index.d.ts +26 -0
  19. package/dist/components/viewer/index.d.ts.map +1 -1
  20. package/dist/components/viewer/index.js +108 -29
  21. package/dist/components/viewer/lights.d.ts.map +1 -1
  22. package/dist/components/viewer/lights.js +6 -1
  23. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  24. package/dist/components/viewer/perf-monitor.js +6 -0
  25. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  26. package/dist/components/viewer/post-processing.js +8 -3
  27. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  28. package/dist/components/viewer/registered-systems.js +16 -2
  29. package/dist/components/viewer/render-error.d.ts +18 -0
  30. package/dist/components/viewer/render-error.d.ts.map +1 -0
  31. package/dist/components/viewer/render-error.js +29 -0
  32. package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
  33. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
  34. package/dist/components/viewer/scene-ground-replacement.js +15 -0
  35. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  36. package/dist/components/viewer/selection-manager.js +10 -0
  37. package/dist/components/viewer/viewer-camera.d.ts +19 -1
  38. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  39. package/dist/components/viewer/viewer-camera.js +53 -5
  40. package/dist/index.d.ts +28 -15
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +26 -18
  43. package/dist/lib/asset-url.d.ts.map +1 -1
  44. package/dist/lib/asset-url.js +7 -1
  45. package/dist/lib/csg-utils.d.ts +1 -1
  46. package/dist/lib/csg-utils.d.ts.map +1 -1
  47. package/dist/lib/csg-utils.js +1 -1
  48. package/dist/lib/display-state.d.ts +25 -0
  49. package/dist/lib/display-state.d.ts.map +1 -0
  50. package/dist/lib/display-state.js +1 -0
  51. package/dist/lib/hero-pose.d.ts +12 -2
  52. package/dist/lib/hero-pose.d.ts.map +1 -1
  53. package/dist/lib/hero-pose.js +72 -5
  54. package/dist/lib/ink-edges.d.ts +4 -2
  55. package/dist/lib/ink-edges.d.ts.map +1 -1
  56. package/dist/lib/ink-edges.js +17 -7
  57. package/dist/lib/key-light-override.d.ts +11 -0
  58. package/dist/lib/key-light-override.d.ts.map +1 -0
  59. package/dist/lib/key-light-override.js +14 -0
  60. package/dist/lib/live-frame-hold.d.ts +14 -0
  61. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  62. package/dist/lib/live-frame-hold.js +28 -0
  63. package/dist/lib/materials.d.ts +14 -7
  64. package/dist/lib/materials.d.ts.map +1 -1
  65. package/dist/lib/materials.js +31 -5
  66. package/dist/lib/node-top-surface-height.d.ts +6 -0
  67. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  68. package/dist/lib/node-top-surface-height.js +74 -0
  69. package/dist/lib/perf-probe-state.d.ts +39 -0
  70. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  71. package/dist/lib/perf-probe-state.js +17 -0
  72. package/dist/lib/pointer-events.d.ts +8 -0
  73. package/dist/lib/pointer-events.d.ts.map +1 -1
  74. package/dist/lib/pointer-events.js +27 -4
  75. package/dist/lib/procedural-emission.d.ts +7 -0
  76. package/dist/lib/procedural-emission.d.ts.map +1 -0
  77. package/dist/lib/procedural-emission.js +54 -0
  78. package/dist/lib/renderer-capability.d.ts +2 -1
  79. package/dist/lib/renderer-capability.d.ts.map +1 -1
  80. package/dist/lib/renderer-capability.js +2 -2
  81. package/dist/lib/scene-support-height.d.ts +4 -0
  82. package/dist/lib/scene-support-height.d.ts.map +1 -0
  83. package/dist/lib/scene-support-height.js +34 -0
  84. package/dist/lib/scene-visibility.d.ts +6 -0
  85. package/dist/lib/scene-visibility.d.ts.map +1 -1
  86. package/dist/lib/scene-visibility.js +15 -3
  87. package/dist/lib/scripted-opening.d.ts +11 -0
  88. package/dist/lib/scripted-opening.d.ts.map +1 -0
  89. package/dist/lib/scripted-opening.js +20 -0
  90. package/dist/lib/snapshot-pipeline.d.ts +32 -2
  91. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  92. package/dist/lib/snapshot-pipeline.js +215 -105
  93. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  94. package/dist/lib/terrain-perimeter-fill.js +22 -14
  95. package/dist/lib/zone-geometry.d.ts +11 -0
  96. package/dist/lib/zone-geometry.d.ts.map +1 -0
  97. package/dist/lib/zone-geometry.js +46 -0
  98. package/dist/store/use-item-light-pool.d.ts +14 -10
  99. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  100. package/dist/store/use-item-light-pool.js +52 -26
  101. package/dist/store/use-viewer.d.ts +11 -0
  102. package/dist/store/use-viewer.d.ts.map +1 -1
  103. package/dist/store/use-viewer.js +13 -6
  104. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  105. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  106. package/dist/systems/ceiling/ceiling-system.js +86 -6
  107. package/dist/systems/door/door-system.d.ts.map +1 -1
  108. package/dist/systems/door/door-system.js +73 -22
  109. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  110. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  111. package/dist/systems/floor-elevation/floor-elevation-system.js +148 -5
  112. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  113. package/dist/systems/geometry/geometry-system.js +71 -11
  114. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  115. package/dist/systems/interactive/interactive-system.js +40 -15
  116. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  117. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  118. package/dist/systems/interactive/procedural-controls.js +17 -0
  119. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  120. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  121. package/dist/systems/interactive/scripted-clips.js +134 -0
  122. package/dist/systems/item/item-system.d.ts.map +1 -1
  123. package/dist/systems/item/item-system.js +9 -3
  124. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  125. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  126. package/dist/systems/item/object-cut-invalidation.js +68 -0
  127. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  128. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  129. package/dist/systems/item-light/item-light-system.js +47 -52
  130. package/dist/systems/level/level-system.d.ts.map +1 -1
  131. package/dist/systems/level/level-system.js +17 -19
  132. package/dist/systems/level/level-utils.d.ts +28 -1
  133. package/dist/systems/level/level-utils.d.ts.map +1 -1
  134. package/dist/systems/level/level-utils.js +36 -4
  135. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  136. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  137. package/dist/systems/level/shadow-only-levels.js +7 -0
  138. package/dist/systems/roof/roof-materials.d.ts +14 -2
  139. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  140. package/dist/systems/roof/roof-materials.js +54 -15
  141. package/dist/systems/roof/roof-system.d.ts +13 -3
  142. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  143. package/dist/systems/roof/roof-system.js +188 -13
  144. package/dist/systems/slab/slab-system.d.ts +1 -1
  145. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  146. package/dist/systems/slab/slab-system.js +49 -19
  147. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  148. package/dist/systems/stair/stair-system.js +133 -354
  149. package/dist/systems/wall/wall-cutout-cache.d.ts +11 -1
  150. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  151. package/dist/systems/wall/wall-cutout-cache.js +58 -20
  152. package/dist/systems/wall/wall-cutout.d.ts +9 -1
  153. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-cutout.js +46 -32
  155. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  156. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  157. package/dist/systems/wall/wall-finish-data.js +17 -0
  158. package/dist/systems/wall/wall-materials.d.ts +17 -3
  159. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  160. package/dist/systems/wall/wall-materials.js +109 -156
  161. package/dist/systems/wall/wall-system.d.ts +43 -4
  162. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  163. package/dist/systems/wall/wall-system.js +601 -163
  164. package/dist/systems/window/window-system.d.ts +1 -1
  165. package/dist/systems/window/window-system.d.ts.map +1 -1
  166. package/dist/systems/window/window-system.js +44 -24
  167. package/dist/xr/presentation-context.d.ts +7 -0
  168. package/dist/xr/presentation-context.d.ts.map +1 -0
  169. package/dist/xr/presentation-context.js +10 -0
  170. package/package.json +6 -6
  171. package/dist/systems/fence/fence-system.d.ts +0 -22
  172. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  173. package/dist/systems/fence/fence-system.js +0 -357
@@ -1,5 +1,5 @@
1
- import { getEffectiveRoofSurfaceMaterial, } from '@pascal-app/core';
2
- import { createMaterial, createMaterialFromPresetRef, createSurfaceRoleMaterial, resolveSlotDefaultMaterial, } from '../../lib/materials';
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
- if (materialPreset) {
22
- return createMaterialFromPresetRef(materialPreset, shading);
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
- export function getRoofMaterialArray(node, shading = 'rendered', textures = true, colorPreset = 'clay', sceneTheme) {
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
- top: getSurfaceMaterialSignature(top),
39
- edge: getSurfaceMaterialSignature(edge),
40
- wall: getSurfaceMaterialSignature(wall),
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 topMaterial = createResolvedMaterial(top.material, top.materialPreset, shading);
72
- const edgeMaterial = createResolvedMaterial(edge.material, edge.materialPreset, shading);
73
- const wallMaterial = createResolvedMaterial(wall.material, wall.materialPreset, shading);
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
- * Generate complete hollow-shell geometry for a roof segment.
40
- * Ports the prototype's CSG approach using three-bvh-csg.
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): RoofSegmentBrushSet | null;
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;AAkMvE,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;AA0FD,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;AAoWD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,KAAK,CAAC,cAAc,GAAG,IAAI,EACrC,OAAO,EAAE,eAAe,GACvB,KAAK,CAAC,cAAc,GAAG,IAAI,CAyB7B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,mBAAmB,GAAG,IAAI,CA8TvF;AAED,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,eAAe,EACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,KAAK,CAAC,cAAc,CA2GtB;AAuyCD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,eAAe,QA0ExF;AA+qBD,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"}
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
- export function getRoofSegmentBrushes(node) {
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 getVol = (wExt, vOffset, baseY, matIndex, isVoid, materialRule) => {
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
- const whV = Math.max(0.05, wallHeight - autoDrop + vOffset);
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
- const shedRoofSideMaterialRule = roofType === 'shed'
1154
- ? (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX
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 = roofType === 'shed' ? ROOF_EDGE_MATERIAL_INDEX : 1;
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
- const deckSlab = csgEvaluator.evaluate(deckTopBrush, deckBotBrush, SUBTRACTION);
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
- if (node.roofType !== 'shed')
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,GAC1B,KAAK,CAAC,cAAc,CAItB"}
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
- let area2 = 0;
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.length < 3)
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.length < 3)
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, bottom, a.y);
90
- uvs.push(0, 0);
91
- positions.push(b.x, bottom, b.y);
92
- uvs.push(len, 0);
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
- const contour2d = ensureCounterClockwisePolygon(region.contour).map(([x, z]) => new THREE.Vector2(x, z));
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
- indices.push(bottomBase + a, bottomBase + b, bottomBase + c);
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.length < 3)
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":"AA2BA,eAAO,MAAM,WAAW,YAuGvB,CAAA"}
1
+ {"version":3,"file":"stair-system.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-system.tsx"],"names":[],"mappings":"AAiCA,eAAO,MAAM,WAAW,YAuGvB,CAAA"}