@pascal-app/viewer 1.0.6 → 1.1.0

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 (238) hide show
  1. package/README.md +54 -21
  2. package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
  3. package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
  4. package/dist/components/viewer/daylight-atmosphere.js +315 -0
  5. package/dist/components/viewer/drone-controller.d.ts +15 -0
  6. package/dist/components/viewer/drone-controller.d.ts.map +1 -0
  7. package/dist/components/viewer/drone-controller.js +194 -0
  8. package/dist/components/viewer/glb-interactive.d.ts +4 -18
  9. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  10. package/dist/components/viewer/glb-interactive.js +32 -268
  11. package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
  12. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
  13. package/dist/components/viewer/glb-replace-instances.js +5 -4
  14. package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
  15. package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-scene-motion.js +57 -0
  17. package/dist/components/viewer/glb-scene.d.ts +5 -1
  18. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  19. package/dist/components/viewer/glb-scene.js +290 -49
  20. package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
  21. package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
  22. package/dist/components/viewer/glb-selection-callout.js +54 -0
  23. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  24. package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
  25. package/dist/components/viewer/index.d.ts +5 -0
  26. package/dist/components/viewer/index.d.ts.map +1 -1
  27. package/dist/components/viewer/index.js +38 -10
  28. package/dist/components/viewer/lights.d.ts.map +1 -1
  29. package/dist/components/viewer/lights.js +192 -5
  30. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  31. package/dist/components/viewer/post-processing.js +150 -94
  32. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  33. package/dist/components/viewer/registered-systems.js +2 -2
  34. package/dist/components/viewer/scene-environment.js +4 -5
  35. package/dist/components/viewer/selection-callout.d.ts +51 -0
  36. package/dist/components/viewer/selection-callout.d.ts.map +1 -0
  37. package/dist/components/viewer/selection-callout.js +197 -0
  38. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
  39. package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
  40. package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
  41. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
  42. package/dist/components/viewer/use-daylight-rooms.js +278 -0
  43. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  44. package/dist/components/viewer/viewer-camera.js +12 -1
  45. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  46. package/dist/hooks/use-gltf-ktx2.js +2 -1
  47. package/dist/index.d.ts +26 -8
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +28 -8
  50. package/dist/lib/asset-url.d.ts +0 -1
  51. package/dist/lib/asset-url.d.ts.map +1 -1
  52. package/dist/lib/asset-url.js +3 -3
  53. package/dist/lib/daylight/ground.d.ts +21 -0
  54. package/dist/lib/daylight/ground.d.ts.map +1 -0
  55. package/dist/lib/daylight/ground.js +53 -0
  56. package/dist/lib/daylight/location.d.ts +32 -0
  57. package/dist/lib/daylight/location.d.ts.map +1 -0
  58. package/dist/lib/daylight/location.js +74 -0
  59. package/dist/lib/daylight/look.d.ts +33 -0
  60. package/dist/lib/daylight/look.d.ts.map +1 -0
  61. package/dist/lib/daylight/look.js +13 -0
  62. package/dist/lib/daylight/material-hooks.d.ts +2 -0
  63. package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
  64. package/dist/lib/daylight/material-hooks.js +150 -0
  65. package/dist/lib/daylight/room-context.d.ts +12 -0
  66. package/dist/lib/daylight/room-context.d.ts.map +1 -0
  67. package/dist/lib/daylight/room-context.js +54 -0
  68. package/dist/lib/daylight/room-map.d.ts +124 -0
  69. package/dist/lib/daylight/room-map.d.ts.map +1 -0
  70. package/dist/lib/daylight/room-map.js +630 -0
  71. package/dist/lib/daylight/room-scene.d.ts +56 -0
  72. package/dist/lib/daylight/room-scene.d.ts.map +1 -0
  73. package/dist/lib/daylight/room-scene.js +391 -0
  74. package/dist/lib/daylight/rooms.d.ts +292 -0
  75. package/dist/lib/daylight/rooms.d.ts.map +1 -0
  76. package/dist/lib/daylight/rooms.js +1268 -0
  77. package/dist/lib/daylight/runtime.d.ts +33 -0
  78. package/dist/lib/daylight/runtime.d.ts.map +1 -0
  79. package/dist/lib/daylight/runtime.js +42 -0
  80. package/dist/lib/daylight/sky.d.ts +26 -0
  81. package/dist/lib/daylight/sky.d.ts.map +1 -0
  82. package/dist/lib/daylight/sky.js +217 -0
  83. package/dist/lib/daylight/solar.d.ts +46 -0
  84. package/dist/lib/daylight/solar.d.ts.map +1 -0
  85. package/dist/lib/daylight/solar.js +156 -0
  86. package/dist/lib/device-tier.d.ts +29 -0
  87. package/dist/lib/device-tier.d.ts.map +1 -0
  88. package/dist/lib/device-tier.js +71 -0
  89. package/dist/lib/dispose-object3d.d.ts.map +1 -1
  90. package/dist/lib/dispose-object3d.js +4 -2
  91. package/dist/lib/drone-flight.d.ts +44 -0
  92. package/dist/lib/drone-flight.d.ts.map +1 -0
  93. package/dist/lib/drone-flight.js +121 -0
  94. package/dist/lib/editor-highlight.d.ts +16 -0
  95. package/dist/lib/editor-highlight.d.ts.map +1 -0
  96. package/dist/lib/editor-highlight.js +33 -0
  97. package/dist/lib/foundation-materials.d.ts.map +1 -1
  98. package/dist/lib/foundation-materials.js +2 -4
  99. package/dist/lib/glb-merge.d.ts +23 -0
  100. package/dist/lib/glb-merge.d.ts.map +1 -0
  101. package/dist/lib/glb-merge.js +184 -0
  102. package/dist/lib/hardware-geometry.d.ts +21 -0
  103. package/dist/lib/hardware-geometry.d.ts.map +1 -0
  104. package/dist/lib/hardware-geometry.js +283 -0
  105. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  106. package/dist/lib/ktx2-loader.js +2 -1
  107. package/dist/lib/layer-pass.d.ts +2 -0
  108. package/dist/lib/layer-pass.d.ts.map +1 -1
  109. package/dist/lib/layer-pass.js +20 -0
  110. package/dist/lib/materials.d.ts.map +1 -1
  111. package/dist/lib/materials.js +50 -23
  112. package/dist/lib/measurement-label.d.ts +4 -0
  113. package/dist/lib/measurement-label.d.ts.map +1 -0
  114. package/dist/lib/measurement-label.js +54 -0
  115. package/dist/lib/measurement-presentation.d.ts +5 -0
  116. package/dist/lib/measurement-presentation.d.ts.map +1 -0
  117. package/dist/lib/measurement-presentation.js +8 -0
  118. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  119. package/dist/lib/merged-outline-node.js +7 -1
  120. package/dist/lib/occlusion-culler.d.ts +34 -0
  121. package/dist/lib/occlusion-culler.d.ts.map +1 -0
  122. package/dist/lib/occlusion-culler.js +698 -0
  123. package/dist/lib/pointer-events.d.ts.map +1 -1
  124. package/dist/lib/pointer-events.js +3 -3
  125. package/dist/lib/post-processing-resources.d.ts +4 -0
  126. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  127. package/dist/lib/post-processing-resources.js +5 -0
  128. package/dist/lib/readback-rows.d.ts +14 -0
  129. package/dist/lib/readback-rows.d.ts.map +1 -0
  130. package/dist/lib/readback-rows.js +31 -0
  131. package/dist/lib/reveal-pose.d.ts.map +1 -1
  132. package/dist/lib/reveal-pose.js +3 -0
  133. package/dist/lib/scene-matrices.d.ts +12 -0
  134. package/dist/lib/scene-matrices.d.ts.map +1 -0
  135. package/dist/lib/scene-matrices.js +287 -0
  136. package/dist/lib/scene-pass-mrt.d.ts +5 -2
  137. package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
  138. package/dist/lib/scene-pass-mrt.js +7 -2
  139. package/dist/lib/scene-themes.d.ts +0 -2
  140. package/dist/lib/scene-themes.d.ts.map +1 -1
  141. package/dist/lib/scene-themes.js +0 -2
  142. package/dist/lib/scene-visibility.d.ts +2 -0
  143. package/dist/lib/scene-visibility.d.ts.map +1 -1
  144. package/dist/lib/scene-visibility.js +12 -3
  145. package/dist/lib/site-surfaces.d.ts +1 -8
  146. package/dist/lib/site-surfaces.d.ts.map +1 -1
  147. package/dist/lib/site-surfaces.js +0 -19
  148. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  149. package/dist/lib/snapshot-pipeline.js +24 -40
  150. package/dist/lib/texture-reference.d.ts.map +1 -1
  151. package/dist/lib/texture-reference.js +13 -10
  152. package/dist/lib/underground-ambiance.d.ts +0 -2
  153. package/dist/lib/underground-ambiance.d.ts.map +1 -1
  154. package/dist/lib/underground-ambiance.js +0 -2
  155. package/dist/lib/viewer-config.d.ts +21 -0
  156. package/dist/lib/viewer-config.d.ts.map +1 -0
  157. package/dist/lib/viewer-config.js +21 -0
  158. package/dist/lib/viewer-preference-storage.d.ts +7 -0
  159. package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
  160. package/dist/lib/viewer-preference-storage.js +23 -0
  161. package/dist/store/use-item-light-pool.d.ts +3 -6
  162. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  163. package/dist/store/use-item-light-pool.js +12 -0
  164. package/dist/store/use-viewer.d.ts +62 -59
  165. package/dist/store/use-viewer.d.ts.map +1 -1
  166. package/dist/store/use-viewer.js +106 -19
  167. package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
  168. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  169. package/dist/systems/ceiling/ceiling-system.js +11 -1
  170. package/dist/systems/door/door-system.d.ts.map +1 -1
  171. package/dist/systems/door/door-system.js +501 -145
  172. package/dist/systems/door/leaf-profile.d.ts +11 -0
  173. package/dist/systems/door/leaf-profile.d.ts.map +1 -0
  174. package/dist/systems/door/leaf-profile.js +140 -0
  175. package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
  176. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  177. package/dist/systems/elevator/elevator-interaction-system.js +41 -8
  178. package/dist/systems/geometry/geometry-status.d.ts +8 -0
  179. package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
  180. package/dist/systems/geometry/geometry-status.js +24 -0
  181. package/dist/systems/interactive/callout-actions.d.ts +72 -0
  182. package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
  183. package/dist/systems/interactive/callout-actions.js +212 -0
  184. package/dist/systems/interactive/interactive-system.d.ts +5 -1
  185. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  186. package/dist/systems/interactive/interactive-system.js +44 -121
  187. package/dist/systems/interactive/opening-settled.d.ts +16 -0
  188. package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
  189. package/dist/systems/interactive/opening-settled.js +53 -0
  190. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  191. package/dist/systems/item-light/item-light-system.js +22 -214
  192. package/dist/systems/item-light/pooled-lights.d.ts +70 -0
  193. package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
  194. package/dist/systems/item-light/pooled-lights.js +449 -0
  195. package/dist/systems/level/level-system.d.ts.map +1 -1
  196. package/dist/systems/level/level-system.js +19 -1
  197. package/dist/systems/roof/roof-material-slots.d.ts +14 -0
  198. package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
  199. package/dist/systems/roof/roof-material-slots.js +13 -0
  200. package/dist/systems/roof/roof-materials.d.ts +25 -2
  201. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  202. package/dist/systems/roof/roof-materials.js +85 -26
  203. package/dist/systems/roof/roof-system.d.ts +34 -1
  204. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  205. package/dist/systems/roof/roof-system.js +582 -168
  206. package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
  207. package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
  208. package/dist/systems/wall/opening-casing-geometry.js +102 -0
  209. package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
  210. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
  211. package/dist/systems/wall/opening-cutout-geometry.js +7 -3
  212. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  213. package/dist/systems/wall/wall-materials.js +28 -35
  214. package/dist/systems/wall/wall-system.d.ts +11 -0
  215. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  216. package/dist/systems/wall/wall-system.js +86 -25
  217. package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
  218. package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
  219. package/dist/systems/wall/wall-t-junctions.js +149 -0
  220. package/dist/systems/window/window-animation-system.d.ts +3 -12
  221. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  222. package/dist/systems/window/window-animation-system.js +37 -122
  223. package/dist/systems/window/window-pose.d.ts +31 -0
  224. package/dist/systems/window/window-pose.d.ts.map +1 -0
  225. package/dist/systems/window/window-pose.js +152 -0
  226. package/dist/systems/window/window-roller-shutter.d.ts +8 -0
  227. package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
  228. package/dist/systems/window/window-roller-shutter.js +64 -0
  229. package/dist/systems/window/window-system.d.ts +2 -11
  230. package/dist/systems/window/window-system.d.ts.map +1 -1
  231. package/dist/systems/window/window-system.js +470 -303
  232. package/package.json +6 -6
  233. package/dist/systems/interactive/control-widget.d.ts +0 -10
  234. package/dist/systems/interactive/control-widget.d.ts.map +0 -1
  235. package/dist/systems/interactive/control-widget.js +0 -35
  236. package/dist/systems/interactive/procedural-controls.d.ts +0 -13
  237. package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
  238. package/dist/systems/interactive/procedural-controls.js +0 -17
