@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
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
+ floorPlacedCollides,
6
7
  nodeRegistry,
7
8
  type TerrainVerb,
8
9
  useScene,
@@ -20,11 +21,52 @@ import {
20
21
  resolveSelectModeHelpHints,
21
22
  } from '../../../lib/contextual-help'
22
23
  import { getContextualHelpNodeExtension } from '../../../lib/contextual-help-extension'
23
- import { continuationContextOf } from '../../../lib/continuation'
24
+ import { continuationContextOf, keyCyclableContinuationContext } from '../../../lib/continuation'
24
25
  import { canDirectMoveNode, canDirectRotateNode } from '../../../lib/direct-manipulation'
26
+ import {
27
+ DIVIDE_ROOM_HUD_TITLE,
28
+ MEZZANINE_HUD_TITLE,
29
+ OPENING_HUD_TITLE,
30
+ TERRACE_HUD_TITLE,
31
+ MOVE_ROOM_HUD_TITLE,
32
+ MOVE_SELECTION_HUD_TITLE,
33
+ nodeKindHudTitle,
34
+ ERASE_HUD_TITLE,
35
+ PAINT_HUD_TITLE,
36
+ PICK_MATERIAL_HUD_TITLE,
37
+ paintRegionHudTitle,
38
+ RESIZE_HUD_TITLE,
39
+ ROTATE_HUD_TITLE,
40
+ SELECT_HUD_TITLE,
41
+ SMART_MEASURE_HUD_TITLE,
42
+ terrainHudTitle,
43
+ toolHudTitle,
44
+ } from '../../../lib/hud-title'
25
45
  import type { ReshapeKind } from '../../../lib/interaction/scope'
46
+ import {
47
+ type MezzanineShape,
48
+ useMezzanineDraft,
49
+ } from '../../../lib/mezzanine-draft'
50
+ import {
51
+ TERRACE_SHAPE_HINT,
52
+ type TerraceShape,
53
+ useTerraceDraft,
54
+ } from '../../../lib/terrace-draft'
55
+ import {
56
+ type OpeningShape,
57
+ useOpeningDraft,
58
+ } from '../../../lib/floor-opening-draft'
26
59
  import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
60
+ import { MEZZANINE_EDGE_DRAG_LABEL, ROOM_ELEVATION_DRAG_LABEL } from '../../../lib/room-handle-drag'
61
+ import { ROOM_MOVE_DRAG_LABEL } from '../../../lib/room-transform-session'
62
+ import { usePaintRegionHovering } from '../../../lib/paint-region-hover'
63
+ import {
64
+ type PaintMode,
65
+ paintRegionHoverHint,
66
+ usePaintRegionMode,
67
+ } from '../../../lib/paint-region-mode'
27
68
  import { snapContextOf } from '../../../lib/snapping-mode'
69
+ import { cornerHintRows, useCornerSnapHint } from '../../../store/use-corner-snap-hint'
28
70
  import useEditor, { getActiveContinuationContext } from '../../../store/use-editor'
