@pascal-app/viewer 1.0.7 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +209 -19
  2. package/dist/components/viewer/drone-controller.d.ts.map +1 -1
  3. package/dist/components/viewer/drone-controller.js +1 -5
  4. package/dist/components/viewer/first-person/build-collider-world.d.ts +14 -0
  5. package/dist/components/viewer/first-person/build-collider-world.d.ts.map +1 -0
  6. package/dist/components/viewer/first-person/build-collider-world.js +426 -0
  7. package/dist/components/viewer/first-person/ground-holes.d.ts +22 -0
  8. package/dist/components/viewer/first-person/ground-holes.d.ts.map +1 -0
  9. package/dist/components/viewer/first-person/ground-holes.js +116 -0
  10. package/dist/components/viewer/first-person/mechanism-targets.d.ts +27 -0
  11. package/dist/components/viewer/first-person/mechanism-targets.d.ts.map +1 -0
  12. package/dist/components/viewer/first-person/mechanism-targets.js +79 -0
  13. package/dist/components/viewer/first-person/terrain-collider.d.ts +32 -0
  14. package/dist/components/viewer/first-person/terrain-collider.d.ts.map +1 -0
  15. package/dist/components/viewer/first-person/terrain-collider.js +175 -0
  16. package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
  17. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
  18. package/dist/components/viewer/glb-replace-instances.js +5 -4
  19. package/dist/components/viewer/glb-scene.d.ts +3 -1
  20. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  21. package/dist/components/viewer/glb-scene.js +2 -1
  22. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  23. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  24. package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
  25. package/dist/components/viewer/index.d.ts.map +1 -1
  26. package/dist/components/viewer/index.js +8 -4
  27. package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
  28. package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
  30. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  31. package/dist/components/viewer/post-processing.js +22 -1
  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/unsupported-gpu-fallback.d.ts.map +1 -1
  35. package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
  36. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
  37. package/dist/components/viewer/use-daylight-rooms.js +75 -33
  38. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  39. package/dist/hooks/use-gltf-ktx2.js +2 -1
  40. package/dist/index.d.ts +8 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +8 -1
  43. package/dist/lib/asset-url.d.ts +0 -1
  44. package/dist/lib/asset-url.d.ts.map +1 -1
  45. package/dist/lib/asset-url.js +3 -3
  46. package/dist/lib/daylight/room-context.d.ts.map +1 -1
  47. package/dist/lib/daylight/room-context.js +3 -2
  48. package/dist/lib/daylight/room-map.d.ts +149 -0
  49. package/dist/lib/daylight/room-map.d.ts.map +1 -0
  50. package/dist/lib/daylight/room-map.js +813 -0
  51. package/dist/lib/daylight/room-scene.d.ts +4 -2
  52. package/dist/lib/daylight/room-scene.d.ts.map +1 -1
  53. package/dist/lib/daylight/room-scene.js +81 -40
  54. package/dist/lib/daylight/rooms.d.ts +71 -32
  55. package/dist/lib/daylight/rooms.d.ts.map +1 -1
  56. package/dist/lib/daylight/rooms.js +371 -286
  57. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  58. package/dist/lib/ktx2-loader.js +2 -1
  59. package/dist/lib/measurement-label.d.ts +4 -0
  60. package/dist/lib/measurement-label.d.ts.map +1 -0
  61. package/dist/lib/measurement-label.js +54 -0
  62. package/dist/lib/measurement-presentation.d.ts +7 -0
  63. package/dist/lib/measurement-presentation.d.ts.map +1 -0
  64. package/dist/lib/measurement-presentation.js +14 -0
  65. package/dist/lib/pointer-events.d.ts.map +1 -1
  66. package/dist/lib/pointer-events.js +3 -3
  67. package/dist/lib/texture-reference.d.ts.map +1 -1
  68. package/dist/lib/texture-reference.js +4 -3
  69. package/dist/lib/typing-target.d.ts +2 -0
  70. package/dist/lib/typing-target.d.ts.map +1 -0
  71. package/dist/lib/typing-target.js +7 -0
  72. package/dist/lib/viewer-config.d.ts +27 -0
  73. package/dist/lib/viewer-config.d.ts.map +1 -0
  74. package/dist/lib/viewer-config.js +23 -0
  75. package/dist/lib/viewer-preference-storage.d.ts +7 -0
  76. package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
  77. package/dist/lib/viewer-preference-storage.js +23 -0
  78. package/dist/sfx/audio-settings.d.ts +81 -0
  79. package/dist/sfx/audio-settings.d.ts.map +1 -0
  80. package/dist/sfx/audio-settings.js +166 -0
  81. package/dist/sfx/index.d.ts +6 -0
  82. package/dist/sfx/index.d.ts.map +1 -0
  83. package/dist/sfx/index.js +5 -0
  84. package/dist/sfx/interaction-sounds.d.ts +7 -0
  85. package/dist/sfx/interaction-sounds.d.ts.map +1 -0
  86. package/dist/sfx/interaction-sounds.js +39 -0
  87. package/dist/sfx/player.d.ts +31 -0
  88. package/dist/sfx/player.d.ts.map +1 -0
  89. package/dist/sfx/player.js +255 -0
  90. package/dist/sfx/sounds.d.ts +60 -0
  91. package/dist/sfx/sounds.d.ts.map +1 -0
  92. package/dist/sfx/sounds.js +25 -0
  93. package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
  94. package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
  95. package/dist/sfx/use-viewer-ui-sounds.js +42 -0
  96. package/dist/store/use-first-person-hud.d.ts +13 -0
  97. package/dist/store/use-first-person-hud.d.ts.map +1 -0
  98. package/dist/store/use-first-person-hud.js +21 -0
  99. package/dist/store/use-viewer.d.ts +2 -56
  100. package/dist/store/use-viewer.d.ts.map +1 -1
  101. package/dist/store/use-viewer.js +3 -1
  102. package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
  103. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  104. package/dist/systems/elevator/elevator-interaction-system.js +41 -8
  105. package/dist/systems/geometry/geometry-status.d.ts +8 -0
  106. package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
  107. package/dist/systems/geometry/geometry-status.js +24 -0
  108. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  109. package/dist/systems/roof/roof-system.js +134 -78
  110. package/dist/systems/wall/wall-system.d.ts +11 -0
  111. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  112. package/dist/systems/wall/wall-system.js +56 -10
  113. package/dist/ui/components/action-button.d.ts +14 -0
  114. package/dist/ui/components/action-button.d.ts.map +1 -0
  115. package/dist/ui/components/action-button.js +21 -0
  116. package/dist/ui/components/action-menu-placement.d.ts +7 -0
  117. package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
  118. package/dist/ui/components/action-menu-placement.js +10 -0
  119. package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
  120. package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
  121. package/dist/ui/components/floorplan-compass-button.js +22 -0
  122. package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
  123. package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
  124. package/dist/ui/components/follow-pascal-toggle.js +21 -0
  125. package/dist/ui/components/localized-html.d.ts +9 -0
  126. package/dist/ui/components/localized-html.d.ts.map +1 -0
  127. package/dist/ui/components/localized-html.js +16 -0
  128. package/dist/ui/components/render-settings-menu.d.ts +9 -0
  129. package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
  130. package/dist/ui/components/render-settings-menu.js +145 -0
  131. package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
  132. package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
  133. package/dist/ui/components/rooms/room-highlight.js +56 -0
  134. package/dist/ui/components/rooms/room-labels.d.ts +278 -0
  135. package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
  136. package/dist/ui/components/rooms/room-labels.js +1295 -0
  137. package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
  138. package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
  139. package/dist/ui/components/rooms/viewer-rooms.js +123 -0
  140. package/dist/ui/components/scene-loader.d.ts +18 -0
  141. package/dist/ui/components/scene-loader.d.ts.map +1 -0
  142. package/dist/ui/components/scene-loader.js +29 -0
  143. package/dist/ui/components/sky-control.d.ts +37 -0
  144. package/dist/ui/components/sky-control.d.ts.map +1 -0
  145. package/dist/ui/components/sky-control.js +382 -0
  146. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
  147. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
  148. package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
  149. package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
  150. package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
  151. package/dist/ui/components/viewer-controls-bar.js +130 -0
  152. package/dist/ui/components/viewer-hover-label.d.ts +14 -0
  153. package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
  154. package/dist/ui/components/viewer-hover-label.js +28 -0
  155. package/dist/ui/components/viewer-scene-header.d.ts +34 -0
  156. package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
  157. package/dist/ui/components/viewer-scene-header.js +92 -0
  158. package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
  159. package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
  160. package/dist/ui/components/viewer-selection-manager.js +116 -0
  161. package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
  162. package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
  163. package/dist/ui/components/viewer-stage-modes.js +42 -0
  164. package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
  165. package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
  166. package/dist/ui/components/viewer-stage-switcher.js +16 -0
  167. package/dist/ui/components/viewer-stage.d.ts +25 -0
  168. package/dist/ui/components/viewer-stage.d.ts.map +1 -0
  169. package/dist/ui/components/viewer-stage.js +107 -0
  170. package/dist/ui/components/walkthrough-hud.d.ts +29 -0
  171. package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
  172. package/dist/ui/components/walkthrough-hud.js +55 -0
  173. package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
  174. package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
  175. package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
  176. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
  177. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
  178. package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
  179. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
  180. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
  181. package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
  182. package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
  183. package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
  184. package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
  185. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
  186. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
  187. package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
  188. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
  189. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
  190. package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
  191. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
  192. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
  193. package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
  194. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
  195. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
  196. package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
  197. package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
  198. package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
  199. package/dist/ui/floorplan/floorplan-preview.js +700 -0
  200. package/dist/ui/hooks/use-mobile.d.ts +2 -0
  201. package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
  202. package/dist/ui/hooks/use-mobile.js +14 -0
  203. package/dist/ui/index.d.ts +49 -0
  204. package/dist/ui/index.d.ts.map +1 -0
  205. package/dist/ui/index.js +48 -0
  206. package/dist/ui/lib/below-grade.d.ts +38 -0
  207. package/dist/ui/lib/below-grade.d.ts.map +1 -0
  208. package/dist/ui/lib/below-grade.js +102 -0
  209. package/dist/ui/lib/camera-pose.d.ts +19 -0
  210. package/dist/ui/lib/camera-pose.d.ts.map +1 -0
  211. package/dist/ui/lib/camera-pose.js +113 -0
  212. package/dist/ui/lib/cn.d.ts +3 -0
  213. package/dist/ui/lib/cn.d.ts.map +1 -0
  214. package/dist/ui/lib/cn.js +5 -0
  215. package/dist/ui/lib/level-name.d.ts +10 -0
  216. package/dist/ui/lib/level-name.d.ts.map +1 -0
  217. package/dist/ui/lib/level-name.js +33 -0
  218. package/dist/ui/lib/node-display-label.d.ts +8 -0
  219. package/dist/ui/lib/node-display-label.d.ts.map +1 -0
  220. package/dist/ui/lib/node-display-label.js +36 -0
  221. package/dist/ui/lib/room-area.d.ts +3 -0
  222. package/dist/ui/lib/room-area.d.ts.map +1 -0
  223. package/dist/ui/lib/room-area.js +8 -0
  224. package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
  225. package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
  226. package/dist/ui/lib/room-assembly-overlay.js +222 -0
  227. package/dist/ui/lib/room-name-messages.d.ts +28 -0
  228. package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
  229. package/dist/ui/lib/room-name-messages.js +112 -0
  230. package/dist/ui/lib/room-selection.d.ts +58 -0
  231. package/dist/ui/lib/room-selection.d.ts.map +1 -0
  232. package/dist/ui/lib/room-selection.js +297 -0
  233. package/dist/ui/lib/scene-theme-name.d.ts +4 -0
  234. package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
  235. package/dist/ui/lib/scene-theme-name.js +22 -0
  236. package/dist/ui/lib/selection-modifiers.d.ts +20 -0
  237. package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
  238. package/dist/ui/lib/selection-modifiers.js +15 -0
  239. package/dist/ui/lib/viewer-selection.d.ts +132 -0
  240. package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
  241. package/dist/ui/lib/viewer-selection.js +462 -0
  242. package/dist/ui/lib/zone-content.d.ts +6 -0
  243. package/dist/ui/lib/zone-content.d.ts.map +1 -0
  244. package/dist/ui/lib/zone-content.js +96 -0
  245. package/dist/ui/primitives/button.d.ts +11 -0
  246. package/dist/ui/primitives/button.d.ts.map +1 -0
  247. package/dist/ui/primitives/button.js +35 -0
  248. package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
  249. package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
  250. package/dist/ui/primitives/dropdown-menu.js +53 -0
  251. package/dist/ui/primitives/popover.d.ts +8 -0
  252. package/dist/ui/primitives/popover.d.ts.map +1 -0
  253. package/dist/ui/primitives/popover.js +17 -0
  254. package/dist/ui/primitives/switch.d.ts +5 -0
  255. package/dist/ui/primitives/switch.d.ts.map +1 -0
  256. package/dist/ui/primitives/switch.js +8 -0
  257. package/dist/ui/primitives/tooltip.d.ts +8 -0
  258. package/dist/ui/primitives/tooltip.d.ts.map +1 -0
  259. package/dist/ui/primitives/tooltip.js +17 -0
  260. package/dist/ui/store/camera-pose-store.d.ts +17 -0
  261. package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
  262. package/dist/ui/store/camera-pose-store.js +15 -0
  263. package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
  264. package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
  265. package/dist/ui/store/navigation-sync-pose-store.js +16 -0
  266. package/dist/ui/store/use-follow-pascal.d.ts +30 -0
  267. package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
  268. package/dist/ui/store/use-follow-pascal.js +28 -0
  269. package/dist/ui/theme.css +339 -0
  270. package/package.json +34 -7
