@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,13 +1,20 @@
1
1
  'use client'
2
2
 
3
3
  import { emitter } from '@pascal-app/core'
4
- import { Camera, Check, Crop, Loader2, Maximize2, Monitor, X } from 'lucide-react'
4
+ import { Check, Crop, Loader2, Maximize2, Monitor, X } from 'lucide-react'
5
5
  import { useCallback, useEffect, useRef, useState } from 'react'
6
6
  import { useIsMobile } from '../../hooks/use-mobile'
7
7
  import { triggerSFX } from '../../lib/sfx-bus'
8
- import useEditor from '../../store/use-editor'
9
-
10
- type CaptureMode = 'standard' | 'viewport' | 'area'
8
+ import useEditor, {
9
+ type SnapshotCropMode,
10
+ type SnapshotStandardAspect,
11
+ } from '../../store/use-editor'
12
+
13
+ // Local alias — distinct from `useEditor.captureMode` (which describes *why*
14
+ // a capture is happening, e.g. `preset`). This one says HOW the captured
15
+ // pixels are cropped: full-frame 16:9 (`standard`), raw canvas viewport, or
16
+ // user-dragged area. Hosts can preselect it via `captureMode.crop`.
17
+ type CropMode = SnapshotCropMode
11
18
  type CaptureState = 'idle' | 'capturing' | 'saved'
12
19
 
