@pascal-app/editor 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 (382) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +110 -42
  6. package/src/components/editor/duplicate-node.ts +2 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +77 -68
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +31 -11
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +2 -2
  15. package/src/components/editor/floorplan-panel.test.ts +220 -0
  16. package/src/components/editor/floorplan-panel.tsx +458 -140
  17. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  18. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  19. package/src/components/editor/footprint-height-handle.tsx +2 -2
  20. package/src/components/editor/grid.tsx +24 -15
  21. package/src/components/editor/group-actions.ts +19 -1
  22. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  23. package/src/components/editor/group-move-3d.ts +27 -2
  24. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  25. package/src/components/editor/group-transform-shared.ts +23 -1
  26. package/src/components/editor/index.tsx +150 -20
  27. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  28. package/src/components/editor/localized-html.tsx +22 -0
  29. package/src/components/editor/measurement-pill.tsx +1 -1
  30. package/src/components/editor/node-action-menu.tsx +46 -12
  31. package/src/components/editor/node-arrow-handles.tsx +2 -3
  32. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  33. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  34. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  35. package/src/components/editor/quick-measurement-card.tsx +1 -1
  36. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  37. package/src/components/editor/registry-action-contributions.tsx +115 -15
  38. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  39. package/src/components/editor/room-controls.tsx +4 -3
  40. package/src/components/editor/room-handles-2d.tsx +158 -7
  41. package/src/components/editor/room-handles.tsx +244 -54
  42. package/src/components/editor/room-highlight.tsx +2 -0
  43. package/src/components/editor/room-labels.tsx +33 -17
  44. package/src/components/editor/selection-manager.tsx +15 -102
  45. package/src/components/editor/separator-end-grip.tsx +74 -0
  46. package/src/components/editor/site-edge-labels.tsx +7 -3
  47. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  48. package/src/components/editor/thumbnail-generator.tsx +26 -1
  49. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  50. package/src/components/editor/views/view-bar.tsx +111 -26
  51. package/src/components/editor/wall-measurement-label.tsx +31 -15
  52. package/src/components/editor/wall-move-side-handles.tsx +15 -7
  53. package/src/components/editor/wall-pull-grip.tsx +193 -0
  54. package/src/components/editor/wall-region-handles.tsx +2 -2
  55. package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
  56. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  57. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  58. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  59. package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
  60. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  61. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
  62. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
  63. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  64. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
  65. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  66. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  67. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  68. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
  69. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  70. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
  72. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
  73. package/src/components/procedural-items/inspector.tsx +59 -12
  74. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  75. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  76. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  77. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  78. package/src/components/systems/roof/roof-edit-system.tsx +30 -19
  79. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  80. package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
  81. package/src/components/tools/item/move-tool.tsx +1 -6
  82. package/src/components/tools/item/placement-history.test.tsx +1 -1
  83. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  84. package/src/components/tools/item/placement-strategies.ts +5 -9
  85. package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
  86. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
  87. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  88. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  89. package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
  90. package/src/components/tools/select/select-candidates.test.ts +45 -0
  91. package/src/components/tools/select/select-candidates.ts +13 -2
  92. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  93. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  94. package/src/components/tools/shared/placement-box.tsx +2 -6
  95. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  96. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  97. package/src/components/tools/shared/segment-angle.ts +0 -4
  98. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  99. package/src/components/tools/tool-manager.tsx +1 -24
  100. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  101. package/src/components/tools/wall/wall-drafting.ts +132 -24
  102. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  103. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  104. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  105. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  106. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  107. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  108. package/src/components/tools/zone/zone-tool.tsx +3 -3
  109. package/src/components/ui/action-menu/action-button.tsx +2 -0
  110. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  111. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  112. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  113. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  114. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  115. package/src/components/ui/action-menu/index.tsx +1 -0
  116. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  117. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  118. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  119. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  120. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  121. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  122. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  123. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  124. package/src/components/ui/controls/material-picker.tsx +24 -17
  125. package/src/components/ui/controls/metric-control.test.ts +52 -0
  126. package/src/components/ui/controls/metric-control.tsx +62 -24
  127. package/src/components/ui/controls/segmented-control.tsx +5 -2
  128. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  129. package/src/components/ui/controls/slider-control.tsx +47 -19
  130. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  131. package/src/components/ui/floating-level-selector.tsx +100 -48
  132. package/src/components/ui/helpers/building-helper.tsx +5 -3
  133. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  134. package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
  135. package/src/components/ui/helpers/helper-manager.tsx +136 -57
  136. package/src/components/ui/helpers/item-helper.tsx +6 -4
  137. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  138. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  139. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  140. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  141. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  142. package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
  143. package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
  144. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  145. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  146. package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
  147. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  148. package/src/components/ui/panels/multi-field-value.ts +20 -1
  149. package/src/components/ui/panels/multi-height-mode.tsx +11 -10
  150. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  151. package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
  152. package/src/components/ui/panels/node-display.ts +35 -24
  153. package/src/components/ui/panels/panel-manager.tsx +9 -5
  154. package/src/components/ui/panels/panel-wrapper.tsx +63 -127
  155. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  156. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  157. package/src/components/ui/panels/reference-panel.tsx +51 -35
  158. package/src/components/ui/panels/room-construction-rows.tsx +465 -66
  159. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  160. package/src/components/ui/panels/room-name-options.ts +110 -11
  161. package/src/components/ui/panels/room-openings-field.tsx +22 -13
  162. package/src/components/ui/panels/room-panel.tsx +207 -100
  163. package/src/components/ui/panels/shape-choice.tsx +7 -5
  164. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  165. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  166. package/src/components/ui/right-stack.test.ts +13 -6
  167. package/src/components/ui/right-stack.tsx +21 -10
  168. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  169. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  170. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  171. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  172. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  173. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
  174. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  175. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
  176. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  177. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  178. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  179. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  180. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  181. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  182. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  183. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  184. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  185. package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
  186. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  187. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  188. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  189. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  190. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  191. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  192. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
  193. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  194. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  195. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  196. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  197. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  198. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  199. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  200. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  201. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  202. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
  203. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  204. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  205. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  206. package/src/components/ui/snap-target-badge.tsx +7 -6
  207. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  208. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  209. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  210. package/src/components/viewer/floorplan-preview.tsx +27 -19
  211. package/src/components/viewer/render-settings-menu.tsx +294 -0
  212. package/src/components/viewer/sky-control.tsx +710 -0
  213. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  214. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  215. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  216. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  217. package/src/components/viewer/viewer-rooms.tsx +62 -5
  218. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  219. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  220. package/src/components/viewer-overlay.tsx +8 -0
  221. package/src/components/walkthrough-hud.tsx +75 -2
  222. package/src/hooks/use-keyboard.test.ts +85 -4
  223. package/src/hooks/use-keyboard.ts +65 -7
  224. package/src/hooks/use-selected-room.ts +12 -1
  225. package/src/i18n/floor-footprint-name.ts +30 -0
  226. package/src/i18n/level-name.ts +39 -0
  227. package/src/i18n/locales/en/messages.po +15849 -0
  228. package/src/i18n/locales/en/messages.ts +1 -0
  229. package/src/i18n/locales/en-XA/messages.ts +1 -0
  230. package/src/i18n/locales/es/messages.po +39331 -0
  231. package/src/i18n/locales/es/messages.ts +1 -0
  232. package/src/i18n/locales/fr/messages.po +39331 -0
  233. package/src/i18n/locales/fr/messages.ts +1 -0
  234. package/src/i18n/locales/ja/messages.po +39331 -0
  235. package/src/i18n/locales/ja/messages.ts +1 -0
  236. package/src/i18n/locales/zh-CN/messages.po +39331 -0
  237. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  238. package/src/i18n/locales.test.ts +63 -0
  239. package/src/i18n/locales.ts +32 -0
  240. package/src/i18n/message-slot.tsx +25 -0
  241. package/src/i18n/node-messages.ts +169 -0
  242. package/src/i18n/pseudo-locale.ts +17 -0
  243. package/src/i18n/published-entry.test.ts +17 -0
  244. package/src/i18n/scene-theme-name.ts +16 -0
  245. package/src/index.tsx +224 -45
  246. package/src/lib/add-storey.test.ts +3 -3
  247. package/src/lib/contextual-help.test.ts +20 -1
  248. package/src/lib/contextual-help.ts +101 -19
  249. package/src/lib/continuation.ts +38 -11
  250. package/src/lib/elevation-guides.ts +21 -6
  251. package/src/lib/elevator-support.ts +1 -72
  252. package/src/lib/floor-footprints.test.ts +3 -2
  253. package/src/lib/floor-footprints.ts +1 -9
  254. package/src/lib/floor-foundation-support.test.ts +15 -6
  255. package/src/lib/floor-opening-draft.ts +1 -1
  256. package/src/lib/floor-region-session.ts +1 -1
  257. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  258. package/src/lib/floorplan/dimension-drive.ts +68 -3
  259. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  260. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  261. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  262. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  263. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  264. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  265. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  266. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  267. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  268. package/src/lib/floorplan/schedules.ts +10 -6
  269. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  270. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  271. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  272. package/src/lib/geometry-script/author.test.ts +114 -0
  273. package/src/lib/geometry-script/author.ts +55 -23
  274. package/src/lib/geometry-script/client.ts +3 -0
  275. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  276. package/src/lib/gesture-lifecycle.ts +3 -1
  277. package/src/lib/glb-export.test.ts +327 -0
  278. package/src/lib/glb-export.ts +70 -2
  279. package/src/lib/grid-frame.test.ts +56 -3
  280. package/src/lib/grid-frame.ts +28 -1
  281. package/src/lib/history.test.ts +2 -1
  282. package/src/lib/hud-title.ts +48 -24
  283. package/src/lib/ifc-export.test.ts +41 -1
  284. package/src/lib/inspector-card-mode.test.ts +3 -31
  285. package/src/lib/inspector-card-mode.ts +5 -10
  286. package/src/lib/inspector-expanded.test.ts +13 -0
  287. package/src/lib/inspector-expanded.ts +13 -1
  288. package/src/lib/interaction/scope.ts +1 -0
  289. package/src/lib/keyboard-pan.test.ts +7 -0
  290. package/src/lib/keyboard-pan.ts +9 -2
  291. package/src/lib/layer-thickness.test.ts +22 -0
  292. package/src/lib/layer-thickness.ts +79 -0
  293. package/src/lib/level-selection.test.ts +18 -2
  294. package/src/lib/level-selection.ts +31 -8
  295. package/src/lib/linear-display.ts +1 -1
  296. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  297. package/src/lib/lot/drop-in.test.ts +59 -1
  298. package/src/lib/lot/drop-in.ts +42 -50
  299. package/src/lib/material-swatch-style.ts +24 -0
  300. package/src/lib/measurement-label.test.ts +1 -1
  301. package/src/lib/measurement-parser.ts +7 -3
  302. package/src/lib/measurements.test.ts +24 -6
  303. package/src/lib/measurements.ts +1 -203
  304. package/src/lib/mezzanine-selection.test.ts +3 -1
  305. package/src/lib/node-action-movement.test.tsx +115 -0
  306. package/src/lib/node-action-movement.ts +2 -0
  307. package/src/lib/orbit-near-plane.test.ts +12 -0
  308. package/src/lib/orbit-near-plane.ts +13 -0
  309. package/src/lib/paint-regions.test.ts +8 -5
  310. package/src/lib/paint-scope.test.ts +53 -0
  311. package/src/lib/paint-scope.ts +18 -4
  312. package/src/lib/panel-rows.ts +17 -8
  313. package/src/lib/placement-metadata.ts +4 -0
  314. package/src/lib/point-ask/describe-target.ts +6 -8
  315. package/src/lib/point-ask/region.ts +1 -1
  316. package/src/lib/print-shell-compiler.ts +12 -1
  317. package/src/lib/print-wall-solids.test.ts +11 -0
  318. package/src/lib/print-wall-solids.ts +9 -0
  319. package/src/lib/reference-scale.ts +83 -0
  320. package/src/lib/room-assembly-overlay.ts +1 -1
  321. package/src/lib/room-built-on.test.ts +28 -17
  322. package/src/lib/room-built-on.ts +21 -17
  323. package/src/lib/room-construction-commands.ts +3 -2
  324. package/src/lib/room-construction.test.ts +28 -1
  325. package/src/lib/room-construction.ts +14 -0
  326. package/src/lib/room-controls-phase6.test.ts +141 -1
  327. package/src/lib/room-handle-drag.ts +554 -58
  328. package/src/lib/room-name-catalog.test.ts +8 -0
  329. package/src/lib/room-name-catalog.ts +8 -1
  330. package/src/lib/room-name-messages.test.ts +132 -0
  331. package/src/lib/room-name-messages.ts +147 -0
  332. package/src/lib/room-name-storage.test.ts +334 -0
  333. package/src/lib/room-push-dimensions.test.ts +38 -1
  334. package/src/lib/room-push-dimensions.ts +226 -2
  335. package/src/lib/room-selection.test.ts +23 -6
  336. package/src/lib/scene-bounds.test.ts +144 -0
  337. package/src/lib/scene-bounds.ts +79 -15
  338. package/src/lib/scene-clipboard.test.ts +70 -49
  339. package/src/lib/scene-clipboard.ts +11 -5
  340. package/src/lib/selection-cycle.ts +63 -0
  341. package/src/lib/separator-edit.test.ts +431 -0
  342. package/src/lib/separator-edit.ts +179 -0
  343. package/src/lib/separator-lines.ts +38 -0
  344. package/src/lib/sfx-bus.ts +27 -1
  345. package/src/lib/sfx-player.ts +16 -0
  346. package/src/lib/snapping-mode.test.ts +23 -1
  347. package/src/lib/tool-cursor-icon.ts +5 -1
  348. package/src/lib/tool-shortcuts.ts +130 -0
  349. package/src/lib/viewer-selection.test.ts +28 -24
  350. package/src/lib/viewer-selection.ts +21 -6
  351. package/src/lib/wall-draw-variant.test.ts +7 -6
  352. package/src/lib/wall-draw-variant.ts +107 -6
  353. package/src/lib/wall-nudge.test.ts +44 -0
  354. package/src/lib/wall-nudge.ts +131 -0
  355. package/src/lib/wall-parallel-snap.test.ts +83 -0
  356. package/src/lib/wall-parallel-snap.ts +176 -0
  357. package/src/lib/wall-placement-pointer.test.ts +23 -0
  358. package/src/lib/wall-placement-pointer.ts +18 -0
  359. package/src/lib/wall-pull.test.ts +245 -0
  360. package/src/lib/wall-pull.ts +209 -0
  361. package/src/lib/wall-push-line-up.test.ts +132 -0
  362. package/src/lib/wall-region-list.ts +17 -6
  363. package/src/lib/wall-run-snap.test.ts +188 -0
  364. package/src/lib/wall-run-snap.ts +83 -0
  365. package/src/lib/wall-type.test.ts +125 -0
  366. package/src/lib/wall-type.ts +119 -0
  367. package/src/lib/window-interaction.test.ts +44 -0
  368. package/src/lib/world-grid-snap.test.ts +139 -2
  369. package/src/lib/world-grid-snap.ts +102 -105
  370. package/src/store/use-alignment-guides.ts +27 -1
  371. package/src/store/use-editor.tsx +53 -0
  372. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  373. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  374. package/src/store/use-placement-typing.test.ts +165 -0
  375. package/src/store/use-placement-typing.ts +186 -0
  376. package/src/store/use-wall-snap-indicator.ts +15 -2
  377. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  378. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  379. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  380. package/src/lib/door-interaction.ts +0 -102
  381. package/src/lib/measurement-label.ts +0 -68
  382. package/src/lib/window-interaction.ts +0 -100