@@ -1,4 +1,4 @@
1
- import { buildWallFinishLayout, containsPoint, cornerPair, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getDoorOpeningSize, getEffectiveNode, getOpeningWallCut, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallLevelZones, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, isCutterName, openingHostFacing, 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';
@@ -1227,6 +1227,41 @@ function buildWallFinishContext(wall, layout, faceBase, slabElevation) {
1227
1227
  .sort((left, right) => left.start - right.start);
1228
1228
  return { layout, faceBase: { a: runs(faceBase.a), b: runs(faceBase.b) }, refs, foundation };
1229
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
+ }
1230
1265
  export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0, baseElevation = slabElevation, baseSegments = [
1231
1266
  { start: 0, end: 1, elevation: baseElevation },
1232
1267
  ], storeyHeight = DEFAULT_LEVEL_HEIGHT, terrainBottomAt, faceBase, finishLayout = buildWallFinishLayout(wallNode, []), sceneNodes = useScene.getState().nodes) {
@@ -1236,6 +1271,7 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1236
1271
  const level = sceneNodes[levelId];
1237
1272
  const authoredStoreyHeight = level?.type === 'level' ? (level.height ?? null) : null;
1238
1273
  const topElevation = resolveWallTop(wallNode, storeyHeight, slabElevation, authoredStoreyHeight);
1274
+ const effectiveWallHeight = topElevation - slabElevation;
1239
1275
  const faceDatum = faceBase;
1240
1276
  if (faceBase)
1241
1277
  faceBase = {
@@ -1266,6 +1302,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1266
1302
  if (L < 1e-9) {
1267
1303
  return new THREE.BufferGeometry();
1268
1304
  }
1305
+ const wallTop = {
1306
+ height: effectiveWallHeight,
1307
+ slope: clampWallEndHeightOffset(wallNode.endHeightOffset, effectiveWallHeight) / L,
1308
+ };
1269
1309
  const boundaryPoints = getWallMiterBoundaryPoints(wallNode, miterData);
1270
1310
  const polyPoints = isCurvedWall(wallNode)
1271
1311
  ? getWallSurfacePolygon(wallNode, 24, insetCurvedWallBoundaryPointsFor3D(wallNode, boundaryPoints, miterData) ?? undefined)
@@ -1312,10 +1352,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1312
1352
  depth: height,
1313
1353
  bevelEnabled: false,
1314
1354
  });