@@ -0,0 +1,14 @@
1
+ import { type DoorNode, type OpeningCasingHost, type WindowNode } from '@pascal-app/core';
2
+ import * as THREE from 'three';
3
+ /** Name of the meshes a door or window builds its casing from. */
4
+ export declare const OPENING_CASING_MESH_NAME = "opening-casing";
5
+ /**
6
+ * A door's or window's casing as meshes in the unit's own frame, to hang on the
7
+ * mesh its frame is built on: the boards of `openingCasingBoards` on each cased
8
+ * face of the host, turned half round with a unit that faces the host's back. A
9
+ * rectangular opening is cased with butted boards. An arched or rounded one is
10
+ * cased with a band that follows the cutout's own profile over its head and down
11
+ * its sides; a window's bottom board stays straight below it.
12
+ */
13
+ export declare function buildOpeningCasingMeshes(opening: DoorNode | WindowNode, host: OpeningCasingHost, material: THREE.Material): THREE.Mesh[];
14
+ //# sourceMappingURL=opening-casing-geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opening-casing-geometry.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/opening-casing-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAGb,KAAK,iBAAiB,EAKtB,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,kEAAkE;AAClE,eAAO,MAAM,wBAAwB,mBAAmB,CAAA;AAExD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,QAAQ,GAAG,UAAU,EAC9B,IAAI,EAAE,iBAAiB,EACvB,QAAQ,EAAE,KAAK,CAAC,QAAQ,GACvB,KAAK,CAAC,IAAI,EAAE,CAwCd"}
@@ -0,0 +1,102 @@
1
+ import { getDoorOpeningSize, OPENING_CASING_PROUD, openingCasingBoards, openingCasingFaces, openingCasingWidth, openingHostFacing, } from '@pascal-app/core';
2
+ import * as THREE from 'three';
3
+ import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
4
+ import { buildOpeningCutoutShape, getRoundedOpeningRadii } from './opening-cutout-geometry';
5
+ /** Name of the meshes a door or window builds its casing from. */
6
+ export const OPENING_CASING_MESH_NAME = 'opening-casing';
7
+ /**
8
+ * A door's or window's casing as meshes in the unit's own frame, to hang on the
9
+ * mesh its frame is built on: the boards of `openingCasingBoards` on each cased
10
+ * face of the host, turned half round with a unit that faces the host's back. A
11
+ * rectangular opening is cased with butted boards. An arched or rounded one is
12
+ * cased with a band that follows the cutout's own profile over its head and down
13
+ * its sides; a window's bottom board stays straight below it.
14
+ */
15
+ export function buildOpeningCasingMeshes(opening, host, material) {
16
+ const facing = openingHostFacing(opening);
17
+ const size = getDoorOpeningSize(opening);
18
+ const shaped = size.width === opening.width &&
19
+ size.height === opening.height &&
20
+ (opening.openingShape === 'arch' || opening.openingShape === 'rounded');
21
+ const meshes = [];
22
+ const add = (geometry, face) => {
23
+ // Turned half round, the unit's x and z run against the host's.
24
+ if (facing < 0)
25
+ geometry.rotateY(Math.PI);
26
+ const mesh = new THREE.Mesh(geometry, material);
27
+ mesh.name = OPENING_CASING_MESH_NAME;
28
+ mesh.userData.slotId = `casing-${face}`;
29
+ meshes.push(mesh);
30
+ };
31
+ for (const board of openingCasingBoards(opening, host)) {
32
+ if (shaped && (board.part === 'head' || board.part === 'leg'))
33
+ continue;
34
+ const [width, height, depth] = board.size;
35
+ const geometry = new THREE.BoxGeometry(width, height, depth);
36
+ applyWorldScaleBoxUVs(geometry, width, height, depth);
37
+ geometry.translate(...board.center);
38
+ add(geometry, board.face);
39
+ }
40
+ if (shaped) {
41
+ const band = casingBandShape(opening, openingCasingWidth(opening));
42
+ for (const { face, plane, out } of openingCasingFaces(opening, host)) {
43
+ const geometry = new THREE.ExtrudeGeometry(band, {
44
+ depth: OPENING_CASING_PROUD,
45
+ bevelEnabled: false,
46
+ curveSegments: 24,
47
+ });
48
+ geometry.translate(0, 0, out > 0 ? plane : plane - OPENING_CASING_PROUD);
49
+ add(geometry, face);
50
+ }
51
+ }
52
+ return meshes;
53
+ }
54
+ /**
55
+ * The casing band of an arched or rounded opening as one outline, in the host
56
+ * frame: out from the cutout's profile by `width` over the head and down both
57
+ * sides, open along the bottom (a door's floor, a window's bottom board).
58
+ */
59
+ function casingBandShape(opening, width) {
60
+ const halfWidth = opening.width / 2;
61
+ const halfHeight = opening.height / 2;
62
+ const inner = buildOpeningCutoutShape(opening, {
63
+ left: -halfWidth,
64
+ right: halfWidth,
65
+ bottom: -halfHeight,
66
+ top: halfHeight,
67
+ }).getPoints(24);
68
+ const outer = buildOpeningCutoutShape(grownOpening(opening, width), {
69
+ left: -halfWidth - width,
70
+ right: halfWidth + width,
71
+ bottom: -halfHeight,
72
+ top: halfHeight + width,
73
+ }).getPoints(24);
74
+ // Both profiles start along the bottom, left to right, and end back at their
75
+ // first point: the band runs up and over the outer one from its bottom right,
76
+ // then back over the inner one from its bottom left.
77
+ return new THREE.Shape([...outer.slice(1), ...inner.slice(1).reverse()]);
78
+ }
79
+ /**
80
+ * The opening a casing band's outer edge follows: its arch or its top corners
81
+ * grown by `width`, so the band keeps one width all round, and square at the
82
+ * bottom, where the band stops.
83
+ */
84
+ function grownOpening(opening, width) {
85
+ if (opening.openingShape === 'arch') {
86
+ const archHeight = Math.min(Math.max(opening.archHeight ?? opening.width / 2, 0.01), opening.height);
87
+ return { ...opening, archHeight: archHeight + width };
88
+ }
89
+ const radii = getRoundedOpeningRadii(opening, opening.width, opening.height);
90
+ const grow = (radius) => (radius > 1e-6 ? radius + width : 0);
91
+ return opening.type === 'window'
92
+ ? {
93
+ ...opening,
94
+ openingRadiusMode: 'individual',
95
+ openingCornerRadii: [grow(radii.topLeft), grow(radii.topRight), 0, 0],
96
+ }
97
+ : {
98
+ ...opening,
99
+ openingRadiusMode: 'individual',
100
+ openingTopRadii: [grow(radii.topLeft), grow(radii.topRight)],
101
+ };
102
+ }
@@ -8,6 +8,12 @@ export type OpeningCutoutRect = {
8
8
  top: number;
9
9
  };
