@pascal-app/editor 1.0.4 → 1.0.6

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 (309) hide show
  1. package/README.md +106 -0
  2. package/package.json +40 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/bake-thumbnail.tsx +6 -15
  5. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  6. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  7. package/src/components/editor/custom-camera-controls.tsx +81 -8
  8. package/src/components/editor/editor-layout-mobile.tsx +1 -1
  9. package/src/components/editor/editor-layout-v2.tsx +1 -1
  10. package/src/components/editor/export-manager.tsx +14 -19
  11. package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +22 -5
  13. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  14. package/src/components/editor/first-person/ground-holes.ts +130 -0
  15. package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
  16. package/src/components/editor/first-person/mechanism-targets.ts +52 -0
  17. package/src/components/editor/first-person-controls.tsx +19 -11
  18. package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  20. package/src/components/editor/floorplan-panel.tsx +72 -23
  21. package/src/components/editor/follow-pascal.test.tsx +912 -0
  22. package/src/components/editor/follow-pascal.tsx +354 -0
  23. package/src/components/editor/group-actions.ts +2 -0
  24. package/src/components/editor/group-move-3d.ts +2 -0
  25. package/src/components/editor/group-rotate-handle.tsx +2 -0
  26. package/src/components/editor/group-transform-shared.test.ts +460 -0
  27. package/src/components/editor/index.tsx +166 -79
  28. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  29. package/src/components/editor/room-highlight.tsx +13 -5
  30. package/src/components/editor/room-labels.test.ts +621 -128
  31. package/src/components/editor/room-labels.tsx +1090 -190
  32. package/src/components/editor/selection-manager.tsx +38 -117
  33. package/src/components/editor/slab-hole-highlights.tsx +2 -2
  34. package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
  35. package/src/components/editor/snapshot-capture.test.ts +69 -58
  36. package/src/components/editor/snapshot-capture.ts +7 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +350 -4
  38. package/src/components/editor/thumbnail-generator.tsx +128 -54
  39. package/src/components/editor/use-floorplan-background-placement.ts +3 -1
  40. package/src/components/editor/views/plugins-view.tsx +1 -8
  41. package/src/components/editor/views/view-bar.test.tsx +54 -0
  42. package/src/components/editor/views/view-bar.tsx +140 -17
  43. package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
  44. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  45. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  46. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
  47. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
  49. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  50. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  51. package/src/components/point-ask/bubble.tsx +538 -0
  52. package/src/components/point-ask/hover-chip.tsx +91 -0
  53. package/src/components/point-ask/mode-pill.tsx +92 -0
  54. package/src/components/point-ask/optional-motion.ts +22 -0
  55. package/src/components/point-ask/outline-corners.tsx +93 -0
  56. package/src/components/point-ask/pins.tsx +711 -0
  57. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  58. package/src/components/point-ask/point-ask-styles.ts +105 -0
  59. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  60. package/src/components/point-ask/region-marquee.tsx +64 -0
  61. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  62. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  63. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  64. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  65. package/src/components/tools/distribution/distribution-public.ts +9 -0
  66. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  67. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  68. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  69. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  70. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  71. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  72. package/src/components/tools/distribution/ports.test.ts +56 -0
  73. package/src/components/tools/distribution/ports.ts +378 -0
  74. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  75. package/src/components/tools/distribution/run-cursor.ts +36 -0
  76. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  77. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  78. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  79. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  80. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  81. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  82. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  83. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  84. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  85. package/src/components/tools/item/placement-history.test.tsx +38 -0
  86. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  87. package/src/components/tools/item/placement-strategies.ts +3 -6
  88. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  89. package/src/components/tools/item/use-draft-node.test.tsx +94 -94
  90. package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
  91. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  92. package/src/components/tools/registry/item-surface-move.ts +2 -0
  93. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  94. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
  96. package/src/components/tools/tool-manager.tsx +14 -5
  97. package/src/components/tools/wall/wall-drafting.test.ts +170 -57
  98. package/src/components/ui/action-menu/action-button.tsx +6 -1
  99. package/src/components/ui/action-menu/camera-actions.tsx +7 -3
  100. package/src/components/ui/action-menu/control-modes.tsx +13 -7
  101. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  102. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  103. package/src/components/ui/action-menu/index.tsx +56 -33
  104. package/src/components/ui/action-menu/measurement-control.tsx +11 -9
  105. package/src/components/ui/action-menu/placement.ts +20 -0
  106. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  107. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  108. package/src/components/ui/build-panel/build-panel.tsx +10 -3
  109. package/src/components/ui/command-palette/editor-commands.tsx +3 -12
  110. package/src/components/ui/command-palette/index.tsx +45 -15
  111. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  112. package/src/components/ui/controls/action-button.tsx +5 -2
  113. package/src/components/ui/controls/metric-control.tsx +1 -1
  114. package/src/components/ui/controls/segmented-control.tsx +21 -8
  115. package/src/components/ui/controls/select-control.tsx +1 -1
  116. package/src/components/ui/controls/slider-control.tsx +1 -1
  117. package/src/components/ui/controls/toggle-control.tsx +3 -3
  118. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  119. package/src/components/ui/floating-level-selector.tsx +150 -45
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
  121. package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
  122. package/src/components/ui/helpers/helper-manager.tsx +7 -1
  123. package/src/components/ui/helpers/item-helper.tsx +9 -1
  124. package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
  125. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  126. package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
  127. package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
  128. package/src/components/ui/panels/panel-wrapper.tsx +53 -27
  129. package/src/components/ui/panels/parametric-field-control.tsx +2 -2
  130. package/src/components/ui/panels/parametric-inspector.tsx +3 -3
  131. package/src/components/ui/panels/room-panel.tsx +65 -0
  132. package/src/components/ui/primitives/badge.tsx +8 -0
  133. package/src/components/ui/primitives/sidebar.tsx +1 -1
  134. package/src/components/ui/right-stack.tsx +21 -2
  135. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  136. package/src/components/ui/sidebar/icon-rail.tsx +3 -3
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  139. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  140. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  141. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  142. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  143. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
  144. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  145. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  146. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  147. package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
  148. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
  149. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
  150. package/src/components/ui/sidebar/tab-bar.tsx +4 -4
  151. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  152. package/src/components/viewer/viewer-rooms.tsx +16 -0
  153. package/src/hooks/use-auto-save.integration.test.tsx +96 -38
  154. package/src/hooks/use-auto-save.ts +26 -1
  155. package/src/hooks/use-ceiling-events.test.ts +123 -118
  156. package/src/hooks/use-grid-events.test.ts +105 -105
  157. package/src/hooks/use-keyboard.test.ts +65 -16
  158. package/src/hooks/use-keyboard.ts +29 -12
  159. package/src/hooks/use-placement-notice.test.ts +24 -1
  160. package/src/hooks/use-placement-notice.ts +37 -6
  161. package/src/index.tsx +71 -24
  162. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  163. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  164. package/src/lib/add-storey.test.ts +177 -0
  165. package/src/lib/add-storey.ts +59 -0
  166. package/src/lib/below-grade.test.tsx +171 -0
  167. package/src/lib/below-grade.ts +133 -0
  168. package/src/lib/camera-facing.test.ts +24 -0
  169. package/src/lib/camera-facing.ts +24 -0
  170. package/src/lib/direct-manipulation.ts +2 -0
  171. package/src/lib/download-project-file.ts +17 -0
  172. package/src/lib/editor-level-display.ts +9 -2
  173. package/src/lib/editor-views.ts +35 -1
  174. package/src/lib/export-texture-utils.ts +2 -4
  175. package/src/lib/floor-foundation-support.test.ts +1 -0
  176. package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
  177. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
  178. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
  179. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  180. package/src/lib/floorplan/sheet-export.test.ts +24 -0
  181. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
  182. package/src/lib/floorplan/site-plan/coverage.ts +2 -4
  183. package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
  184. package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
  185. package/src/lib/floorplan/site-plan/geometry.ts +1 -416
  186. package/src/lib/floorplan/site-plan/index.ts +0 -16
  187. package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
  188. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
  189. package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
  190. package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
  191. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  192. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  193. package/src/lib/follow-pascal-exterior.ts +39 -0
  194. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  195. package/src/lib/follow-pascal-presentation.ts +129 -0
  196. package/src/lib/follow-pascal-sight.ts +84 -0
  197. package/src/lib/follow-pascal-targets.test.ts +406 -0
  198. package/src/lib/follow-pascal-targets.ts +198 -0
  199. package/src/lib/follow-pascal.test.ts +350 -0
  200. package/src/lib/follow-pascal.ts +284 -0
  201. package/src/lib/fresh-planar-placement.test.ts +112 -0
  202. package/src/lib/fresh-planar-placement.ts +57 -3
  203. package/src/lib/geometry-script/author.ts +6 -1
  204. package/src/lib/geometry-script/client.ts +8 -1
  205. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  206. package/src/lib/geometry-script/protocol.ts +1 -0
  207. package/src/lib/glb-export.test.ts +187 -162
  208. package/src/lib/glb-export.ts +4 -1
  209. package/src/lib/history.test.ts +12 -7
  210. package/src/lib/interaction/scope.ts +4 -0
  211. package/src/lib/level-selection.test.ts +57 -0
  212. package/src/lib/level-selection.ts +16 -1
  213. package/src/lib/lot/clean-ring.test.ts +2 -1
  214. package/src/lib/lot/drop-in.ts +9 -24
  215. package/src/lib/lot/lot-patch.test.ts +1 -1
  216. package/src/lib/lot/lot-patch.ts +2 -2
  217. package/src/lib/measurement-kind.test.ts +6 -8
  218. package/src/lib/measurements.test.ts +23 -0
  219. package/src/lib/orbit-floor-clearance.test.ts +36 -0
  220. package/src/lib/orbit-floor-clearance.ts +48 -0
  221. package/src/lib/paint-pass-through.test.ts +11 -0
  222. package/src/lib/paint-pass-through.ts +4 -2
  223. package/src/lib/paint-scope.ts +1 -0
  224. package/src/lib/parametric-node-update.ts +16 -7
  225. package/src/lib/plugin-chat.ts +15 -0
  226. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  227. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  228. package/src/lib/point-ask/ask-created.test.ts +195 -0
  229. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  230. package/src/lib/point-ask/build-context.test.ts +173 -0
  231. package/src/lib/point-ask/build-context.ts +94 -0
  232. package/src/lib/point-ask/capture-crop.ts +129 -0
  233. package/src/lib/point-ask/choreography.ts +69 -0
  234. package/src/lib/point-ask/compare.test.ts +145 -0
  235. package/src/lib/point-ask/compare.ts +73 -0
  236. package/src/lib/point-ask/describe-target.test.ts +165 -0
  237. package/src/lib/point-ask/describe-target.ts +234 -0
  238. package/src/lib/point-ask/dictation.ts +79 -0
  239. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  240. package/src/lib/point-ask/edge-marker.ts +27 -0
  241. package/src/lib/point-ask/frame-loop.ts +25 -0
  242. package/src/lib/point-ask/history-marks.test.ts +94 -0
  243. package/src/lib/point-ask/history-marks.ts +39 -0
  244. package/src/lib/point-ask/host-api.ts +32 -0
  245. package/src/lib/point-ask/outline-box.test.ts +30 -0
  246. package/src/lib/point-ask/outline-box.ts +11 -0
  247. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  248. package/src/lib/point-ask/pick.test.ts +220 -0
  249. package/src/lib/point-ask/pick.ts +121 -0
  250. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  251. package/src/lib/point-ask/pin-machine.ts +217 -0
  252. package/src/lib/point-ask/placement.test.ts +203 -0
  253. package/src/lib/point-ask/placement.ts +141 -0
  254. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  255. package/src/lib/point-ask/projector.ts +83 -0
  256. package/src/lib/point-ask/region.test.ts +157 -0
  257. package/src/lib/point-ask/region.ts +136 -0
  258. package/src/lib/point-ask/retry.test.ts +217 -0
  259. package/src/lib/point-ask/retry.ts +37 -0
  260. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  261. package/src/lib/point-ask/scene-commit.ts +32 -0
  262. package/src/lib/point-ask/session.ts +1391 -0
  263. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  264. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  265. package/src/lib/point-ask/spring.test.ts +63 -0
  266. package/src/lib/point-ask/spring.ts +107 -0
  267. package/src/lib/point-ask/suggestions.test.ts +80 -0
  268. package/src/lib/point-ask/suggestions.ts +125 -0
  269. package/src/lib/point-ask/target-scope.test.ts +622 -0
  270. package/src/lib/point-ask/target-scope.ts +338 -0
  271. package/src/lib/point-ask/types.ts +48 -0
  272. package/src/lib/portable-export.test.ts +34 -1
  273. package/src/lib/portable-export.ts +119 -53
  274. package/src/lib/print-feature-thickness.ts +10 -1
  275. package/src/lib/print-roof-solids.test.ts +39 -1
  276. package/src/lib/print-roof-solids.ts +43 -10
  277. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  278. package/src/lib/room-assembly-overlay.ts +2 -3
  279. package/src/lib/room-selection.test.ts +45 -1
  280. package/src/lib/scene-clipboard.test.ts +246 -1
  281. package/src/lib/scene-clipboard.ts +44 -14
  282. package/src/lib/selection-routing.ts +1 -1
  283. package/src/lib/sfx-bus.ts +13 -0
  284. package/src/lib/sfx-player.test.ts +70 -9
  285. package/src/lib/sfx-player.ts +6 -0
  286. package/src/lib/stair-levels.test.ts +25 -0
  287. package/src/lib/stair-levels.ts +22 -3
  288. package/src/lib/terrace-draft.ts +2 -1
  289. package/src/lib/viewer-selection.ts +22 -11
  290. package/src/lib/wall-placement-pointer.test.ts +42 -0
  291. package/src/lib/wall-placement-pointer.ts +57 -0
  292. package/src/lib/window-changes.ts +35 -0
  293. package/src/store/use-audio.test.ts +146 -0
  294. package/src/store/use-audio.tsx +127 -3
  295. package/src/store/use-construction-reveal.test.ts +173 -0
  296. package/src/store/use-construction-reveal.ts +145 -0
  297. package/src/store/use-corner-snap-hint.test.ts +24 -0
  298. package/src/store/use-corner-snap-hint.ts +47 -0
  299. package/src/store/use-follow-pascal.test.ts +62 -0
  300. package/src/store/use-follow-pascal.ts +53 -0
  301. package/src/store/use-point-ask.ts +247 -0
  302. package/src/store/use-stair-build-preview.ts +25 -5
  303. package/src/store/view-layout.test.ts +27 -1
  304. package/src/store/view-layout.ts +4 -1
  305. package/src/test-utils/render-current-markup.tsx +16 -0
  306. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  307. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  308. package/src/components/tools/stair/stair-defaults.ts +0 -15
  309. package/src/components/tools/stair/stair-tool.tsx +0 -682