1315
- // Rotate so extrusion direction (Z) becomes height direction (Y)
1316
1355
  geometry.rotateX(-Math.PI / 2);
1317
1356
  if (Math.abs(localBottom) > 1e-9)
1318
1357
  geometry.translate(0, localBottom, 0);
1358
+ applyWallEndHeightSlope(geometry, wallTop);
1319
1359
  geometry.computeVertexNormals();
1320
1360
  assignWallMaterialGroups(geometry, wallNode, boundaryEdges, finish);
1321
1361
  ensureRenderableGeometryAttributes(geometry);
@@ -1431,7 +1471,7 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1431
1471
  }
1432
1472
  const cutoutBrushes = [
1433
1473
  ...baseProfileCutouts,
1434
- ...collectCutoutBrushes(wallNode, childrenNodes, thickness, faceDatum, slabElevation, sceneNodes, baseSegments),
1474
+ ...collectCutoutBrushes(wallNode, childrenNodes, thickness, wallTop, faceDatum, slabElevation, sceneNodes, baseSegments),
1435
1475
  ];
1436
1476
  if (cutoutBrushes.length === 0) {
1437
1477
  const splitGeometry = finishWallFaces(splitGeometryAtPlanes(geometry, finishPlanes.y, finishPlanes.x), wallNode, boundaryEdges, finish);
@@ -1527,7 +1567,7 @@ function authoredCutoutBrush(cutoutMesh, wallMatrixInverse, wallThickness, wallN
1527
1567
  computeGeometryBoundsTree(geometry);
1528
1568
  return new Brush(geometry);
1529
1569
  }
1530
- 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 }]) {
1531
1571
  const brushes = [];
1532
1572
  const wallMesh = sceneRegistry.nodes.get(wallNode.id);
1533
1573
  if (!wallMesh)
@@ -1584,7 +1624,7 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1584
1624
  child.position[1] + cut.datum - elevation,
1585
1625
  child.position[2],
1586
1626
  ],