10
10
  export declare function getOpeningCutoutProxyDepth(wallThickness: number): number;
11
+ type CornerRadii = {
12
+ topLeft: number;
13
+ topRight: number;
14
+ bottomRight: number;
15
+ bottomLeft: number;
16
+ };
11
17
  /**
12
18
  * Pure cutout profile for a shaped door / window opening. `rect` is in
13
19
  * the caller's coordinate frame — the wall CSG pipeline passes wall-local
@@ -28,4 +34,7 @@ export declare function hasFlatOpeningCutoutBottom(opening: OpeningCutoutNode):
28
34
  * subtract a face exactly coplanar with the wall base.
29
35
  */
30
36
  export declare function getOpeningCutoutBottomPadding(opening: OpeningCutoutNode, bottom: number): number;
37
+ /** The corner radii a rounded opening's cutout is cut with, clamped to fit `width` × `height`. */
38
+ export declare function getRoundedOpeningRadii(opening: OpeningCutoutNode, width: number, height: number): CornerRadii;
39
+ export {};
31
40
  //# sourceMappingURL=opening-cutout-geometry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"opening-cutout-geometry.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/opening-cutout-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,UAAU,CAAA;AAErD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAYD,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAExE;AASD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,iBAAiB,GACtB,KAAK,CAAC,KAAK,CAuCb;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,KAAK,CAAC,cAAc,CAqBtB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAW9E;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEhG"}