29
71
  import useInteractionScope, {
30
72
  useActiveHandleDrag,
@@ -33,6 +75,7 @@ import useInteractionScope, {
33
75
  } from '../../../store/use-interaction-scope'
34
76
  import { BuildingHelper } from './building-helper'
35
77
  import { ContextualHelperPanel } from './contextual-helper-panel'
78
+ import { transientDraftId, usePlacementNotice } from '../../../hooks/use-placement-notice'
36
79
  import { ItemHelper } from './item-helper'
37
80
  import { RegisteredToolHelper } from './registered-tool-helper'
38
81
 
@@ -91,6 +134,89 @@ type ActiveModifierKeys = {
91
134
  }
92
135
 
93
136
  const EMPTY_CONTEXTUAL_HINTS: ContextualShortcutHint[] = []
137
+
138
+ // The paint tool's region sub-modes: what one gesture draws, then Esc. A
139
+ // refused gesture (the per-face cap) shows as the first row.
140
+ /** What a click does in each whole-surface sub-mode; only painting teaches the held eyedropper. */
141
+ export function paintHints(mode: PaintMode): ContextualShortcutHint[] {
142
+ if (mode === 'pick')
143
+ return [
144
+ { keys: ['Left click'], label: 'Paint with this material' },
145
+ { keys: ['Esc'], label: 'Cancel' },
146
+ ]
147
+ if (mode === 'erase')
148
+ return [{ keys: ['Left click'], label: 'Remove a painted part, or reset a surface' }]
149
+ if (mode === 'surface') return [{ keys: [['Alt', 'Option']], label: 'Hold to pick a material' }]
150
+ return []
151
+ }
152
+
153
+ export function paintRegionHints(
154
+ mode: 'rectangle' | 'polygon',
155
+ notice: string | null,
156
+ hovering = true,
157
+ ): ContextualShortcutHint[] {
158
+ const gesture: Record<typeof mode, ContextualShortcutHint> = {
159
+ rectangle: { keys: ['Drag'], label: 'Press anywhere, drag to the opposite corner' },
160
+ polygon: { keys: ['Left click'], label: 'Add point · click the first to close' },
161
+ }
162
+ // Off anything the sub-mode draws on, the HUD says where to go instead.
163
+ if (!hovering)
164
+ return [
165
+ ...(notice ? [{ keys: ['!'], label: notice, active: true }] : []),
166
+ { keys: ['Hover'], label: paintRegionHoverHint(mode), active: true },
167
+ { keys: ['Esc'], label: 'Cancel' },
168
+ ]
169
+ return [
170
+ ...(notice ? [{ keys: ['!'], label: notice, active: true }] : []),
171
+ gesture[mode],
172
+ ...(mode === 'polygon' ? [{ keys: ['Backspace'], label: 'Remove last point' }] : []),
173
+ { keys: ['Esc'], label: 'Cancel' },
174
+ ]
175
+ }
176
+ // "Add mezzanine": the outline gesture for the shape picked in the room panel,
177
+ // then Esc; the snapping chip sits below.
178
+ export function mezzanineHints(shape: MezzanineShape): ContextualShortcutHint[] {
179
+ return shape === 'rectangle'
180
+ ? [
181
+ { keys: ['Left click'], label: 'Set one corner, then the opposite' },
182
+ { keys: ['Drag'], label: 'Draw the mezzanine' },
183
+ { keys: ['Esc'], label: 'Cancel' },
184
+ ]
185
+ : [
186
+ { keys: ['Left click'], label: 'Add point · click the first to close' },
187
+ { keys: ['Backspace'], label: 'Remove last point' },
188
+ { keys: ['Enter'], label: 'Finish' },
189
+ { keys: ['Esc'], label: 'Cancel' },
190
+ ]
191
+ }
192
+ /** "Cut opening": the outline gesture for the chosen shape, then Esc. */
193
+ export function openingHints(shape: OpeningShape): ContextualShortcutHint[] {
194
+ return mezzanineHints(shape).map((hint) =>
195
+ hint.label === 'Draw the mezzanine' ? { ...hint, label: 'Draw the opening' } : hint,
196
+ )
197
+ }
198
+ const TERRACE_CHIP_HINTS = [TERRACE_SHAPE_HINT]
199
+ // The terrace tool: the outline gesture for the chosen shape, then Esc.
200
+ export function terraceHints(shape: TerraceShape): ContextualShortcutHint[] {
201
+ return shape === 'rectangle'
202
+ ? [
203
+ { keys: ['Left click'], label: 'Set one corner, then the opposite' },
204
+ { keys: ['Drag'], label: 'Draw the terrace' },
205
+ { keys: ['Esc'], label: 'Done' },
206
+ ]
207
+ : [
208
+ { keys: ['Left click'], label: 'Add point · click the first to close' },
209
+ { keys: ['Backspace'], label: 'Remove last point' },
210
+ { keys: ['Enter'], label: 'Finish' },
211
+ { keys: ['Esc'], label: 'Done' },
212
+ ]
213
+ }
214
+ export const ROOM_DIVIDE_HINTS: ContextualShortcutHint[] = [
215
+ { keys: ['Left click'], label: 'Add point' },
216
+ { keys: ['Backspace'], label: 'Remove last point' },
217
+ { keys: ['Enter'], label: 'Finish at the edge' },
218
+ { keys: ['Esc'], label: 'Cancel' },
219
+ ]
94
220
  const NO_CONTEXTUAL_HELP_SUBSCRIPTION = () => () => {}
95
221
 
96
222
  function useActiveModifierKeys(): ActiveModifierKeys {
@@ -129,6 +255,43 @@ function useActiveModifierKeys(): ActiveModifierKeys {
129
255
  return modifiers
130
256
  }
131
257
 
258
+ export type MezzanineGesture = 'raise' | 'resize' | 'move'
259
+
260
+ /**
261
+ * A handle drag on a mezzanine — its floor handle, an edge arrow, or its pick-up
262
+ * (Move / Duplicate) — or null. Its HUD names the mezzanine, not "Resize" or
263
+ * "Move room".
264
+ */
265
+ export function mezzanineGesture(
266
+ drag: { nodeId: string; label: string } | null,
267
+ nodes: Readonly<Record<string, AnyNode>>,
268
+ ): MezzanineGesture | null {
269
+ if (!drag) return null
270
+ const zone = nodes[drag.nodeId]
271
+ if (zone?.type !== 'zone' || zone.floor?.support !== 'open') return null
272
+ if (drag.label === ROOM_ELEVATION_DRAG_LABEL) return 'raise'
273
+ if (drag.label === MEZZANINE_EDGE_DRAG_LABEL) return 'resize'
274
+ if (drag.label === ROOM_MOVE_DRAG_LABEL) return 'move'
275
+ return null
276
+ }
277
+
278
+ export const MEZZANINE_GESTURE_HINTS: Record<MezzanineGesture, ContextualShortcutHint[]> = {
279
+ raise: [
280
+ { keys: ['Drag'], label: 'Raise or lower' },
281
+ { keys: ['Esc'], label: 'Cancel' },
282
+ ],
283
+ resize: [
284
+ { keys: ['Drag'], label: 'Resize' },
285
+ { keys: ['Esc'], label: 'Cancel' },
286
+ ],
287
+ move: [
288
+ { keys: ['Left click'], label: 'Place inside the room' },
289
+ { keys: [['R', 'T']], label: 'Rotate ±45°' },
290
+ { keys: ['Alt'], label: 'Free move' },
291
+ { keys: ['Esc'], label: 'Cancel' },
292
+ ],
293
+ }
294
+
132
295
  export function HelperManager() {
133
296
  const mode = useEditor((s) => s.mode)
134
297
  const tool = useEditor((s) => s.tool)
@@ -136,11 +299,25 @@ export function HelperManager() {
136
299
  const terrainSampling = useEditor((s) => s.terrainSampling)
137
300
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
138
301
  const measurementToolKind = useEditor((s) => s.toolDefaults.measurement?.kind)
302
+ const wallMode = useEditor((s) => s.continuationByContext.wall)
139
303
  const workspaceMode = useEditor((s) => s.workspaceMode)
140
304
  const scope = useInteractionScope((s) => s.scope)
305
+ const paintRegionMode = usePaintRegionMode((s) => s.mode)
306
+ const paintRegionDrawing = paintRegionMode === 'rectangle' || paintRegionMode === 'polygon'
307
+ const paintRegionNotice = usePaintRegionMode((s) => s.notice)
308
+ const paintRegionHovering = usePaintRegionHovering()
309
+ const mezzanineShape = useMezzanineDraft((s) => (s.host ? s.shape : null))
310
+ const openingShape = useOpeningDraft((s) => (s.host ? s.shape : null))
311
+ const terraceShape = useTerraceDraft((s) => (s.host ? s.shape : null))
141
312
  const movingNode = useMovingNode()
313
+ const cornerHint = useCornerSnapHint((s) => s.hint)
314
+ // A tool whose kind gives placement notices: its transient draft is the node in hand.
315
+ const toolNotices = !!(tool && nodeRegistry.get(tool)?.placementNotice)
316
+ const toolDraftId = useScene((s) => (toolNotices ? transientDraftId(s.nodes) : null))
317
+ const placementNotice = usePlacementNotice(movingNode?.id ?? toolDraftId, toolNotices)
142
318
  const reshapingNode = useReshapingNode()
143
319
  const activeHandleDrag = useActiveHandleDrag()
320
+ const mezzanineHandle = useScene((s) => mezzanineGesture(activeHandleDrag, s.nodes))
144
321
  const selectedIds = useViewer((s) => s.selection.selectedIds)
145
322
  const isMobile = useIsMobile()
146
323
  const modifiers = useActiveModifierKeys()
@@ -180,11 +357,14 @@ export function HelperManager() {
180
357
  return node ? nodeRegistry.get(node.type)?.snapProfile : undefined
181
358
  },
182
359
  draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
360
+ paintRegion: mode === 'material-paint' && paintRegionDrawing,
183
361
  }),
184
- [scope, mode, tool],
362
+ [scope, mode, tool, paintRegionDrawing],
185
363
  )
364
+ // Contexts whose mode is picked in the Build panel (the wall's Rooms variant)
365
+ // get no HUD chip: the header names the variant instead.
186
366
  const continuationContext = useMemo(
187
- () => getActiveContinuationContext(),
367
+ () => keyCyclableContinuationContext(getActiveContinuationContext()),
188
368
  [scope, mode, tool],
189
369
  )
190
370
  const selectModeHints = useMemo(() => {
@@ -226,6 +406,7 @@ export function HelperManager() {
226
406
  <ContextualHelperPanel
227
407
  hints={resolveRotateHandleHelpHints(modifiers.alt)}
228
408
  snapContext={snapContext}
409
+ title={ROTATE_HUD_TITLE}
229
410
  />
230
411
  )
231
412
  }
@@ -238,6 +419,79 @@ export function HelperManager() {
238
419
  <ContextualHelperPanel
239
420
  hints={[{ keys: ['R / T'], label: 'Rotate the selection ±45°' }]}
240
421
  snapContext={snapContext}
422
+ title={MOVE_SELECTION_HUD_TITLE}
423
+ />
424
+ )
425
+ }
426
+
427
+ if (mezzanineHandle) {
428
+ return (
429
+ <ContextualHelperPanel
430
+ hints={MEZZANINE_GESTURE_HINTS[mezzanineHandle]}
431
+ snapContext={snapContext}
432
+ title={MEZZANINE_HUD_TITLE}
433
+ />
434
+ )
435
+ }
436
+
437
+ // A picked-up room (Move / Duplicate / Rotate from its pill): the same keys
438
+ // as a group pick-up.
439
+ if (activeHandleDrag?.label === ROOM_MOVE_DRAG_LABEL) {
440
+ return (
441
+ <ContextualHelperPanel
442
+ hints={[
443
+ { keys: ['Left click'], label: 'Place' },
444
+ { keys: [['R', 'T']], label: 'Rotate ±45°' },
445
+ { keys: ['Alt'], label: 'Free move · slide doors and windows' },
446
+ { keys: ['Esc'], label: 'Cancel' },
447
+ ]}
448
+ snapContext={snapContext}
449
+ title={MOVE_ROOM_HUD_TITLE}
450
+ />
451
+ )
452
+ }
453
+ // A paint region gesture holds a handle-drag scope while it draws; its HUD
454
+ // stays the region one (Backspace / close hints for a polygon mid-draft).
455
+ if (mode === 'material-paint' && paintRegionDrawing) {
456
+ return (
457
+ <ContextualHelperPanel
458
+ hints={paintRegionHints(paintRegionMode, paintRegionNotice, paintRegionHovering)}
459
+ snapContext={snapContext}
460
+ title={paintRegionHudTitle(paintRegionMode)}
461
+ />
462
+ )
463
+ }
464
+
465
+ // The terrace tool draws like "Add mezzanine": same outline gestures and chip.
466
+ if (terraceShape) {
467
+ return (
468
+ <ContextualHelperPanel
469
+ chipHints={TERRACE_CHIP_HINTS}
470
+ hints={terraceHints(terraceShape)}
471
+ snapContext={snapContext}
472
+ title={TERRACE_HUD_TITLE}
473
+ />
474
+ )
475
+ }
476
+
477
+ if (openingShape) {
478
+ return (
479
+ <ContextualHelperPanel
480
+ hints={openingHints(openingShape)}
481
+ snapContext={snapContext}
482
+ title={OPENING_HUD_TITLE}
483
+ />
484
+ )
485
+ }
486
+
487
+ // "Add mezzanine" holds a handle-drag scope on its host room for the whole
488
+ // tool, so it must win over the generic resize HUD below.
489
+ if (mezzanineShape) {
490
+ return (
491
+ <ContextualHelperPanel
492
+ hints={mezzanineHints(mezzanineShape)}
493
+ snapContext={snapContext}
494
+ title={MEZZANINE_HUD_TITLE}
241
495
  />
242
496
  )
243
497
  }
@@ -254,6 +508,7 @@ export function HelperManager() {
254
508
  { keys: ['Esc'], label: 'Cancel' },
255
509
  ]}