13
20
  interface DragPoint {
@@ -20,12 +27,23 @@ interface Drag {
20
27
  end: DragPoint
21
28
  }
22
29
 
30
+ // Output presets for `standard` captures — long edge stays near 1920.
31
+ const STANDARD_SIZES: Record<SnapshotStandardAspect, { w: number; h: number }> = {
32
+ '16:9': { w: 1920, h: 1080 },
33
+ '9:16': { w: 1080, h: 1920 },
34
+ '4:3': { w: 1920, h: 1440 },
35
+ '3:4': { w: 1440, h: 1920 },
36
+ '1:1': { w: 1440, h: 1440 },
37
+ }
38
+ type StandardAspect = SnapshotStandardAspect
39
+
23
40
  function getResolution(
24
- mode: CaptureMode,
41
+ mode: CropMode,
25
42
  overlayEl: HTMLDivElement | null,
26
43
  drag: Drag | null,
44
+ standardAspect: StandardAspect,
27
45
  ): { w: number; h: number } | null {
28
- if (mode === 'standard') return { w: 1920, h: 1080 }
46
+ if (mode === 'standard') return STANDARD_SIZES[standardAspect]
29
47
 
30
48
  if (!overlayEl) return null
31
49
  const rect = overlayEl.getBoundingClientRect()
@@ -45,16 +63,73 @@ function getResolution(
45
63
  return null
46
64
  }
47
65
 
66
+ /** Rule-of-thirds guide rendered inside a framing surface. */
67
+ function ThirdsGrid() {
68
+ return (
69
+ <div
70
+ className="pointer-events-none absolute inset-0"
71
+ style={{
72
+ background: `
73
+ linear-gradient(to right, transparent calc(33.33% - 0.5px), rgba(255,255,255,0.16) 33.33%, transparent calc(33.33% + 0.5px)),
74
+ linear-gradient(to right, transparent calc(66.66% - 0.5px), rgba(255,255,255,0.16) 66.66%, transparent calc(66.66% + 0.5px)),
75
+ linear-gradient(to bottom, transparent calc(33.33% - 0.5px), rgba(255,255,255,0.16) 33.33%, transparent calc(33.33% + 0.5px)),
76
+ linear-gradient(to bottom, transparent calc(66.66% - 0.5px), rgba(255,255,255,0.16) 66.66%, transparent calc(66.66% + 0.5px))`,
77
+ }}
78
+ />
79
+ )
80
+ }
81
+
82
+ /** Accented corner brackets on the framing rect. */
83
+ function CornerAccents() {
84
+ return (
85
+ <>
86
+ <span className="pointer-events-none absolute -top-0.5 -left-0.5 h-5 w-5 rounded-tl-md border-primary border-t-[2.5px] border-l-[2.5px]" />
87
+ <span className="pointer-events-none absolute -right-0.5 -bottom-0.5 h-5 w-5 rounded-br-md border-primary border-r-[2.5px] border-b-[2.5px]" />
88
+ </>
89
+ )
90
+ }
91
+
92
+ const HUD_CHIP_CLASS =
93
+ 'flex flex-col gap-px rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5 backdrop-blur-md'
94
+
95
+ const CROP_LABELS: Record<CropMode, string> = {
96
+ standard: 'Standard',
97
+ viewport: 'Viewport',
98
+ area: 'Area',
99
+ }
100
+
48
101
  export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
49
102
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
103
+ const captureMode = useEditor((s) => s.captureMode)
50
104
  const setCaptureMode = useEditor((s) => s.setCaptureMode)
51
105
  const isMobile = useIsMobile()
52
-
53
- const [mode, setMode] = useState<CaptureMode>('standard')
106
+ // `preset` capture mode locks the overlay to a square area crop with
107
+ // a transparent background — the user picks framing but not the
108
+ // crop shape. Matches the unified preset-thumbnail capture flow.
109
+ const isPreset = captureMode.mode === 'preset'
110
+ const requestedCrop = captureMode.mode === 'standard' ? captureMode.crop : undefined
111
+ const requestedAspect = captureMode.mode === 'standard' ? captureMode.standardAspect : undefined
112
+
113
+ const [mode, setMode] = useState<CropMode>('standard')
114
+ const [standardAspect, setStandardAspect] = useState<StandardAspect>('16:9')
115
+ const [aspectMenuOpen, setAspectMenuOpen] = useState(false)
54
116
  const [drag, setDrag] = useState<Drag | null>(null)
55
117
  const [isDragging, setIsDragging] = useState(false)
56
118
  const [captureState, setCaptureState] = useState<CaptureState>('idle')
57
119
  const overlayRef = useRef<HTMLDivElement>(null)
120
+ // Overlay size drives the computed standard-frame rect + resolution HUD.
121
+ const [overlaySize, setOverlaySize] = useState<{ w: number; h: number } | null>(null)
122
+
123
+ useEffect(() => {
124
+ if (!isCaptureMode) return
125
+ const el = overlayRef.current
126
+ if (!el) return
127
+ const update = () => setOverlaySize({ w: el.clientWidth, h: el.clientHeight })
128
+ update()
129
+ const observer = new ResizeObserver(update)
130
+ observer.observe(el)
131
+ return () => observer.disconnect()
132
+ }, [isCaptureMode])
58
133
 
59
134
  // Dismiss on Esc
60
135
  useEffect(() => {
@@ -66,15 +141,31 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
66
141
  return () => window.removeEventListener('keydown', onKey)
67
142
  }, [isCaptureMode, setCaptureMode])
68
143
 
69
- // Reset local state when entering capture mode
144
+ // Reset local state when entering capture mode. Preset mode also
145
+ // auto-stages a centered square crop sized to ~75% of the shorter
146
+ // viewport dimension so the user can capture immediately — the
147
+ // overlay's pan/move/resize handles still apply if they want to
148
+ // tweak the framing, but they don't have to draw the rect first.
70
149
  useEffect(() => {
71
- if (isCaptureMode) {
72
- setMode('standard')
150
+ if (!isCaptureMode) return
151
+ setMode(isPreset ? 'area' : (requestedCrop ?? 'standard'))
152
+ setStandardAspect(requestedAspect ?? '16:9')
153
+ setAspectMenuOpen(false)
154
+ setIsDragging(false)
155
+ setCaptureState('idle')
156
+ if (isPreset && overlayRef.current) {
157
+ const rect = overlayRef.current.getBoundingClientRect()
158
+ const side = Math.min(rect.width, rect.height) * 0.75
159
+ const cx = rect.width / 2
160
+ const cy = rect.height / 2
161
+ setDrag({
162
+ start: { x: cx - side / 2, y: cy - side / 2 },
163
+ end: { x: cx + side / 2, y: cy + side / 2 },
164
+ })
165
+ } else {
73
166
  setDrag(null)
74
- setIsDragging(false)
75
- setCaptureState('idle')
76
167
  }
77
- }, [isCaptureMode])
168
+ }, [isCaptureMode, isPreset, requestedCrop, requestedAspect])
78
169
 
79
170
  // Listen for snapshot saved to show feedback then exit
80
171
  useEffect(() => {
@@ -142,11 +233,28 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
142
233
  start: { x: snapshot.start.x + dx, y: snapshot.start.y + dy },
143
234
  end: { x: snapshot.end.x + dx, y: snapshot.end.y + dy },
144
235
  })
236
+ } else if (isPreset) {
237
+ // Preset mode locks the rect to a square — use the smaller
238
+ // axis to keep the drag predictable, sign-correct so the user
239
+ // can still drag in any quadrant.
240
+ setDrag((d) => {
241
+ if (!d) return null
242
+ const dx = pt.x - d.start.x
243
+ const dy = pt.y - d.start.y
244
+ const side = Math.min(Math.abs(dx), Math.abs(dy))
245
+ return {
246
+ start: d.start,
247
+ end: {
248
+ x: d.start.x + Math.sign(dx || 1) * side,
249
+ y: d.start.y + Math.sign(dy || 1) * side,
250
+ },
251
+ }
252
+ })
145
253
  } else {
146
254
  setDrag((d) => (d ? { start: d.start, end: pt } : null))
147
255
  }