@@ -7,15 +7,22 @@ import {
7
7
  containsPoint,
8
8
  DEFAULT_LEVEL_HEIGHT,
9
9
  emitter,
10
+ formatNumber,
11
+ getAreaUnitLabel,
12
+ type LinearUnit,
13
+ type PascalI18n,
10
14
  type Polygon,
11
15
  polygonInteriorPoint,
12
16
  type Ring,
17
+ roomFloorElevation,
13
18
  sceneRegistry,
19
+ squareMetersToAreaUnit,
20
+ useLocale,
21
+ usePascalI18n,
14
22
  useScene,
15
23
  type ZoneNode,
16
24
  } from '@pascal-app/core'
17
25
  import { timeSpan, useViewer } from '@pascal-app/viewer'
18
- import { Html } from '@react-three/drei'
19
26
  import { useFrame, useThree } from '@react-three/fiber'
20
27
  import {
21
28
  createContext,
@@ -45,19 +52,14 @@ import {
45
52
  } from 'three'
46
53
  import { useShallow } from 'zustand/react/shallow'
47
54
  import { useRoomRecords } from '../../hooks/use-selected-room'
48
- import { getAreaUnitLabel, type LinearUnit, squareMetersToAreaUnit } from '../../lib/measurements'
49
- import { roomFloorElevation } from '../../lib/room-handle-drag'
55
+ import { displayRoomName } from '../../lib/room-name-messages'
50
56
  import type { RoomSelectionRecord } from '../../lib/room-selection'
51
- import {
52
- clickZoneArea,
53
- hoverZoneArea,
54
- leaveZoneArea,
55
- zoneKindLabel,
56
- } from '../../lib/room-zone-routing'
57
+ import { clickZoneArea, hoverZoneArea, leaveZoneArea } from '../../lib/room-zone-routing'
57
58
  import { selectionModifiersFromEvent } from '../../lib/selection-routing'
58
59
  import useEditor from '../../store/use-editor'
59
60
  import useInteractionScope from '../../store/use-interaction-scope'
60
61
  import { isViewVisible, VIEW_2D, VIEW_3D } from '../../store/view-layout'
62
+ import { Html } from './localized-html'
61
63
 
62
64
  const CONTEXT_TYPES = new Set(['site', 'building', 'level'])
63
65
 
@@ -92,8 +94,12 @@ export function roomLabelsVisible(state: RoomLabelVisibilityState): boolean {
92
94
  )
93
95
  }