256
510
  snapContext={snapContext}
511
+ title={RESIZE_HUD_TITLE}
257
512
  />
258
513
  )
259
514
  }
@@ -266,30 +521,54 @@ export function HelperManager() {
266
521
  const hints = reshapingNode
267
522
  ? nodeRegistry.get(reshapingNode.type)?.affordanceHints?.[scope.reshape]
268
523
  : undefined
524
+ const title = reshapingNode
525
+ ? nodeKindHudTitle(reshapingNode.type, scope.reshape === 'split' ? 'Split' : 'Edit')
526
+ : null
269
527
  if (hints) {
270
528
  return (
271
- <RegisteredToolHelper hints={hints} shiftPressed={modifiers.shift} snapContext={snapContext} />
529
+ <RegisteredToolHelper
530
+ hints={hints}
531
+ shiftPressed={modifiers.shift}
532
+ snapContext={snapContext}
533
+ title={title}
534
+ />
272
535
  )
273
536
  }
274
- return <ContextualHelperPanel hints={reshapingHints(scope.reshape)} snapContext={snapContext} />
537
+ return (
538
+ <ContextualHelperPanel
539
+ hints={reshapingHints(scope.reshape)}
540
+ snapContext={snapContext}
541
+ title={title}
542
+ />
543
+ )
275
544
  }
