@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,83 @@
1
+ 'use client'
2
+
3
+ import {
4
+ emitter,
5
+ type PointAskStatus,
6
+ registerPointAskHandler,
7
+ useHasPointAskHandler,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { subscribeRevealEvents, useViewer } from '@pascal-app/viewer'
11
+ import { useEffect } from 'react'
12
+ import { startPointAskSession } from '../../lib/point-ask/session'
13
+ import { usePointAsk } from '../../store/use-point-ask'
14
+ import { PointAskBubble } from './bubble'
15
+ import { HoverChip } from './hover-chip'
16
+ import { BeforeTag, ModePill, ModeVignette } from './mode-pill'
17
+ import { usePointMotionCalm } from './optional-motion'
18
+ import { OutlineCorners } from './outline-corners'
19
+ import { PointAskPins } from './pins'
20
+ import { POINT_ASK_CSS } from './point-ask-styles'
21
+ import { RegionMarquee } from './region-marquee'
22
+ import { REGION_MARQUEE_CSS } from './region-marquee-styles'
23
+
24
+ const CALM_CSS = `
25
+ .pa-layer[data-calm] .pa-hlabel{transform:none;transition-property:opacity}
26
+ .pa-layer[data-calm] .pa-send,.pa-layer[data-calm] .pa-dict,.pa-layer[data-calm] .pa-btn,.pa-layer[data-calm] .pa-hold{transform:none;transition-property:opacity,color,background}
27
+ .pa-layer[data-calm] .pa-pk,.pa-layer[data-calm] .pa-edgemark{transition:none}
28
+ `
29
+
30
+ /**
31
+ * Point and ask's overlay (the owner, 8 October): the outline's corners, the label chip, the
32
+ * anchored bubble and the pins, drawn over the 3D view and following its camera. It renders only
33
+ * where a host has registered a handler for asks (the open-source editor has no chat), though pins
34
+ * already planted stay until they retire.
35
+ */
36
+ export function PointAskLayer() {
37
+ const available = useHasPointAskHandler()
38
+ const hasPins = usePointAsk((s) => s.pinOrder.length > 0)
39
+ const active = usePointAsk((s) => s.active)
40
+ const region = usePointAsk((s) => s.region)
41
+ const calm = usePointMotionCalm()
42
+
43
+ useEffect(() => startPointAskSession(), [])
44
+
45
+ // A test door in development only, like the camera controls': a page script registers a fake
46
+ // chat and plays its statuses, so the scene side can be driven without a model.
47
+ useEffect(() => {
48
+ if (process.env.NODE_ENV !== 'development') return
49
+ const w = window as typeof window & { __pascalPointAskTest?: unknown }
50
+ w.__pascalPointAskTest = {
51
+ register: registerPointAskHandler,
52
+ status: (status: PointAskStatus) => emitter.emit('point-ask:status', status),
53
+ store: usePointAsk,
54
+ scene: useScene,
55
+ emitter,
56
+ viewer: useViewer,
57
+ reveal: subscribeRevealEvents,
58
+ }
59
+ return () => {
60
+ w.__pascalPointAskTest = undefined
61
+ }
62
+ }, [])
63
+
64
+ if (!(available || hasPins || active)) return null
65
+ return (
66
+ <div className="pa-layer" data-calm={calm || undefined} data-point-ask-layer>
67
+ <style>{POINT_ASK_CSS + REGION_MARQUEE_CSS + CALM_CSS}</style>
68
+ <ModeVignette />
69
+ <OutlineCorners />
70
+ <RegionMarquee
71
+ flashKey={region?.flashKey ?? 0}
72
+ rect={region?.rect ?? null}
73
+ settled={region?.settled ?? false}
74
+ sizeLabel={region?.sizeLabel ?? null}
75
+ />
76
+ <PointAskPins />
77
+ <PointAskBubble />
78
+ <HoverChip />
79
+ <ModePill />
80
+ <BeforeTag />
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,105 @@
1
+ // The look of Point and ask: one stylesheet for the whole overlay, its tokens and moments taken
2
+ // from the chat prototype the owner approved (chat-prototype/pascal-builds.html). Every class is
3
+ // prefixed `pa-`; motion that follows the camera is set from script, the rest is declared here.
4
+ export const POINT_ASK_CSS = `
5
+ .pa-layer{--pa-accent:#a99bff;--pa-ok:#79e2b0;--pa-warm:#f6b866;--pa-bad:#ff8a80;--pa-tx:#edece9;--pa-tx2:#a7a7ae;--pa-tx3:#73747c;--pa-ink:#16171b;--pa-ease:cubic-bezier(.23,1,.32,1);
6
+ position:fixed;inset:0;pointer-events:none;z-index:45;color:var(--pa-tx);font-size:13px;line-height:1.35;-webkit-font-smoothing:antialiased}
7
+ .pa-layer *{box-sizing:border-box}
8
+ :where(.pa-layer) button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
9
+ .pa-vignette{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity 150ms var(--pa-ease);box-shadow:inset 0 0 90px 6px rgba(169,155,255,.16),inset 0 0 0 1.5px rgba(169,155,255,.26)}
10
+ .pa-vignette.on{opacity:1;transition-duration:180ms}
11
+ .pa-pill{position:absolute;left:50%;top:64px;transform:translateX(-50%);padding:6px 14px;border-radius:999px;background:rgba(19,20,24,.82);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%);box-shadow:inset 0 0 0 1px rgba(255,255,255,.09),0 10px 28px -12px rgba(0,0,0,.7);white-space:nowrap;font-weight:500;color:var(--pa-tx2)}
12
+ .pa-pill b{color:var(--pa-tx);font-weight:600}
13
+ .pa-fc{position:absolute;left:0;top:0;width:12px;height:12px;border:0 solid var(--pa-accent);opacity:0;transition:opacity 120ms ease;filter:drop-shadow(0 0 3px rgba(169,155,255,.6));will-change:transform}
14
+ .pa-fc.tl{border-width:2px 0 0 2px;border-top-left-radius:4px;margin:-3px 0 0 -3px}
15
+ .pa-fc.tr{border-width:2px 2px 0 0;border-top-right-radius:4px;margin:-3px 0 0 -9px}
16
+ .pa-fc.bl{border-width:0 0 2px 2px;border-bottom-left-radius:4px;margin:-9px 0 0 -3px}
17
+ .pa-fc.br{border-width:0 2px 2px 0;border-bottom-right-radius:4px;margin:-9px 0 0 -9px}
18
+ .pa-chipwrap{position:absolute;left:0;top:0;will-change:transform}
19
+ .pa-hlabel{padding:4px 10px;border-radius:12px;background:rgba(18,19,23,.9);box-shadow:inset 0 0 0 1px rgba(255,255,255,.1);font-weight:600;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap;opacity:0;transform:translateY(4px);transition:opacity 120ms ease,transform 180ms var(--pa-ease)}
20
+ .pa-hlabel.show{opacity:1;transform:none}
21
+ .pa-hlabel.press{transform:scale(.97);transition-duration:120ms}
22
+ .pa-hlabel .sub{display:block;font-weight:500;font-size:11px;color:var(--pa-tx3)}
23
+ .pa-hlabel .sz{color:var(--pa-tx2);font-weight:500;margin-left:6px}
24
+ .pa-bwrap{position:absolute;left:0;top:0;pointer-events:auto;will-change:transform}
25
+ .pa-bub{position:relative;width:320px;padding:12px;border-radius:14px;background:rgba(32,33,39,.95);backdrop-filter:blur(18px) saturate(150%);-webkit-backdrop-filter:blur(18px) saturate(150%);box-shadow:inset 0 1px 0 rgba(255,255,255,.08),inset 0 0 0 1px rgba(255,255,255,.06),0 18px 40px -16px rgba(0,0,0,.75)}
26
+ .pa-tail{position:absolute;left:0;top:0;width:12px;height:12px;border-radius:2px;background:rgba(32,33,39,.95)}
27
+ .pa-ctx{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:10px;cursor:grab;position:relative}
28
+ .pa-ctx:active{cursor:grabbing}
29
+ .pa-thumb{width:40px;height:40px;border-radius:8px;object-fit:cover;margin-left:auto;background:#0f1013}
30
+ .pa-nopic{margin-left:auto;font-size:11px;color:var(--pa-tx3);font-weight:500}
31
+ .pa-tchip{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 6px 0 8px;border-radius:999px;background:rgba(169,155,255,.16);color:#d9d2ff;font-weight:600;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap;max-width:210px}
32
+ .pa-tchip i{width:6px;height:6px;border-radius:50%;background:var(--pa-accent);flex:none}
33
+ .pa-tchip span{overflow:hidden;text-overflow:ellipsis}
34
+ .pa-tchip .tx{width:16px;height:16px;display:grid;place-items:center;border-radius:50%;color:#cfc7ff;flex:none}
35
+ .pa-tchip .tx:hover{background:rgba(255,255,255,.12)}
36
+ .pa-tchip .tx svg{width:10px;height:10px}
37
+ .pa-field{display:flex;align-items:center;gap:6px;padding:4px 4px 4px 12px;border-radius:999px;background:rgba(255,255,255,.06)}
38
+ .pa-field input{flex:1;min-width:0;height:30px;font:inherit;font-size:14px;color:var(--pa-tx);background:none;border:0;outline:0}
39
+ .pa-field input::placeholder{color:var(--pa-tx3)}
40
+ .pa-send{height:30px;min-width:30px;border-radius:999px;background:#edece9;color:var(--pa-ink);display:grid;place-items:center;transition:transform 120ms var(--pa-ease),opacity 160ms ease}
41
+ .pa-send:active{transform:scale(.97)}
42
+ .pa-send:disabled{opacity:.4;cursor:default}
43
+ .pa-send svg{width:15px;height:15px}
44
+ .pa-send.queue{padding:0 12px;font-weight:600;font-size:13px}
45
+ .pa-dict{width:30px;height:30px;border-radius:50%;color:var(--pa-tx2);display:grid;place-items:center;transition:transform 120ms var(--pa-ease),color 160ms ease,background 160ms ease}
46
+ .pa-dict:hover{color:var(--pa-tx);background:rgba(255,255,255,.08)}
47
+ .pa-dict:active{transform:scale(.97)}
48
+ .pa-dict.on{color:var(--pa-ink);background:var(--pa-accent)}
49
+ .pa-dict svg{width:16px;height:16px}
50
+ .pa-sugs{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
51
+ .pa-btn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 12px;border-radius:999px;background:rgba(255,255,255,.08);color:var(--pa-tx);font-weight:500;font-size:12.5px;white-space:nowrap;transition:transform 120ms var(--pa-ease),background 160ms ease}
52
+ .pa-btn:hover{background:rgba(255,255,255,.13)}
53
+ .pa-btn:active{transform:scale(.97)}
54
+ .pa-btn.primary{background:#edece9;color:var(--pa-ink)}
55
+ .pa-btn.primary:hover{background:#fff}
56
+ .pa-btn.ghost{background:transparent;box-shadow:inset 0 0 0 1px rgba(255,255,255,.14)}
57
+ .pa-err{margin:8px 2px 0;font-size:12px;color:var(--pa-warm);display:flex;gap:8px;align-items:center;flex-wrap:wrap}
58
+ .pa-pin{position:absolute;left:0;top:0;width:32px;height:32px;margin:-38px 0 0 -16px;pointer-events:auto;cursor:pointer;display:grid;place-items:center;will-change:transform}
59
+ .pa-pin::after{content:"";position:absolute;left:50%;top:28px;width:2px;height:8px;margin-left:-1px;border-radius:1px;background:var(--pa-accent);opacity:.85}
60
+ .pa-pdisc{width:24px;height:24px;border-radius:50%;background:rgba(24,25,30,.92);box-shadow:inset 0 0 0 1.5px rgba(169,155,255,.9),0 6px 16px -6px rgba(0,0,0,.7);display:grid;place-items:center;transition:box-shadow 240ms ease}
61
+ .pa-pdisc svg{width:20px;height:20px;overflow:visible}
62
+ .pa-pq,.pa-pa{fill:none;stroke:#a99bff;stroke-width:1.8;opacity:0;transition:opacity 160ms ease}
63
+ .pa-pq{stroke-dasharray:2.4 2.4}
64
+ .pa-pa{stroke-width:2;stroke-linecap:round}
65
+ .pa-pk{fill:none;stroke:var(--pa-ok);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:14;stroke-dashoffset:14;transition:stroke-dashoffset 240ms var(--pa-ease) 80ms}
66
+ .pa-sg{fill:none;stroke:var(--pa-accent);stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;opacity:0;transition:opacity 160ms ease}
67
+ .pa-pin.queued .pa-pq,.pa-pin.working .pa-pa{opacity:1}
68
+ .pa-pin.needs-input .pa-pdisc{box-shadow:inset 0 0 0 1.5px rgba(246,184,102,.95),0 0 14px 1px rgba(246,184,102,.45),0 6px 16px -6px rgba(0,0,0,.7)}
69
+ .pa-pin.needs-input .pa-pa{opacity:1;stroke:var(--pa-warm)}
70
+ .pa-pin.done .pa-pk{stroke-dashoffset:0}
71
+ .pa-pin.done .pa-pdisc{box-shadow:inset 0 0 0 1.5px rgba(121,226,176,.9),0 6px 16px -6px rgba(0,0,0,.7)}
72
+ .pa-pin.done::after{background:var(--pa-ok)}
73
+ .pa-pin.answered .pa-sg{opacity:1}
74
+ .pa-pin.no-change .pa-pdisc{box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.35),0 6px 16px -6px rgba(0,0,0,.7)}
75
+ .pa-pin.no-change .pa-pk{stroke:var(--pa-tx3);stroke-dashoffset:0;opacity:.8}
76
+ .pa-pin.no-change::after{background:var(--pa-tx3)}
77
+ .pa-pin.error .pa-pdisc{box-shadow:inset 0 0 0 1.5px rgba(255,138,128,.95),0 6px 16px -6px rgba(0,0,0,.7)}
78
+ .pa-pin.error::after{background:var(--pa-bad)}
79
+ .pa-pin.undone .pa-pq{opacity:.7;stroke:var(--pa-tx3)}
80
+ .pa-pin.undone .pa-pdisc{box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.25),0 6px 16px -6px rgba(0,0,0,.7)}
81
+ .pa-pin.undone::after{background:var(--pa-tx3)}
82
+ .pa-pin.edge{pointer-events:auto}
83
+ .pa-ring{position:absolute;left:4px;top:4px;width:24px;height:24px;border-radius:50%;box-shadow:0 0 0 1.5px var(--pa-accent);opacity:0;pointer-events:none}
84
+ .pa-edgemark{position:absolute;left:0;top:0;display:flex;align-items:center;gap:6px;height:28px;padding:0 12px 0 8px;border-radius:999px;background:rgba(19,20,24,.88);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%);box-shadow:inset 0 0 0 1px rgba(169,155,255,.45),0 8px 22px -10px rgba(0,0,0,.7);color:var(--pa-tx);font-weight:600;font-size:12px;white-space:nowrap;pointer-events:auto;cursor:pointer;will-change:transform;transition:transform 120ms var(--pa-ease)}
85
+ .pa-edgemark svg{width:14px;height:14px;color:var(--pa-accent);flex:none}
86
+ .pa-cardwrap{position:absolute;left:0;top:0;pointer-events:auto;will-change:transform}
87
+ .pa-card{width:250px;padding:10px;border-radius:14px;background:rgba(30,31,37,.95);backdrop-filter:blur(18px) saturate(150%);-webkit-backdrop-filter:blur(18px) saturate(150%);box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 18px 40px -16px rgba(0,0,0,.75);transform-origin:50% 100%}
88
+ .pa-card-t{font-weight:600;font-size:13.5px;line-height:1.3}
89
+ .pa-card-s{margin-top:2px;font-size:12px;color:var(--pa-tx2)}
90
+ .pa-card-a{margin-top:6px;font-size:12px;color:var(--pa-tx2);text-decoration:underline;text-decoration-color:rgba(255,255,255,.2);text-underline-offset:2px;cursor:default}
91
+ .pa-card-row{display:flex;gap:6px;margin-top:8px}
92
+ .pa-card-head{display:flex;align-items:center;gap:10px}
93
+ .pa-card-head .pa-card-t{flex:1;min-width:0}
94
+ .pa-card-thumb{flex:none;width:64px;height:48px;border-radius:8px;overflow:hidden;background:#0f1013;box-shadow:inset 0 0 0 1px rgba(255,255,255,.08)}
95
+ .pa-card-thumb img{width:100%;height:100%;object-fit:cover;display:block}
96
+ .pa-hold{display:flex;align-items:center;justify-content:center;width:100%;height:28px;margin-top:8px;border-radius:999px;box-shadow:inset 0 0 0 1px rgba(255,255,255,.14);color:var(--pa-tx2);font-weight:500;font-size:12.5px;touch-action:none;user-select:none;-webkit-user-select:none;transition:background 160ms ease,color 160ms ease,transform 120ms var(--pa-ease)}
97
+ .pa-hold:hover{background:rgba(255,255,255,.06);color:var(--pa-tx)}
98
+ .pa-hold:active,.pa-hold.on{transform:scale(.98)}
99
+ .pa-hold.on{background:var(--pa-accent);color:var(--pa-ink);box-shadow:none}
100
+ .pa-before{position:fixed;display:flex;align-items:center;gap:7px;padding:5px 12px 5px 10px;border-radius:999px;background:rgba(19,20,24,.82);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%);box-shadow:inset 0 0 0 1px rgba(255,255,255,.09),0 10px 28px -12px rgba(0,0,0,.7);font-weight:600;color:var(--pa-tx);pointer-events:none}
101
+ .pa-before i{width:7px;height:7px;border-radius:50%;background:var(--pa-warm)}
102
+ .pa-edge{position:absolute;pointer-events:auto}
103
+ @media (prefers-reduced-transparency:reduce){.pa-bub,.pa-card,.pa-pill{background:#1c1d22;backdrop-filter:none;-webkit-backdrop-filter:none}.pa-tail{background:#1c1d22}}
104
+ @media (prefers-contrast:more){.pa-bub,.pa-card{box-shadow:inset 0 0 0 1px rgba(255,255,255,.5)}}
105
+ `
@@ -0,0 +1,15 @@
1
+ // The region marquee's look (spec 2.3, Region): the capture overlay's corner accents on a thin
2
+ // accent border with a faint fill, the live size pinned inside its bottom-right, drawn back to 60%
3
+ // once the person has let go. Uses the layer's `--pa-*` tokens; append it to POINT_ASK_CSS.
4
+ export const REGION_MARQUEE_CSS = `
5
+ .pa-rgn{position:fixed;left:0;top:0;pointer-events:none;border:1.5px solid var(--pa-accent);border-radius:3px;background:rgba(169,155,255,.07);transition:opacity 160ms ease;will-change:transform,width,height}
6
+ .pa-rgn.settled{opacity:.6}
7
+ .pa-rgn-c{position:absolute;width:14px;height:14px;border:0 solid var(--pa-accent);filter:drop-shadow(0 0 3px rgba(169,155,255,.5))}
8
+ .pa-rgn-c.tl{left:-3px;top:-3px;border-width:2.5px 0 0 2.5px;border-top-left-radius:5px}
9
+ .pa-rgn-c.tr{right:-3px;top:-3px;border-width:2.5px 2.5px 0 0;border-top-right-radius:5px}
10
+ .pa-rgn-c.bl{left:-3px;bottom:-3px;border-width:0 0 2.5px 2.5px;border-bottom-left-radius:5px}
11
+ .pa-rgn-c.br{right:-3px;bottom:-3px;border-width:0 2.5px 2.5px 0;border-bottom-right-radius:5px}
12
+ .pa-rgn-size{position:absolute;right:6px;bottom:6px;padding:2px 8px;border-radius:999px;background:rgba(18,19,23,.85);font-weight:600;font-size:11.5px;font-variant-numeric:tabular-nums;color:var(--pa-tx);white-space:nowrap}
13
+ .pa-rgn-size:empty{display:none}
14
+ .pa-rgn-flash{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none;border-radius:inherit}
15
+ `
@@ -0,0 +1,64 @@
1
+ 'use client'
2
+
3
+ import { prefersReducedMotion } from '@pascal-app/core'
4
+ import { useEffect, useRef } from 'react'
5
+ import { EASE_OUT, REGION_FLASH_MS } from '../../lib/point-ask/choreography'
6
+ import type { Rect } from '../../lib/point-ask/types'
7
+ import { usePointAsk } from '../../store/use-point-ask'
8
+
9
+ const CORNERS = ['tl', 'tr', 'bl', 'br'] as const
10
+
11
+ type RegionMarqueeProps = {
12
+ /** The rectangle in viewport pixels, or null when no region is being drawn. */
13
+ rect: Rect | null
14
+ /** The live size inside the rectangle ("4.2 × 3.1 m"), or null while it cannot be measured. */
15
+ sizeLabel: string | null
16
+ /** The person let go: the rectangle stays at 60% until the ask is sent or dismissed. */
17
+ settled: boolean
18
+ /** Changes when a capture happens: the shutter flash fills the rectangle once. */
19
+ flashKey: number
20
+ }
21
+
22
+ /**
23
+ * The rectangle of a region capture (the owner's "the marquee shows its live size in metres; on
24
+ * release a shutter flash fills the rectangle"). It follows the pointer 1:1, with no spring: a
25
+ * trailing rectangle would read as latency. The flash is a "Playful touches" moment; under
26
+ * reduced motion it is a quiet fade.
27
+ */
28
+ export function RegionMarquee({ rect, sizeLabel, settled, flashKey }: RegionMarqueeProps) {
29
+ const flash = useRef<HTMLDivElement>(null)
30
+ const seen = useRef(flashKey)
31
+ const playful = usePointAsk((s) => s.playful)
32
+
33
+ useEffect(() => {
34
+ if (flashKey === seen.current) return
35
+ seen.current = flashKey
36
+ const el = flash.current
37
+ if (!(el && playful)) return
38
+ const peak = prefersReducedMotion() ? 0.3 : 0.9
39
+ el.animate([{ opacity: peak }, { opacity: 0 }], {
40
+ duration: REGION_FLASH_MS,
41
+ easing: EASE_OUT,
42
+ fill: 'forwards',
43
+ })
44
+ }, [flashKey, playful])
45
+
46
+ if (!rect) return null
47
+ return (
48
+ <div
49
+ aria-hidden
50
+ className={`pa-rgn${settled ? ' settled' : ''}`}
51
+ style={{
52
+ transform: `translate3d(${rect.x0}px,${rect.y0}px,0)`,
53
+ width: rect.x1 - rect.x0,
54
+ height: rect.y1 - rect.y0,
55
+ }}
56
+ >
57
+ {CORNERS.map((corner) => (
58
+ <span className={`pa-rgn-c ${corner}`} key={corner} />
59
+ ))}
60
+ <div className="pa-rgn-flash" ref={flash} />
61
+ <div className="pa-rgn-size">{sizeLabel}</div>
62
+ </div>
63
+ )
64
+ }
@@ -9,7 +9,9 @@ import { useViewer } from '@pascal-app/viewer'
9
9
  import { useEffect, useState } from 'react'
10
10
  import { useHandleGroup } from '../../store/use-handle-group'
11
11
  import { PanelSection } from '../ui/controls/panel-section'
12
+ import { SegmentedControl } from '../ui/controls/segmented-control'
12
13
  import { SliderControl } from '../ui/controls/slider-control'
14
+ import { ToggleControl } from '../ui/controls/toggle-control'
13
15
  import { PanelWrapper } from '../ui/panels/panel-wrapper'
14
16
  export function ProceduralInspector({
15
17
  nodeId,
@@ -75,21 +77,44 @@ export function ProceduralInspector({
75
77
  )}
76
78
  </div>
77
79
  <div className="space-y-3">
78
- {fields.map((p) => (
79
- <SliderControl
80
- key={p.id}
81
- label={p.label}
82
- value={node.parameters[p.id] ?? p.default}
83
- min={p.min}
84
- max={p.max}
85
- step={p.step}
86
- precision={3}
87
- unit={p.unit === 'm' ? 'm' : ''}
88
- onChange={(v) => change(p.id, v, false)}
89
- onCommit={(v) => change(p.id, v, true)}
90
- restoreOnCommit={false}
91
- />
92
- ))}
80
+ {fields.map((p) => {
81
+ const value = node.parameters[p.id] ?? p.default
82
+ if (p.unit === 'bool')
83
+ return (
84
+ <ToggleControl
85
+ key={p.id}
86
+ label={p.label}
87
+ checked={value === 1}
88
+ onChange={(checked) => change(p.id, checked ? 1 : 0, true)}
89
+ />
90
+ )
91
+ if (p.unit === 'choice' && p.options && p.options.length <= 4)
92
+ return (
93
+ <div key={p.id} className="space-y-1.5">
94
+ <div className="text-muted-foreground text-xs">{p.label}</div>
95
+ <SegmentedControl
96
+ value={String(value)}
97
+ options={p.options.map((label, index) => ({ label, value: String(index) }))}
98
+ onChange={(index) => change(p.id, Number(index), true)}
99
+ />
100
+ </div>
101
+ )
102
+ return (
103
+ <SliderControl
104
+ key={p.id}
105
+ label={p.unit === 'choice' ? `${p.label}: ${p.options?.[value] ?? value}` : p.label}
106
+ value={value}
107
+ min={p.min}
108
+ max={p.max}
109
+ step={p.step}
110
+ precision={p.unit === 'choice' ? 0 : 3}
111
+ unit={p.unit === 'm' ? 'm' : ''}
112
+ onChange={(v) => change(p.id, v, false)}
113
+ onCommit={(v) => change(p.id, v, true)}
114
+ restoreOnCommit={false}
115
+ />
116
+ )
117
+ })}
93
118
  </div>
94
119
  {partId && (
95
120
  <button
@@ -10,7 +10,8 @@ import {
10
10
  StaticDrawUsage,
11
11
  Vector3,
12
12
  } from 'three'
13
- import Attributes from 'three/src/renderers/common/Attributes.js'
13
+ // @ts-expect-error -- @types/three 0.186 no longer declares this renderer internal
14
+ import UntypedAttributes from 'three/src/renderers/common/Attributes.js'
14
15
  import { AttributeType } from 'three/src/renderers/common/Constants.js'
15
16
  import Info from 'three/src/renderers/common/Info.js'
16
17
  import {
@@ -25,6 +26,15 @@ import {
25
26
  growBracketCapacity,
26
27
  } from './ceiling-bracket-batch'
27
28
 
29
+ type AttributesBackend = {
30
+ createAttribute(): void
31
+ updateAttribute(attribute: BufferAttribute): void
32
+ }
33
+ const Attributes = UntypedAttributes as new (
34
+ backend: AttributesBackend,
35
+ info: Info,
36
+ ) => { update(attribute: BufferAttribute, type: AttributeType): void }
37
+
28
38
  const polygon: Array<[number, number]> = [
29
39
  [0, 0],
30
40
  [4, 0],
@@ -365,7 +375,7 @@ test('WebGPU attribute updates skip resting frames and upload only written slots
365
375
  uploads.push({ attribute, ranges: attribute.updateRanges.map((range) => ({ ...range })) })
366
376
  attribute.clearUpdateRanges()
367
377
  },
368
- } as unknown as ConstructorParameters<typeof Attributes>[0],
378
+ },
369
379
  new Info(),
370
380
  )
371
381
  const render = () => {
@@ -12,6 +12,7 @@ import { useViewer } from '@pascal-app/viewer'
12
12
  import { _roots, act, createRoot, events, extend, type RootState } from '@react-three/fiber'
13
13
  import { createElement } from 'react'
14
14
  import { Group, InstancedMesh, OrthographicCamera, Vector3, type WebGLRenderer } from 'three'
15
+ import { endCeilingEdit, startCeilingEdit } from '../../../lib/ceiling-edit-session'
15
16
  import { sfxEmitter } from '../../../lib/sfx-bus'
16
17
  import useEditor from '../../../store/use-editor'
17
18
  import useInteractionScope from '../../../store/use-interaction-scope'
@@ -33,7 +34,13 @@ type BracketHarness = {
33
34
  sounds: string[]
34
35
  }
35
36
 
36
- async function withMountedBrackets(run: (harness: BracketHarness) => Promise<void>) {
37
+ async function withMountedBrackets(
38
+ run: (harness: BracketHarness) => Promise<void>,
39
+ {
40
+ ceilingData = {},
41
+ session = true,
42
+ }: { ceilingData?: Partial<CeilingNode>; session?: boolean } = {},
43
+ ) {
37
44
  const previousScene = useScene.getState()
38
45
  const previousViewer = useViewer.getState()
39
46
  const previousEditor = useEditor.getState()
@@ -54,6 +61,7 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
54
61
  const building = BuildingNode.parse({})
55
62
  const level = LevelNode.parse({ parentId: building.id, height: 3 })
56
63
  const ceiling = CeilingNode.parse({
64
+ ...ceilingData,
57
65
  parentId: level.id,
58
66
  height: 3,
59
67
  polygon: [
@@ -81,8 +89,9 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
81
89
  hoveredId: null,
82
90
  selection: { buildingId: building.id, levelId: level.id, zoneId: null, selectedIds: [] },
83
91
  })
84
- useEditor.setState({ phase: 'structure', mode: 'select', structureLayer: 'elements' })
92
+ useEditor.setState({ phase: 'building', mode: 'select' })
85
93
  useInteractionScope.setState({ scope: { kind: 'idle' } })
94
+ if (session) startCeilingEdit(ceiling.id)
86
95
  const camera = Object.assign(new OrthographicCamera(-1, 5, 5, -1, 0.1, 100), { manual: true })
87
96
  camera.position.set(0, 10, 0)
88
97
  camera.up.set(0, 0, -1)
@@ -145,7 +154,10 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
145
154
  sounds,
146
155
  })
147
156
  } finally {
148
- await act(async () => root.render(null))
157
+ await act(async () => {
158
+ endCeilingEdit()
159
+ root.render(null)
160
+ })
149
161
  clearBoxSelectHandled()
150
162
  emitter.off('ceiling:click', onClick)
151
163
  sfxEmitter.off('*', onSound)
@@ -186,6 +198,7 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
186
198
  await dispatch('onPointerDown', 4, 0)
187
199
 
188
200
  const oldNormal = meshes().find((mesh) => mesh.renderOrder === 1000)
201
+ // Ceilings outside the session get no brackets.
189
202
  await act(async () => {
190
203
  const additions = Array.from({ length: 8 }, (_, index) =>
191
204
  CeilingNode.parse({
@@ -206,12 +219,43 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
206
219
  },
207
220
  })
208
221
  })
222
+ expect(meshes().find((mesh) => mesh.renderOrder === 1000)).toBe(oldNormal)
223
+ expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
224
+ // Twelve corners overflow the initial capacity: the batch meshes are replaced
225
+ // while the pressed corner keeps its index.
226
+ await act(async () =>
227
+ useScene.setState({
228
+ nodes: {
229
+ ...useScene.getState().nodes,
230
+ [ceiling.id]: CeilingNode.parse({
231
+ ...ceiling,
232
+ polygon: [
233
+ [0, 0],
234
+ [4, 0],
235
+ [4, 1],
236
+ [4, 2],
237
+ [4, 3],
238
+ [4, 4],
239
+ [3, 4],
240
+ [2, 4],
241
+ [1, 4],
242
+ [0, 4],
243
+ [0, 3],
244
+ [0, 2],
245
+ ],
246
+ }),
247
+ },
248
+ }),
249
+ )
209
250
  expect(meshes()).toHaveLength(2)
210
251
  expect(meshes().find((mesh) => mesh.renderOrder === 1000)).not.toBe(oldNormal)
211
252
  expect(useViewer.getState().hoveredId).toBe(ceiling.id)
212
253
  await dispatch('onClick', 4, 0)
213
254
  expect(clicks).toHaveLength(2)
214
255
  expect(clicks[1].position).toEqual([4, 3, 0])
256
+ await act(async () =>
257
+ useScene.setState({ nodes: { ...useScene.getState().nodes, [ceiling.id]: ceiling } }),
258
+ )
215
259
  await dispatch('onPointerLeave', 4, 0)
216
260
  expect(useViewer.getState().hoveredId).toBeNull()
217
261
 
@@ -277,7 +321,7 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
277
321
  )
278
322
  })
279
323
 
280
- test('coincident ceiling corners keep the same hover, click, and drag owner across batch transfers', async () => {
324
+ test('an adjacent ceiling outside the session has no brackets at a shared corner', async () => {
281
325
  await withMountedBrackets(async ({ ceiling, dispatch, dispatchWindow, meshes, clicks }) => {
282
326
  const adjacent = CeilingNode.parse({
283
327
  ...ceiling,
@@ -294,25 +338,64 @@ test('coincident ceiling corners keep the same hover, click, and drag owner acro
294
338
  nodes: { ...useScene.getState().nodes, [adjacent.id]: adjacent },
295
339
  }),
296
340
  )
297
- const owner = ceiling.id < adjacent.id ? ceiling : adjacent
298
- const other = owner === ceiling ? adjacent : ceiling
299
- for (let move = 0; move < 20; move++) {
300
- await dispatch('onPointerMove', 0, 0)
301
- expect(useViewer.getState().hoveredId).toBe(owner.id)
302
- expect(meshes().find((mesh) => mesh.renderOrder === 1001)!.count).toBe(7)
303
- }
341
+ expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
342
+ await dispatch('onPointerMove', 0, 0)
343
+ expect(useViewer.getState().hoveredId).toBe(ceiling.id)
304
344
  await dispatch('onPointerDown', 0, 0)
305
345
  await dispatch('onClick', 0, 0)
306
- expect(clicks[0].node.id).toBe(owner.id)
346
+ expect(clicks[0].node.id).toBe(ceiling.id)
307
347
  await dispatchWindow('pointermove', 0.5, 0.5)
308
- expect(useLiveNodeOverrides.getState().overrides.has(owner.id)).toBe(true)
309
- expect(useLiveNodeOverrides.getState().overrides.has(other.id)).toBe(false)
348
+ expect(useLiveNodeOverrides.getState().overrides.has(ceiling.id)).toBe(true)
349
+ expect(useLiveNodeOverrides.getState().overrides.has(adjacent.id)).toBe(false)
310
350
  await dispatchWindow('pointercancel', 0.5, 0.5)
311
351
  await dispatch('onPointerLeave', 0, 0)
312
352
  expect(useViewer.getState().hoveredId).toBeNull()
313
353
  })
314
354
  })
315
355
 
356
+ test('brackets and their hit boxes exist only inside the Edit ceiling session', async () => {
357
+ await withMountedBrackets(
358
+ async ({ ceiling, dispatch, meshes, levelObject }) => {
359
+ // A canvas selection of the ceiling alone shows nothing.
360
+ await act(async () => useViewer.getState().setSelection({ selectedIds: [ceiling.id] }))
361
+ expect(meshes()).toHaveLength(0)
362
+ expect(levelObject.children).toHaveLength(0)
363
+ await dispatch('onPointerMove', 0, 0)
364
+ expect(useViewer.getState().hoveredId).toBeNull()
365
+
366
+ await act(async () => {
367
+ startCeilingEdit(ceiling.id)
368
+ })
369
+ expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
370
+
371
+ await act(async () => useViewer.getState().setSelection({ selectedIds: [] }))
372
+ expect(meshes()).toHaveLength(0)
373
+ expect(levelObject.children).toHaveLength(0)
374
+ },
375
+ { session: false },
376
+ )
377
+ })
378
+
379
+ test('a corner drag detaches an auto ceiling; the session stays open', async () => {
380
+ await withMountedBrackets(
381
+ async ({ ceiling, dispatch, dispatchWindow, meshes }) => {
382
+ expect(ceiling.boundary).toBe('auto')
383
+ await dispatch('onPointerDown', 0, 0)
384
+ await dispatchWindow('pointermove', 0.5, 0.5)
385
+ const preview = useLiveNodeOverrides.getState().overrides.get(ceiling.id)
386
+ ?.polygon as CeilingNode['polygon']
387
+ await dispatchWindow('pointerup', 0.5, 0.5)
388
+ const committed = useScene.getState().nodes[ceiling.id] as CeilingNode
389
+ expect(committed.polygon).toEqual(preview)
390
+ expect(committed.boundary).toBeUndefined()
391
+ expect(committed.zoneId).toBeUndefined()
392
+ expect(useViewer.getState().selection.selectedIds).toEqual([ceiling.id])
393
+ expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
394
+ },
395
+ { ceilingData: { boundary: 'auto', zoneId: 'zone_auto_ceiling' } },
396
+ )
397
+ })
398
+
316
399
  test('same-id registry replacement rebinds the portal and drag plane without rewriting instance data', async () => {
317
400
  await withMountedBrackets(
318
401
  async ({ ceiling, level, levelObject, state, dispatch, dispatchWindow, meshes, clicks }) => {
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNodeId,
4
5
  type CeilingNode,
5
6
  emitter,
6
7
  resolveCeilingHeight,
@@ -24,6 +25,7 @@ import {
24
25
  } from 'react'
25
26
  import { Group, type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
26
27
  import { useShallow } from 'zustand/react/shallow'
28
+ import { useCeilingEditCeilingId } from '../../../lib/ceiling-edit-session'
27
29
  import {
28
30
  clearCeilingSnapFeedback,
29
31
  resolveCeilingPlanPointSnap,
@@ -94,9 +96,7 @@ function clearCornerDragPreview(drag: CornerDragState) {
94
96
  }
95
97
 
96
98
  export const CeilingSelectionAffordanceSystem = () => {
97
- const phase = useEditor((state) => state.phase)
98
99
  const mode = useEditor((state) => state.mode)
99
- const structureLayer = useEditor((state) => state.structureLayer)
100
100
  // ANY active interaction (moving/placing a node, reshaping a boundary or
101
101
  // curve, dragging a handle) unmounts the brackets: their ceiling-height hit
102
102
  // boxes would otherwise catch drag-time hover, set `hoveredId` to the
@@ -105,26 +105,24 @@ export const CeilingSelectionAffordanceSystem = () => {
105
105
  // scope, so it can't unmount itself.
106
106
  const scopeIdle = useInteractionScope((state) => state.scope.kind === 'idle')
107
107
  const currentLevelId = useViewer((state) => state.selection.levelId)
108
+ // Brackets and their hit boxes exist only for the ceiling in an explicit
109
+ // Edit ceiling session (`lib/ceiling-edit-session.ts`).
110
+ const editCeilingId = useCeilingEditCeilingId()
108
111
 
109
112
  const ceilings = useScene(
110
- useShallow((state) =>
111
- Object.values(state.nodes).filter((node): node is CeilingNode => {
112
- return (
113
- node.type === 'ceiling' &&
114
- node.visible !== false &&
115
- currentLevelId !== null &&
116
- resolveLevelId(node, state.nodes) === currentLevelId
117
- )
118
- }),
119
- ),
113
+ useShallow((state) => {
114
+ const node = editCeilingId ? state.nodes[editCeilingId as AnyNodeId] : undefined
115
+ return node?.type === 'ceiling' &&
116
+ node.visible !== false &&
117
+ currentLevelId !== null &&
118
+ resolveLevelId(node, state.nodes) === currentLevelId
119
+ ? [node]
120
+ : []
121
+ }),
120
122
  )
121
123
 
122
124
  const shouldRender =
123
- phase === 'structure' &&
124
- mode === 'select' &&
125
- structureLayer === 'elements' &&
126
- scopeIdle &&
127
- currentLevelId !== null
125
+ mode === 'select' && scopeIdle && currentLevelId !== null && ceilings.length > 0
128
126
 
129
127
  if (!shouldRender) return null
130
128
 
@@ -528,7 +526,9 @@ const CeilingSelectionAffordance = memo(function CeilingSelectionAffordance({
528
526
  suppressNextClick()
529
527
 
530
528
  if (drag.previewPolygon) {
531
- useScene.getState().updateNode(drag.ceilingId, { polygon: drag.previewPolygon })
529
+ // A polygon edit converts a derived (`boundary: 'auto'`) ceiling into
530
+ // an authored one, like the boundary editor's commit.
531
+ useScene.getState().detachDerivedNode(drag.ceilingId, { polygon: drag.previewPolygon })
532
532
  useViewer.getState().setSelection({ selectedIds: [drag.ceilingId] })
533
533
  }
534
534
 
@@ -511,6 +511,7 @@ function segmentShapeKey(segment: RoofSegmentNode): string {
511
511
  segment.wallThickness,
512
512
  segment.deckThickness,
513
513
  segment.overhang,
514
+ segment.gableOverhang,
514
515
  segment.shingleThickness,
515
516
  segment.gambrelLowerWidthRatio,
516
517
  segment.gambrelLowerHeightRatio,