@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,1299 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- DoorNode,
7
- emitter,
8
- useInteractive,
9
- useScene,
10
- } from '@pascal-app/core'
11
- import { useViewer } from '@pascal-app/viewer'
12
- import { BookMarked, Copy, DoorOpen, FlipHorizontal2, Move, Trash2 } from 'lucide-react'
13
- import { useCallback, useRef } from 'react'
14
- import { usePresetsAdapter } from '../../../contexts/presets-context'
15
- import { sfxEmitter } from '../../../lib/sfx-bus'
16
- import { cn } from '../../../lib/utils'
17
- import useEditor from '../../../store/use-editor'
18
- import { ActionButton, ActionGroup } from '../controls/action-button'
19
- import { MetricControl } from '../controls/metric-control'
20
- import { PanelSection } from '../controls/panel-section'
21
- import { SegmentedControl } from '../controls/segmented-control'
22
- import { SliderControl } from '../controls/slider-control'
23
- import { ToggleControl } from '../controls/toggle-control'
24
- import { PanelWrapper } from './panel-wrapper'
25
- import { PresetsPopover } from './presets/presets-popover'
26
-
27
- const doorTypeOptions = [
28
- { label: 'Hinged', value: 'hinged', available: true },
29
- { label: 'Double', value: 'double', available: true },
30
- { label: 'French', value: 'french', available: true },
31
- { label: 'Folding', value: 'folding', available: true },
32
- { label: 'Pocket', value: 'pocket', available: true },
33
- { label: 'Barn', value: 'barn', available: true },
34
- { label: 'Sliding', value: 'sliding', available: true },
35
- ] satisfies {
36
- label: string
37
- value: DoorNode['doorType']
38
- available: boolean
39
- }[]
40
-
41
- const garageDoorTypeOptions = [
42
- { label: 'Sectional', value: 'garage-sectional', available: true },
43
- { label: 'Roll-up', value: 'garage-rollup', available: true },
44
- { label: 'Tilt-up', value: 'garage-tiltup', available: true },
45
- ] satisfies {
46
- label: string
47
- value: DoorNode['doorType']
48
- available: boolean
49
- }[]
50
-
51
- const frenchDoorSegments: DoorNode['segments'] = [
52
- {
53
- type: 'glass',
54
- heightRatio: 0.76,
55
- columnRatios: [1, 1],
56
- dividerThickness: 0.025,
57
- panelDepth: 0.01,
58
- panelInset: 0.04,
59
- },
60
- {
61
- type: 'panel',
62
- heightRatio: 0.24,
63
- columnRatios: [1],
64
- dividerThickness: 0.03,
65
- panelDepth: 0.012,
66
- panelInset: 0.035,
67
- },
68
- ]
69
-
70
- const foldingDoorSegments: DoorNode['segments'] = [
71
- {
72
- type: 'panel',
73
- heightRatio: 1,
74
- columnRatios: [1],
75
- dividerThickness: 0.02,
76
- panelDepth: 0.008,
77
- panelInset: 0.025,
78
- },
79
- ]
80
-
81
- const defaultDoorDimensions: Record<DoorNode['doorType'], { width: number; height: number }> = {
82
- hinged: { width: 0.9, height: 2.1 },
83
- double: { width: 1.5, height: 2.1 },
84
- french: { width: 1.5, height: 2.1 },
85
- folding: { width: 1.8, height: 2.1 },
86
- pocket: { width: 0.9, height: 2.1 },
87
- barn: { width: 1, height: 2.1 },
88
- sliding: { width: 1.5, height: 2.1 },
89
- 'garage-sectional': { width: 2.7, height: 2.4 },
90
- 'garage-rollup': { width: 2.7, height: 2.4 },
91
- 'garage-tiltup': { width: 2.7, height: 2.4 },
92
- }
93
-
94
- function isSameDoorValue(current: unknown, next: unknown): boolean {
95
- if (typeof current === 'number' && typeof next === 'number') {
96
- return Math.abs(current - next) < 1e-6
97
- }
98
-
99
- if (Array.isArray(current) && Array.isArray(next)) {
100
- return (
101
- current.length === next.length &&
102
- current.every((value, index) => isSameDoorValue(value, next[index]))
103
- )
104
- }
105
-
106
- return Object.is(current, next)
107
- }
108
-
109
- export function DoorPanel() {
110
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
111
- const setSelection = useViewer((s) => s.setSelection)
112
- const updateNode = useScene((s) => s.updateNode)
113
- const deleteNode = useScene((s) => s.deleteNode)
114
- const setMovingNode = useEditor((s) => s.setMovingNode)
115
- const previewRef = useRef<{
116
- id: AnyNodeId
117
- key: keyof DoorNode
118
- value: unknown
119
- } | null>(null)
120
-
121
- const adapter = usePresetsAdapter()
122
-
123
- const node = useScene((s) =>
124
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as DoorNode | undefined) : undefined,
125
- )
126
-
127
- const handleUpdate = useCallback(
128
- (updates: Partial<DoorNode>) => {
129
- if (!(selectedId && node)) return
130
- const hasChange = Object.entries(updates).some(([key, value]) => {
131
- const currentValue = node[key as keyof DoorNode]
132
- return !isSameDoorValue(currentValue, value)
133
- })
134
- if (!hasChange) return
135
-
136
- if ('operationState' in updates || 'swingAngle' in updates || 'doorType' in updates) {
137
- useInteractive.getState().removeDoorOpenState(selectedId as AnyNodeId)
138
- }
139
- updateNode(selectedId as AnyNode['id'], updates)
140
- useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
141
- },
142
- [selectedId, node, updateNode],
143
- )
144
-
145
- const previewDoorUpdate = useCallback(
146
- <K extends keyof DoorNode>(key: K, value: DoorNode[K]) => {
147
- if (!selectedId) return
148
- const liveNode = useScene.getState().nodes[selectedId as AnyNodeId]
149
- if (liveNode?.type !== 'door') return
150
-
151
- if (
152
- !(
153
- previewRef.current &&
154
- previewRef.current.id === selectedId &&
155
- previewRef.current.key === key
156
- )
157
- ) {
158
- previewRef.current = {
159
- id: selectedId as AnyNodeId,
160
- key,
161
- value: liveNode[key],
162
- }
163
- }
164
-
165
- if (isSameDoorValue(liveNode[key], value)) return
166
-
167
- ;(liveNode as DoorNode)[key] = value
168
- useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
169
- },
170
- [selectedId],
171
- )
172
-
173
- const commitDoorPreview = useCallback(
174
- <K extends keyof DoorNode>(key: K, value: DoorNode[K]) => {
175
- if (!selectedId) return
176
-
177
- const scene = useScene.getState()
178
- const liveNode = scene.nodes[selectedId as AnyNodeId]
179
- const preview = previewRef.current
180
- if (liveNode?.type === 'door' && preview?.id === selectedId && preview.key === key) {
181
- ;(liveNode as DoorNode)[key] = preview.value as DoorNode[K]
182
- scene.dirtyNodes.add(selectedId as AnyNodeId)
183
- }
184
- previewRef.current = null
185
-
186
- updateNode(selectedId as AnyNode['id'], { [key]: value } as Partial<DoorNode>)
187
- scene.dirtyNodes.add(selectedId as AnyNodeId)
188
- },
189
- [selectedId, updateNode],
190
- )
191
-
192
- const handleClose = useCallback(() => {
193
- setSelection({ selectedIds: [] })
194
- }, [setSelection])
195
-
196
- const handleFlip = useCallback(() => {
197
- if (!node) return
198
- handleUpdate({
199
- side: node.side === 'front' ? 'back' : 'front',
200
- rotation: [node.rotation[0], node.rotation[1] + Math.PI, node.rotation[2]],
201
- })
202
- }, [node, handleUpdate])
203
-
204
- const handleMove = useCallback(() => {
205
- if (!node) return
206
- sfxEmitter.emit('sfx:item-pick')
207
- setMovingNode(node)
208
- setSelection({ selectedIds: [] })
209
- }, [node, setMovingNode, setSelection])
210
-
211
- const handleDelete = useCallback(() => {
212
- if (!(selectedId && node)) return
213
- sfxEmitter.emit('sfx:item-delete')
214
- deleteNode(selectedId as AnyNode['id'])
215
- if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
216
- setSelection({ selectedIds: [] })
217
- }, [selectedId, node, deleteNode, setSelection])
218
-
219
- const handleDuplicate = useCallback(() => {
220
- if (!node?.parentId) return
221
- sfxEmitter.emit('sfx:item-pick')
222
- useScene.temporal.getState().pause()
223
- const cloned = structuredClone(node) as any
224
- delete cloned.id
225
- cloned.metadata = { ...cloned.metadata, isNew: true }
226
- const duplicate = DoorNode.parse(cloned)
227
- useScene.getState().createNode(duplicate, node.parentId as AnyNodeId)
228
- setMovingNode(duplicate)
229
- setSelection({ selectedIds: [] })
230
- }, [node, setMovingNode, setSelection])
231
-
232
- const setSegmentHeightRatio = (segIdx: number, newVal: number) => {
233
- if (!node) return
234
- const numSegs = node.segments.length
235
- const totalH = node.segments.reduce((sum, s) => sum + s.heightRatio, 0)
236
- const normH = node.segments.map((s) => s.heightRatio / totalH)
237
- const clamped = Math.max(0.05, Math.min(0.95, newVal))
238
- const neighborIdx = segIdx < numSegs - 1 ? segIdx + 1 : segIdx - 1
239
- const delta = clamped - normH[segIdx]!
240
- const neighborVal = Math.max(0.05, normH[neighborIdx]! - delta)
241
- const newRatios = normH.map((v, i) => {
242
- if (i === segIdx) return clamped
243
- if (i === neighborIdx) return neighborVal
244
- return v
245
- })
246
- const updated = node.segments.map((s, idx) => ({ ...s, heightRatio: newRatios[idx]! }))
247
- handleUpdate({ segments: updated })
248
- }
249
-
250
- const setSegmentColumnRatio = (segIdx: number, colIdx: number, newVal: number) => {
251
- const seg = node?.segments[segIdx]
252
- if (!seg) return
253
- const normRatios = (() => {
254
- const sum = seg.columnRatios.reduce((a, b) => a + b, 0)
255
- return seg.columnRatios.map((r) => r / sum)
256
- })()
257
- const numCols = normRatios.length
258
- const clamped = Math.max(0.05, Math.min(0.95, newVal))
259
- const neighborIdx = colIdx < numCols - 1 ? colIdx + 1 : colIdx - 1
260
- const delta = clamped - normRatios[colIdx]!
261
- const neighborVal = Math.max(0.05, normRatios[neighborIdx]! - delta)
262
- const newRatios = normRatios.map((v, i) => {
263
- if (i === colIdx) return clamped
264
- if (i === neighborIdx) return neighborVal
265
- return v
266
- })
267
- const updated = node?.segments.map((s, idx) =>
268
- idx === segIdx ? { ...s, columnRatios: newRatios } : s,
269
- )
270
- handleUpdate({ segments: updated })
271
- }
272
-
273
- const getDoorPresetData = useCallback(() => {
274
- if (!node) return null
275
- return {
276
- doorCategory: node.doorCategory,
277
- doorType: node.doorType,
278
- leafCount: node.leafCount,
279
- operationState: node.operationState,
280
- slideDirection: node.slideDirection,
281
- trackStyle: node.trackStyle,
282
- garagePanelCount: node.garagePanelCount,
283
- width: node.width,
284
- height: node.height,
285
- frameThickness: node.frameThickness,
286
- frameDepth: node.frameDepth,
287
- openingKind: node.openingKind,
288
- openingShape: node.openingShape,
289
- openingRadiusMode: node.openingRadiusMode ?? 'all',
290
- openingTopRadii: node.openingTopRadii ?? [0.15, 0.15],
291
- cornerRadius: node.cornerRadius,
292
- archHeight: node.archHeight,
293
- openingRevealRadius: node.openingRevealRadius,
294
- contentPadding: node.contentPadding,
295
- hingesSide: node.hingesSide,
296
- swingDirection: node.swingDirection,
297
- threshold: node.threshold,
298
- thresholdHeight: node.thresholdHeight,
299
- handle: node.handle,
300
- handleHeight: node.handleHeight,
301
- handleSide: node.handleSide,
302
- doorCloser: node.doorCloser,
303
- panicBar: node.panicBar,
304
- panicBarHeight: node.panicBarHeight,
305
- segments: node.segments,
306
- }
307
- }, [node])
308
-
309
- const handleSavePreset = useCallback(
310
- async (name: string) => {
311
- const data = getDoorPresetData()
312
- if (!(data && selectedId)) return
313
- const presetId = await adapter.savePreset('door', name, data)
314
- if (presetId) emitter.emit('preset:generate-thumbnail', { presetId, nodeId: selectedId })
315
- },
316
- [getDoorPresetData, selectedId, adapter],
317
- )
318
-
319
- const handleOverwritePreset = useCallback(
320
- async (id: string) => {
321
- const data = getDoorPresetData()
322
- if (!(data && selectedId)) return
323
- await adapter.overwritePreset('door', id, data)
324
- emitter.emit('preset:generate-thumbnail', { presetId: id, nodeId: selectedId })
325
- },
326
- [getDoorPresetData, selectedId, adapter],
327
- )
328
-
329
- const handleApplyPreset = useCallback(
330
- (data: Record<string, unknown>) => {
331
- handleUpdate(data as Partial<DoorNode>)
332
- },
333
- [handleUpdate],
334
- )
335
-
336
- if (!(node && node.type === 'door' && selectedId)) return null
337
-
338
- const hSum = node.segments.reduce((s, seg) => s + seg.heightRatio, 0)
339
- const normHeights = node.segments.map((seg) => seg.heightRatio / hSum)
340
- const isOpening = node.openingKind === 'opening'
341
- const openingShape = node.openingShape ?? 'rectangle'
342
- const doorShape =
343
- openingShape === 'arch' || openingShape === 'rounded' ? openingShape : 'rectangle'
344
- const openingRadiusMode = node.openingRadiusMode ?? 'all'
345
- const openingTopRadii = node.openingTopRadii ?? [0.15, 0.15]
346
- const cornerRadius = node.cornerRadius ?? 0.15
347
- const archHeight = node.archHeight ?? 0.45
348
- const openingRevealRadius = node.openingRevealRadius ?? 0.025
349
- const maxRoundedRadius = Math.max(0.01, Math.min(node.width / 2, node.height))
350
- const doorType = node.doorType ?? 'hinged'
351
- const isSwingDoor = doorType === 'hinged' || doorType === 'double' || doorType === 'french'
352
- const isSlidingDoor = doorType === 'pocket' || doorType === 'barn' || doorType === 'sliding'
353
- const isGarageDoor = node.doorCategory === 'garage' || doorType.startsWith('garage-')
354
- const isSectionalGarageDoor = doorType === 'garage-sectional'
355
- const isRollupGarageDoor = doorType === 'garage-rollup'
356
- const isTiltupGarageDoor = doorType === 'garage-tiltup'
357
- const typeMode = isOpening ? 'opening' : isGarageDoor ? 'garage' : 'door'
358
- const supportsHandleSide = isSwingDoor
359
- const maxDoorWidth = isGarageDoor ? 6 : 3
360
-
361
- const setOpeningTopRadius = (index: number, value: number, commit = false) => {
362
- const next = [...openingTopRadii] as [number, number]
363
- next[index] = value
364
- if (commit) {
365
- commitDoorPreview('openingTopRadii', next)
366
- } else {
367
- previewDoorUpdate('openingTopRadii', next)
368
- }
369
- }
370
-
371
- const getDoorTypeUpdates = (nextDoorType: DoorNode['doorType']): Partial<DoorNode> => {
372
- const dimensions = defaultDoorDimensions[nextDoorType]
373
- const dimensionUpdates = {
374
- width: dimensions.width,
375
- height: dimensions.height,
376
- position: [node.position[0], dimensions.height / 2, node.position[2]] as DoorNode['position'],
377
- }
378
-
379
- if (nextDoorType === 'double' || nextDoorType === 'french') {
380
- return {
381
- doorCategory: 'interior',
382
- doorType: nextDoorType,
383
- leafCount: 2,
384
- ...dimensionUpdates,
385
- handleSide: 'right',
386
- ...(nextDoorType === 'french'
387
- ? {
388
- contentPadding: [0.045, 0.055],
389
- segments: frenchDoorSegments,
390
- }
391
- : {}),
392
- }
393
- }
394
-
395
- if (nextDoorType === 'folding') {
396
- return {
397
- doorCategory: 'interior',
398
- doorType: nextDoorType,
399
- leafCount: 4,
400
- ...dimensionUpdates,
401
- handle: true,
402
- handleSide: 'right',
403
- trackStyle: 'visible',
404
- operationState: Math.max(node.operationState ?? 0, 0.65),
405
- contentPadding: [0.03, 0.04],
406
- segments: foldingDoorSegments,
407
- }
408
- }
409
-
410
- if (nextDoorType === 'pocket') {
411
- return {
412
- doorCategory: 'interior',
413
- doorType: nextDoorType,
414
- leafCount: 1,
415
- ...dimensionUpdates,
416
- handle: true,
417
- handleSide: 'right',
418
- trackStyle: 'pocket',
419
- slideDirection: node.slideDirection ?? 'left',
420
- operationState: node.operationState ?? 0,
421
- contentPadding: [0.035, 0.045],
422
- segments: foldingDoorSegments,
423
- }
424
- }
425
-
426
- if (nextDoorType === 'barn') {
427
- return {
428
- doorCategory: 'interior',
429
- doorType: nextDoorType,
430
- leafCount: 1,
431
- ...dimensionUpdates,
432
- handle: true,
433
- handleSide: 'right',
434
- trackStyle: 'visible',
435
- slideDirection: node.slideDirection ?? 'left',
436
- operationState: node.operationState ?? 0,
437
- contentPadding: [0.035, 0.045],
438
- segments: foldingDoorSegments,
439
- }
440
- }
441
-
442
- if (nextDoorType === 'sliding') {
443
- return {
444
- doorCategory: 'interior',
445
- doorType: nextDoorType,
446
- leafCount: 2,
447
- ...dimensionUpdates,
448
- handle: true,
449
- handleSide: 'right',
450
- trackStyle: 'visible',
451
- slideDirection: node.slideDirection ?? 'left',
452
- operationState: node.operationState ?? 0,
453
- contentPadding: [0.03, 0.04],
454
- segments: frenchDoorSegments,
455
- }
456
- }
457
-
458
- if (nextDoorType === 'garage-sectional') {
459
- return {
460
- doorCategory: 'garage',
461
- doorType: nextDoorType,
462
- leafCount: 1,
463
- ...dimensionUpdates,
464
- handle: false,
465
- threshold: false,
466
- trackStyle: 'overhead',
467
- operationState: 0,
468
- garagePanelCount: Math.max(3, Math.min(8, node.garagePanelCount ?? 4)),
469
- contentPadding: [0.04, 0.04],
470
- segments: foldingDoorSegments,
471
- }
472
- }
473
-
474
- if (nextDoorType === 'garage-rollup') {
475
- return {
476
- doorCategory: 'garage',
477
- doorType: nextDoorType,
478
- leafCount: 1,
479
- ...dimensionUpdates,
480
- handle: false,
481
- threshold: false,
482
- trackStyle: 'overhead',
483
- operationState: 0,
484
- garagePanelCount: 4,
485
- contentPadding: [0.04, 0.04],
486
- segments: foldingDoorSegments,
487
- }
488
- }
489
-
490
- if (nextDoorType === 'garage-tiltup') {
491
- return {
492
- doorCategory: 'garage',
493
- doorType: nextDoorType,
494
- leafCount: 1,
495
- ...dimensionUpdates,
496
- handle: false,
497
- threshold: false,
498
- trackStyle: 'overhead',
499
- operationState: 0,
500
- garagePanelCount: 4,
501
- contentPadding: [0.04, 0.04],
502
- segments: foldingDoorSegments,
503
- }
504
- }
505
-
506
- return {
507
- doorCategory: 'interior',
508
- doorType: nextDoorType,
509
- leafCount: 1,
510
- ...dimensionUpdates,
511
- threshold: true,
512
- }
513
- }
514
-
515
- return (
516
- <PanelWrapper
517
- icon="/icons/door.png"
518
- onClose={handleClose}
519
- title={node.name || 'Door'}
520
- width={320}
521
- >
522
- {/* Presets strip */}
523
- <div className="border-border/30 border-b px-3 pt-2.5 pb-1.5">
524
- <PresetsPopover
525
- isAuthenticated={adapter.isAuthenticated}
526
- onApply={handleApplyPreset}
527
- onDelete={(id) => adapter.deletePreset(id)}
528
- onFetchPresets={(tab) => adapter.fetchPresets('door', tab)}
529
- onOverwrite={handleOverwritePreset}
530
- onRename={(id, name) => adapter.renamePreset(id, name)}
531
- onSave={handleSavePreset}
532
- onToggleCommunity={adapter.togglePresetCommunity}
533
- tabs={adapter.tabs}
534
- type="door"
535
- >
536
- <button className="flex w-full items-center gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 font-medium text-muted-foreground text-xs transition-colors hover:bg-[#3e3e3e] hover:text-foreground">
537
- <BookMarked className="h-3.5 w-3.5 shrink-0" />
538
- <span>Presets</span>
539
- </button>
540
- </PresetsPopover>
541
- </div>
542
-
543
- <PanelSection title="Type">
544
- <div className="flex flex-col gap-2 px-1 pb-1">
545
- <SegmentedControl
546
- onChange={(v) =>
547
- handleUpdate(
548
- v === 'opening'
549
- ? {
550
- openingKind: v,
551
- openingShape,
552
- openingRadiusMode,
553
- openingTopRadii,
554
- cornerRadius,
555
- archHeight,
556
- openingRevealRadius,
557
- }
558
- : v === 'garage'
559
- ? {
560
- openingKind: 'door',
561
- ...getDoorTypeUpdates(isGarageDoor ? doorType : 'garage-sectional'),
562
- }
563
- : {
564
- openingKind: 'door',
565
- ...(isGarageDoor ? getDoorTypeUpdates('hinged') : {}),
566
- },
567
- )
568
- }
569
- options={[
570
- { label: 'Door', value: 'door' },
571
- { label: 'Opening', value: 'opening' },
572
- { label: 'Garage', value: 'garage' },
573
- ]}
574
- value={typeMode}
575
- />
576
- </div>
577
- {!isOpening && (
578
- <div className="grid grid-cols-2 gap-1.5 px-1 pt-1">
579
- {(isGarageDoor ? garageDoorTypeOptions : doorTypeOptions).map((option) => {
580
- const isSelected = doorType === option.value
581
- return (
582
- <button
583
- className={cn(
584
- 'flex min-h-12 items-center gap-2 rounded-lg border px-2.5 text-left text-xs transition-colors',
585
- isSelected
586
- ? 'border-orange-400/60 bg-orange-400/10 text-foreground'
587
- : 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
588
- !option.available &&
589
- 'cursor-not-allowed opacity-45 hover:bg-[#2C2C2E] hover:text-muted-foreground',
590
- )}
591
- disabled={!option.available}
592
- key={option.value}
593
- onClick={() => handleUpdate(getDoorTypeUpdates(option.value))}
594
- type="button"
595
- >
596
- <DoorOpen className="h-3.5 w-3.5 shrink-0" />
597
- <span className="truncate font-medium">{option.label}</span>
598
- </button>
599
- )
600
- })}
601
- </div>
602
- )}
603
- </PanelSection>
604
-
605
- <PanelSection title="Position">
606
- <SliderControl
607
- label={
608
- <>
609
- X<sub className="ml-[1px] text-[11px] opacity-70">wall</sub>
610
- </>
611
- }
612
- max={10}
613
- min={-10}
614
- onChange={(v) => handleUpdate({ position: [v, node.position[1], node.position[2]] })}
615
- precision={2}
616
- step={0.1}
617
- unit="m"
618
- value={Math.round(node.position[0] * 100) / 100}
619
- />
620
- {!isOpening && (
621
- <div className="px-1 pt-2 pb-1">
622
- <ActionButton
623
- className="w-full"
624
- icon={<FlipHorizontal2 className="h-4 w-4" />}
625
- label="Flip Side"
626
- onClick={handleFlip}
627
- />
628
- </div>
629
- )}
630
- </PanelSection>
631
-
632
- {doorType === 'folding' && !isOpening && (
633
- <PanelSection title="Fold">
634
- <div className="flex flex-col gap-2 px-1 pb-1">
635
- <div className="space-y-1">
636
- <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
637
- Panels
638
- </span>
639
- <SegmentedControl
640
- onChange={(v) => handleUpdate({ leafCount: v === '2' ? 2 : 4 })}
641
- options={[
642
- { label: '2', value: '2' },
643
- { label: '4', value: '4' },
644
- ]}
645
- value={node.leafCount === 2 ? '2' : '4'}
646
- />
647
- </div>
648
- </div>
649
- <SliderControl
650
- label="Open"
651
- max={100}
652
- min={0}
653
- onChange={(v) => handleUpdate({ operationState: v / 100 })}
654
- precision={0}
655
- restoreOnCommit={false}
656
- step={5}
657
- unit="%"
658
- value={Math.round((node.operationState ?? 0) * 100)}
659
- />
660
- </PanelSection>
661
- )}
662
-
663
- {isSlidingDoor && !isOpening && (
664
- <PanelSection title="Slide">
665
- <div className="flex flex-col gap-2 px-1 pb-1">
666
- <div className="space-y-1">
667
- <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
668
- {doorType === 'pocket' ? 'Pocket' : doorType === 'barn' ? 'Rail' : 'Panel'}
669
- </span>
670
- <SegmentedControl
671
- onChange={(v) => handleUpdate({ slideDirection: v })}
672
- options={[
673
- { label: 'Left', value: 'left' },
674
- { label: 'Right', value: 'right' },
675
- ]}
676
- value={node.slideDirection ?? 'left'}
677
- />
678
- </div>
679
- </div>
680
- <SliderControl
681
- label="Open"
682
- max={100}
683
- min={0}
684
- onChange={(v) => handleUpdate({ operationState: v / 100 })}
685
- precision={0}
686
- restoreOnCommit={false}
687
- step={5}
688
- unit="%"
689
- value={Math.round((node.operationState ?? 0) * 100)}
690
- />
691
- </PanelSection>
692
- )}
693
-
694
- {(isSectionalGarageDoor || isRollupGarageDoor || isTiltupGarageDoor) && !isOpening && (
695
- <PanelSection title="Garage">
696
- <SliderControl
697
- label="Open"
698
- max={100}
699
- min={0}
700
- onChange={(v) => handleUpdate({ operationState: v / 100 })}
701
- precision={0}
702
- restoreOnCommit={false}
703
- step={5}
704
- unit="%"
705
- value={Math.round((node.operationState ?? 0) * 100)}
706
- />
707
- {isSectionalGarageDoor && (
708
- <SliderControl
709
- label="Panels"
710
- max={8}
711
- min={3}
712
- onChange={(v) => handleUpdate({ garagePanelCount: Math.round(v) })}
713
- precision={0}
714
- restoreOnCommit={false}
715
- step={1}
716
- value={node.garagePanelCount ?? 4}
717
- />
718
- )}
719
- </PanelSection>
720
- )}
721
-
722
- <PanelSection title="Dimensions">
723
- <SliderControl
724
- label="Width"
725
- max={maxDoorWidth}
726
- min={0.5}
727
- onChange={(v) => handleUpdate({ width: v })}
728
- precision={2}
729
- restoreOnCommit={false}
730
- step={0.05}
731
- unit="m"
732
- value={Math.round(node.width * 100) / 100}
733
- />
734
- <SliderControl
735
- label="Height"
736
- max={4}
737
- min={1.0}
738
- onChange={(v) =>
739
- handleUpdate({ height: v, position: [node.position[0], v / 2, node.position[2]] })
740
- }
741
- precision={2}
742
- restoreOnCommit={false}
743
- step={0.05}
744
- unit="m"
745
- value={Math.round(node.height * 100) / 100}
746
- />
747
- </PanelSection>
748
-
749
- {!isOpening && (
750
- <PanelSection title="Top Shape">
751
- <div className="flex flex-col gap-2 px-1 pb-1">
752
- <SegmentedControl
753
- onChange={(v) =>
754
- handleUpdate({
755
- openingShape: v as DoorNode['openingShape'],
756
- ...(v === 'rounded'
757
- ? {
758
- openingRadiusMode,
759
- openingTopRadii,
760
- cornerRadius: Math.min(cornerRadius, maxRoundedRadius),
761
- openingRevealRadius,
762
- }
763
- : {}),
764
- ...(v === 'arch' ? { archHeight } : {}),
765
- })
766
- }
767
- options={[
768
- { label: 'Rect', value: 'rectangle' },
769
- { label: 'Rounded', value: 'rounded' },
770
- { label: 'Arch', value: 'arch' },
771
- ]}
772
- value={doorShape}
773
- />
774
- </div>
775
- {doorShape === 'rounded' && (
776
- <>
777
- <div className="flex flex-col gap-2 px-1 pb-1">
778
- <SegmentedControl
779
- onChange={(v) =>
780
- handleUpdate({ openingRadiusMode: v as DoorNode['openingRadiusMode'] })
781
- }
782
- options={[
783
- { label: 'All', value: 'all' },
784
- { label: 'Individual', value: 'individual' },
785
- ]}
786
- value={openingRadiusMode}
787
- />
788
- </div>
789
- {openingRadiusMode === 'all' ? (
790
- <SliderControl
791
- label="Corner Radius"
792
- max={maxRoundedRadius}
793
- min={0}
794
- onChange={(v) => previewDoorUpdate('cornerRadius', v)}
795
- onCommit={(v) => commitDoorPreview('cornerRadius', v)}
796
- precision={2}
797
- step={0.05}
798
- unit="m"
799
- value={Math.round(cornerRadius * 100) / 100}
800
- />
801
- ) : (
802
- <>
803
- {[
804
- ['Top Left', 0],
805
- ['Top Right', 1],
806
- ].map(([label, index]) => (
807
- <SliderControl
808
- key={label}
809
- label={label}
810
- max={maxRoundedRadius}
811
- min={0}
812
- onChange={(v) => setOpeningTopRadius(index as number, v)}
813
- onCommit={(v) => setOpeningTopRadius(index as number, v, true)}
814
- precision={2}
815
- step={0.05}
816
- unit="m"
817
- value={Math.round((openingTopRadii[index as number] ?? 0) * 100) / 100}
818
- />
819
- ))}
820
- </>
821
- )}
822
- <SliderControl
823
- label="Reveal Radius"
824
- max={0.08}
825
- min={0}
826
- onChange={(v) => previewDoorUpdate('openingRevealRadius', v)}
827
- onCommit={(v) => commitDoorPreview('openingRevealRadius', v)}
828
- precision={3}
829
- step={0.005}
830
- unit="m"
831
- value={Math.round(openingRevealRadius * 1000) / 1000}
832
- />
833
- </>
834
- )}
835
- {doorShape === 'arch' && (
836
- <SliderControl
837
- label="Arch Height"
838
- max={node.height}
839
- min={0.05}
840
- onChange={(v) => handleUpdate({ archHeight: v })}
841
- precision={2}
842
- restoreOnCommit={false}
843
- step={0.05}
844
- unit="m"
845
- value={Math.round(archHeight * 100) / 100}
846
- />
847
- )}
848
- </PanelSection>
849
- )}
850
-
851
- {isOpening && (
852
- <PanelSection title="Opening Shape">
853
- <div className="flex flex-col gap-2 px-1 pb-1">
854
- <SegmentedControl
855
- onChange={(v) =>
856
- handleUpdate({
857
- openingShape: v,
858
- ...(v === 'rounded'
859
- ? { openingRadiusMode, openingTopRadii, cornerRadius, openingRevealRadius }
860
- : {}),
861
- ...(v === 'arch' ? { archHeight } : {}),
862
- })
863
- }
864
- options={[
865
- { label: 'Rect', value: 'rectangle' },
866
- { label: 'Rounded', value: 'rounded' },
867
- { label: 'Arch', value: 'arch' },
868
- ]}
869
- value={openingShape}
870
- />
871
- </div>
872
- {openingShape === 'rounded' && (
873
- <>
874
- <div className="flex flex-col gap-2 px-1 pb-1">
875
- <SegmentedControl
876
- onChange={(v) =>
877
- handleUpdate({ openingRadiusMode: v as DoorNode['openingRadiusMode'] })
878
- }
879
- options={[
880
- { label: 'All', value: 'all' },
881
- { label: 'Individual', value: 'individual' },
882
- ]}
883
- value={openingRadiusMode}
884
- />
885
- </div>
886
- {openingRadiusMode === 'all' ? (
887
- <SliderControl
888
- label="Corner Radius"
889
- max={maxRoundedRadius}
890
- min={0}
891
- onChange={(v) => previewDoorUpdate('cornerRadius', v)}
892
- onCommit={(v) => commitDoorPreview('cornerRadius', v)}
893
- precision={2}
894
- step={0.05}
895
- unit="m"
896
- value={Math.round(cornerRadius * 100) / 100}
897
- />
898
- ) : (
899
- <>
900
- {[
901
- ['Top Left', 0],
902
- ['Top Right', 1],
903
- ].map(([label, index]) => (
904
- <SliderControl
905
- key={label}
906
- label={label}
907
- max={maxRoundedRadius}
908
- min={0}
909
- onChange={(v) => setOpeningTopRadius(index as number, v)}
910
- onCommit={(v) => setOpeningTopRadius(index as number, v, true)}
911
- precision={2}
912
- step={0.05}
913
- unit="m"
914
- value={Math.round((openingTopRadii[index as number] ?? 0) * 100) / 100}
915
- />
916
- ))}
917
- </>
918
- )}
919
- <SliderControl
920
- label="Reveal Radius"
921
- max={0.08}
922
- min={0}
923
- onChange={(v) => previewDoorUpdate('openingRevealRadius', v)}
924
- onCommit={(v) => commitDoorPreview('openingRevealRadius', v)}
925
- precision={3}
926
- step={0.005}
927
- unit="m"
928
- value={Math.round(openingRevealRadius * 1000) / 1000}
929
- />
930
- </>
931
- )}
932
- {openingShape === 'arch' && (
933
- <SliderControl
934
- label="Arch Height"
935
- max={node.height}
936
- min={0.05}
937
- onChange={(v) => handleUpdate({ archHeight: v })}
938
- precision={2}
939
- restoreOnCommit={false}
940
- step={0.05}
941
- unit="m"
942
- value={Math.round(archHeight * 100) / 100}
943
- />
944
- )}
945
- </PanelSection>
946
- )}
947
-
948
- {!isOpening && (
949
- <>
950
- <PanelSection title="Frame">
951
- <SliderControl
952
- label="Thickness"
953
- max={0.2}
954
- min={0.01}
955
- onChange={(v) => handleUpdate({ frameThickness: v })}
956
- precision={3}
957
- step={0.01}
958
- unit="m"
959
- value={Math.round(node.frameThickness * 1000) / 1000}
960
- />
961
- <SliderControl
962
- label="Depth"
963
- max={0.3}
964
- min={0.01}
965
- onChange={(v) => handleUpdate({ frameDepth: v })}
966
- precision={3}
967
- step={0.01}
968
- unit="m"
969
- value={Math.round(node.frameDepth * 1000) / 1000}
970
- />
971
- </PanelSection>
972
-
973
- {!isGarageDoor && (
974
- <PanelSection title="Content Padding">
975
- <SliderControl
976
- label="Horizontal"
977
- max={0.2}
978
- min={0}
979
- onChange={(v) => handleUpdate({ contentPadding: [v, node.contentPadding[1]] })}
980
- precision={3}
981
- step={0.005}
982
- unit="m"
983
- value={Math.round(node.contentPadding[0] * 1000) / 1000}
984
- />
985
- <SliderControl
986
- label="Vertical"
987
- max={0.2}
988
- min={0}
989
- onChange={(v) => handleUpdate({ contentPadding: [node.contentPadding[0], v] })}
990
- precision={3}
991
- step={0.005}
992
- unit="m"
993
- value={Math.round(node.contentPadding[1] * 1000) / 1000}
994
- />
995
- </PanelSection>
996
- )}
997
-
998
- {isSwingDoor && (
999
- <PanelSection title="Swing">
1000
- <div className="flex flex-col gap-2 px-1 pb-1">
1001
- <div className="space-y-1">
1002
- <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
1003
- Hinges Side
1004
- </span>
1005
- <SegmentedControl
1006
- onChange={(v) => handleUpdate({ hingesSide: v })}
1007
- options={[
1008
- { label: 'Left', value: 'left' },
1009
- { label: 'Right', value: 'right' },
1010
- ]}
1011
- value={node.hingesSide}
1012
- />
1013
- </div>
1014
- <div className="space-y-1">
1015
- <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
1016
- Direction
1017
- </span>
1018
- <SegmentedControl
1019
- onChange={(v) => handleUpdate({ swingDirection: v })}
1020
- options={[
1021
- { label: 'Inward', value: 'inward' },
1022
- { label: 'Outward', value: 'outward' },
1023
- ]}
1024
- value={node.swingDirection}
1025
- />
1026
- </div>
1027
- </div>
1028
- </PanelSection>
1029
- )}
1030
-
1031
- {isSwingDoor && (
1032
- <PanelSection title="Threshold">
1033
- <ToggleControl
1034
- checked={node.threshold}
1035
- label="Enable Threshold"
1036
- onChange={(checked) => handleUpdate({ threshold: checked })}
1037
- />
1038
- {node.threshold && (
1039
- <div className="mt-1 flex flex-col gap-1">
1040
- <SliderControl
1041
- label="Height"
1042
- max={0.1}
1043
- min={0.005}
1044
- onChange={(v) => handleUpdate({ thresholdHeight: v })}
1045
- precision={3}
1046
- step={0.005}
1047
- unit="m"
1048
- value={Math.round(node.thresholdHeight * 1000) / 1000}
1049
- />
1050
- </div>
1051
- )}
1052
- </PanelSection>
1053
- )}
1054
-
1055
- {!isGarageDoor && (
1056
- <PanelSection title="Handle">
1057
- {isSwingDoor && (
1058
- <ToggleControl
1059
- checked={node.handle}
1060
- label="Enable Handle"
1061
- onChange={(checked) => handleUpdate({ handle: checked })}
1062
- />
1063
- )}
1064
- {(node.handle || !isSwingDoor) && (
1065
- <div className="mt-1 flex flex-col gap-1">
1066
- <SliderControl
1067
- label="Height"
1068
- max={node.height - 0.1}
1069
- min={0.5}
1070
- onChange={(v) => handleUpdate({ handleHeight: v })}
1071
- precision={2}
1072
- step={0.05}
1073
- unit="m"
1074
- value={Math.round(node.handleHeight * 100) / 100}
1075
- />
1076
- {supportsHandleSide && (
1077
- <div className="space-y-1">
1078
- <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
1079
- Handle Side
1080
- </span>
1081
- <SegmentedControl
1082
- onChange={(v) => handleUpdate({ handleSide: v })}
1083
- options={[
1084
- { label: 'Left', value: 'left' },
1085
- { label: 'Right', value: 'right' },
1086
- ]}
1087
- value={node.handleSide}
1088
- />
1089
- </div>
1090
- )}
1091
- </div>
1092
- )}
1093
- </PanelSection>
1094
- )}
1095
-
1096
- {isSwingDoor && (
1097
- <PanelSection title="Hardware">
1098
- <ToggleControl
1099
- checked={node.doorCloser}
1100
- label="Door Closer"
1101
- onChange={(checked) => handleUpdate({ doorCloser: checked })}
1102
- />
1103
- <ToggleControl
1104
- checked={node.panicBar}
1105
- label="Panic Bar"
1106
- onChange={(checked) => handleUpdate({ panicBar: checked })}
1107
- />
1108
- {node.panicBar && (
1109
- <div className="mt-1 flex flex-col gap-1">
1110
- <SliderControl
1111
- label="Bar Height"
1112
- max={node.height - 0.1}
1113
- min={0.5}
1114
- onChange={(v) => handleUpdate({ panicBarHeight: v })}
1115
- precision={2}
1116
- step={0.05}
1117
- unit="m"
1118
- value={Math.round(node.panicBarHeight * 100) / 100}
1119
- />
1120
- </div>
1121
- )}
1122
- </PanelSection>
1123
- )}
1124
-
1125
- {!isGarageDoor && (
1126
- <PanelSection title="Segments">
1127
- {node.segments.map((seg, i) => {
1128
- const numCols = seg.columnRatios.length
1129
- const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
1130
- const normCols = seg.columnRatios.map((r) => r / colSum)
1131
- return (
1132
- <div className="mb-2 flex flex-col gap-1" key={i}>
1133
- <div className="flex items-center justify-between pb-1">
1134
- <span className="font-medium text-white/80 text-xs">Segment {i + 1}</span>
1135
- </div>
1136
-
1137
- <SegmentedControl
1138
- onChange={(t) => {
1139
- const updated = node.segments.map((s, idx) =>
1140
- idx === i ? { ...s, type: t } : s,
1141
- )
1142
- handleUpdate({ segments: updated })
1143
- }}
1144
- options={[
1145
- { label: 'Panel', value: 'panel' },
1146
- { label: 'Glass', value: 'glass' },
1147
- { label: 'Empty', value: 'empty' },
1148
- ]}
1149
- value={seg.type}
1150
- />
1151
-
1152
- <SliderControl
1153
- label="Height"
1154
- max={95}
1155
- min={5}
1156
- onChange={(v) => setSegmentHeightRatio(i, v / 100)}
1157
- precision={1}
1158
- step={1}
1159
- unit="%"
1160
- value={Math.round(normHeights[i]! * 100 * 10) / 10}
1161
- />
1162
-
1163
- <SliderControl
1164
- label="Columns"
1165
- max={8}
1166
- min={1}
1167
- onChange={(v) => {
1168
- const n = Math.max(1, Math.min(8, Math.round(v)))
1169
- const updated = node.segments.map((s, idx) =>
1170
- idx === i ? { ...s, columnRatios: Array(n).fill(1 / n) } : s,
1171
- )
1172
- handleUpdate({ segments: updated })
1173
- }}
1174
- precision={0}
1175
- step={1}
1176
- value={numCols}
1177
- />
1178
-
1179
- {numCols > 1 && (
1180
- <div className="mt-1 border-border/50 border-t pt-1">
1181
- {normCols.map((ratio, ci) => (
1182
- <SliderControl
1183
- key={`c-${ci}`}
1184
- label={`C${ci + 1}`}
1185
- max={95}
1186
- min={5}
1187
- onChange={(v) => setSegmentColumnRatio(i, ci, v / 100)}
1188
- precision={1}
1189
- step={1}
1190
- unit="%"
1191
- value={Math.round(ratio * 100 * 10) / 10}
1192
- />
1193
- ))}
1194
- <SliderControl
1195
- label="Divider"
1196
- max={0.1}
1197
- min={0.005}
1198
- onChange={(v) => {
1199
- const updated = node.segments.map((s, idx) =>
1200
- idx === i ? { ...s, dividerThickness: v } : s,
1201
- )
1202
- handleUpdate({ segments: updated })
1203
- }}
1204
- precision={3}
1205
- step={0.005}
1206
- unit="m"
1207
- value={Math.round(seg.dividerThickness * 1000) / 1000}
1208
- />
1209
- </div>
1210
- )}
1211
-
1212
- {seg.type === 'panel' && (
1213
- <div className="mt-1 border-border/50 border-t pt-1">
1214
- <SliderControl
1215
- label="Inset"
1216
- max={0.1}
1217
- min={0}
1218
- onChange={(v) => {
1219
- const updated = node.segments.map((s, idx) =>
1220
- idx === i ? { ...s, panelInset: v } : s,
1221
- )
1222
- handleUpdate({ segments: updated })
1223
- }}
1224
- precision={3}
1225
- step={0.005}
1226
- unit="m"
1227
- value={Math.round(seg.panelInset * 1000) / 1000}
1228
- />
1229
- <SliderControl
1230
- label="Depth"
1231
- max={0.1}
1232
- min={0}
1233
- onChange={(v) => {
1234
- const updated = node.segments.map((s, idx) =>
1235
- idx === i ? { ...s, panelDepth: v } : s,
1236
- )
1237
- handleUpdate({ segments: updated })
1238
- }}
1239
- precision={3}
1240
- step={0.005}
1241
- unit="m"
1242
- value={Math.round(seg.panelDepth * 1000) / 1000}
1243
- />
1244
- </div>
1245
- )}
1246
- </div>
1247
- )
1248
- })}
1249
-
1250
- <div className="flex gap-1.5 px-1 pt-1">
1251
- <ActionButton
1252
- label="+ Add Segment"
1253
- onClick={() => {
1254
- const updated = [
1255
- ...node.segments,
1256
- {
1257
- type: 'panel' as const,
1258
- heightRatio: 1,
1259
- columnRatios: [1],
1260
- dividerThickness: 0.03,
1261
- panelDepth: 0.01,
1262
- panelInset: 0.04,
1263
- },
1264
- ]
1265
- handleUpdate({ segments: updated })
1266
- }}
1267
- />
1268
- {node.segments.length > 1 && (
1269
- <ActionButton
1270
- className="text-white/60 hover:text-white"
1271
- label="- Remove"
1272
- onClick={() => handleUpdate({ segments: node.segments.slice(0, -1) })}
1273
- />
1274
- )}
1275
- </div>
1276
- </PanelSection>
1277
- )}
1278
- </>
1279
- )}
1280
-
1281
- <PanelSection title="Actions">
1282
- <ActionGroup>
1283
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
1284
- <ActionButton
1285
- icon={<Copy className="h-3.5 w-3.5" />}
1286
- label="Duplicate"
1287
- onClick={handleDuplicate}
1288
- />
1289
- <ActionButton
1290
- className="hover:bg-red-500/20"
1291
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
1292
- label="Delete"
1293
- onClick={handleDelete}
1294
- />
1295
- </ActionGroup>
1296
- </PanelSection>
1297
- </PanelWrapper>
1298
- )
1299
- }