276
545
 
277
546
  if (movingNode) {
278
- if (movingNode.type === 'building') return <BuildingHelper showRotate />
279
547
  // A fresh placement (e.g. a positioned preset like a shelf) advertises its
280
548
  // once/repeat continuation, exactly like the GLB item tool — but an existing
281
549
  // node being *moved* is not a placement, so it gets no continuation chip.
282
- const movingContinuationContext = isFreshPlacementMetadata(movingNode.metadata)
283
- ? continuationContextOf(movingNode.type)
550
+ const isFreshPlacement = isFreshPlacementMetadata(movingNode.metadata)
551
+ const movingTitle = nodeKindHudTitle(movingNode.type, isFreshPlacement ? undefined : 'Move')
552
+ if (movingNode.type === 'building') return <BuildingHelper showRotate title={movingTitle} />
553
+ const movingContinuationContext = isFreshPlacement
554
+ ? keyCyclableContinuationContext(continuationContextOf(movingNode.type))
284
555
  : null
285
- const collisionValidatesDrop =
286
- nodeRegistry.get(movingNode.type)?.capabilities.floorPlaced?.collides === true
556
+ const collisionValidatesDrop = floorPlacedCollides(
557
+ nodeRegistry.get(movingNode.type)?.capabilities.floorPlaced,
558
+ movingNode,
559
+ )
287
560
  return (
288
561
  <ItemHelper
289
562
  continuationContext={movingContinuationContext}
563
+ notice={placementNotice?.line}
290
564
  showEsc
565
+ extraHints={movingNode.type === 'window' ? cornerHintRows(cornerHint) : []}
566
+ replacesPlace={
567
+ movingNode.type === 'window' && (cornerHint === 'join' || cornerHint === 'new')
568
+ }
291
569
  showForce={collisionValidatesDrop}
292
570
  snapContext={snapContext}
571
+ title={movingTitle}
293
572
  />
294
573
  )
295
574
  }