@@ -0,0 +1,39 @@
1
+ // Counting what an ask's turn wrote in the scene's undo history. The history is bounded (the last
2
+ // 50 steps) and the person can Cmd+Z into it, so a length cannot say how many steps are the turn's:
3
+ // a mark holds the last step standing when the turn began, and the steps since are counted from
4
+ // where that step is now.
5
+
6
+ export type HistoryMark = { ref: unknown | null; length: number }
7
+
8
+ export function markHistory(past: readonly unknown[]): HistoryMark {
9
+ return { ref: past.at(-1) ?? null, length: past.length }
10
+ }
11
+
12
+ /**
13
+ * How many steps were pushed since `mark`, or null when the marked step is gone from the history
14
+ * (it fell off the front, or the person undid back past it): the turn can no longer be undone as a
15
+ * whole.
16
+ */
17
+ export function stepsSince(past: readonly unknown[], mark: HistoryMark): number | null {
18
+ if (mark.ref === null) return past.length
19
+ const at = past.lastIndexOf(mark.ref)
20
+ return at < 0 ? null : past.length - 1 - at
21
+ }
22
+
23
+ /**
24
+ * The history state the turn's first step was taken from: the scene as it stood when the turn began.
25
+ * Null when nothing was pushed since the mark, or the marked step has gone from the history.
26
+ */
27
+ export function stateAfterMark<T>(past: readonly T[], mark: HistoryMark): T | null {
28
+ if (mark.ref === null) return past[0] ?? null
29
+ const at = past.lastIndexOf(mark.ref as T)
30
+ return at < 0 ? null : (past[at + 1] ?? null)
31
+ }
32
+
33
+ /** The nodes in `now` that `before` had no entry for: what an undo of the steps in between will remove. */
34
+ export function createdSince(
35
+ before: Record<string, unknown>,
36
+ now: Record<string, unknown>,
37
+ ): string[] {
38
+ return Object.keys(now).filter((id) => !(id in before))
39
+ }
@@ -0,0 +1,32 @@
1
+ import { usePointAsk } from '../../store/use-point-ask'
2
+ import { chatWentIdle, retryAsk } from './session'
3
+
4
+ // What a host app (the hosted chat) tells Point and ask about itself, without importing the store.
5
+
6
+ /** The host's "Playful touches" setting: false turns the region flash, the ripples and the sounds off. */
7
+ export function setPointAskPlayful(playful: boolean) {
8
+ usePointAsk.getState().setPlayful(playful)
9
+ }
10
+
11
+ /** Pascal is on a turn: the bubble's Send says Queue, and a new ask waits its turn. */
12
+ export function setPointAskBusy(busy: boolean) {
13
+ usePointAsk.getState().setBusy(busy)
14
+ if (!busy) chatWentIdle()
15
+ }
16
+
17
+ /**
18
+ * Messages already wait in the chat's queue (or are held there after an error): a new ask goes
19
+ * behind them, and the bubble says Queue, though Pascal is on no turn.
20
+ */
21
+ export function setPointAskBehindQueue(behindQueue: boolean) {
22
+ usePointAsk.getState().setBehindQueue(behindQueue)
23
+ }
24
+
25
+ /**
26
+ * The chat's Retry on a message that was a Point and ask: the same ask again, with its own pin. False
27
+ * when the editor no longer has the pin (a reload, a collaborator's ask): the chat sends the words
28
+ * again as a plain message.
29
+ */
30
+ export function retryPointAsk(askId: string): Promise<boolean> {
31
+ return retryAsk(askId)
32
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { outlineBox } from './outline-box'
3
+
4
+ // A room is a floor to the person pointing: its corner brackets frame the footprint, not the
5
+ // storey-high box that would leave two of them floating in the sky (seen on a two-storey house).
6
+ describe('outlineBox', () => {
7
+ const tall = {
8
+ min: [0, 0, 0] as [number, number, number],
9
+ max: [6, 4.5, 6] as [number, number, number],
10
+ }
11
+
12
+ test('a room is its footprint at the floor', () => {
13
+ expect(outlineBox('zone', tall)).toEqual({ min: [0, 0, 0], max: [6, 0.02, 6] })
14
+ })
15
+
16
+ test('everything else keeps its own box', () => {
17
+ for (const type of ['wall', 'window', 'door', 'item', 'stair', 'roof']) {
18
+ expect(outlineBox(type, tall)).toEqual(tall)
19
+ }
20
+ })
21
+
22
+ test('a room that already is flat is left as it is', () => {
23
+ const flat = {
24
+ min: [0, 2.55, 0] as [number, number, number],
25
+ max: [4, 2.55, 4] as [number, number, number],
26
+ }
27
+
28
+ expect(outlineBox('zone', flat)).toEqual({ min: [0, 2.55, 0], max: [4, 2.57, 4] })
29
+ })
30
+ })
@@ -0,0 +1,11 @@
1
+ import type { WorldBox } from '../../store/use-point-ask'
2
+
3
+ /**
4
+ * The box the overlay frames for a pointed element: its own world box, except a room, whose box
5
+ * spans the storey but which the person means as a floor. A flat footprint keeps the corner
6
+ * brackets on the ground and the bubble beside the room, not up in the air.
7
+ */
8
+ export function outlineBox(type: string, box: WorldBox): WorldBox {
9
+ if (type !== 'zone') return box
10
+ return { min: [...box.min], max: [box.max[0], box.min[1] + 0.02, box.max[2]] }
11
+ }
@@ -0,0 +1,6 @@
1
+ // Where the editor floats its own chrome over the canvas, so a bubble keeps clear of it.
2
+ export const VIEWPORT_INSET = 12
3
+ /** The top bar and the camera controls strip. */
4
+ export const BUBBLE_TOP_INSET = 56
5
+ /** The action bar at the bottom centre. */
6
+ export const BUBBLE_BOTTOM_INSET = 84
@@ -0,0 +1,220 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode, NodeEvent } from '@pascal-app/core'
3
+ import { type PointPickInput, resolvePointPick } from './pick'
4
+
5
+ // Point and ask (the owner, 8 October): the hover shows exactly what the click sends, by the
6
+ // editor's own predicate. Written first: a wall or floor picking the wall or floor instead of its
7
+ // room; the room still picked once it is the bubble's context; Alt not reaching the surface; a
8
+ // facade seen from outside picking the room behind it; a window picked through its wall; a node of
9
+ // another level picked; the ambiguity line missing where a deeper pick exists.
10
+
11
+ const LEVEL = 'level_0'
12
+ const OTHER_LEVEL = 'level_1'
13
+
14
+ const node = (fields: Record<string, unknown>) => fields as unknown as AnyNode
15
+ const nodes: Record<string, AnyNode> = {
16
+ [LEVEL]: node({ id: LEVEL, type: 'level', parentId: 'building_1' }),
17
+ [OTHER_LEVEL]: node({ id: OTHER_LEVEL, type: 'level', parentId: 'building_1' }),
18
+ building_1: node({ id: 'building_1', type: 'building', parentId: 'site_1' }),
19
+ site_1: node({ id: 'site_1', type: 'site' }),
20
+ zone_kitchen: node({ id: 'zone_kitchen', type: 'zone', parentId: LEVEL }),
21
+ wall_north: node({
22
+ id: 'wall_north',
23
+ type: 'wall',
24
+ parentId: LEVEL,
25
+ start: [0, 0],
26
+ end: [4, 0],
27
+ }),
28
+ slab_kitchen: node({ id: 'slab_kitchen', type: 'slab', parentId: LEVEL }),
29
+ ceiling_kitchen: node({ id: 'ceiling_kitchen', type: 'ceiling', parentId: LEVEL }),
30
+ window_north: node({ id: 'window_north', type: 'window', parentId: 'wall_north' }),
31
+ sofa: node({ id: 'sofa', type: 'item', parentId: LEVEL }),
32
+ wall_up: node({ id: 'wall_up', type: 'wall', parentId: OTHER_LEVEL, start: [0, 0], end: [4, 0] }),
33
+ roof_1: node({ id: 'roof_1', type: 'roof', parentId: LEVEL }),
34
+ roof_segment_1: node({ id: 'roof_segment_1', type: 'roof-segment', parentId: 'roof_1' }),
35
+ }
36
+
37
+ const event = (
38
+ id: string,
39
+ extra: { position?: [number, number, number]; normal?: [number, number, number] } = {},
40
+ ) =>
41
+ ({
42
+ node: nodes[id],
43
+ position: extra.position ?? [2, 1, 0],
44
+ localPosition: [0, 0, 0],
45
+ normal: extra.normal,
46
+ object: {},
47
+ stopPropagation: () => {},
48
+ nativeEvent: {},
49
+ }) as unknown as NodeEvent
50
+
51
+ const KITCHEN = { levelId: LEVEL, zoneId: 'zone_kitchen' }
52
+
53
+ /** A scene where the kitchen is the room at every point south of the north wall (z > 0). */
54
+ const input = (id: string, over: Partial<PointPickInput> = {}): PointPickInput => ({
55
+ event: event(id),
56
+ alt: false,
57
+ selectedTargetIds: [],
58
+ nodes,
59
+ currentLevelId: LEVEL,
60
+ resolveRoom: () => KITCHEN,
61
+ roomAtPoint: (_level, _x, z) => (z > 0 ? KITCHEN : null),
62
+ ...over,
63
+ })
64
+
65
+ describe('room first', () => {
66
+ test('a wall picks its room, and says the wall is a level deeper', () => {
67
+ // The camera is inside: the hit normal points into the kitchen (+z).
68
+ const pick = resolvePointPick(
69
+ input('wall_north', { event: event('wall_north', { normal: [0, 0, 1] }) }),
70
+ )
71
+
72
+ expect(pick).toEqual({
73
+ nodeId: 'zone_kitchen',
74
+ via: 'wall_north',
75
+ face: 'interior',
76
+ ambiguous: true,
77
+ })
78
+ })
79
+
80
+ test('a floor picks its room (face top), a ceiling its room (face bottom)', () => {
81
+ expect(resolvePointPick(input('slab_kitchen'))).toEqual({
82
+ nodeId: 'zone_kitchen',
83
+ via: 'slab_kitchen',
84
+ face: 'top',
85
+ ambiguous: true,
86
+ })
87
+ expect(resolvePointPick(input('ceiling_kitchen'))).toEqual({
88
+ nodeId: 'zone_kitchen',
89
+ via: 'ceiling_kitchen',
90
+ face: 'bottom',
91
+ ambiguous: true,
92
+ })
93
+ })
94
+
95
+ test('a plate or wall with no room behind it picks itself', () => {
96
+ expect(resolvePointPick(input('slab_kitchen', { resolveRoom: () => null }))).toEqual({
97
+ nodeId: 'slab_kitchen',
98
+ face: 'top',
99
+ ambiguous: false,
100
+ })
101
+ })
102
+
103
+ test('once the room is the bubble’s context, the surface itself picks', () => {
104
+ const wall = resolvePointPick(
105
+ input('wall_north', {
106
+ event: event('wall_north', { normal: [0, 0, 1] }),
107
+ selectedTargetIds: ['zone_kitchen'],
108
+ }),
109
+ )
110
+ expect(wall).toEqual({ nodeId: 'wall_north', face: 'interior', ambiguous: false })
111
+
112
+ const floor = resolvePointPick(input('slab_kitchen', { selectedTargetIds: ['zone_kitchen'] }))
113
+ expect(floor).toEqual({ nodeId: 'slab_kitchen', face: 'top', ambiguous: false })
114
+ })
115
+ })
116
+
117
+ describe('Alt goes one level deeper', () => {
118
+ test('to the wall, the floor or the ceiling under the cursor', () => {
119
+ expect(
120
+ resolvePointPick(
121
+ input('wall_north', { alt: true, event: event('wall_north', { normal: [0, 0, 1] }) }),
122
+ ),
123
+ ).toEqual({ nodeId: 'wall_north', face: 'interior', ambiguous: false })
124
+ expect(resolvePointPick(input('slab_kitchen', { alt: true }))).toEqual({
125
+ nodeId: 'slab_kitchen',
126
+ face: 'top',
127
+ ambiguous: false,
128
+ })
129
+ expect(resolvePointPick(input('ceiling_kitchen', { alt: true }))?.nodeId).toBe(
130
+ 'ceiling_kitchen',
131
+ )
132
+ })
133
+ })
134
+
135
+ describe('from outside, “this” is the facade', () => {
136
+ const outside = event('wall_north', { normal: [0, 0, -1], position: [2, 1, 0] })
137
+
138
+ test('an exterior wall face picks the wall, not the room behind it', () => {
139
+ expect(resolvePointPick(input('wall_north', { event: outside }))).toEqual({
140
+ nodeId: 'wall_north',
141
+ face: 'exterior',
142
+ ambiguous: false,
143
+ })
144
+ })
145
+
146
+ test('the same with Alt', () => {
147
+ expect(resolvePointPick(input('wall_north', { event: outside, alt: true }))?.face).toBe(
148
+ 'exterior',
149
+ )
150
+ })
151
+
152
+ test('a wall hit that carries no normal falls back to the editor’s room', () => {
153
+ expect(resolvePointPick(input('wall_north'))?.nodeId).toBe('zone_kitchen')
154
+ })
155
+ })
156
+
157
+ describe('picked directly', () => {
158
+ test('a window, an item and a roof segment pick themselves, with no ambiguity line', () => {
159
+ expect(resolvePointPick(input('window_north'))).toEqual({
160
+ nodeId: 'window_north',
161
+ ambiguous: false,
162
+ })
163
+ expect(resolvePointPick(input('sofa'))).toEqual({ nodeId: 'sofa', ambiguous: false })
164
+ })
165
+
166
+ test('a roof picks the segment under the cursor when the editor can say', () => {
167
+ expect(
168
+ resolvePointPick(
169
+ input('roof_1', { resolveRoofSegment: () => nodes.roof_segment_1 as AnyNode }),
170
+ ),
171
+ ).toEqual({ nodeId: 'roof_segment_1', ambiguous: false })
172
+ expect(resolvePointPick(input('roof_1'))?.nodeId).toBe('roof_1')
173
+ })
174
+
175
+ test('the editor’s selection proxy decides which node a body pick lands on', () => {
176
+ const picked = resolvePointPick(
177
+ input('window_north', { resolveNode: () => nodes.wall_north as AnyNode, alt: true }),
178
+ )
179
+
180
+ expect(picked?.nodeId).toBe('wall_north')
181
+ })
182
+ })
183
+
184
+ describe('what is not pickable', () => {
185
+ test('a node of another level', () => {
186
+ expect(resolvePointPick(input('wall_up'))).toBeNull()
187
+ })
188
+
189
+ test('with no level in view, every level is pickable', () => {
190
+ expect(resolvePointPick(input('wall_up', { currentLevelId: null, alt: true }))?.nodeId).toBe(
191
+ 'wall_up',
192
+ )
193
+ })
194
+
195
+ test('the building and the site, inside a building', () => {
196
+ expect(resolvePointPick(input('building_1'))).toBeNull()
197
+ expect(resolvePointPick(input('site_1'))).toBeNull()
198
+ })
199
+
200
+ test('the building from the site phase', () => {
201
+ expect(resolvePointPick(input('building_1', { phase: 'site', currentLevelId: null }))).toEqual({
202
+ nodeId: 'building_1',
203
+ ambiguous: false,
204
+ })
205
+ })
206
+
207
+ test('a zone outside the zone layer', () => {
208
+ expect(resolvePointPick(input('zone_kitchen'))).toBeNull()
209
+ expect(resolvePointPick(input('zone_kitchen', { zonesPickable: true }))?.nodeId).toBe(
210
+ 'zone_kitchen',
211
+ )
212
+ })
213
+
214
+ test('the ceiling grid, which belongs to the ceiling tool', () => {
215
+ const grid = event('ceiling_kitchen')
216
+ ;(grid.object as { name?: string }).name = 'ceiling-grid'
217
+
218
+ expect(resolvePointPick(input('ceiling_kitchen', { event: grid }))).toBeNull()
219
+ })
220
+ })
@@ -0,0 +1,121 @@
1
+ import { type AnyNode, type NodeEvent, resolveLevelId, type ZoneNode } from '@pascal-app/core'
2
+
3
+ // What a pointer event picks in point mode (the owner, 8 October), by the editor's own predicate
4
+ // (selection-manager.tsx: "hover, press and click share this predicate"), so the hover shows exactly
5
+ // what the click sends. Pure: the editor's room lookups are handed in, so none of this needs a
6
+ // three scene.
7
+
8
+ /** A room, as the editor's room lookup names it (`RoomKey`) or as its zone. */
9
+ type RoomRef = { zoneId: string } | Pick<ZoneNode, 'id'>
10
+
11
+ export type PointPick = {
12
+ /** What the click sends: the room's zone, or the surface itself. */
13
+ nodeId: string
14
+ /** The surface under the cursor when the pick is its room (the wall, floor or ceiling). */
15
+ via?: string
16
+ /** Which face of the picked wall, floor or ceiling the finger is on. */
17
+ face?: 'interior' | 'exterior' | 'top' | 'bottom'
18
+ /** A deeper pick exists (Alt): the label's second line says "⌥ the wall". */
19
+ ambiguous: boolean
20
+ }
21
+
22
+ export type PointPickInput = {
23
+ event: NodeEvent
24
+ alt: boolean
25
+ /** The bubble's targets, so a room that is already the context lets its surfaces pick. */
26
+ selectedTargetIds: readonly string[]
27
+ nodes: Readonly<Record<string, AnyNode | undefined>>
28
+ /** The level in view; null picks on every level. */
29
+ currentLevelId: string | null
30
+ phase?: 'site' | 'building'
31
+ /** Zones pick directly only in the zone layer; elsewhere they are reached through their surfaces. */
32
+ zonesPickable?: boolean
33
+ /** The editor's room for a wall, floor or ceiling hit (`roomForEvent`), null when it bounds none. */
34
+ resolveRoom: (event: NodeEvent) => RoomRef | null
35
+ /** The room standing at a plan point of a level, for the side of a wall the viewer is on. */
36
+ roomAtPoint: (levelId: string, x: number, z: number) => RoomRef | null
37
+ /** The roof segment under the cursor (`resolveRoofSegmentSelectionTarget`). */
38
+ resolveRoofSegment?: (event: NodeEvent) => AnyNode | null
39
+ /** The node a body pick lands on once the editor's selection proxies are applied
40
+ * (`resolveCanvasSelectionNode`). */
41
+ resolveNode?: (node: AnyNode) => AnyNode
42
+ }
43
+
44
+ /** How far out of a wall, along the hit normal, the viewer's side is probed for a room (m). */
45
+ const FACE_PROBE_M = 0.15
46
+
47
+ const zoneIdOf = (room: RoomRef) => ('zoneId' in room ? room.zoneId : room.id)
48
+
49
+ /** A ceiling's own grid belongs to the ceiling tool, not to a pick. */
50
+ const isCeilingGridHit = (event: NodeEvent) =>
51
+ !event.viaHandle &&
52
+ event.node.type === 'ceiling' &&
53
+ (event.object as { name?: string } | undefined)?.name === 'ceiling-grid'
54
+
55
+ export function resolvePointPick(input: PointPickInput): PointPick | null {
56
+ const { event, nodes } = input
57
+ if (isCeilingGridHit(event)) return null
58
+
59
+ let node = input.resolveNode?.(event.node) ?? event.node
60
+ if (node.type === 'roof') node = input.resolveRoofSegment?.(event) ?? node
61
+
62
+ // The building and the site are not pointed at from inside a building (the select hover
63
+ // ignores them there too): ground and sky are an area, not a target.
64
+ if (node.type === 'building' || node.type === 'site') {
65
+ if ((input.phase ?? 'building') !== 'site') return null
66
+ return { nodeId: node.id, ambiguous: false }
67
+ }
68
+ if (node.type === 'zone' && !input.zonesPickable) return null
69
+ // Elevators are building-scoped and stay pickable across level filters.
70
+ if (input.currentLevelId && node.type !== 'elevator') {
71
+ if (resolveLevelId(node, nodes as Record<string, AnyNode>) !== input.currentLevelId) return null
72
+ }
73
+
74
+ const levelId = input.currentLevelId ?? resolveLevelId(node, nodes as Record<string, AnyNode>)
75
+
76
+ if (node.type === 'wall') return pickWall(input, node, levelId)
77
+ if (node.type === 'slab' || node.type === 'ceiling') return pickPlate(input, node)
78
+
79
+ return { nodeId: node.id, ambiguous: false }
80
+ }
81
+
82
+ function pickWall(input: PointPickInput, wall: AnyNode, levelId: string): PointPick {
83
+ const { event } = input
84
+ // From outside "this" means the facade: the side the viewer is on has no room, so the wall
85
+ // itself is picked, not the room behind it. A hit with no normal cannot say, so the editor's
86
+ // own room-first rule decides.
87
+ let face: 'interior' | 'exterior' | null = null
88
+ let viewerRoom: RoomRef | null = null
89
+ if (event.normal) {
90
+ viewerRoom = input.roomAtPoint(
91
+ levelId,
92
+ event.position[0] + event.normal[0] * FACE_PROBE_M,
93
+ event.position[2] + event.normal[2] * FACE_PROBE_M,
94
+ )
95
+ face = viewerRoom ? 'interior' : 'exterior'
96
+ }
97
+ const surface = (): PointPick => ({
98
+ nodeId: wall.id,
99
+ ...(face ? { face } : {}),
100
+ ambiguous: false,
101
+ })
102
+ if (face === 'exterior' || input.alt) return surface()
103
+
104
+ const room = viewerRoom ?? input.resolveRoom(event)
105
+ if (!room) return surface()
106
+ const zoneId = zoneIdOf(room)
107
+ if (input.selectedTargetIds.includes(zoneId)) return surface()
108
+ return { nodeId: zoneId, via: wall.id, ...(face ? { face } : {}), ambiguous: true }
109
+ }
110
+
111
+ function pickPlate(input: PointPickInput, plate: AnyNode): PointPick {
112
+ const face = plate.type === 'ceiling' ? 'bottom' : 'top'
113
+ const surface = (): PointPick => ({ nodeId: plate.id, face, ambiguous: false })
114
+ if (input.alt) return surface()
115
+
116
+ const room = input.resolveRoom(input.event)
117
+ if (!room) return surface()
118
+ const zoneId = zoneIdOf(room)
119
+ if (input.selectedTargetIds.includes(zoneId)) return surface()
120
+ return { nodeId: zoneId, via: plate.id, face, ambiguous: true }
121
+ }