1
+ {"version":3,"file":"opening-cutout-geometry.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/opening-cutout-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE5D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,UAAU,CAAA;AAErD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAYD,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,iBAAiB,GACtB,KAAK,CAAC,KAAK,CAyCb;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,KAAK,CAAC,cAAc,CAqBtB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAW9E;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEhG;AAED,kGAAkG;AAClG,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,WAAW,CAyCb"}
@@ -1,3 +1,4 @@
1
+ import { getDoorOpeningSize } from '@pascal-app/core';
1
2
  import * as THREE from 'three';
2
3
  // The cutout proxy doubles as the invisible raycast hit target for an opening:
3
4
  // centered on the wall and extending past both faces so it wins the scene
@@ -22,7 +23,9 @@ export function buildOpeningCutoutShape(opening, rect) {
22
23
  const width = Math.max(right - left, 1e-6);
23
24
  const height = Math.max(top - bottom, 1e-6);
24
25
  const shape = new THREE.Shape();
25
- if (opening.openingShape === 'arch') {
26
+ const size = getDoorOpeningSize(opening);
27
+ const fixedLights = size.width !== opening.width || size.height !== opening.height;
28
+ if (!fixedLights && opening.openingShape === 'arch') {
26
29
  const halfWidth = width / 2;
27
30
  const centerX = (left + right) / 2;
28
31
  const archHeight = Math.min(Math.max(opening.archHeight ?? width / 2, 0.01), height);
@@ -41,7 +44,7 @@ export function buildOpeningCutoutShape(opening, rect) {
41
44
  shape.closePath();
42
45
  return shape;
43
46
  }
44
- if (opening.openingShape === 'rounded') {
47
+ if (!fixedLights && opening.openingShape === 'rounded') {
45
48
  const radii = getRoundedOpeningRadii(opening, width, height);
46
49
  applyRoundedOpeningShape(shape, left, right, bottom, top, radii);
47
50
  return shape;
@@ -93,7 +96,8 @@ export function hasFlatOpeningCutoutBottom(opening) {
93
96
  export function getOpeningCutoutBottomPadding(opening, bottom) {
94
97
  return bottom < 0.005 && hasFlatOpeningCutoutBottom(opening) ? OPENING_CUTOUT_BOTTOM_PADDING : 0;
95
98
  }
96
- function getRoundedOpeningRadii(opening, width, height) {
99
+ /** The corner radii a rounded opening's cutout is cut with, clamped to fit `width` × `height`. */
100
+ export function getRoundedOpeningRadii(opening, width, height) {
97
101
  if (opening.type !== 'window') {
98
102
  if (opening.openingRadiusMode === 'individual') {
99
103
  const [topLeft = 0, topRight = 0] = opening.openingTopRadii ?? [0.15, 0.15];
@@ -1 +1 @@
1
- {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,aAAa,EAClB,KAAK,eAAe,EAGpB,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAS5C,OAAO,EAEL,KAAK,WAAW,EAMhB,KAAK,aAAa,EAInB,MAAM,qBAAqB,CAAA;AAE5B,KAAK,cAAc,GAAG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;AAexE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,EAAE,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAA;CAC1B;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM;QAAE,OAAO,EAAE,iBAAiB,CAAC;QAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAA;KAAE,CAAA;CACjE,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,aAAa,EACvB,QAAQ,CAAC,EAAE,OAAO,EAClB,WAAW,CAAC,EAAE,WAAW,EACzB,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,KAC3B,aAAa,CAAA;AA4SlB,wFAAwF;AACxF,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS9F;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS1F;AA4DD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,EAClC,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAQR;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,EAClC,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,aAAa,CAmFf;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,GACjC,iBAAiB,CAUnB"}
1
+ {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,KAAK,aAAa,EAClB,KAAK,eAAe,EAGpB,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAU5C,OAAO,EAEL,KAAK,WAAW,EAMhB,KAAK,aAAa,EAInB,MAAM,qBAAqB,CAAA;AAE5B,KAAK,cAAc,GAAG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;AAcxE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,EAAE,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAA;CAC1B;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM;QAAE,OAAO,EAAE,iBAAiB,CAAC;QAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAA;KAAE,CAAA;CACjE,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,aAAa,EACvB,QAAQ,CAAC,EAAE,OAAO,EAClB,WAAW,CAAC,EAAE,WAAW,EACzB,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,KAC3B,aAAa,CAAA;AAiSlB,wFAAwF;AACxF,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAI9F;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAI1F;AA4DD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,EAClC,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAQR;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,EAClC,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,aAAa,CAmFf;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,GACjC,iBAAiB,CAUnB"}
@@ -1,7 +1,8 @@
1
- import { getEffectiveWallFaceMaterial, getMaterialPresetByRef, getWallSurfaceMaterialSignature, parseMaterialColor, parseMaterialRef, resolveMaterial, resolveWallExteriorSide, resolveWallFaceChain, WALL_SLOT_DEFAULT, wallAssemblyExposedStructure, wallAssemblyFinishRef, } from '@pascal-app/core';
1
+ import { getEffectiveWallFaceMaterial, getMaterialPresetByRef, getWallFaceDefault, getWallSurfaceMaterialSignature, parseMaterialColor, parseMaterialRef, resolveMaterial, resolveWallExteriorSide, resolveWallFaceChain, WALL_SLOT_DEFAULT, wallAssemblyExposedStructure, wallAssemblyFaceFinish, } from '@pascal-app/core';
2
2
  import { Color } from 'three';
3
3
  import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, uv, vec2 } from 'three/tsl';
4
4
  import { MeshLambertNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
5
+ import { markHighlight, tintEmissive } from '../../lib/editor-highlight';
5
6
  import { BLOCK_MASONRY, concreteMaterial, masonryMaterial, STRUCTURE_COLORS, } from '../../lib/foundation-materials';
6
7
  import { baseMaterial, createDefaultMaterial, createMaterial, createMaterialFromPresetRef, createSurfaceRoleMaterial, materialPresetRefSignature, resolveMaterialRef, resolveSlotDefaultRef, resolveSurfaceColor, } from '../../lib/materials';
7
8
  const DEFAULT_WALL_COLOR = '#e9e7e3';
@@ -9,8 +10,7 @@ const WALL_HIGHLIGHT_PROFILES = {
9
10
  delete: {
10
11
  color: new Color('#dc2626'),
11
12
  blend: 0.76,
12
- emissiveBlend: 0.92,
13
- emissiveIntensity: 0.46,
13
+ emissive: 0.92,
14
14
  },
15
15
  };
16
16
  const wallMaterialCache = new Map();
@@ -43,11 +43,11 @@ function resolveWallSlotDefault(declaredDefault, shading) {
43
43
  }
44
44
  return createDefaultMaterial(slotDefault, 0.9, shading);
45
45
  }
46
- // The assembly cladding's ref when `face` is the wall's exterior face (a is the
47
- // +normal face; an unresolved side keeps the cladding on b, as legacy did).
46
+ // The assembly's finish for `face`: its cladding when `face` is the wall's exterior face (a is the
47
+ // +normal face; an unresolved side keeps the cladding on b, as legacy did), and the paint picked
48
+ // for a lining while drafting on a lined face.
48
49
  function wallAssemblyFaceFinishRef(wallNode, face) {
49
- const exteriorFace = resolveWallExteriorSide(wallNode) === 1 ? 'a' : 'b';
50
- return face === exteriorFace ? wallAssemblyFinishRef(wallNode) : null;
50
+ return wallAssemblyFaceFinish(wallNode, face);
51
51
  }
52
52
  // What `face` shows when it is the exterior face of a wall with no cladding over a bare masonry or
53
53
  // concrete core: the structure itself (null otherwise, and for any face with a cladding).
@@ -57,7 +57,7 @@ function wallFaceExposedStructure(wallNode, face) {
57
57
  const exteriorFace = resolveWallExteriorSide(wallNode) === 1 ? 'a' : 'b';
58
58
  return face === exteriorFace ? wallAssemblyExposedStructure(wallNode) : null;
59
59
  }
60
- // The structure as the generic foundation draws it (same courses, joints and colours), made once:
60
+ // The bare structure (courses, joints and colours), made once:
61
61
  // the block's bond runs along the face's world-planar U, the wall's own metres. Solid shading
62
62
  // keeps to a flat colour, like every other wall finish.
63
63
  const structureMaterials = new Map();
@@ -73,20 +73,22 @@ function exposedStructureMaterial(structure, shading) {
73
73
  return material;
74
74
  }
75
75
  // Slot-first resolution for one wall face, matching every other paintable kind:
76
- // node.slots[face] ref → legacy inline finish → declared slot default.
76
+ // node.slots[face] ref → legacy inline finish → the face's default.
77
77
  // A dangling `scene:` ref (material deleted / copied across scenes) falls back
78
- // to the declared default — it never blocks rendering (the dangling-ref rule).
78
+ // to the face's default — it never blocks rendering (the dangling-ref rule).
79
79
  function resolveWallFaceMaterial(wallNode, face, shading, sceneMaterials) {
80
80
  const finish = resolveWallFaceChain(wallNode, face);
81
81
  if (finish.kind === 'ref') {
82
82
  return (resolveMaterialRef(finish.ref, sceneMaterials, shading) ??
83
- resolveWallSlotDefault(WALL_SLOT_DEFAULT[face], shading));
83
+ resolveWallSlotDefault(getWallFaceDefault(wallNode, face), shading));
84
84
  }
85
85
  if (finish.kind === 'legacy')
86
86
  return getSurfaceVisibleMaterial(finish.spec, shading);
87
87
  // No paint on this face: the wall ASSEMBLY's cladding (WS5) skins the
88
88
  // exterior — a "2x6 lap siding" wall reads as lap siding without the user
89
- // painting it. Painting the face still wins.
89
+ // painting it. Painting the face still wins. Any other unpainted face wears
90
+ // its default (`getWallFaceDefault`): the interior paint
91
+ // (`DEFAULT_INTERIOR_WALL_PAINT`) inside, raw drywall on the exterior side.
90
92
  const assemblyRef = wallAssemblyFaceFinishRef(wallNode, face);
91
93
  const exposed = assemblyRef ? null : wallFaceExposedStructure(wallNode, face);
92
94
  if (exposed)
@@ -116,11 +118,15 @@ function refMaterialSignature(ref, sceneMaterials) {
116
118
  // Falls back to the legacy signature when the face has no slot ref.
117
119
  function wallFaceMaterialSignature(wallNode, face, sceneMaterials) {
118
120
  const ref = wallNode.slots?.[face];
121
+ // The face's default follows its side, so a room detected outside it re-skins
122
+ // an unpainted face (and the fallback of a dangling ref).
123
+ const fallback = getWallFaceDefault(wallNode, face);
119
124
  if (ref)
120
- return refMaterialSignature(ref, sceneMaterials);
125
+ return JSON.stringify({ ref: refMaterialSignature(ref, sceneMaterials), fallback });
121
126
  const spec = getEffectiveWallFaceMaterial(wallNode, face);
122
127
  const assemblyRef = wallAssemblyFaceFinishRef(wallNode, face);
123
128
  return JSON.stringify({
129
+ fallback,
124
130
  legacy: getWallSurfaceMaterialSignature(spec),
125
131
  preset: spec.materialPreset ? materialPresetRefSignature(spec.materialPreset) : null,
126
132
  // Changing the assembly's cladding must re-skin the face.
@@ -178,16 +184,9 @@ function createHighlightedWallMaterial(material, kind) {
178
184
  if ('color' in highlightedMaterial && highlightedMaterial.color) {
179
185
  highlightedMaterial.color = getHighlightedColor(highlightedMaterial.color, kind);
180
186
  }
181
- if ('emissive' in highlightedMaterial && highlightedMaterial.emissive) {
182
- highlightedMaterial.emissive = highlightedMaterial.emissive
183
- .clone()
184
- .lerp(profile.color, profile.emissiveBlend);
185
- }
186
- if ('emissiveIntensity' in highlightedMaterial) {
187
- highlightedMaterial.emissiveIntensity = Math.max(highlightedMaterial.emissiveIntensity ?? 0, profile.emissiveIntensity);
188
- }
187
+ tintEmissive(highlightedMaterial, profile.color, profile.emissive);
189
188
  highlightedMaterial.needsUpdate = true;
190
- return highlightedMaterial;
189
+ return markHighlight(highlightedMaterial);
191
190
  }
192
191
  // Light selection highlight for walls (walls are excluded from the generic
193
192
  // editor selection highlight, so they need their own). Adds a gentle indigo
@@ -199,15 +198,13 @@ function createHighlightedWallMaterial(material, kind) {
199
198
  // cache keyed by the source `.map` and rebuild when it changes (self-heals
200
199
  // once the texture lands).
201
200
  const SELECTION_HIGHLIGHT_COLOR = new Color('#818cf8');
202
- const SELECTION_EMISSIVE_BLEND = 0.4;
203
- const SELECTION_EMISSIVE_INTENSITY = 0.12;
201
+ const SELECTION_EMISSIVE = 0.12;
204
202
  // Softer sibling of the selection glow, for HOVERING a hidden wall (the
205
203
  // X-ray nearest-first selection made hidden walls hover targets; without a
206
204
  // material affordance the only thing lighting up was the furniture behind
207
205
  // them). Same indigo so hover reads as "this will select", weaker so a
208
206
  // hovered-then-selected wall still steps up on click.
209
- const HOVER_EMISSIVE_BLEND = 0.4;
210
- const HOVER_EMISSIVE_INTENSITY = 0.2;
207
+ const HOVER_EMISSIVE = 0.08;
211
208
  const SELECTION_TEXTURE_MAP_KEYS = [
212
209
  'map',
213
210
  'normalMap',
@@ -222,7 +219,7 @@ const SELECTION_TEXTURE_MAP_KEYS = [
222
219
  ];
223
220
  const selectionHighlightCache = new WeakMap();
224
221
  const hoverHighlightCache = new WeakMap();
225
- function getEmissiveHighlightMaterial(base, cache, emissiveBlend, emissiveIntensity) {
222
+ function getEmissiveHighlightMaterial(base, cache, emissive) {
226
223
  const baseMap = base.map ?? null;
227
224
  const cached = cache.get(base);
228
225
  if (cached && cached.map === baseMap)
@@ -235,19 +232,15 @@ function getEmissiveHighlightMaterial(base, cache, emissiveBlend, emissiveIntens
235
232
  if (src[key])
236
233
  dst[key] = src[key];
237
234
  }
238
- if ('emissive' in clone && clone.emissive) {
239
- clone.emissive = clone.emissive.clone().lerp(SELECTION_HIGHLIGHT_COLOR, emissiveBlend);
240
- }
241
- if ('emissiveIntensity' in clone) {
242
- clone.emissiveIntensity = Math.max(clone.emissiveIntensity ?? 0, emissiveIntensity);
243
- }
235
+ tintEmissive(clone, SELECTION_HIGHLIGHT_COLOR, emissive);
244
236
  clone.needsUpdate = true;
237
+ markHighlight(clone);
245
238
  cache.set(base, { clone, map: baseMap });
246
239
  return clone;
247
240
  }
248
241
  /** Lazy light-emissive selection variant of a wall's material array (keeps texture). */
249
242
  export function getSelectionHighlightMaterials(materials) {
250
- return materials.map((material) => getEmissiveHighlightMaterial(material, selectionHighlightCache, SELECTION_EMISSIVE_BLEND, SELECTION_EMISSIVE_INTENSITY));
243
+ return materials.map((material) => getEmissiveHighlightMaterial(material, selectionHighlightCache, SELECTION_EMISSIVE));
251
244
  }
252
245
  /**
253
246
  * Softer hover sibling of the selection variant — the affordance for a
@@ -255,7 +248,7 @@ export function getSelectionHighlightMaterials(materials) {
255
248
  * film so the wall the click would select reads under the cursor).
256
249
  */
257
250
  export function getHoverHighlightMaterials(materials) {
258
- return materials.map((material) => getEmissiveHighlightMaterial(material, hoverHighlightCache, HOVER_EMISSIVE_BLEND, HOVER_EMISSIVE_INTENSITY));
251
+ return materials.map((material) => getEmissiveHighlightMaterial(material, hoverHighlightCache, HOVER_EMISSIVE));
259
252
  }
260
253
  function createInvisibleWallMaterial(color, shading) {
261
254
  const material = shading === 'solid'
@@ -55,6 +55,17 @@ export declare function markWallsForZoneFinishChanges(): void;
55
55
  */
56
56
  export declare function wallGeometryChildren(node: WallNode, nodes: Readonly<Record<string, AnyNode>>): AnyNode[];
57
57
  type WallTerrainBottomSampler = (x: number, z: number) => number | null;
58
+ /** The wall's top in wall-local coordinates: `height` at the start, rising `slope` per metre of x. */
59
+ type WallTopLine = {
60
+ height: number;
61
+ slope: number;
62
+ };
63
+ /**
64
+ * Wall-local Y of the sloped top at `x`. Mitered corners sample `x` outside
65
+ * [0, L], where the line can fall below the wall base; the top never drops
66
+ * under {@link MIN_WALL_END_HEIGHT} (or the flat height, if lower).
67
+ */
68
+ export declare function wallTopYAt(wallTop: WallTopLine, x: number): number;
58
69
  export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number, baseElevation?: number, baseSegments?: readonly WallSlabSupportSegment[], storeyHeight?: number, terrainBottomAt?: WallTerrainBottomSampler, faceBase?: {
59
70
  a: readonly WallSlabSupportSegment[];
60
71
  b: readonly WallSlabSupportSegment[];
@@ -1 +1 @@
1
- {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { buildWallFinishLayout, containsPoint, cornerPair, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getOpeningWallCut, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallLevelZones, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, isCutterName, pointToKey, resolveCutterHost, resolveLevelId, resolveWallFaceBottom, resolveWallFinish, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, WALL_SURFACE_SLOT_DEFAULTS, wallFinishMaterialIndex, zoneHasWallFinish, } from '@pascal-app/core';
1
+ import { buildWallFinishLayout, clampWallEndHeightOffset, containsPoint, cornerPair, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getDoorOpeningSize, getEffectiveNode, getOpeningWallCut, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallLevelZones, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, isCutterName, MIN_WALL_END_HEIGHT, openingHostFacing, pointToKey, resolveCutterHost, resolveLevelId, resolveWallFaceBottom, resolveWallFinish, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, WALL_SURFACE_SLOT_DEFAULTS, wallFinishMaterialIndex, zoneHasWallFinish, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import { useEffect } from 'react';
4
4
  import * as THREE from 'three';
@@ -17,6 +17,7 @@ import { drainStats, endInitialBuild, initiallyBuiltWalls, isWallInitialBuildAct
17
17
  import { getWallFaceBaseAt, } from './wall-finish-data';
18
18
  import { sweepUnbuiltWalls, WALL_PLACEHOLDER_SWEEP_INTERVAL } from './wall-placeholder-sweep';
19
19
  import { notifyWallRebuilt } from './wall-rebuild-notifications';
20
+ import { repairWallTJunctions } from './wall-t-junctions';
20
21
  export { isWallInitialBuildActive } from './wall-build-lifecycle';
21
22
  export { drainRebuiltWalls } from './wall-rebuild-notifications';
22
23
  // Reusable CSG evaluator for better performance
@@ -332,6 +333,9 @@ function interpolateSplitVertex(a, b, t) {
332
333
  z: a.z + (b.z - a.z) * t,
333
334
  };
334
335
  }
336
+ function compareSplitVertices(a, b) {
337
+ return a.x - b.x || a.y - b.y || a.z - b.z;
338
+ }
335
339
  function clipPolygonByPlane(polygon, axis, plane, keepBelow) {
336
340
  const out = [];
337
341
  if (polygon.length === 0)
@@ -345,9 +349,12 @@ function clipPolygonByPlane(polygon, axis, plane, keepBelow) {
345
349
  const currentInside = isInside(current);
346
350
  const previousInside = isInside(previous);
347
351
  if (currentInside !== previousInside) {
348
- const denom = current[axis] - previous[axis];
352
+ // Interpolate from the same endpoint whichever way the edge is walked: the
353
+ // two triangles sharing it then get a bit-identical seam vertex.
354
+ const [from, to] = compareSplitVertices(previous, current) <= 0 ? [previous, current] : [current, previous];
355
+ const denom = to[axis] - from[axis];
349
356
  if (Math.abs(denom) > WALL_BAND_SPLIT_EPSILON) {
350
- out.push(interpolateSplitVertex(previous, current, (plane - previous[axis]) / denom));
357
+ out.push(interpolateSplitVertex(from, to, (plane - from[axis]) / denom));
351
358
  }
352
359
  }
353
360
  if (currentInside)
@@ -1174,10 +1181,22 @@ function mergeWallTerrainFill(body, fills, wall, boundaryEdges, finish) {
1174
1181
  bodyGeometry.dispose();
1175
1182
  for (const fill of splitFills)
1176
1183
  fill.dispose();
1177
- merged.computeVertexNormals();
1178
- assignWallMaterialGroups(merged, wall, boundaryEdges, finish);
1179
- ensureRenderableGeometryAttributes(merged);
1180
- return merged;
1184
+ return finishWallFaces(merged, wall, boundaryEdges, finish);
1185
+ }
1186
+ /**
1187
+ * Classifies the faces into material groups, then removes the T-junctions that
1188
+ * CSG cuts, finish-band splits and fill seams leave behind (they crack open
1189
+ * once a bake quantizes positions). Classification runs first, on the source
1190
+ * triangles, so the repair's sliver pieces inherit their material.
1191
+ */
1192
+ function finishWallFaces(geometry, wall, boundaryEdges, finish) {
1193
+ geometry.computeVertexNormals();
1194
+ assignWallMaterialGroups(geometry, wall, boundaryEdges, finish);
1195
+ const repaired = repairWallTJunctions(geometry);
1196
+ if (repaired !== geometry)
1197
+ repaired.computeVertexNormals();
1198
+ ensureRenderableGeometryAttributes(repaired);
1199
+ return repaired;
1181
1200
  }
1182
1201
  /** Wall-local x of a support-segment parameter (curve parameter on arcs). */
1183
1202
  function wallStationX(wall, t, length) {
@@ -1208,6 +1227,41 @@ function buildWallFinishContext(wall, layout, faceBase, slabElevation) {
1208
1227
  .sort((left, right) => left.start - right.start);
1209
1228
  return { layout, faceBase: { a: runs(faceBase.a), b: runs(faceBase.b) }, refs, foundation };
1210
1229
  }
1230
+ /**
1231
+ * Wall-local Y of the sloped top at `x`. Mitered corners sample `x` outside
1232
+ * [0, L], where the line can fall below the wall base; the top never drops
1233
+ * under {@link MIN_WALL_END_HEIGHT} (or the flat height, if lower).
1234
+ */
1235
+ export function wallTopYAt(wallTop, x) {
1236
+ const { height, slope } = wallTop;
1237
+ if (slope === 0)
1238
+ return height;
1239
+ return Math.max(height + slope * x, Math.min(height, MIN_WALL_END_HEIGHT));
1240
+ }
1241
+ /**
1242
+ * Tilts a wall's top edge along its length so the `end` side sits taller (or
1243
+ * shorter) than the `start` side — e.g. a knee wall following a single-pitch
1244
+ * roof slope — instead of requiring a non-rectangular footprint. Only
1245
+ * vertices sitting exactly at the flat extruded top (`topY`) move.
1246
+ *
1247
+ * Evaluates the linear plane equation `slope * localX` continuously across
1248
+ * all top vertices (including mitered corner vertices extending beyond [0, L])
1249
+ * so the extruded top face remains a single coplanar surface without corner
1250
+ * creases or triangulation folds — except where {@link wallTopYAt} holds an
1251
+ * extended miter vertex above the base.
1252
+ */
1253
+ function applyWallEndHeightSlope(geometry, wallTop) {
1254
+ const { height: topY, slope } = wallTop;
1255
+ if (slope === 0)
1256
+ return;
1257
+ const position = geometry.getAttribute('position');
1258
+ for (let i = 0; i < position.count; i++) {
1259
+ if (Math.abs(position.getY(i) - topY) > 1e-4)
1260
+ continue;
1261
+ position.setY(i, wallTopYAt(wallTop, position.getX(i)));
1262
+ }
1263
+ position.needsUpdate = true;
1264
+ }
1211
1265
  export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0, baseElevation = slabElevation, baseSegments = [
1212
1266
  { start: 0, end: 1, elevation: baseElevation },
1213
1267
  ], storeyHeight = DEFAULT_LEVEL_HEIGHT, terrainBottomAt, faceBase, finishLayout = buildWallFinishLayout(wallNode, []), sceneNodes = useScene.getState().nodes) {
@@ -1217,6 +1271,7 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1217
1271
  const level = sceneNodes[levelId];
1218
1272
  const authoredStoreyHeight = level?.type === 'level' ? (level.height ?? null) : null;
1219
1273
  const topElevation = resolveWallTop(wallNode, storeyHeight, slabElevation, authoredStoreyHeight);
1274
+ const effectiveWallHeight = topElevation - slabElevation;
1220
1275
  const faceDatum = faceBase;
1221
1276
  if (faceBase)
1222
1277
  faceBase = {
@@ -1247,6 +1302,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1247
1302
  if (L < 1e-9) {
1248
1303
  return new THREE.BufferGeometry();
1249
1304
  }
1305
+ const wallTop = {
1306
+ height: effectiveWallHeight,
1307
+ slope: clampWallEndHeightOffset(wallNode.endHeightOffset, effectiveWallHeight) / L,
1308
+ };
1250
1309
  const boundaryPoints = getWallMiterBoundaryPoints(wallNode, miterData);
1251
1310
  const polyPoints = isCurvedWall(wallNode)
1252
1311
  ? getWallSurfacePolygon(wallNode, 24, insetCurvedWallBoundaryPointsFor3D(wallNode, boundaryPoints, miterData) ?? undefined)
@@ -1293,10 +1352,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1293
1352
  depth: height,
1294
1353
  bevelEnabled: false,
1295
1354
  });
1296
- // Rotate so extrusion direction (Z) becomes height direction (Y)
1297
1355
  geometry.rotateX(-Math.PI / 2);
1298
1356
  if (Math.abs(localBottom) > 1e-9)
1299
1357
  geometry.translate(0, localBottom, 0);
1358
+ applyWallEndHeightSlope(geometry, wallTop);
1300
1359
  geometry.computeVertexNormals();
1301
1360
  assignWallMaterialGroups(geometry, wallNode, boundaryEdges, finish);
1302
1361
  ensureRenderableGeometryAttributes(geometry);
@@ -1412,13 +1471,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1412
1471
  }
1413
1472
  const cutoutBrushes = [
1414
1473
  ...baseProfileCutouts,
1415
- ...collectCutoutBrushes(wallNode, childrenNodes, thickness, faceDatum, slabElevation, sceneNodes, baseSegments),
1474
+ ...collectCutoutBrushes(wallNode, childrenNodes, thickness, wallTop, faceDatum, slabElevation, sceneNodes, baseSegments),
1416
1475
  ];
1417
1476
  if (cutoutBrushes.length === 0) {
1418
- const splitGeometry = splitGeometryAtPlanes(geometry, finishPlanes.y, finishPlanes.x);
1419
- splitGeometry.computeVertexNormals();
1420
- assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges, finish);
1421
- ensureRenderableGeometryAttributes(splitGeometry);
1477
+ const splitGeometry = finishWallFaces(splitGeometryAtPlanes(geometry, finishPlanes.y, finishPlanes.x), wallNode, boundaryEdges, finish);
1422
1478
  return withWallFinishData(mergeWallTerrainFill(splitGeometry, belowBaseFills, wallNode, boundaryEdges, finish), finish);
1423
1479
  }
1424
1480
  // Create wall brush from geometry
@@ -1462,10 +1518,7 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1462
1518
  csgGeometry(brush).dispose();
1463
1519
  }
1464
1520
  const resultGeometry = csgGeometry(resultBrush);
1465
- const splitResultGeometry = splitGeometryAtPlanes(resultGeometry, finishPlanes.y, finishPlanes.x);
1466
- splitResultGeometry.computeVertexNormals();
1467
- assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges, finish);
1468
- ensureRenderableGeometryAttributes(splitResultGeometry);
1521
+ const splitResultGeometry = finishWallFaces(splitGeometryAtPlanes(resultGeometry, finishPlanes.y, finishPlanes.x), wallNode, boundaryEdges, finish);
1469
1522
  return withWallFinishData(mergeWallTerrainFill(splitResultGeometry, belowBaseFills, wallNode, boundaryEdges, finish), finish);
1470
1523
  }
1471
1524
  function withWallFinishData(geometry, finish) {
@@ -1514,7 +1567,7 @@ function authoredCutoutBrush(cutoutMesh, wallMatrixInverse, wallThickness, wallN
1514
1567
  computeGeometryBoundsTree(geometry);
1515
1568
  return new Brush(geometry);
1516
1569
  }
1517
- function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase, elevation = 0, sceneNodes = useScene.getState().nodes, baseSegments = [{ start: 0, end: 1, elevation }]) {
1570
+ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, wallTop, faceBase, elevation = 0, sceneNodes = useScene.getState().nodes, baseSegments = [{ start: 0, end: 1, elevation }]) {
1518
1571
  const brushes = [];
1519
1572
  const wallMesh = sceneRegistry.nodes.get(wallNode.id);
1520
1573
  if (!wallMesh)
@@ -1571,7 +1624,7 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1571
1624
  child.position[1] + cut.datum - elevation,
1572
1625
  child.position[2],
1573
1626
  ],
1574
- }, wallThickness, getWallBodyCenterOffset(wallNode), cut.bottom - elevation, child.type === 'window' ? cornerPair(sceneNodes, child)?.end : undefined));
1627
+ }, wallThickness, getWallBodyCenterOffset(wallNode), cut.bottom - elevation, child.type === 'window' ? cornerPair(sceneNodes, child)?.end : undefined, wallTop));
1575
1628
  continue;
1576
1629
  }
1577
1630
  if (cut.band.length < 3)
@@ -1582,8 +1635,9 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1582
1635
  return new THREE.Vector2(dx * Math.cos(angle) + dz * Math.sin(angle), dx * Math.sin(angle) - dz * Math.cos(angle));
1583
1636
  });
1584
1637
  const padding = getOpeningCutoutBottomPadding(child, cut.bottom - elevation);
1638
+ const top = Math.min(cut.top - elevation, Math.min(...points.map((point) => wallTopYAt(wallTop, point.x))));
1585
1639
  const geometry = new THREE.ExtrudeGeometry(new THREE.Shape(points), {
1586
- depth: cut.top - cut.bottom + padding,
1640
+ depth: top - (cut.bottom - elevation) + padding,
1587
1641
  bevelEnabled: false,
1588
1642
  });
1589
1643
  geometry.rotateX(-Math.PI / 2);
@@ -1668,16 +1722,23 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1668
1722
  * cut only takes this wall's own geometry, so running past it touches nothing else.
1669
1723
  */
1670
1724
  const CORNER_CUT_REACH = 6;
1671
- function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom, cornerEnd) {
1672
- const halfWidth = opening.width / 2;
1725
+ function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom, cornerEnd, wallTop) {
1726
+ const size = getDoorOpeningSize(opening);
1727
+ const centerX = opening.position[0] - size.leafOffsetX * openingHostFacing(opening);
1728
+ const halfWidth = size.width / 2;
1673
1729
  const bottom = cutBottom ?? opening.position[1] - opening.height / 2;
1674
1730
  const bottomPadding = getOpeningCutoutBottomPadding(opening, bottom);
1675
1731
  const reach = CORNER_CUT_REACH * wallThickness;
1732
+ const left = centerX - halfWidth;
1733
+ const right = centerX + halfWidth;
1734
+ const openingTop = opening.position[1] - opening.height / 2 + size.height;
1676
1735
  const geometry = buildOpeningCutoutGeometry(opening, {
1677
- left: opening.position[0] - halfWidth - (cornerEnd === 'start' ? reach : 0),
1678
- right: opening.position[0] + halfWidth + (cornerEnd === 'end' ? reach : 0),
1736
+ left: left - (cornerEnd === 'start' ? reach : 0),
1737
+ right: right + (cornerEnd === 'end' ? reach : 0),
1679
1738
  bottom: bottom - bottomPadding,
1680
- top: opening.position[1] + opening.height / 2,
1739
+ top: wallTop
1740
+ ? Math.min(openingTop, wallTopYAt(wallTop, wallTop.slope > 0 ? left : right))
1741
+ : openingTop,
1681
1742
  }, wallThickness * 2, wallThickness);
1682
1743
  if (centerOffset !== 0)
1683
1744
  geometry.translate(0, 0, centerOffset);
@@ -0,0 +1,15 @@
1
+ import * as THREE from 'three';
2
+ /**
3
+ * Splits every triangle whose edge carries another triangle's vertex, so the
4
+ * neighbours share that vertex instead of meeting at a T-junction. CSG cuts and
5
+ * finish-band splits leave T-junctions on wall faces: they look closed at full
6
+ * float precision, but crack open into dotted light leaks once a GLB bake
7
+ * quantizes positions.
8
+ *
9
+ * Each piece keeps its source triangle's material group, so the repair never
10
+ * reclassifies a face. Geometry without T-junctions is returned untouched;
11
+ * otherwise the result is non-indexed and position-only (callers recompute
12
+ * normals) and the input is disposed.
13
+ */
14
+ export declare function repairWallTJunctions(geometry: THREE.BufferGeometry): THREE.BufferGeometry;
15
+ //# sourceMappingURL=wall-t-junctions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wall-t-junctions.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-t-junctions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAW9B;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,CAiHzF"}