@@ -298,7 +577,19 @@ export function HelperManager() {
298
577
  // only contextual control here. The chip hides itself for targets that only
299
578
  // paint one surface, so this renders nothing until a scoped target is active.
300
579
  if (mode === 'material-paint') {
301
- return <ContextualHelperPanel hints={[]} showPaintScope />
580
+ return (
581
+ <ContextualHelperPanel
582
+ hints={paintHints(paintRegionMode)}
583
+ showPaintScope
584
+ title={
585
+ paintRegionMode === 'pick'
586
+ ? PICK_MATERIAL_HUD_TITLE
587
+ : paintRegionMode === 'erase'
588
+ ? ERASE_HUD_TITLE
589
+ : PAINT_HUD_TITLE
590
+ }
591
+ />
592
+ )
302
593
  }
303
594
 
304
595
  // Sculpt mode. The HUD is what makes a sustained brush mode legible: it names
@@ -307,18 +598,41 @@ export function HelperManager() {
307
598
  // mode-specific — bracket resize, and an Esc that abandons the stroke rather
308
599
  // than exiting the mode.
309
600
  if (mode === 'terrain-sculpt') {
310
- return <ContextualHelperPanel hints={terrainSculptHints(terrainVerb, terrainSampling)} />
601
+ return (
602
+ <ContextualHelperPanel
603
+ hints={terrainSculptHints(terrainVerb, terrainSampling)}
604
+ title={terrainHudTitle(terrainVerb)}
605
+ />
606
+ )
311
607
  }
312
608
 
313
609
  if (scope.kind === 'mesh-editing') {
314
- return <ContextualHelperPanel hints={contextualEditHints} snapContext={snapContext} />
610
+ return (
611
+ <ContextualHelperPanel
612
+ hints={contextualEditHints}
613
+ snapContext={snapContext}
614
+ title={contextualHelpNode ? nodeKindHudTitle(contextualHelpNode.type, 'Edit') : null}
615
+ />
616
+ )
315
617
  }
316
618
 
317
619
 
620
+ // Divide draws a separator path across the selected room — it reads as a
621
+ // wall draft: the wall's snapping chips (Shift / Ctrl) plus the path keys.
622
+ if (scope.kind === 'room-divide') {
623
+ return (
624
+ <ContextualHelperPanel
625
+ hints={ROOM_DIVIDE_HINTS}
626
+ snapContext={snapContext}
627
+ title={DIVIDE_ROOM_HUD_TITLE}
628
+ />
629
+ )
630
+ }
631
+
318
632
  // Idle select only — an active scope (handle-drag, box-select, …) must not show
319
633
  // the idle selection hints.
320
634
  if (mode === 'select' && scope.kind === 'idle') {
321
- return <ContextualHelperPanel hints={selectModeHints} />
635
+ return <ContextualHelperPanel hints={selectModeHints} title={SELECT_HUD_TITLE} />
322
636
  }
323
637
 
324
638
  if (tool === 'measurement' && measurementToolKind === 'smart') {
@@ -329,6 +643,7 @@ export function HelperManager() {
329
643
  { keys: ['Click'], label: 'Pin measurement lens' },
330
644
  { keys: ['Esc'], label: 'Exit smart measure' },
331
645
  ]}
646
+ title={SMART_MEASURE_HUD_TITLE}
332
647
  />
333
648
  )
334
649
  }