94
96
 
95
- export function formatRoomArea(squareMeters: number, unit: LinearUnit): string {
96
- return `${squareMetersToAreaUnit(squareMeters, unit).toFixed(1)} ${getAreaUnitLabel(unit)}`
97
+ export function formatRoomArea(squareMeters: number, unit: LinearUnit, locale?: string): string {
98
+ return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), {
99
+ min: 1,
100
+ max: 1,
101
+ locale,
102
+ })} ${getAreaUnitLabel(unit)}`
97
103
  }
98
104
 
99
105
  function useRoomLabelsVisible() {
@@ -1169,11 +1175,12 @@ export function RoomPillChip({
1169
1175
  onLeave: () => void
1170
1176
  }) {
1171
1177
  const unit = useViewer((s) => s.unit)
1172
- const areaLabel = formatRoomArea(areaM2, unit)
1178
+ const locale = useLocale()
1179
+ const areaLabel = formatRoomArea(areaM2, unit, locale)
1173
1180
  // biome-ignore lint/correctness/useExhaustiveDependencies: A dot's border box does not resize when its hidden text changes.
1174
1181
  useLayoutEffect(() => {
1175
1182
  if (elements instanceof PillElements) elements.measure(zoneId)
1176
- }, [areaM2, elements, name, unit, zoneId])
1183
+ }, [areaM2, elements, locale, name, unit, zoneId])
1177
1184
  const register = useCallback(
1178
1185
  (element: HTMLElement | null) => {
1179
1186
  if (element) elements.set(zoneId, element)
@@ -1227,6 +1234,7 @@ export function RoomPillDetail({
1227
1234
  areaM2: number
1228
1235
  }) {
1229
1236
  const unit = useViewer((s) => s.unit)
1237
+ const locale = useLocale()
1230
1238
  return (
1231
1239
  <div
1232
1240
  aria-hidden
@@ -1234,7 +1242,7 @@ export function RoomPillDetail({
1234
1242
  data-room-label-detail={zoneId}
1235
1243
  data-room-label-view={view}
1236
1244
  >
1237
- <LabelParts areaLabel={formatRoomArea(areaM2, unit)} color={color} full name={name} />
1245
+ <LabelParts areaLabel={formatRoomArea(areaM2, unit, locale)} color={color} full name={name} />
1238
1246
  </div>
1239
1247
  )
1240
1248
  }
@@ -1244,7 +1252,13 @@ function useLabelZone(zoneId: string) {
1244
1252
  return zone && zone.visible !== false ? zone : null
1245
1253
  }
1246
1254
 
1247
- const labelName = (zone: ZoneNode) => zone.name?.trim() || zoneKindLabel(zone)
1255
+ const labelName = (zone: ZoneNode, i18n: PascalI18n) => {
1256
+ const name = zone.name?.trim()
1257
+ if (name) return displayRoomName(name, i18n)
1258
+ return zone.spaceRole === 'room'
1259
+ ? i18n._({ id: 'editor.roomLabel.room', message: 'Room' })
1260
+ : i18n._({ id: 'editor.roomLabel.zone', message: 'Zone' })
1261
+ }
1248
1262
 
1249
1263
  /**
1250
1264
  * One room's pill in the editor. Hover and click take the same rule as the
@@ -1260,6 +1274,7 @@ function RoomPill({
1260
1274
  view: '2d' | '3d'
1261
1275
  elements: Map<string, HTMLElement>
1262
1276
  }) {
1277
+ const i18n = usePascalI18n()
1263
1278
  const zone = useLabelZone(entry.zoneId)
1264
1279
  // A pill that disappears under the cursor (its click selected something)
1265
1280
  // fires no leave: end the hover it set.
@@ -1270,7 +1285,7 @@ function RoomPill({
1270
1285
  areaM2={entry.area}
1271
1286
  color={zone.color}
1272
1287
  elements={elements}
1273
- name={labelName(zone)}
1288
+ name={labelName(zone, i18n)}
1274
1289
  onClick={(event) => clickZoneArea(entry.zoneId, selectionModifiersFromEvent(event))}
1275
1290
  onHover={(event) => hoverZoneArea(entry.zoneId, selectionModifiersFromEvent(event))}
1276
1291
  onLeave={() => leaveZoneArea(entry.zoneId)}
@@ -1281,13 +1296,14 @@ function RoomPill({
1281
1296
  }
1282
1297
 
1283
1298
  function RoomDetail({ entry, view }: { entry: LabelEntry; view: '2d' | '3d' }) {
1299
+ const i18n = usePascalI18n()
1284
1300
  const zone = useLabelZone(entry.zoneId)
1285
1301
  if (!zone) return null
1286
1302
  return (
1287
1303
  <RoomPillDetail
1288
1304
  areaM2={entry.area}
1289
1305
  color={zone.color}
1290
- name={labelName(zone)}
1306
+ name={labelName(zone, i18n)}
1291
1307
  view={view}
1292
1308
  zoneId={entry.zoneId}
1293
1309
  />
@@ -15,7 +15,6 @@ import {
15
15
  type NodeEvent,
16
16
  nodeRegistry,
17
17
  type RoofEvent,
18
- type RoofNode,
19
18
  type RoofSegmentEvent,
20
19
  type RoofSegmentNode,
21
20
  resolveLevelId,
@@ -29,7 +28,13 @@ import {
29
28
  useScene,
30
29
  wallFaceScriptPaintRefusal,
31
30
  } from '@pascal-app/core'
32
- import { getRoofMaterialArray, registerMaterialCacheCleanup, useViewer } from '@pascal-app/viewer'
31
+ import {
32
+ markHighlight,
33
+ ROOF_HEEL_MATERIAL_INDEX,
34
+ registerMaterialCacheCleanup,
35
+ tintEmissive,
36
+ useViewer,
37
+ } from '@pascal-app/viewer'
33
38
  import { useThree } from '@react-three/fiber'
34
39
  import { useCallback, useEffect, useRef } from 'react'
35
40
  import { type BufferGeometry, Color, type Material, type Mesh, type Object3D, Vector3 } from 'three'
@@ -57,7 +62,6 @@ import {
57
62
  import { selectionEnabled } from '../../lib/interaction/scope'
58
63
  import {
59
64
  type ActivePaintMaterial,
60
- buildRoofSurfaceMaterialPatch,
61
65
  hasActivePaintMaterial,
62
66
  resolveActivePaintMaterialFromSelection,
63
67
  } from '../../lib/material-paint'
@@ -90,7 +94,6 @@ import {
90
94
  paintScopeRole,
91
95
  paintSurfaceLabel,
92
96
  resolvePaintScopeTargets,
93
- slotDisplayLabel,
94
97
  type WallPaintHit,
95
98
  } from '../../lib/paint-scope'
96
99
  import {
@@ -248,9 +251,10 @@ function resolveStairMaterialTarget(
248
251
 
249
252
  function resolveRoofMaterialTarget(
250
253
  event: RoofEvent | RoofSegmentEvent,
251
- ): 'top' | 'edge' | 'wall' | null {
254
+ ): 'top' | 'edge' | 'wall' | 'heel' | null {
252
255
  const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
253
256
  if (materialIndex === 3) return 'top'
257
+ if (materialIndex === ROOF_HEEL_MATERIAL_INDEX) return 'heel'
254
258
  if (materialIndex === 0) return 'edge'
255
259
  if (materialIndex === 1 || materialIndex === 2) return 'wall'
256
260
 
@@ -451,14 +455,6 @@ function resolveSelectModeNodeTarget(event: NodeEvent, drill = false): AnyNode {
451
455
  return event.node
452
456
  }
453
457
 
454
- function previewMeshMaterial(mesh: Mesh, material: Material | Material[]): PaintPreviewCleanup {
455
- const previousMaterial = mesh.material
456
- mesh.material = material
457
- return () => {
458
- mesh.material = previousMaterial
459
- }
460
- }
461
-
462
458
  function previewCursor(cursor: string): PaintPreviewCleanup {
463
459
  const previousCursor = document.body.style.cursor
464
460
  document.body.style.cursor = cursor
@@ -467,33 +463,6 @@ function previewCursor(cursor: string): PaintPreviewCleanup {
467
463
  }
468
464
  }
469
465
 
470
- function applyRoofPaintPreview(
471
- node: RoofNode,
472
- role: 'top' | 'edge' | 'wall',
473
- material: ActivePaintMaterial,
474
- ): PaintPreviewCleanup | null {
475
- const root = getRegisteredNodeObject(node.id)
476
- const mesh = root?.getObjectByName('merged-roof') as Mesh | undefined
477
- if (!mesh) return null
478
-
479
- const previewNode = {
480
- ...node,
481
- ...buildRoofSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
482
- }
483
- const previewMaterial = getRoofMaterialArray(
484
- previewNode,
485
- useViewer.getState().shading,
486
- useViewer.getState().textures,
487
- useViewer.getState().colorPreset,
488
- useViewer.getState().sceneTheme,
489
- null,
490
- useScene.getState().materials,
491
- )
492
- if (!previewMaterial) return null
493
-
494
- return previewMeshMaterial(mesh, previewMaterial)
495
- }
496
-
497
466
  // Chimney + dormer paint dispatch lives on their NodeDefinition's
498
467
  // `capabilities.paint` (see packages/nodes/src/{chimney,dormer}/
499
468
  // paint.ts). The generic registry-driven arm in this file consults
@@ -518,16 +487,14 @@ const HIGHLIGHT_PROFILES = {
518
487
  delete: {
519
488
  color: new Color('#dc2626'),
520
489
  blend: 0.76,
521
- emissiveBlend: 0.92,
522
- emissiveIntensity: 0.46,
490
+ emissive: 0.92,
523
491
  },
524
492
  selection: {
525
493
  // Keep the real material/texture readable: no albedo tint, just a gentle
526
494
  // indigo emissive glow so it reads as selected.
527
495
  color: new Color('#818cf8'),
528
496
  blend: 0,
529
- emissiveBlend: 0.4,
530
- emissiveIntensity: 0.12,
497
+ emissive: 0.12,
531
498
  },
532
499
  } as const
533
500
 
@@ -582,15 +549,7 @@ function createHighlightedMaterial(material: Material, kind: HighlightKind): Mat
582
549
  highlightedMaterial.color = highlightedMaterial.color.clone().lerp(profile.color, profile.blend)
583
550
  }
584
551
 
585
- if (highlightedMaterial.emissive instanceof Color) {
586
- highlightedMaterial.emissive = highlightedMaterial.emissive
587
- .clone()
588
- .lerp(profile.color, profile.emissiveBlend)
589
- highlightedMaterial.emissiveIntensity = Math.max(
590
- highlightedMaterial.emissiveIntensity ?? 0,
591
- profile.emissiveIntensity,
592
- )
593
- }
552
+ tintEmissive(highlightedMaterial, profile.color, profile.emissive)
594
553
 
595
554
  if (typeof highlightedMaterial.opacity === 'number' && highlightedMaterial.opacity < 1) {
596
555
  highlightedMaterial.transparent = true
@@ -598,7 +557,7 @@ function createHighlightedMaterial(material: Material, kind: HighlightKind): Mat
598
557
  }
599
558
 
600
559
  highlightedMaterial.needsUpdate = true
601
- return highlightedMaterial
560
+ return markHighlight(highlightedMaterial)
602
561
  }
603
562
 
604
563
  function createHighlightedMaterials(
@@ -892,9 +851,7 @@ export const SelectionManager = () => {
892
851
 
893
852
  // Registry-driven paint dispatch — kinds that declare
894
853
  // `capabilities.paint` route hover / click / preview through
895
- // their definition. Wall, chimney, and dormer use this; legacy
896
- // roof / stair / single-surface arms below stay until they
897
- // migrate too.
854
+ // their definition (wall, chimney, dormer, roof, …).
898
855
  const paintCap = nodeRegistry.get(node.type)?.capabilities?.paint
899
856
  if (paintCap) {
900
857
  const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
@@ -1063,51 +1020,7 @@ export const SelectionManager = () => {
1063
1020
  }
1064
1021
  }
1065
1022
 
1066
- // A segment paints through its definition's `capabilities.paint`; the merged shell's hit is
1067
- // the roof's, and fans to it.
1068
- if (node.type === 'roof') {
1069
- const role = resolveRoofMaterialTarget(event as RoofEvent)
1070
- if (isPaintPicking()) return pickInteraction(node, role, event, null)
1071
- const compatible = role !== null && paintEnabled
1072
- return {
1073
- key: `roof:${node.id}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}`,
1074
- hoveredId: node.id as AnyNodeId,
1075
- hoverMode: compatible ? (eraser ? 'erase-ready' : 'paint-ready') : 'paint-disabled',
1076
- // Roof isn't on the slot model (role-specific fields, custom commit),
1077
- // so it offers only the single surface — but still labels it.
1078
- paintHover:
1079
- compatible && role
1080
- ? {
1081
- scopes: ['single'],
1082
- slotLabel: slotDisplayLabel(node, role),
1083
- nodeNoun: 'roof',
1084
- }
1085
- : null,
1086
- apply:
1087
- compatible && role
1088
- ? () => {
1089
- useScene
1090
- .getState()
1091
- .updateNode(
1092
- node.id as AnyNodeId,
1093
- buildRoofSurfaceMaterialPatch(
1094
- node,
1095
- role,
1096
- paintSpec.material,
1097
- paintSpec.materialPreset,
1098
- ),
1099
- )
1100
- }
1101
- : null,
1102
- preview:
1103
- compatible && role
1104
- ? () => applyRoofPaintPreview(node, role, paintSpec)
1105
- : () => previewCursor('not-allowed'),
1106
- }
1107
- }
1108
-
1109
- // Only `roof` reaches a legacy paint arm (above) — every
1110
- // other paintable kind declares `capabilities.paint` and returns from the
1023
+ // Every paintable kind declares `capabilities.paint` and returns from the
1111
1024
  // registry-driven dispatch at the top of this function.
1112
1025
 
1113
1026
  const disabledNodeTypes = ['zone']
@@ -0,0 +1,74 @@
1
+ 'use client'
2
+
3
+ import { type SeparatorNode, sceneRegistry } from '@pascal-app/core'
4
+ import { type ThreeEvent, useThree } from '@react-three/fiber'
5
+ import { useState } from 'react'
6
+ import { OrthographicCamera, Plane, Vector2, Vector3 } from 'three'
7
+ import { runSeparatorEndDrag } from '../../lib/separator-edit'
8
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
9
+ import { HandleArrow } from './handles/handle-arrow'
10
+
11
+ /**
12
+ * An invisible wall's 3D end grip, the twin of its end handle on the plan: a
13
+ * cube at the end, on the floor at `height`. Dragging it moves the end across
14
+ * that floor (`runSeparatorEndDrag`), the invisible walls joined there
15
+ * following. Rendered in the level frame.
16
+ */
17
+ export function SeparatorEndGrip({
18
+ separator,
19
+ endpoint,
20
+ height,
21
+ levelId,
22
+ }: {
23
+ separator: SeparatorNode
24
+ endpoint: 'start' | 'end'
25
+ height: number
26
+ levelId: string
27
+ }) {
28
+ const [hover, setHover] = useState(false)
29
+ const { camera, raycaster, gl } = useThree()
30
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
31
+ const [x, z] = separator[endpoint]
32
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
33
+ if (event.button !== 0) return
34
+ event.stopPropagation()
35
+ suppressBoxSelectForPointer(event)
36
+ const level = sceneRegistry.nodes.get(levelId)
37
+ if (!level) return
38
+ level.updateWorldMatrix(true, false)
39
+ const toLocal = level.matrixWorld.clone().invert()
40
+ const floor = new Plane().setFromNormalAndCoplanarPoint(
41
+ new Vector3(0, 1, 0).transformDirection(level.matrixWorld),
42
+ new Vector3(x, height, z).applyMatrix4(level.matrixWorld),
43
+ )
44
+ runSeparatorEndDrag({
45
+ separatorId: separator.id,
46
+ endpoint,
47
+ levelId,
48
+ toPlan: (clientX, clientY) => {
49
+ const rect = gl.domElement.getBoundingClientRect()
50
+ raycaster.setFromCamera(
51
+ new Vector2(
52
+ ((clientX - rect.left) / rect.width) * 2 - 1,
53
+ -((clientY - rect.top) / rect.height) * 2 + 1,
54
+ ),
55
+ camera,
56
+ )
57
+ const hit = raycaster.ray.intersectPlane(floor, new Vector3())
58
+ if (!hit) return null
59
+ const local = hit.applyMatrix4(toLocal)
60
+ return [local.x, local.z]
61
+ },
62
+ })
63
+ }
64
+ return (
65
+ <HandleArrow
66
+ cursor="grab"
67
+ hover={hover}
68
+ onHoverChange={setHover}
69
+ onPointerDown={onPointerDown}
70
+ placement={{ position: [x, height, z], baseScale: zoom }}
71
+ shape="tracker"
72
+ />
73
+ )
74
+ }
@@ -1,16 +1,20 @@
1
1
  'use client'
2
2
 
3
3
  import type { SiteNode } from '@pascal-app/core'
4
- import { sceneRegistry, useLiveNodeOverrides, useScene } from '@pascal-app/core'
4
+ import {
5
+ formatLinearMeasurement,
6
+ sceneRegistry,
7
+ useLiveNodeOverrides,
8
+ useScene,
9
+ } from '@pascal-app/core'
5
10
  import { getSceneTheme, useViewer } from '@pascal-app/viewer'
6
- import { Html } from '@react-three/drei'
7
11
  import { createPortal, useFrame, useThree } from '@react-three/fiber'
8
12
  import { useCallback, useMemo, useRef, useState } from 'react'
9
13
  import { type Camera, type Object3D, Vector3 } from 'three'
10
14
  import { groundHeightAt } from '../../lib/ground-surface'
11
- import { formatLinearMeasurement } from '../../lib/measurements'
12
15
  import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
13
16
  import { useActiveHandleDrag } from '../../store/use-interaction-scope'
17
+ import { Html } from './localized-html'
14
18
 
15
19
  type ViewportSize = {
16
20
  width: number
@@ -205,31 +205,38 @@ describe('a sheet capture session', () => {
205
205
  const viewer = useViewer.getState()
206
206
  viewer.setWallMode('down')
207
207
  viewer.setLevelMode('exploded')
208
- viewer.setShading('solid')
208
+ viewer.setRenderQuality('low')
209
209
  viewer.setTextures(false)
210
210
  viewer.setColorPreset('white')
211
+ viewer.setTimeOfDay('night')
212
+ viewer.setWeather('overcast')
211
213
  const view = () => {
212
- const { wallMode, levelMode, shading, textures, colorPreset } = useViewer.getState()
213
- return { wallMode, levelMode, shading, textures, colorPreset }
214
+ const { wallMode, levelMode, renderQuality, textures, colorPreset, timeOfDay, weather } =
215
+ useViewer.getState()
216
+ return { wallMode, levelMode, renderQuality, textures, colorPreset, timeOfDay, weather }
214
217
  }
215
- // the shading stays the user's: the capture brings its own AO
218
+ // the quality stays the user's: the capture brings its own AO
216
219
 
217
220
  const restore = presentFinished()
218
221
  expect(view()).toEqual({
219
222
  wallMode: 'up',
220
223
  levelMode: 'stacked',
221
- shading: 'solid',
224
+ renderQuality: 'low',
222
225
  textures: true,
223
226
  colorPreset: 'clay',
227
+ timeOfDay: 'midday',
228
+ weather: 'fair',
224
229
  })
225
230
 
226
231
  restore()
227
232
  expect(view()).toEqual({
228
233
  wallMode: 'down',
229
234
  levelMode: 'exploded',
230
- shading: 'solid',
235
+ renderQuality: 'low',
231
236
  textures: false,
232
237
  colorPreset: 'white',
238
+ timeOfDay: 'night',
239
+ weather: 'overcast',
233
240
  })
234
241
  })
235
242
 
@@ -239,6 +246,8 @@ describe('a sheet capture session', () => {
239
246
  viewer.setLevelMode('stacked')
240
247
  viewer.setTextures(true)
241
248
  viewer.setColorPreset('clay')
249
+ viewer.setTimeOfDay('midday')
250
+ viewer.setWeather('fair')
242
251
  let writes = 0
243
252
  const unsubscribe = useViewer.subscribe(() => {
244
253
  writes++
@@ -16,6 +16,7 @@ import {
16
16
  GRID_LAYER,
17
17
  heroCameraPose,
18
18
  holdLiveFrame,
19
+ itemLightsForCapture,
19
20
  pendingSceneBuildCount,
20
21
  presentLevelsForCapture,
21
22
  refreshIsolation,
@@ -437,13 +438,15 @@ const HOLD_MAX_MS = 300_000
437
438
  * What the sheets' pictures are drawn in, whatever the user's view: full-
438
439
  * height walls, the storeys stacked at their true heights, the materials'
439
440
  * own textures and colours (a white or blueprint palette is a view, not the
440
- * house). The shading stays the user's: the capture brings its own AO.
441
+ * house), lit by a fair midday. The shading stays the user's: the capture brings its own AO.
441
442
  */
442
443
  const FINISHED_PRESENTATION = {
443
444
  wallMode: 'up',
444
445
  levelMode: 'stacked',
445
446
  textures: true,
446
447
  colorPreset: 'clay',
448
+ timeOfDay: 'midday',
449
+ weather: 'fair',
447
450
  } as const
448
451
 
449
452
  /** The viewer switched to FINISHED_PRESENTATION; the returned function puts the user's view back. */
@@ -470,6 +473,25 @@ export function presentFinished(): () => void {
470
473
  viewer.setColorPreset(FINISHED_PRESENTATION.colorPreset)
471
474
  restores.push(() => useViewer.getState().setColorPreset(colorPreset))
472
475
  }
476
+ if (
477
+ viewer.timeOfDay !== FINISHED_PRESENTATION.timeOfDay ||
478
+ viewer.timeOfDayMinutes !== null ||
479
+ viewer.skyLive
480
+ ) {
481
+ const { timeOfDay, timeOfDayMinutes, skyLive } = viewer
482
+ viewer.setTimeOfDay(FINISHED_PRESENTATION.timeOfDay)
483
+ restores.push(() => {
484
+ const next = useViewer.getState()
485
+ next.setTimeOfDay(timeOfDay)
486
+ if (timeOfDayMinutes !== null) next.setTimeOfDayMinutes(timeOfDayMinutes)
487
+ if (skyLive) next.setSkyLive(true)
488
+ })
489
+ }
490
+ if (viewer.weather !== FINISHED_PRESENTATION.weather) {
491
+ const weather = viewer.weather
492
+ viewer.setWeather(FINISHED_PRESENTATION.weather)
493
+ restores.push(() => useViewer.getState().setWeather(weather))
494
+ }
473
495
  return () => {
474
496
  for (const restore of restores.reverse()) restore()
475
497
  }
@@ -995,6 +1017,9 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
995
1017
  } else if (snapLevels || pose || cameraPose) {
996
1018
  exteriorGroundForCapture(restore, Boolean(cameraPose || pose), thumbnailCamera)
997
1019
  }
1020
+ // Last, with the camera where the picture is taken from: the item lights that
1021
+ // matter to it, at full strength.
1022
+ restore(itemLightsForCapture(scene, thumbnailCamera))
998
1023
  }
999
1024
  // The scene pass is a FRAME-updated node: it renders once per node
1000
1025
  // frame, and only the animation loop advances that frame. With the