148
256
  },
149
- [isDragging],
257
+ [isDragging, isPreset],
150
258
  )
151
259
 
152
260
  const onPointerUp = useCallback(() => {
@@ -225,12 +333,36 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
225
333
  projectId,
226
334
  captureMode: mode,
227
335
  cropRegion,
336
+ standardSize: mode === 'standard' ? STANDARD_SIZES[standardAspect] : undefined,
337
+ // In preset mode, the ThumbnailGenerator should keep the alpha
338
+ // channel transparent so the saved preset thumbnail composes
339
+ // cleanly onto any palette background.
340
+ transparent: isPreset,
228
341
  })
229
- }, [captureState, mode, drag, projectId])
342
+ }, [captureState, mode, drag, projectId, isPreset, standardAspect])
230
343
 
231
344
  if (!isCaptureMode) return null
232
345
 
233
- const resolution = getResolution(mode, overlayRef.current, drag)
346
+ const resolution = getResolution(mode, overlayRef.current, drag, standardAspect)
347
+
348
+ // Standard mode framing: the output is a center-crop of the canvas to the
349
+ // chosen aspect (see ThumbnailGenerator) — show exactly that region as a
350
+ // letterboxed frame.
351
+ const standardFrame =
352
+ mode === 'standard' && overlaySize
353
+ ? (() => {
354
+ const size = STANDARD_SIZES[standardAspect]
355
+ const targetRatio = size.w / size.h
356
+ const w = Math.min(overlaySize.w, overlaySize.h * targetRatio)
357
+ const h = w / targetRatio
358
+ return {
359
+ left: (overlaySize.w - w) / 2,
360
+ top: (overlaySize.h - h) / 2,
361
+ width: w,
362
+ height: h,
363
+ }
364
+ })()
365
+ : null
234
366
 
235
367
  // Area selection rect (CSS px, relative to overlay)
236
368
  const selectionStyle =
@@ -250,33 +382,66 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
250
382
 