@@ -346,8 +661,10 @@ export function HelperManager() {
346
661
  <RegisteredToolHelper
347
662
  continuationContext={continuationContext}
348
663
  hints={hints}
664
+ notice={placementNotice?.line ?? null}
349
665
  shiftPressed={modifiers.shift}
350
666
  snapContext={snapContext}
667
+ title={toolHudTitle(tool, wallMode)}
351
668
  />
352
669
  )
353
670
  }
@@ -1,4 +1,5 @@
1
1
  import type { ContinuationContext } from '../../../lib/continuation'
2
+ import type { HudTitle } from '../../../lib/hud-title'
2
3
  import type { SnapContext } from '../../../lib/snapping-mode'
3
4
  import { ContextualHelperPanel } from './contextual-helper-panel'
4
5
 
@@ -11,6 +12,13 @@ interface ItemHelperProps {
11
12
  // Set for a fresh point-kind placement (e.g. a positioned preset) so the
12
13
  // once/repeat continuation chip shows; null for an existing-node move.
13
14
  continuationContext?: ContinuationContext | null
15
+ title?: HudTitle | null
16
+ // Why the item would not fit where it is (a door's way, a room too small): a warning, not a block.
17
+ notice?: string | null
18
+ // Rows a kind adds while it is in hand (a window near a corner says what dropping it does).
19
+ extraHints?: { keys: string[]; label: string }[]
20
+ // The extra rows say what a click does, so the plain Place row is left out.
21
+ replacesPlace?: boolean
14
22
  }