1587
- }, 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));
1588
1628
  continue;
1589
1629
  }
1590
1630
  if (cut.band.length < 3)
@@ -1595,8 +1635,9 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1595
1635
  return new THREE.Vector2(dx * Math.cos(angle) + dz * Math.sin(angle), dx * Math.sin(angle) - dz * Math.cos(angle));
1596
1636
  });
1597
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))));
1598
1639
  const geometry = new THREE.ExtrudeGeometry(new THREE.Shape(points), {
1599
- depth: cut.top - cut.bottom + padding,
1640
+ depth: top - (cut.bottom - elevation) + padding,
1600
1641
  bevelEnabled: false,
1601
1642
  });
1602
1643
  geometry.rotateX(-Math.PI / 2);
@@ -1681,18 +1722,23 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1681
1722
  * cut only takes this wall's own geometry, so running past it touches nothing else.
1682
1723
  */
1683
1724
  const CORNER_CUT_REACH = 6;
1684
- function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom, cornerEnd) {
1725
+ function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom, cornerEnd, wallTop) {
1685
1726
  const size = getDoorOpeningSize(opening);
1686
1727
  const centerX = opening.position[0] - size.leafOffsetX * openingHostFacing(opening);
1687
1728
  const halfWidth = size.width / 2;
1688
1729
  const bottom = cutBottom ?? opening.position[1] - opening.height / 2;
1689
1730
  const bottomPadding = getOpeningCutoutBottomPadding(opening, bottom);
1690
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;
1691
1735
  const geometry = buildOpeningCutoutGeometry(opening, {
1692
- left: centerX - halfWidth - (cornerEnd === 'start' ? reach : 0),
1693
- right: centerX + halfWidth + (cornerEnd === 'end' ? reach : 0),
1736
+ left: left - (cornerEnd === 'start' ? reach : 0),
1737
+ right: right + (cornerEnd === 'end' ? reach : 0),
1694
1738
  bottom: bottom - bottomPadding,
1695
- top: opening.position[1] - opening.height / 2 + size.height,
1739
+ top: wallTop
1740
+ ? Math.min(openingTop, wallTopYAt(wallTop, wallTop.slope > 0 ? left : right))
1741
+ : openingTop,
1696
1742
  }, wallThickness * 2, wallThickness);
1697
1743
  if (centerOffset !== 0)
1698
1744
  geometry.translate(0, 0, centerOffset);
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ import { Button } from '../primitives/button';
3
+ interface ActionButtonProps extends React.ComponentProps<typeof Button> {
4
+ label: string;
5
+ shortcut?: string;
6
+ isActive?: boolean;
7
+ tooltipContent?: React.ReactNode;
8
+ tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
9
+ /** The tooltip portals to `<body>`, so a button in a themed scope passes the scope here. */
10
+ tooltipClassName?: string;
11
+ }
12
+ export declare const ActionButton: React.ForwardRefExoticComponent<Omit<ActionButtonProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
13
+ export {};
14
+ //# sourceMappingURL=action-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-button.d.ts","sourceRoot":"","sources":["../../../src/ui/components/action-button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAI7C,UAAU,iBAAkB,SAAQ,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAChC,WAAW,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;IACjD,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,eAAO,MAAM,YAAY,0GAkExB,CAAA"}
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { playSound } from '../../sfx/player';
4
+ import { cn } from '../lib/cn';
5
+ import { Button } from '../primitives/button';
6
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip';
7
+ import { useActionMenuPopupSide } from './action-menu-placement';
8
+ export const ActionButton = React.forwardRef(({ className, children, label, shortcut, isActive, tooltipContent, tooltipSide, tooltipClassName, onClick, onMouseEnter, ...props }, ref) => {
9
+ const popupSide = useActionMenuPopupSide();
10
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button
11
+ // These are toggles, not commands: the armed/unarmed state has to be
12
+ // readable without decoding the active-state styling.
13
+ , { "aria-pressed": isActive, "aria-label": label, className: cn('relative h-11 w-11 transition-all', className), onClick: (event) => {
14
+ playSound('menuClick');
15
+ onClick?.(event);
16
+ }, onMouseEnter: (event) => {
17
+ playSound('menuHover');
18
+ onMouseEnter?.(event);
19
+ }, ref: ref, ...props, children: [_jsx("div", { className: cn('flex h-full w-full items-center justify-center transition-transform', shortcut && '-translate-x-0.5 -translate-y-0.5'), children: children }), shortcut && (_jsx("div", { className: "absolute right-1 bottom-1 rounded border border-border/40 bg-background/40 px-1 py-[2px] backdrop-blur-md", children: _jsx("span", { className: "block font-medium font-mono text-[9px] text-muted-foreground/70 leading-none", children: shortcut }) }))] }) }), _jsx(TooltipContent, { className: tooltipClassName, side: tooltipSide ?? popupSide, children: tooltipContent || (_jsxs("p", { children: [label, " ", shortcut && `(${shortcut})`] })) })] }));
20
+ });
21
+ ActionButton.displayName = 'ActionButton';
@@ -0,0 +1,7 @@
1
+ /** Viewport edge the action menu docks to. */
2
+ export type ActionMenuPlacement = 'bottom' | 'top';
3
+ export declare const ActionMenuPlacementProvider: import("react").Provider<ActionMenuPlacement>;
4
+ export declare function useActionMenuPlacement(): ActionMenuPlacement;
5
+ /** Side tooltips and popovers open toward, away from the docked edge. */
6
+ export declare function useActionMenuPopupSide(): 'top' | 'bottom';
7
+ //# sourceMappingURL=action-menu-placement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-menu-placement.d.ts","sourceRoot":"","sources":["../../../src/ui/components/action-menu-placement.ts"],"names":[],"mappings":"AAEA,8CAA8C;AAC9C,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,KAAK,CAAA;AAIlD,eAAO,MAAM,2BAA2B,+CAAsC,CAAA;AAE9E,wBAAgB,sBAAsB,IAAI,mBAAmB,CAE5D;AAED,yEAAyE;AACzE,wBAAgB,sBAAsB,IAAI,KAAK,GAAG,QAAQ,CAEzD"}
@@ -0,0 +1,10 @@
1
+ import { createContext, useContext } from 'react';
2
+ const ActionMenuPlacementContext = createContext('bottom');
3
+ export const ActionMenuPlacementProvider = ActionMenuPlacementContext.Provider;
4
+ export function useActionMenuPlacement() {
5
+ return useContext(ActionMenuPlacementContext);
6
+ }
7
+ /** Side tooltips and popovers open toward, away from the docked edge. */
8
+ export function useActionMenuPopupSide() {
9
+ return useActionMenuPlacement() === 'top' ? 'bottom' : 'top';
10
+ }
@@ -0,0 +1,10 @@
1
+ import type React from 'react';
2
+ export type FloorplanCompassButtonProps = {
3
+ northRotationDeg: number;
4
+ onAlignNorth: () => void;
5
+ needleRef?: React.RefObject<SVGSVGElement | null>;
6
+ /** Horizontal anchor; defaults to the bottom-left corner. */
7
+ style?: React.CSSProperties;
8
+ };
9
+ export declare function FloorplanCompassButton({ northRotationDeg, onAlignNorth, needleRef, style, }: FloorplanCompassButtonProps): React.JSX.Element;
10
+ //# sourceMappingURL=floorplan-compass-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floorplan-compass-button.d.ts","sourceRoot":"","sources":["../../../src/ui/components/floorplan-compass-button.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,MAAM,MAAM,2BAA2B,GAAG;IACxC,gBAAgB,EAAE,MAAM,CAAA;IACxB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IACjD,6DAA6D;IAC7D,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;CAC5B,CAAA;AAED,wBAAgB,sBAAsB,CAAC,EACrC,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,KAAK,GACN,EAAE,2BAA2B,qBA6C7B"}
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { usePascalI18n } from '@pascal-app/core';
4
+ import { cn } from '../lib/cn';
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip';
6
+ import { SKY_COMPASS_BOTTOM, useSkyPanel } from './sky-control';
7
+ export function FloorplanCompassButton({ northRotationDeg, onAlignNorth, needleRef, style, }) {
8
+ const i18n = usePascalI18n();
9
+ // Beside the sky chip (not when moved to the plan's right corner), centre on the docks' row.
10
+ const besideSky = useSkyPanel((s) => s.hosts > 0) && style?.right === undefined;
11
+ const label = i18n._({
12
+ id: 'viewer.floorplan.alignNorth',
13
+ message: 'Align view to north',
14
+ });
15
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { "aria-label": label, className: cn('group pointer-events-auto absolute bottom-3 z-30 flex h-8 w-8 items-center justify-center rounded-full border border-black/10 bg-white/85 shadow-sm backdrop-blur-md transition hover:bg-white hover:shadow-md dark:border-white/10 dark:bg-neutral-900/85 dark:hover:bg-neutral-900', !style && 'left-3'), style: besideSky ? { bottom: SKY_COMPASS_BOTTOM, ...style } : style, onClick: (event) => {
16
+ event.preventDefault();
17
+ event.stopPropagation();
18
+ onAlignNorth();
19
+ }, onPointerDown: (event) => {
20
+ event.stopPropagation();
21
+ }, type: "button", children: _jsx("span", { className: "relative flex h-6 w-6 items-center justify-center rounded-full bg-[#b8b8b8] shadow-inner dark:bg-neutral-700", children: _jsxs("svg", { "aria-hidden": "true", className: "h-6 w-6", ref: needleRef, style: { transform: `rotate(${northRotationDeg}deg)` }, viewBox: "0 0 48 48", children: [_jsx("path", { d: "M24 4.5 31.5 25 24 21.5 16.5 25Z", fill: "#f15b5b" }), _jsx("path", { d: "M24 43.5 16.5 23 24 26.5 31.5 23Z", fill: "#ffffff" })] }) }) }) }), _jsx(TooltipContent, { side: "right", children: label })] }));
22
+ }
@@ -0,0 +1,6 @@
1
+ /** The toolbar's switch for Follow Pascal: the camera follows what the agent builds. */
2
+ export declare function FollowPascalToggle({ className, tooltipClassName, }: {
3
+ className?: string;
4
+ tooltipClassName?: string;
5
+ }): import("react").JSX.Element;
6
+ //# sourceMappingURL=follow-pascal-toggle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follow-pascal-toggle.d.ts","sourceRoot":"","sources":["../../../src/ui/components/follow-pascal-toggle.tsx"],"names":[],"mappings":"AAQA,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,gBAAgB,GACjB,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,+BA6BA"}
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { usePascalI18n } from '@pascal-app/core';
4
+ import { Video } from 'lucide-react';
5
+ import { cn } from '../lib/cn';
6
+ import { useFollowPascal } from '../store/use-follow-pascal';
7
+ import { ActionButton } from './action-button';
8
+ /** The toolbar's switch for Follow Pascal: the camera follows what the agent builds. */
9
+ export function FollowPascalToggle({ className, tooltipClassName, }) {
10
+ const i18n = usePascalI18n();
11
+ const on = useFollowPascal((state) => state.on);
12
+ const setOn = useFollowPascal((state) => state.setOn);
13
+ return (_jsx(ActionButton, { "aria-pressed": on, className: cn(on
14
+ ? 'bg-violet-500/20 text-violet-600 dark:text-violet-300 dark:hover:text-accent-foreground'
15
+ : 'hover:bg-black/4 hover:text-violet-600 dark:hover:text-violet-300', className), label: on
16
+ ? i18n._({ id: 'editor.actionMenu.followPascalToggle.on', message: 'Follow Pascal: On' })
17
+ : i18n._({
18
+ id: 'editor.actionMenu.followPascalToggle.off',
19
+ message: 'Follow Pascal: Off',
20
+ }), onClick: () => setOn(!on), size: "icon", tooltipClassName: tooltipClassName, variant: "ghost", children: _jsx(Video, { className: "h-6 w-6" }) }));
21
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * drei's `Html`, keeping the surrounding i18n scope. `Html` renders its
3
+ * children in a separate React root, which sees none of the surrounding
4
+ * context, so a component inside it would translate and format with the
5
+ * default (English) scope instead of the host's (the editor's `?lang=` or
6
+ * `<Editor locale>` one).
7
+ */
8
+ export declare const LocalizedHtml: import("react").ForwardRefExoticComponent<Omit<import("@react-three/drei/web/Html").HtmlProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
9
+ //# sourceMappingURL=localized-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localized-html.d.ts","sourceRoot":"","sources":["../../../src/ui/components/localized-html.tsx"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,wMAUxB,CAAA"}
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { PascalI18nProvider, usePascalI18n } from '@pascal-app/core';
4
+ import { Html as DreiHtml } from '@react-three/drei';
5
+ import { forwardRef } from 'react';
6
+ /**
7
+ * drei's `Html`, keeping the surrounding i18n scope. `Html` renders its
8
+ * children in a separate React root, which sees none of the surrounding
9
+ * context, so a component inside it would translate and format with the
10
+ * default (English) scope instead of the host's (the editor's `?lang=` or
11
+ * `<Editor locale>` one).
12
+ */
13
+ export const LocalizedHtml = forwardRef(function LocalizedHtml({ children, ...props }, ref) {
14
+ const i18n = usePascalI18n();
15
+ return (_jsx(DreiHtml, { ...props, ref: ref, children: _jsx(PascalI18nProvider, { i18n: i18n, children: children }) }));
16
+ });
@@ -0,0 +1,9 @@
1
+ export declare function QualityMenu({ subContentClassName, }: {
2
+ subContentClassName?: string;
3
+ }): import("react").JSX.Element;
4
+ export declare function DaylightMenu({ subContentClassName, }: {
5
+ subContentClassName?: string;
6
+ }): import("react").JSX.Element;
7
+ /** Ink edges on (soft) or off. The menu stays open on toggle. */
8
+ export declare function EdgesToggleItem(): import("react").JSX.Element;
9
+ //# sourceMappingURL=render-settings-menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-settings-menu.d.ts","sourceRoot":"","sources":["../../../src/ui/components/render-settings-menu.tsx"],"names":[],"mappings":"AAuGA,wBAAgB,WAAW,CAAC,EAC1B,mBAAqC,GACtC,EAAE;IACD,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,+BA6CA;AAED,wBAAgB,YAAY,CAAC,EAC3B,mBAAqC,GACtC,EAAE;IACD,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,+BA6GA;AAED,iEAAiE;AACjE,wBAAgB,eAAe,gCAmB9B"}
@@ -0,0 +1,145 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { usePascalI18n, useScene } from '@pascal-app/core';
4
+ import { ArrowDownLeft, Check, Cloud, CloudSun, Gauge, LocateFixed, Moon, PenLine, Sun, Sunrise, Sunset, } from 'lucide-react';
5
+ import { useState } from 'react';
6
+ import { requestDeviceLocation, resolveLocation } from '../../lib/daylight/location';
7
+ import useViewer, {} from '../../store/use-viewer';
8
+ import { DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from '../primitives/dropdown-menu';
9
+ import { openSkyPanel, useSkyPanel, weatherIcon } from './sky-control';
10
+ /**
11
+ * Render settings shared by every display dropdown:
12
+ * - `QualityMenu` (both appearances): Low / Medium / High.
13
+ * - `DaylightMenu` (colored): time-of-day and weather presets, plus "Use my location" when the
14
+ * site has no geocoded point — or, while the sky chip is in the viewport, a row opening it.
15
+ * - `EdgesToggleItem` (monochrome): ink edges on or off.
16
+ */
17
+ function timeOptions(i18n) {
18
+ return [
19
+ {
20
+ id: 'golden',
21
+ icon: Sunrise,
22
+ name: i18n._({ id: 'viewer.daylight.time.golden', message: 'Golden hour' }),
23
+ },
24
+ {
25
+ id: 'midday',
26
+ icon: Sun,
27
+ name: i18n._({ id: 'viewer.daylight.time.midday', message: 'Midday' }),
28
+ },
29
+ {
30
+ id: 'blue',
31
+ icon: Sunset,
32
+ name: i18n._({ id: 'viewer.daylight.time.blue', message: 'Blue hour' }),
33
+ },
34
+ {
35
+ id: 'night',
36
+ icon: Moon,
37
+ name: i18n._({ id: 'viewer.daylight.time.night', message: 'Night' }),
38
+ },
39
+ ];
40
+ }
41
+ function weatherOptions(i18n) {
42
+ return [
43
+ { id: 'clear', name: i18n._({ id: 'viewer.daylight.weather.clear', message: 'Clear' }) },
44
+ { id: 'fair', name: i18n._({ id: 'viewer.daylight.weather.fair', message: 'Fair' }) },
45
+ { id: 'cloudy', name: i18n._({ id: 'viewer.daylight.weather.cloudy', message: 'Cloudy' }) },
46
+ {
47
+ id: 'overcast',
48
+ name: i18n._({ id: 'viewer.daylight.weather.overcast', message: 'Overcast' }),
49
+ },
50
+ ];
51
+ }
52
+ function qualityOptions(i18n) {
53
+ return [
54
+ {
55
+ id: 'low',
56
+ name: i18n._({ id: 'viewer.daylight.quality.low', message: 'Low' }),
57
+ detail: i18n._({
58
+ id: 'viewer.daylight.quality.lowDetail',
59
+ message: 'Fastest — no ambient occlusion or bloom',
60
+ }),
61
+ },
62
+ {
63
+ id: 'medium',
64
+ name: i18n._({ id: 'viewer.daylight.quality.medium', message: 'Medium' }),
65
+ detail: i18n._({
66
+ id: 'viewer.daylight.quality.mediumDetail',
67
+ message: 'Half-resolution ambient occlusion',
68
+ }),
69
+ },
70
+ {
71
+ id: 'high',
72
+ name: i18n._({ id: 'viewer.daylight.quality.high', message: 'High' }),
73
+ detail: i18n._({
74
+ id: 'viewer.daylight.quality.highDetail',
75
+ message: 'Lighting-aware ambient occlusion and bloom',
76
+ }),
77
+ },
78
+ ];
79
+ }
80
+ export function QualityMenu({ subContentClassName = 'dark min-w-56', }) {
81
+ const i18n = usePascalI18n();
82
+ const quality = useViewer((s) => s.renderQuality);
83
+ const choice = useViewer((s) => s.renderQualityChoice);
84
+ const qualities = qualityOptions(i18n);
85
+ const active = qualities.find((o) => o.id === quality) ?? qualities[2];
86
+ const autoName = i18n._({ id: 'viewer.daylight.quality.auto', message: 'Auto' });
87
+ return (_jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { children: [_jsx(Gauge, { className: "h-4 w-4" }), _jsx("span", { children: i18n._({ id: 'viewer.daylight.quality.title', message: 'Quality' }) }), _jsx("span", { className: "ml-auto text-muted-foreground text-xs", children: choice === 'auto' ? `${autoName} (${active.name})` : active.name })] }), _jsxs(DropdownMenuSubContent, { className: subContentClassName, children: [_jsxs(DropdownMenuItem, { onSelect: () => useViewer.getState().setRenderQuality('auto'), children: [_jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "text-foreground", children: autoName }), _jsx("span", { className: "text-muted-foreground text-xs", children: i18n._({
88
+ id: 'viewer.daylight.quality.autoDetail',
89
+ message: 'Picked for this device: {tier}',
90
+ values: { tier: active.name },
91
+ }) })] }), choice === 'auto' ? _jsx(Check, { className: "ml-auto h-4 w-4 text-foreground" }) : null] }), qualities.map((option) => (_jsxs(DropdownMenuItem, { onSelect: () => useViewer.getState().setRenderQuality(option.id), children: [_jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "text-foreground", children: option.name }), _jsx("span", { className: "text-muted-foreground text-xs", children: option.detail })] }), choice === option.id ? _jsx(Check, { className: "ml-auto h-4 w-4 text-foreground" }) : null] }, option.id)))] })] }));
92
+ }
93
+ export function DaylightMenu({ subContentClassName = 'dark min-w-48', }) {
94
+ const i18n = usePascalI18n();
95
+ const timeOfDay = useViewer((s) => s.timeOfDay);
96
+ const weather = useViewer((s) => s.weather);
97
+ const deviceLocation = useViewer((s) => s.deviceLocation);
98
+ const siteHasLocation = useScene((state) => {
99
+ for (const id of state.rootNodeIds) {
100
+ const node = state.nodes[id];
101
+ if (node?.type === 'site')
102
+ return Boolean(node.parcel?.originLngLat || node.dossier?.point);
103
+ }
104
+ return false;
105
+ });
106
+ const [locating, setLocating] = useState(false);
107
+ // The sky chip beside the compass carries time and weather wherever it is mounted.
108
+ const skyChip = useSkyPanel((s) => s.hosts > 0);
109
+ const times = timeOptions(i18n);
110
+ const weathers = weatherOptions(i18n);
111
+ const activeTime = times.find((o) => o.id === timeOfDay) ?? times[1];
112
+ const activeWeather = weathers.find((o) => o.id === weather) ?? weathers[1];
113
+ const source = siteHasLocation ? 'site' : resolveLocation(null, deviceLocation).source;
114
+ if (skyChip) {
115
+ const WeatherIcon = weatherIcon(weather);
116
+ return (_jsxs(DropdownMenuItem, { onSelect: openSkyPanel, children: [_jsx(WeatherIcon, { className: "h-4 w-4" }), _jsx("span", { children: i18n._({ id: 'viewer.daylight.sky.menuItem', message: 'Time & weather' }) }), _jsxs("span", { className: "ml-auto flex items-center gap-1 text-violet-600 text-xs dark:text-violet-300", children: [i18n._({ id: 'viewer.daylight.sky.inViewport', message: 'In viewport' }), _jsx(ArrowDownLeft, { className: "h-3.5 w-3.5" })] })] }));
117
+ }
118
+ return (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { children: [_jsx(activeTime.icon, { className: "h-4 w-4" }), _jsx("span", { children: i18n._({ id: 'viewer.daylight.time.title', message: 'Time of day' }) }), _jsx("span", { className: "ml-auto text-muted-foreground text-xs", children: activeTime.name })] }), _jsxs(DropdownMenuSubContent, { className: subContentClassName, children: [times.map((option) => (_jsxs(DropdownMenuItem, { onSelect: () => useViewer.getState().setTimeOfDay(option.id), children: [_jsx(option.icon, { className: "h-4 w-4" }), _jsx("span", { children: option.name }), timeOfDay === option.id ? _jsx(Check, { className: "ml-auto h-4 w-4" }) : null] }, option.id))), source !== 'site' ? (_jsxs(DropdownMenuItem, { disabled: locating, onSelect: (event) => {
119
+ event.preventDefault();
120
+ setLocating(true);
121
+ requestDeviceLocation()
122
+ .then((location) => useViewer.getState().setDeviceLocation(location))
123
+ .catch(() => { })
124
+ .finally(() => setLocating(false));
125
+ }, children: [_jsx(LocateFixed, { className: "h-4 w-4" }), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { children: i18n._({ id: 'viewer.daylight.location.use', message: 'Use my location' }) }), _jsx("span", { className: "text-muted-foreground text-xs", children: source === 'device'
126
+ ? i18n._({
127
+ id: 'viewer.daylight.location.device',
128
+ message: 'Sun follows your location',
129
+ })
130
+ : i18n._({
131
+ id: 'viewer.daylight.location.approximate',
132
+ message: 'Sun position is approximate',
133
+ }) })] })] })) : null] })] }), _jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { children: [weather === 'clear' ? _jsx(CloudSun, { className: "h-4 w-4" }) : _jsx(Cloud, { className: "h-4 w-4" }), _jsx("span", { children: i18n._({ id: 'viewer.daylight.weather.title', message: 'Weather' }) }), _jsx("span", { className: "ml-auto text-muted-foreground text-xs", children: activeWeather.name })] }), _jsx(DropdownMenuSubContent, { className: subContentClassName, children: weathers.map((option) => (_jsxs(DropdownMenuItem, { onSelect: () => useViewer.getState().setWeather(option.id), children: [_jsx("span", { children: option.name }), weather === option.id ? _jsx(Check, { className: "ml-auto h-4 w-4" }) : null] }, option.id))) })] })] }));
134
+ }
135
+ /** Ink edges on (soft) or off. The menu stays open on toggle. */
136
+ export function EdgesToggleItem() {
137
+ const i18n = usePascalI18n();
138
+ const on = useViewer((s) => s.edges !== 'off');
139
+ return (_jsxs(DropdownMenuItem, { onSelect: (event) => {
140
+ event.preventDefault();
141
+ useViewer.getState().setEdges(on ? 'off' : 'soft');
142
+ }, children: [_jsx(PenLine, { className: "h-4 w-4" }), _jsx("span", { children: i18n._({ id: 'viewer.display.edges.title', message: 'Edges' }) }), _jsx("span", { className: "ml-auto text-muted-foreground text-xs", children: on
143
+ ? i18n._({ id: 'viewer.common.on', message: 'On' })
144
+ : i18n._({ id: 'viewer.common.off', message: 'Off' }) })] }));
145
+ }
@@ -0,0 +1,10 @@
1
+ import { type Object3D } from 'three';
2
+ import { type RoomAssemblyHeights } from '../../lib/room-assembly-overlay';
3
+ import type { RoomSelectionRecord } from '../../lib/room-selection';
4
+ /** A room's floor, wall faces and outline in the highlight colour, riding its level's object. */
5
+ export declare function RoomAssemblyMesh({ room, heights, levelObject, }: {
6
+ room: RoomSelectionRecord;
7
+ heights: RoomAssemblyHeights;
8
+ levelObject: () => Object3D | undefined;
9
+ }): import("react").JSX.Element;
10
+ //# sourceMappingURL=room-highlight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-highlight.d.ts","sourceRoot":"","sources":["../../../../src/ui/components/rooms/room-highlight.tsx"],"names":[],"mappings":"AAIA,OAAO,EAKL,KAAK,QAAQ,EACd,MAAM,OAAO,CAAA;AAGd,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAC3F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AA2BnE,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,OAAO,EACP,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,mBAAmB,CAAA;IACzB,OAAO,EAAE,mBAAmB,CAAA;IAC5B,WAAW,EAAE,MAAM,QAAQ,GAAG,SAAS,CAAA;CACxC,+BAqDA"}
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useFrame } from '@react-three/fiber';
4
+ import { useEffect, useMemo, useRef } from 'react';
5
+ import { BufferGeometry, DoubleSide, Float32BufferAttribute, } from 'three';
6
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu';
7
+ import { OVERLAY_LAYER } from '../../../lib/layers';
8
+ import { getRoomAssembly } from '../../lib/room-assembly-overlay';
9
+ const noRaycast = () => { };
10
+ const outlineMaterial = new LineBasicNodeMaterial({
11
+ color: '#818cf8',
12
+ depthTest: false,
13
+ depthWrite: false,
14
+ });
15
+ const fillMaterial = (opacity) => new MeshBasicNodeMaterial({
16
+ color: '#818cf8',
17
+ depthTest: false,
18
+ depthWrite: false,
19
+ opacity,
20
+ side: DoubleSide,
21
+ transparent: true,
22
+ });
23
+ const floorMaterial = fillMaterial(0.18);
24
+ // Walls and the ceiling stack over the floor when seen from above.
25
+ const surfaceMaterial = fillMaterial(0.1);
26
+ function positions(array) {
27
+ const geometry = new BufferGeometry();
28
+ geometry.setAttribute('position', new Float32BufferAttribute(array, 3));
29
+ return geometry;
30
+ }
31
+ /** A room's floor, wall faces and outline in the highlight colour, riding its level's object. */
32
+ export function RoomAssemblyMesh({ room, heights, levelObject, }) {
33
+ const ref = useRef(null);
34
+ const assembly = useMemo(() => getRoomAssembly(room.geometry, heights), [room, heights]);
35
+ const geometry = useMemo(() => ({
36
+ floor: positions(assembly.floor),
37
+ surfaces: positions(assembly.surfaces),
38
+ outline: positions(assembly.outline),
39
+ }), [assembly]);
40
+ useEffect(() => () => {
41
+ geometry.floor.dispose();
42
+ geometry.surfaces.dispose();
43
+ geometry.outline.dispose();
44
+ }, [geometry]);
45
+ useFrame(() => {
46
+ const level = levelObject();
47
+ if (!ref.current)
48
+ return;
49
+ ref.current.visible = !!level;
50
+ if (level) {
51
+ level.updateWorldMatrix(true, false);
52
+ ref.current.matrix.copy(level.matrixWorld);
53
+ }
54
+ });
55
+ return (_jsxs("group", { matrixAutoUpdate: false, ref: ref, children: [_jsx("mesh", { geometry: geometry.floor, layers: OVERLAY_LAYER, material: floorMaterial, raycast: noRaycast, renderOrder: 100 }), _jsx("mesh", { geometry: geometry.surfaces, layers: OVERLAY_LAYER, material: surfaceMaterial, raycast: noRaycast, renderOrder: 100 }), _jsx("lineSegments", { geometry: geometry.outline, layers: OVERLAY_LAYER, material: outlineMaterial, raycast: noRaycast, renderOrder: 101 })] }));
56
+ }