@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import type { AnyNode } from '@pascal-app/core'
3
+ import { setSurfaceRaycastLayers } from '@pascal-app/viewer'
4
+ import {
5
+ BoxGeometry,
6
+ ExtrudeGeometry,
7
+ Group,
8
+ type Intersection,
9
+ Mesh,
10
+ MeshBasicMaterial,
11
+ Path,
12
+ Ray,
13
+ Shape,
14
+ Vector3,
15
+ } from 'three'
16
+ import { openingPartHidden, paintPassesThrough, wallHitInOpening } from './paint-pass-through'
17
+
18
+ describe('paint passes through an empty opening', () => {
19
+ const door = { id: 'door_g', type: 'door' } as unknown as AnyNode
20
+ const win = { id: 'window_g', type: 'window' } as unknown as AnyNode
21
+ const wall = { id: 'wall_g', type: 'wall' } as unknown as AnyNode
22
+
23
+ it('a door or window with no part under the cursor lets the pointer through', () => {
24
+ expect(paintPassesThrough(door, null)).toBe(true)
25
+ expect(paintPassesThrough(win, null)).toBe(true)
26
+ })
27
+
28
+ it('its parts still paint, and other kinds keep the hover', () => {
29
+ expect(paintPassesThrough(door, 'frame')).toBe(false)
30
+ expect(paintPassesThrough(win, 'glass')).toBe(false)
31
+ expect(paintPassesThrough(wall, null)).toBe(false)
32
+ })
33
+
34
+ // A 4 m × 3 m wall, 0.2 m thick, with a 2 m × 2.4 m opening down to the floor.
35
+ const outline = new Shape()
36
+ outline.moveTo(0, 0)
37
+ outline.lineTo(4, 0)
38
+ outline.lineTo(4, 3)
39
+ outline.lineTo(0, 3)
40
+ outline.lineTo(0, 0)
41
+ const opening = new Path()
42
+ opening.moveTo(1, 0.001)
43
+ opening.lineTo(1, 2.4)
44
+ opening.lineTo(3, 2.4)
45
+ opening.lineTo(3, 0.001)
46
+ opening.lineTo(1, 0.001)
47
+ outline.holes.push(opening)
48
+ const drawn = new Mesh(new ExtrudeGeometry(outline, { depth: 0.2, bevelEnabled: false }))
49
+ drawn.updateMatrixWorld(true)
50
+ // The uncut collision mesh's front face sits at z = 0.2.
51
+ const at = (x: number, y: number) => {
52
+ const ray = new Ray(new Vector3(x, y, 5), new Vector3(0, 0, -1))
53
+ return wallHitInOpening(drawn, ray, 5 - 0.2)
54
+ }
55
+
56
+ it("a wall's collision hit in its opening passes through", () => {
57
+ expect(at(2, 1)).toBe(true)
58
+ expect(at(2, 0.05)).toBe(true)
59
+ })
60
+
61
+ it('a hit on the wall itself, beside or above the opening, stays on the wall', () => {
62
+ expect(at(0.5, 1)).toBe(false)
63
+ expect(at(2, 2.7)).toBe(false)
64
+ })
65
+
66
+ it('a grazing ray that meets the reveal inside the opening is the wall', () => {
67
+ // Enters the opening 2 cm from its left jamb, heading left: it meets the jamb.
68
+ const ray = new Ray(new Vector3(1.02 + 0.3 * 4.8, 1, 5), new Vector3(-0.3, 0, -1).normalize())
69
+ expect(wallHitInOpening(drawn, ray, 4.8 * Math.hypot(0.3, 1))).toBe(false)
70
+ // The same angle through the middle of the opening passes.
71
+ const middle = new Ray(new Vector3(2 + 0.3 * 4.8, 1, 5), new Vector3(-0.3, 0, -1).normalize())
72
+ expect(wallHitInOpening(drawn, middle, 4.8 * Math.hypot(0.3, 1))).toBe(true)
73
+ })
74
+
75
+ it('without a drawn mesh the hit is the wall', () => {
76
+ expect(wallHitInOpening(null, new Ray(), 1)).toBe(false)
77
+ })
78
+
79
+ it('a drawn mesh with a group its material array has no entry for is the wall, without throwing', () => {
80
+ const torn = new Mesh(drawn.geometry.clone(), [new MeshBasicMaterial()])
81
+ torn.geometry.clearGroups()
82
+ torn.geometry.addGroup(0, Number.POSITIVE_INFINITY, 3)
83
+ torn.updateMatrixWorld(true)
84
+ const ray = new Ray(new Vector3(2, 1, 5), new Vector3(0, 0, -1))
85
+ expect(() => wallHitInOpening(torn, ray, 5 - 0.2)).not.toThrow()
86
+ expect(wallHitInOpening(torn, ray, 5 - 0.2)).toBe(false)
87
+ })
88
+ })
89
+
90
+ describe('a door part found behind the floor seen through its opening', () => {
91
+ // A door root with its invisible opening proxy and one tagged jamb at x = 1,
92
+ // standing in a wall along z = 0; the floor in front of it at y = 0.
93
+ const door = new Group()
94
+ const cutout = new Mesh(new BoxGeometry(2, 2.4, 0.3))
95
+ cutout.name = 'cutout'
96
+ cutout.position.set(0, 1.2, 0)
97
+ const jamb = new Mesh(new BoxGeometry(0.1, 2.4, 0.2))
98
+ jamb.userData.slotId = 'frame'
99
+ jamb.position.set(0.95, 1.2, 0)
100
+ door.add(cutout, jamb)
101
+ for (const mesh of [cutout, jamb]) setSurfaceRaycastLayers(mesh.layers)
102
+ door.updateMatrixWorld(true)
103
+ const floor = { distance: 3, object: new Mesh() } as unknown as Intersection
104
+ const collision = { distance: 2.5, object: Object.assign(new Mesh(), { name: 'collision-mesh' }) }
105
+
106
+ it('the floor in front of the part is what the pointer is on', () => {
107
+ // Down through the opening toward the jamb's foot, ~3.8 m away; the floor is at 3 m.
108
+ const ray = new Ray(new Vector3(0.2, 3.5, 2), new Vector3(0.75, -3.2, -2).normalize())
109
+ expect(openingPartHidden(door, ray, [])).toBe(false)
110
+ expect(openingPartHidden(door, ray, [floor])).toBe(true)
111
+ })
112
+
113
+ it('a part nearer than anything else stays the door (the host wall does not count)', () => {
114
+ const ray = new Ray(new Vector3(0.95, 1, 5), new Vector3(0, 0, -1))
115
+ expect(openingPartHidden(door, ray, [collision as unknown as Intersection])).toBe(false)
116
+ })
117
+
118
+ it('an opening with no part along the ray passes through', () => {
119
+ const ray = new Ray(new Vector3(0, 1, 5), new Vector3(0, 0, -1))
120
+ expect(openingPartHidden(door, ray, [])).toBe(true)
121
+ })
122
+ })
@@ -0,0 +1,78 @@
1
+ import type { AnyNode } from '@pascal-app/core'
2
+ import { hasMaterialsForGroups, setSurfaceRaycastLayers } from '@pascal-app/viewer'
3
+ import { type Intersection, Mesh, type Object3D, type Ray, Raycaster } from 'three'
4
+
5
+ /**
6
+ * Whether the paint pointer passes through `node`: a door or window with no
7
+ * part under the cursor (an open garage door, the empty opening its proxy
8
+ * spans) is not there for paint, so the surface behind it is what paints.
9
+ */
10
+ export function paintPassesThrough(node: AnyNode, role: string | null): boolean {
11
+ return role === null && (node.type === 'door' || node.type === 'window')
12
+ }
13
+
14
+ const drawnRaycaster = new Raycaster()
15
+ const partRaycaster = new Raycaster()
16
+ setSurfaceRaycastLayers(partRaycaster.layers)
17
+
18
+ const isWithin = (object: Object3D | null, root: Object3D) => {
19
+ for (let at = object; at; at = at.parent) if (at === root) return true
20
+ return false
21
+ }
22
+
23
+ /**
24
+ * Whether the paintable part a door or window resolves behind its proxy is
25
+ * hidden: its opening proxy wins the scene raycast, and the part is then
26
+ * found along the whole ray — even past the floor seen through the opening.
27
+ * A surface of anything else hit before that part (walls aside: their
28
+ * collision meshes span the opening, see `wallHitInOpening`) is what the
29
+ * pointer is on.
30
+ */
31
+ export function openingPartHidden(
32
+ opening: Object3D | null | undefined,
33
+ ray: Ray,
34
+ intersections: readonly Intersection[],
35
+ ): boolean {
36
+ if (!opening) return false
37
+ partRaycaster.ray.copy(ray)
38
+ const part = partRaycaster
39
+ .intersectObject(opening, true)
40
+ .find((hit) => typeof (hit.object.userData as { slotId?: unknown }).slotId === 'string')
41
+ if (!part) return true
42
+ return intersections.some(
43
+ (hit) =>
44
+ hit.distance < part.distance - 1e-4 &&
45
+ hit.object.name !== WALL_COLLISION_MESH &&
46
+ !isWithin(hit.object, opening),
47
+ )
48
+ }
49
+
50
+ const WALL_COLLISION_MESH = 'collision-mesh'
51
+
52
+ /**
53
+ * Whether a wall hit lands in one of the wall's holes. A wall is picked through
54
+ * an uncut collision mesh, so its hits cover its openings too — the open garage
55
+ * door, the step under a doorway. The wall as drawn (`drawn`, its registered
56
+ * mesh, with the cuts) decides: nothing of it along the ray past the hit means
57
+ * the pointer is looking through the wall, not at it. A hit on a reveal inside
58
+ * the opening is still the wall.
59
+ */
60
+ export function wallHitInOpening(
61
+ drawn: Object3D | null | undefined,
62
+ ray: Ray,
63
+ hitDistance: number,
64
+ ): boolean {
65
+ // A mesh caught between a palette and the groups a preview gave it cannot be
66
+ // raycast (three reads each group's material); the hit stands this move.
67
+ if (!(drawn instanceof Mesh && hasMaterialsForGroups(drawn))) return false
68
+ drawnRaycaster.ray.copy(ray)
69
+ drawnRaycaster.far = Number.POSITIVE_INFINITY
70
+ const hits: Intersection[] = []
71
+ // The drawn mesh opts out of scene raycasts (batched walls); ask it directly.
72
+ Mesh.prototype.raycast.call(drawn, drawnRaycaster, hits)
73
+ // A ray through the opening can still clip the reveal behind the hit; allow
74
+ // for the deepest wall a ray crosses at a grazing angle.
75
+ return !hits.some((hit) => hit.distance <= hitDistance + WALL_DEPTH_ALLOWANCE)
76
+ }
77
+
78
+ const WALL_DEPTH_ALLOWANCE = 2
@@ -0,0 +1,25 @@
1
+ import { useFloorRegionDraft } from './floor-region-session'
2
+ import { type PaintMode, paintRegionTargets, usePaintRegionMode } from './paint-region-mode'
3
+ import { useWallPaintRegionSession } from './wall-paint-region-session'
4
+
5
+ /** Whether a region sub-mode draws on what the pointer is over. */
6
+ export function paintRegionHovering(
7
+ mode: PaintMode,
8
+ over: { wall: boolean; floor: boolean },
9
+ ): boolean {
10
+ const targets = paintRegionTargets(mode)
11
+ return (targets.wall && over.wall) || (targets.floor && over.floor)
12
+ }
13
+
14
+ /**
15
+ * Whether the pointer is over a surface the active region sub-mode draws on —
16
+ * a wall face (Rectangle) or a room floor — or a region is being drawn. The
17
+ * paint cursor and the HUD read it: region modes stand the whole-surface hover
18
+ * down, so its "nothing paintable here" never speaks for them.
19
+ */
20
+ export function usePaintRegionHovering(): boolean {
21
+ const mode = usePaintRegionMode((state) => state.mode)
22
+ const wall = useWallPaintRegionSession((state) => state.preview !== null)
23
+ const floor = useFloorRegionDraft((state) => state.hover !== null || state.draft !== null)
24
+ return paintRegionHovering(mode, { wall, floor })
25
+ }
@@ -0,0 +1,154 @@
1
+ import { create } from 'zustand'
2
+ import type { ActivePaintMaterial } from './material-paint'
3
+ import { isTypingTarget } from './typing-target'
4
+
5
+ /**
6
+ * What a paint click does: paint the whole surface under it, draw part of a
7
+ * surface first, or erase. The one source of truth for the paint sub-mode — the
8
+ * panel, the HUD, the cursor and the gestures all read `mode` here.
9
+ * - `surface` — a click paints the whole surface under it
10
+ * - `rectangle` — a box on a wall face, pressed anywhere on it, or on a room floor
11
+ * - `polygon` — clicked points on a room floor
12
+ * - `erase` — a click removes a painted part, or resets a painted surface
13
+ * - `pick` — the eyedropper: a click takes the material shown under it
14
+ */
15
+ export type PaintRegionMode = 'surface' | 'rectangle' | 'polygon'
16
+ export type PaintMode = PaintRegionMode | 'erase' | 'pick'
17
+
18
+ export const usePaintRegionMode = create<{
19
+ mode: PaintMode
20
+ /** The last painting (not erasing) sub-mode: where the paint tool and a colour pick return. */
21
+ drawMode: PaintRegionMode
22
+ /** The sub-mode the eyedropper came from, where it returns. */
23
+ pickReturn: PaintMode
24
+ /** What the eyedropper would take from the surface under the cursor. */
25
+ picked: ActivePaintMaterial | null
26
+ setMode: (mode: PaintMode) => void
27
+ /** Short text the HUD shows for a refused gesture (e.g. the per-face cap). */
28
+ notice: string | null
29
+ setNotice: (notice: string | null) => void
30
+ }>((set) => ({
31
+ mode: 'surface',
32
+ drawMode: 'surface',
33
+ pickReturn: 'surface',
34
+ picked: null,
35
+ setMode: (mode) =>
36
+ set((state) => ({
37
+ mode,
38
+ notice: null,
39
+ picked: null,
40
+ ...(mode === 'pick'
41
+ ? state.mode === 'pick'
42
+ ? {}
43
+ : { pickReturn: state.mode }
44
+ : mode === 'erase'
45
+ ? {}
46
+ : { drawMode: mode }),
47
+ })),
48
+ notice: null,
49
+ setNotice: (notice) => set({ notice }),
50
+ }))
51
+
52
+ export function isPaintErasing(): boolean {
53
+ return usePaintRegionMode.getState().mode === 'erase'
54
+ }
55
+
56
+ export function isPaintPicking(): boolean {
57
+ return usePaintRegionMode.getState().mode === 'pick'
58
+ }
59
+
60
+ /**
61
+ * Where the eyedropper goes once it took a material: the sub-mode it came from,
62
+ * except erase — a picked material is for painting.
63
+ */
64
+ export function paintPickReturnMode(): PaintRegionMode {
65
+ const { pickReturn, drawMode } = usePaintRegionMode.getState()
66
+ return pickReturn === 'erase' || pickReturn === 'pick' ? drawMode : pickReturn
67
+ }
68
+
69
+ /** Leaves the eyedropper without taking anything (the held key released). */
70
+ export function endPaintPick(): void {
71
+ const { mode, pickReturn } = usePaintRegionMode.getState()
72
+ if (mode === 'pick')
73
+ usePaintRegionMode.getState().setMode(pickReturn === 'pick' ? 'surface' : pickReturn)
74
+ }
75
+
76
+ /** Erasing and the eyedropper end with the paint session: the next one starts on the last painting sub-mode. */
77
+ export function endPaintSession(): void {
78
+ const { mode, drawMode, notice, picked } = usePaintRegionMode.getState()
79
+ if (mode !== drawMode || notice !== null || picked !== null)
80
+ usePaintRegionMode.setState({ mode: drawMode, notice: null, picked: null })
81
+ }
82
+
83
+ /** A new project starts on whole-surface paint. */
84
+ export function resetPaintMode(): void {
85
+ usePaintRegionMode.setState({
86
+ mode: 'surface',
87
+ drawMode: 'surface',
88
+ pickReturn: 'surface',
89
+ picked: null,
90
+ notice: null,
91
+ })
92
+ }
93
+
94
+ /** Whether a region sub-mode owns the paint tool's pointer (not whole-surface paint or erase). */
95
+ export function paintRegionModeActive(editorMode: string): boolean {
96
+ const { mode } = usePaintRegionMode.getState()
97
+ return editorMode === 'material-paint' && (mode === 'rectangle' || mode === 'polygon')
98
+ }
99
+
100
+ /** Which surfaces a sub-mode draws on. */
101
+ export function paintRegionTargets(mode: PaintMode): { wall: boolean; floor: boolean } {
102
+ return { wall: mode === 'rectangle', floor: mode === 'rectangle' || mode === 'polygon' }
103
+ }
104
+
105
+ /** What the HUD asks for while the pointer is over nothing the sub-mode draws on. */
106
+ export function paintRegionHoverHint(mode: 'rectangle' | 'polygon'): string {
107
+ return mode === 'polygon' ? 'Hover a floor or ceiling' : 'Hover a wall, floor or ceiling'
108
+ }
109
+
110
+ /**
111
+ * Holding Alt/Option while painting whole surfaces turns the eyedropper on;
112
+ * letting go (or the window losing focus) turns it off, and Escape leaves it
113
+ * from its icon too. Erasing picks nothing, and the region sub-modes keep Alt
114
+ * for placing a point freely.
115
+ */
116
+ export function bindPaintPickHold(target: EventTarget): () => void {
117
+ let holding = false
118
+ const release = () => {
119
+ if (!holding) return
120
+ holding = false
121
+ endPaintPick()
122
+ }
123
+ const down = (event: Event) => {
124
+ const key = event as KeyboardEvent
125
+ if (key.key !== 'Alt' || key.repeat || holding || isTypingTarget(key.target)) return
126
+ if (usePaintRegionMode.getState().mode !== 'surface') return
127
+ key.preventDefault()
128
+ holding = true
129
+ usePaintRegionMode.getState().setMode('pick')
130
+ }
131
+ const up = (event: Event) => {
132
+ if ((event as KeyboardEvent).key === 'Alt') release()
133
+ }
134
+ // Escape leaves the eyedropper for the sub-mode it came from, not paint mode.
135
+ const onEscape = (event: Event) => {
136
+ const key = event as KeyboardEvent
137
+ if (key.key !== 'Escape' || !isPaintPicking() || isTypingTarget(key.target)) return
138
+ key.preventDefault()
139
+ key.stopPropagation()
140
+ holding = false
141
+ endPaintPick()
142
+ }
143
+ target.addEventListener('keydown', onEscape, true)
144
+ target.addEventListener('keydown', down)
145
+ target.addEventListener('keyup', up)
146
+ target.addEventListener('blur', release)
147
+ return () => {
148
+ target.removeEventListener('keydown', onEscape, true)
149
+ target.removeEventListener('keydown', down)
150
+ target.removeEventListener('keyup', up)
151
+ target.removeEventListener('blur', release)
152
+ release()
153
+ }
154
+ }
@@ -0,0 +1,166 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNodeId,
4
+ clearSceneHistory,
5
+ useScene,
6
+ WALL_FACE_REGION_LIMIT,
7
+ WallNode,
8
+ ZoneNode,
9
+ } from '@pascal-app/core'
10
+ import { paintHints, paintRegionHints } from '../components/ui/helpers/helper-manager'
11
+ import { paintRegionHovering } from './paint-region-hover'
12
+ import { paintRegionModeActive, paintRegionTargets, usePaintRegionMode } from './paint-region-mode'
13
+ import {
14
+ addFloorRegion,
15
+ addWallRegion,
16
+ removeWallRegion,
17
+ updateWallRegion,
18
+ WALL_REGION_CAP_MESSAGE,
19
+ } from './paint-regions'
20
+ import { snapContextOf } from './snapping-mode'
21
+
22
+ const wall = WallNode.parse({ id: 'wall_regions', start: [0, 0], end: [4, 0], thickness: 0.2 })
23
+ const zone = ZoneNode.parse({
24
+ id: 'zone_regions',
25
+ name: 'Room',
26
+ polygon: [
27
+ [0, 0],
28
+ [4, 0],
29
+ [4, 3],
30
+ [0, 3],
31
+ ],
32
+ })
33
+ const before = useScene.getState()
34
+ beforeEach(() => {
35
+ useScene.setState({ nodes: { [wall.id]: wall, [zone.id]: zone }, materials: {}, readOnly: false })
36
+ useScene.temporal.getState().resume()
37
+ clearSceneHistory()
38
+ })
39
+ afterEach(() => {
40
+ useScene.setState(before)
41
+ usePaintRegionMode.getState().setMode('surface')
42
+ })
43
+ const node = (id: string) => useScene.getState().nodes[id as AnyNodeId] as never
44
+
45
+ describe('paint regions', () => {
46
+ test('a wall region is one undo step, carries only the given bounds, and edits in place', () => {
47
+ const added = addWallRegion(wall.id, 'a', { v1: 0.9 }, { materialPreset: 'library:oak' })
48
+ expect(added.ok).toBe(true)
49
+ const regions = (node(wall.id) as WallNode).faceRegions!
50
+ expect(regions).toHaveLength(1)
51
+ expect(regions[0]).toMatchObject({ face: 'a', v1: 0.9, finish: 'library:oak' })
52
+ expect('u0' in regions[0]!).toBe(false)
53
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
54
+ const id = (added as { id: string }).id
55
+ expect(updateWallRegion(wall.id, id, { v0: 0.2, v1: 1.1 })).toBe(true)
56
+ expect((node(wall.id) as WallNode).faceRegions![0]).toMatchObject({ v0: 0.2, v1: 1.1 })
57
+ expect(removeWallRegion(wall.id, id)).toBe(true)
58
+ expect((node(wall.id) as WallNode).faceRegions).toBeUndefined()
59
+ expect(useScene.temporal.getState().pastStates).toHaveLength(3)
60
+ })
61
+
62
+ test('a one-off colour mints one scene material, reused by the next region', () => {
63
+ const material = { properties: { color: '#ff0000' } } as never
64
+ addWallRegion(wall.id, 'a', { v1: 0.9 }, { material })
65
+ addWallRegion(wall.id, 'b', { v0: 1 }, { material })
66
+ expect(Object.keys(useScene.getState().materials)).toHaveLength(1)
67
+ const [first, second] = (node(wall.id) as WallNode).faceRegions!
68
+ expect(first!.finish).toBe(second!.finish)
69
+ expect(addWallRegion(wall.id, 'a', {}, {})).toEqual({
70
+ ok: false,
71
+ message: 'Pick a material first',
72
+ })
73
+ })
74
+
75
+ test('a full face refuses the next region with the cap message', () => {
76
+ for (let i = 0; i < WALL_FACE_REGION_LIMIT; i++)
77
+ expect(
78
+ addWallRegion(wall.id, 'a', { u0: i * 0.4 }, { materialPreset: 'library:oak' }).ok,
79
+ ).toBe(true)
80
+ expect(addWallRegion(wall.id, 'a', { u0: 3.5 }, { materialPreset: 'library:oak' })).toEqual({
81
+ ok: false,
82
+ message: WALL_REGION_CAP_MESSAGE,
83
+ })
84
+ // The other face still has room.
85
+ expect(addWallRegion(wall.id, 'b', {}, { materialPreset: 'library:oak' }).ok).toBe(true)
86
+ })
87
+
88
+ test('a floor region lands on the room floor in one step', () => {
89
+ const square: [number, number][] = [
90
+ [1, 1],
91
+ [2, 1],
92
+ [2, 2],
93
+ [1, 2],
94
+ ]
95
+ expect(addFloorRegion(zone.id, square, { materialPreset: 'library:tile' }).ok).toBe(true)
96
+ expect((node(zone.id) as ZoneNode).floor?.regions).toEqual([
97
+ expect.objectContaining({ polygon: square, finish: 'library:tile' }),
98
+ ])
99
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
100
+ expect(addFloorRegion(zone.id, square.slice(0, 2), { materialPreset: 'library:tile' }).ok).toBe(
101
+ false,
102
+ )
103
+ })
104
+ })
105
+
106
+ describe('paint region sub-mode', () => {
107
+ test('owns the pointer and the snapping context only inside paint mode', () => {
108
+ expect(paintRegionModeActive('material-paint')).toBe(false)
109
+ usePaintRegionMode.getState().setMode('rectangle')
110
+ expect(paintRegionModeActive('material-paint')).toBe(true)
111
+ expect(paintRegionModeActive('select')).toBe(false)
112
+ const ctx = (mode: string) =>
113
+ snapContextOf({
114
+ scope: { kind: 'idle' },
115
+ mode,
116
+ tool: null,
117
+ profileOf: () => undefined,
118
+ paintRegion: true,
119
+ })
120
+ expect(ctx('material-paint')).toBe('polygon')
121
+ expect(ctx('select')).toBeNull()
122
+ })
123
+
124
+ test('each sub-mode names its surfaces and its HUD gesture', () => {
125
+ expect(paintRegionTargets('rectangle')).toEqual({ wall: true, floor: true })
126
+ expect(paintRegionTargets('polygon')).toEqual({ wall: false, floor: true })
127
+ expect(paintRegionHints('polygon', null).map((hint) => hint.keys[0])).toEqual([
128
+ 'Left click',
129
+ 'Backspace',
130
+ 'Esc',
131
+ ])
132
+ expect(paintRegionHints('rectangle', WALL_REGION_CAP_MESSAGE)[0]).toMatchObject({
133
+ label: WALL_REGION_CAP_MESSAGE,
134
+ active: true,
135
+ })
136
+ })
137
+
138
+ test('off anything it draws on, the HUD says where to go: a floor for Polygon', () => {
139
+ expect(paintRegionHints('polygon', null, false).map((hint) => hint.label)).toEqual([
140
+ 'Hover a floor or ceiling',
141
+ 'Cancel',
142
+ ])
143
+ expect(paintRegionHints('rectangle', null, false)[0]).toMatchObject({
144
+ label: 'Hover a wall, floor or ceiling',
145
+ active: true,
146
+ })
147
+ expect(paintRegionHints('rectangle', null, true)[0]!.keys).toEqual(['Drag'])
148
+ })
149
+
150
+ test('the paint cursor is ready over what the sub-mode draws on, never blocked there', () => {
151
+ expect(paintRegionHovering('rectangle', { wall: true, floor: false })).toBe(true)
152
+ expect(paintRegionHovering('rectangle', { wall: false, floor: true })).toBe(true)
153
+ expect(paintRegionHovering('polygon', { wall: true, floor: false })).toBe(false)
154
+ expect(paintRegionHovering('polygon', { wall: false, floor: true })).toBe(true)
155
+ expect(paintRegionHovering('rectangle', { wall: false, floor: false })).toBe(false)
156
+ })
157
+ })
158
+
159
+ test('the whole-surface HUD says what a click does; only painting teaches the held eyedropper', () => {
160
+ const labels = (mode: Parameters<typeof paintHints>[0]) =>
161
+ paintHints(mode).map((hint) => hint.label)
162
+ expect(labels('surface')).toEqual(['Hold to pick a material'])
163
+ expect(labels('erase')).toEqual(['Remove a painted part, or reset a surface'])
164
+ expect(labels('erase').some((label) => /pick|colour|color|paint with/i.test(label))).toBe(false)
165
+ expect(labels('pick')).toEqual(['Paint with this material', 'Cancel'])
166
+ })