15
23
 
16
24
  // Snapping mode is the chip on the right (Shift cycles it), so it's not repeated
@@ -20,17 +28,25 @@ export function ItemHelper({
20
28
  snapContext,
21
29
  showForce,
22
30
  continuationContext = null,
31
+ title = null,
32
+ notice = null,
33
+ extraHints = [],
34
+ replacesPlace = false,
23
35
  }: ItemHelperProps) {
24
36
  return (
25
37
  <ContextualHelperPanel
26
38
  continuationContext={continuationContext}
39
+ notice={notice}
27
40
  hints={[
28
- { keys: ['Left click'], label: 'Place' },
41
+ // Where a click does something else (joining a corner), that row says so instead.
42
+ ...(replacesPlace ? [] : [{ keys: ['Left click'], label: 'Place' }]),
43
+ ...extraHints,
29
44
  { keys: ['R', 'T'], label: 'Rotate' },
30
45
  ...(showForce ? [{ keys: ['Alt'], label: 'Force place' }] : []),
31
46
  { keys: [showEsc ? 'Esc' : 'Right click'], label: 'Cancel' },
32
47
  ]}
33
48
  snapContext={snapContext}
49
+ title={title}
34
50
  />
35
51
  )
36
52
  }
@@ -1,5 +1,6 @@
1
1
  import type { ToolHint } from '@pascal-app/core'
2
2
  import type { ContinuationContext } from '../../../lib/continuation'
3
+ import type { HudTitle } from '../../../lib/hud-title'
3
4
  import type { SnapContext } from '../../../lib/snapping-mode'
4
5
  import { useVisibleToolHints } from '../../../lib/panel-tool-options'
5
6
  import { ContextualHelperPanel } from './contextual-helper-panel'
@@ -18,14 +19,18 @@ export function RegisteredToolHelper({
18
19
  shiftPressed = false,
19
20
  snapContext = null,
20
21
  continuationContext = null,
22
+ title = null,
23
+ notice = null,
21
24
  }: {
22
25
  hints: ToolHint[]
23
26
  shiftPressed?: boolean
24
27
  snapContext?: SnapContext | null
25
28
  continuationContext?: ContinuationContext | null
29
+ title?: HudTitle | null
30
+ notice?: string | null
26
31
  }) {
27
32
  const visible = useVisibleToolHints(hints)
28
- if (visible.length === 0 && !snapContext && !continuationContext) return null
33
+ if (visible.length === 0 && !snapContext && !continuationContext && !notice) return null
29
34
  // Hints carrying a live-state `chip` render as mode chips next to the
30
35
  // snapping / continuation rows; the rest stay static key rows.
31
36
  const chipHints = visible.filter((hint) => hint.chip)
@@ -44,7 +49,9 @@ export function RegisteredToolHelper({
44
49
  }
45
50
  })}
46
51
  continuationContext={continuationContext}
52
+ notice={notice}
47
53
  snapContext={snapContext}
54
+ title={title}
48
55
  />
49
56
  )
50
57
  }
@@ -3,6 +3,50 @@
3
3
  import { Icon } from '@iconify/react'