251
383
  return (
252
384
  <div className="pointer-events-none absolute inset-0 z-40" ref={overlayRef}>
253
- {/* Area mode: dim layer + crosshair cursor + drag-to-select */}
254
- {mode === 'area' && (
385
+ {/* Standard mode: letterboxed 16:9 frame with thirds + corner accents */}
386
+ {standardFrame && (
255
387
  <div
256
- className="pointer-events-auto absolute inset-0 bg-black/30"
257
- onPointerDown={onPointerDown}
258
- onPointerMove={(e) => {
259
- onPointerMove(e)
260
- // Update cursor: 'move' when hovering inside an existing selection
261
- if (!isDragging && drag && overlayRef.current) {
262
- const rect = overlayRef.current.getBoundingClientRect()
263
- const px = e.clientX - rect.left
264
- const py = e.clientY - rect.top
265
- const x0 = Math.min(drag.start.x, drag.end.x)
266
- const y0 = Math.min(drag.start.y, drag.end.y)
267
- const x1 = Math.max(drag.start.x, drag.end.x)
268
- const y1 = Math.max(drag.start.y, drag.end.y)
269
- e.currentTarget.style.cursor =
270
- px >= x0 && px <= x1 && py >= y0 && py <= y1 ? 'move' : 'crosshair'
271
- }
388
+ className="pointer-events-none absolute rounded-md border-[1.5px] border-white/85"
389
+ style={{
390
+ ...standardFrame,
391
+ boxShadow: '0 0 0 9999px rgba(10,10,14,0.4)',
272
392
  }}
273
- onPointerUp={onPointerUp}
274
- style={{ cursor: 'crosshair' }}
275
393
  >
276
- {/* "No selection" hint */}
277
- {!selectionStyle && (
394
+ <ThirdsGrid />
395
+ <CornerAccents />
396
+ </div>
397
+ )}
398
+
399
+ {/* Viewport mode: the whole canvas is the frame — thirds only */}
400
+ {mode === 'viewport' && <ThirdsGrid />}
401
+
402
+ {/* Area mode: dim layer + crosshair cursor + drag-to-select.
403
+ *
404
+ * Preset mode reuses the same DOM but stays click-through: the
405
+ * crop frame is auto-staged and locked, so the user adjusts the
406
+ * camera (orbit / pan / zoom) instead of dragging the rect. The
407
+ * dim letterbox + dashed border still render via the inline
408
+ * `box-shadow` on the selection rect — they're cosmetic. */}
409
+ {mode === 'area' && (
410
+ <div
411
+ className={
412
+ isPreset
413
+ ? 'pointer-events-none absolute inset-0'
414
+ : 'pointer-events-auto absolute inset-0 bg-black/30'
415
+ }
416
+ onPointerDown={isPreset ? undefined : onPointerDown}
417
+ onPointerMove={
418
+ isPreset
419
+ ? undefined
420
+ : (e) => {
421
+ onPointerMove(e)
422
+ // Update cursor: 'move' when hovering inside an existing selection
423
+ if (!isDragging && drag && overlayRef.current) {
424
+ const rect = overlayRef.current.getBoundingClientRect()
425
+ const px = e.clientX - rect.left
426
+ const py = e.clientY - rect.top
427
+ const x0 = Math.min(drag.start.x, drag.end.x)
428
+ const y0 = Math.min(drag.start.y, drag.end.y)
429
+ const x1 = Math.max(drag.start.x, drag.end.x)
430
+ const y1 = Math.max(drag.start.y, drag.end.y)
431
+ e.currentTarget.style.cursor =
432
+ px >= x0 && px <= x1 && py >= y0 && py <= y1 ? 'move' : 'crosshair'
433
+ }
434
+ }
435
+ }
436
+ onPointerUp={isPreset ? undefined : onPointerUp}
437
+ style={isPreset ? undefined : { cursor: 'crosshair' }}
438
+ >
439
+ {/* "No selection" hint — only when the user has to draw the
440
+ area themselves (`standard` capture). Preset mode always
441
+ has a pre-staged square, so we never show it there. */}
442
+ {!selectionStyle && !isPreset && (
278
443
  <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
279
- <span className="rounded-full bg-black/40 px-4 py-2 text-sm text-white backdrop-blur-sm">
444
+ <span className="rounded-full border border-white/10 bg-neutral-950/80 px-4 py-2 text-sm text-white backdrop-blur-md">
280
445
  Drag the area you want to capture
281
446
  </span>
282
447
  </div>
@@ -285,6 +450,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
285
450
  {/* Selection rect */}
286
451
  {selectionStyle && (
287
452
  <div
453
+ className="rounded-sm border-[1.5px] border-white/85"
288
454
  style={{
289
455
  position: 'absolute',
290
456
  left: selectionStyle.left,
@@ -292,46 +458,73 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
292
458
  width: selectionStyle.width,
293
459
  height: selectionStyle.height,
294
460
  pointerEvents: 'none',
295
- boxShadow: '0 0 0 9999px rgba(0,0,0,0.35)',
296
- border: '2px dashed rgba(255,255,255,0.85)',
297
- background: 'rgba(255,255,255,0.04)',
461
+ boxShadow: '0 0 0 9999px rgba(10,10,14,0.4)',
462
+ background: 'rgba(255,255,255,0.03)',
298
463
  }}
299
464
  >
300
- {/* Corner handles */}
301
- {(
302
- [
303
- { pos: { top: -5, left: -5 }, cursor: 'nwse-resize' },
304
- { pos: { top: -5, right: -5 }, cursor: 'nesw-resize' },
305
- { pos: { bottom: -5, left: -5 }, cursor: 'nesw-resize' },
306
- { pos: { bottom: -5, right: -5 }, cursor: 'nwse-resize' },
307
- ] as const
308
- ).map(({ pos, cursor }, i) => (
309
- <div
310
- key={i}
311
- onPointerDown={(e) => onCornerPointerDown(e, i)}
312
- style={{
313
- position: 'absolute',
314
- width: 10,
315
- height: 10,
316
- borderRadius: '50%',
317
- background: 'white',
318
- boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
319
- pointerEvents: 'auto',
320
- cursor,
321
- ...pos,
322
- }}
323
- />
324
- ))}
465
+ {hasSelection && <ThirdsGrid />}
466
+ <CornerAccents />
467
+ {/* Corner handles — preset mode locks the frame to the
468
+ auto-staged centered square; the user adjusts the
469
+ camera instead. */}
470
+ {!isPreset &&
471
+ (
472
+ [
473
+ { pos: { top: -5, left: -5 }, cursor: 'nwse-resize' },
474
+ { pos: { top: -5, right: -5 }, cursor: 'nesw-resize' },
475
+ { pos: { bottom: -5, left: -5 }, cursor: 'nesw-resize' },
476
+ { pos: { bottom: -5, right: -5 }, cursor: 'nwse-resize' },
477
+ ] as const
478
+ ).map(({ pos, cursor }, i) => (
479
+ <div
480
+ key={i}
481
+ onPointerDown={(e) => onCornerPointerDown(e, i)}
482
+ style={{
483
+ position: 'absolute',
484
+ width: 10,
485
+ height: 10,
486
+ borderRadius: '50%',
487
+ background: 'white',
488
+ boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
489
+ pointerEvents: 'auto',
490
+ cursor,
491
+ ...pos,
492
+ }}
493
+ />
494
+ ))}
325
495
  </div>
326
496
  )}
327
497
  </div>
328
498
  )}
329
499
 
500
+ {/* Top-center HUD — what the shot will be. Preset captures are a fixed
501
+ square and carry their own "Frame your item" banner up there. */}
502
+ {!isMobile && !isPreset && (
503
+ <div className="pointer-events-none absolute top-4 left-1/2 flex -translate-x-1/2 gap-2">
504
+ <div className={HUD_CHIP_CLASS}>
505
+ <span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
506
+ Crop
507
+ </span>
508
+ <span className="font-semibold text-white text-xs">
509
+ {isPreset ? 'Preset · square' : CROP_LABELS[mode]}
510
+ </span>
511
+ </div>
512
+ <div className={HUD_CHIP_CLASS}>
513
+ <span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
514
+ Format
515
+ </span>
516
+ <span className="font-semibold text-white text-xs tabular-nums">
517
+ {resolution ? `${resolution.w} × ${resolution.h}` : '—'}
518
+ </span>
519
+ </div>
520
+ </div>
521
+ )}
522
+
330
523
  {/* Top-right dismiss button (icon-only on mobile) */}
331
524
  <div className="pointer-events-auto absolute top-4 right-4">
332
525
  <button
333
526
  aria-label="Close capture mode"
334
- className="flex items-center gap-1.5 rounded-full border border-white/20 bg-black/60 px-3 py-1.5 text-white/80 text-xs backdrop-blur-sm transition-colors hover:bg-black/80 hover:text-white"
527
+ className="flex items-center gap-1.5 rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5 text-white/80 text-xs backdrop-blur-md transition-colors hover:bg-neutral-950 hover:text-white"
335
528
  onClick={dismiss}
336
529
  type="button"
337
530
  >
@@ -340,93 +533,114 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
340
533
  </button>
341
534
  </div>
342
535
 
343
- {/* Bottom-center mode toolbar */}
344
- <div className="pointer-events-auto absolute bottom-6 left-1/2 -translate-x-1/2">
345
- {(() => {
346
- const modeButtons = (
347
- <>
348
- <ModeButton
349
- active={mode === 'standard'}
350
- badge="16:9"
351
- icon={<Monitor className="h-3.5 w-3.5" />}
352
- label="Standard"
353
- onClick={() => {
354
- setMode('standard')
355
- setDrag(null)
356
- }}
357
- />
358
- <ModeButton
359
- active={mode === 'viewport'}
360
- icon={<Maximize2 className="h-3.5 w-3.5" />}
361
- label="Viewport"
362
- onClick={() => {
363
- setMode('viewport')
364
- setDrag(null)
365
- }}
366
- />
367
- <ModeButton
368
- active={mode === 'area'}
369
- icon={<Crop className="h-3.5 w-3.5" />}
370
- label="Area"
371
- onClick={() => setMode('area')}
372
- />
373
- </>
374
- )
375
-
376
- const resolutionDisplay = (
377
- <span className="min-w-[80px] text-center text-white/50 text-xs tabular-nums">
378
- {resolution ? `${resolution.w} × ${resolution.h}` : '—'}
379
- </span>
380
- )
381
-
382
- const captureButton = (
383
- <button
384
- className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-1.5 font-medium text-primary-foreground text-xs transition-opacity disabled:opacity-50"
385
- disabled={captureDisabled}
386
- onClick={handleCapture}
387
- type="button"
388
- >
389
- {captureState === 'capturing' ? (
390
- <>
391
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
392
- Capturing
393
- </>
394
- ) : captureState === 'saved' ? (
395
- <>
396
- <Check className="h-3.5 w-3.5" />
397
- Saved
398
- </>
399
- ) : (
400
- <>
401
- <Camera className="h-3.5 w-3.5" />
402
- Capture
403
- </>
404
- )}
405
- </button>
406
- )
407
-
408
- if (isMobile) {
409
- return (
410
- <div className="flex flex-col items-stretch gap-2 rounded-2xl border border-white/10 bg-neutral-900/95 px-2 py-2 shadow-xl backdrop-blur-md">
411
- <div className="flex items-center justify-center gap-1">{modeButtons}</div>
412
- <div className="flex items-center justify-center gap-2 border-white/10 border-t pt-2">
413
- {resolutionDisplay}
414
- {captureButton}
415
- </div>
536
+ {/* Subtle scrim so the bottom controls stay readable on bright scenes */}
537
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 h-36 bg-gradient-to-t from-black/45 via-black/15 to-transparent" />
538
+
539
+ {/* Bottom-center: crop switcher, caption + shutter */}
540
+ <div className="pointer-events-none absolute right-0 bottom-5 left-0 flex flex-col items-center gap-2.5">
541
+ {!isPreset && (
542
+ <div className="pointer-events-auto relative flex items-center gap-1 rounded-full border border-white/10 bg-neutral-950/85 px-1.5 py-1.5 shadow-xl backdrop-blur-md">
543
+ {/* Clicking Standard while it's active opens the aspect picker */}
544
+ <ModeButton
545
+ active={mode === 'standard'}
546
+ badge={standardAspect}
547
+ icon={<Monitor className="h-3.5 w-3.5" />}
548
+ label={isMobile ? undefined : 'Standard'}
549
+ onClick={() => {
550
+ if (mode === 'standard') setAspectMenuOpen((v) => !v)
551
+ else setAspectMenuOpen(false)
552
+ setMode('standard')
553
+ setDrag(null)
554
+ }}
555
+ />
556
+ {aspectMenuOpen && mode === 'standard' && (
557
+ <div className="absolute bottom-[calc(100%+8px)] left-0 flex gap-1 rounded-full border border-white/10 bg-neutral-950/90 p-1.5 shadow-xl backdrop-blur-md">
558
+ {(Object.keys(STANDARD_SIZES) as StandardAspect[]).map((aspect) => (
559
+ <button
560
+ className={`rounded-full px-2.5 py-1 font-mono text-[11px] transition-colors ${
561
+ aspect === standardAspect
562
+ ? 'bg-white/15 text-white ring-1 ring-white/20'
563
+ : 'text-white/55 hover:text-white/90'
564
+ }`}
565
+ key={aspect}
566
+ onClick={() => {
567
+ setStandardAspect(aspect)
568
+ setAspectMenuOpen(false)
569
+ }}
570
+ type="button"
571
+ >
572
+ {aspect}
573
+ </button>
574
+ ))}
416
575
  </div>
417
- )
418
- }
576
+ )}
577
+ <ModeButton
578
+ active={mode === 'viewport'}
579
+ icon={<Maximize2 className="h-3.5 w-3.5" />}
580
+ label={isMobile ? undefined : 'Viewport'}
581
+ onClick={() => {
582
+ setMode('viewport')
583
+ setDrag(null)
584
+ setAspectMenuOpen(false)
585
+ }}
586
+ />
587
+ <ModeButton
588
+ active={mode === 'area'}
589
+ icon={<Crop className="h-3.5 w-3.5" />}
590
+ label={isMobile ? undefined : 'Area'}
591
+ onClick={() => {
592
+ setMode('area')
593
+ setAspectMenuOpen(false)
594
+ }}
595
+ />
596
+ {isMobile && (
597
+ <span className="px-2 text-white/50 text-xs tabular-nums">
598
+ {resolution ? `${resolution.w} × ${resolution.h}` : '—'}
599
+ </span>
600
+ )}
601
+ </div>
602
+ )}
603
+
604
+ {/* Preset captures carry their own "Frame your item" banner — the
605
+ snapshot pitch only applies to the studio/reference flow. */}
606
+ {!isMobile && !isPreset && (
607
+ <span className="pointer-events-none max-w-90 rounded-lg border border-white/10 bg-neutral-950/85 px-3.5 py-1.5 text-center text-[11.5px] text-white/85 leading-relaxed backdrop-blur-md">
608
+ A <b className="font-semibold text-white">snapshot</b>
609
+ {' freezes this exact camera angle as a reusable reference for renders & videos.'}
610
+ </span>
611
+ )}
419
612
 
420
- return (
421
- <div className="flex items-center gap-1 rounded-full border border-white/10 bg-neutral-900/95 px-2 py-2 shadow-xl backdrop-blur-md">
422
- {modeButtons}
423
- <div className="mx-1 h-4 w-px bg-white/10" />
424
- {resolutionDisplay}
425
- <div className="mx-1 h-4 w-px bg-white/10" />
426
- {captureButton}
427
- </div>
428
- )
429
- })()}
613
+ <button
614
+ aria-label={isPreset ? 'Capture' : 'Take snapshot'}
615
+ className="group pointer-events-auto relative grid h-14 w-14 place-items-center rounded-full disabled:opacity-50"
616
+ disabled={captureDisabled}
617
+ onClick={handleCapture}
618
+ type="button"
619
+ >
620
+ <span className="absolute inset-0 rounded-full border-[3px] border-white shadow-lg" />
621
+ <span
622
+ className={`h-10 w-10 rounded-full transition-transform duration-100 ${
623
+ captureState === 'capturing'
624
+ ? 'scale-[0.55] bg-primary'
625
+ : 'bg-white group-hover:scale-90 group-active:scale-75'
626
+ }`}
627
+ />
628
+ {captureState === 'capturing' && (
629
+ <Loader2 className="absolute h-4 w-4 animate-spin text-white" />
630
+ )}
631
+ {captureState === 'saved' && (
632
+ <Check className="absolute h-5 w-5 stroke-3 text-neutral-900" />
633
+ )}
634
+ </button>
635
+ <span className="pointer-events-none font-mono text-[10.5px] text-white uppercase tracking-[0.12em] drop-shadow">
636
+ {captureState === 'capturing'
637
+ ? 'Capturing…'
638
+ : captureState === 'saved'
639
+ ? 'Saved'
640
+ : isPreset
641
+ ? 'Capture'
642
+ : 'Take snapshot'}
643
+ </span>
430
644
  </div>
431
645
  </div>
432
646
  )
@@ -441,7 +655,7 @@ function ModeButton({
441
655
  }: {
442
656
  active: boolean
443
657
  icon: React.ReactNode
444
- label: string
658
+ label?: string
445
659
  badge?: string
446
660
  onClick: () => void
447
661
  }) {
@@ -0,0 +1,22 @@
1
+ import type { Camera, Raycaster } from 'three'
2
+
3
+ // Escape hatch for DOM-level interaction sessions (group pick-up move) that
4
+ // need to raycast the 3D view without living inside the R3F tree. The
5
+ // editor's SelectionManager (always mounted with the canvas) publishes the
6
+ // live camera / raycaster / canvas here; consumers must handle `null`
7
+ // (canvas not mounted yet, or torn down).
8
+ export type EditorThreeContext = {
9
+ camera: Camera
10
+ raycaster: Raycaster
11
+ domElement: HTMLCanvasElement
12
+ }
13
+
14
+ let current: EditorThreeContext | null = null
15
+
16
+ export function setEditorThreeContext(ctx: EditorThreeContext | null) {
17
+ current = ctx
18
+ }
19
+
20
+ export function getEditorThreeContext(): EditorThreeContext | null {
21
+ return current
22
+ }