4
4
  import type { IconRef } from '@pascal-app/core'
5
5
  import { type ComponentType, lazy, Suspense } from 'react'
6
+ import { cn } from '../../lib/utils'
7
+
8
+ const SVG_DATA_URI = /^data:image\/svg\+xml[^,]*,/
9
+
10
+ /** Whether a `data:image/svg+xml` URI's markup paints with `currentColor`. */
11
+ function isCurrentColorSvg(src: string): boolean {
12
+ const head = SVG_DATA_URI.exec(src)
13
+ if (!head) return false
14
+ const body = src.slice(head[0].length)
15
+ try {
16
+ const markup = head[0].includes(';base64') ? atob(body) : decodeURIComponent(body)
17
+ return markup.includes('currentColor')
18
+ } catch {
19
+ return false
20
+ }
21
+ }
22
+
23
+ /**
24
+ * A `url`-kind icon. An SVG behind an `<img>` renders in its own document, so
25
+ * `currentColor` in the markup resolves to black rather than the surrounding
26
+ * text colour — a monochrome plugin glyph then disappears against the dark
27
+ * sidebar. Such an SVG becomes a CSS mask over the text colour instead. Plugin
28
+ * manifests are third-party, so the markup is never inlined into the page.
29
+ */
30
+ export function IconRefImage({
31
+ className,
32
+ size,
33
+ src,
34
+ }: {
35
+ className?: string
36
+ size?: number
37
+ src: string
38
+ }) {
39
+ const style = size === undefined ? undefined : { height: size, width: size }
40
+ if (!isCurrentColorSvg(src)) return <img alt="" className={className} src={src} style={style} />
41
+ const mask = `url("${src.replace(/["\\\n\r]/g, encodeURIComponent)}") center / contain no-repeat`
42
+ return (
43
+ <span
44
+ aria-hidden
45
+ className={cn('inline-block bg-current', className)}
46
+ style={{ ...style, mask, WebkitMask: mask }}
47
+ />
48
+ )
49
+ }
6
50
 
7
51
  // `React.lazy` must be called once per loader so the resolved component keeps
8
52
  // a stable identity across renders (otherwise every parent re-render remounts
@@ -26,7 +70,7 @@ function resolveLazyIcon(module: () => Promise<{ default: ComponentType }>): Com
26
70
  */
27
71
  export function IconRefGlyph({ icon, size = 16 }: { icon: IconRef; size?: number }) {
28
72
  if (icon.kind === 'url') {
29
- return <img alt="" className="shrink-0 object-contain" height={size} src={icon.src} width={size} />
73
+ return <IconRefImage className="shrink-0" size={size} src={icon.src} />
30
74
  }
31
75
  if (icon.kind === 'iconify') {
32
76
  return <Icon height={size} icon={icon.name} width={size} />
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { LevelNode } from '@pascal-app/core'
4
4
  import { useEffect, useState } from 'react'
5
- import type { LevelDuplicatePreset } from '../../lib/level-duplication'
5
+ import type { LevelDuplicatePreset } from '@pascal-app/core/building'
6
6
  import { getLevelDisplayName } from '@pascal-app/core'
7
7
  import { cn } from '../../lib/utils'
8
8
  import {
@@ -0,0 +1,16 @@
1
+ 'use client'
2
+
3
+ import { ARTIFACT_URL_PREFIX, resolveArtifactUrl } from '@pascal-app/core'
4
+ import Image, { type ImageProps } from 'next/image'
5
+
6
+ /**
7
+ * A node's icon or thumbnail. A scripted object's thumbnail is an
8
+ * `artifact://` image: it resolves through the project's artifact store and
9
+ * skips image optimization, whose server-side fetch has no session to read a
10
+ * private project's artifact with.
11
+ */
12
+ export function NodeIconImage({ src, ...props }: Omit<ImageProps, 'src'> & { src: string }) {
13
+ if (!src.startsWith(ARTIFACT_URL_PREFIX)) return <Image src={src} {...props} />
14
+ const url = resolveArtifactUrl(src)
15
+ return url ? <Image src={url} unoptimized {...props} /> : null
16
+ }