@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,968 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- emitter,
7
- useInteractive,
8
- useScene,
9
- WindowNode,
10
- } from '@pascal-app/core'
11
- import { useViewer } from '@pascal-app/viewer'
12
- import { BookMarked, Copy, 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
- function isSameWindowValue(current: unknown, next: unknown): boolean {
28
- if (typeof current === 'number' && typeof next === 'number') {
29
- return Math.abs(current - next) < 1e-6
30
- }
31
-
32
- if (Array.isArray(current) && Array.isArray(next)) {
33
- return (
34
- current.length === next.length &&
35
- current.every((value, index) => isSameWindowValue(value, next[index]))
36
- )
37
- }
38
-
39
- return Object.is(current, next)
40
- }
41
-
42
- function getMaxSharedWindowRadius(width: number, height: number) {
43
- return Math.max(0, Math.min(width / 2, height / 2))
44
- }
45
-
46
- function normalizeWindowCornerRadii(
47
- radii: [number, number, number, number],
48
- width: number,
49
- height: number,
50
- ): [number, number, number, number] {
51
- const next = radii.map((radius) => Math.max(radius, 0)) as [number, number, number, number]
52
- const scale = Math.min(
53
- 1,
54
- Math.max(width, 0) / Math.max(next[0] + next[1], 1e-6),
55
- Math.max(width, 0) / Math.max(next[3] + next[2], 1e-6),
56
- Math.max(height, 0) / Math.max(next[0] + next[3], 1e-6),
57
- Math.max(height, 0) / Math.max(next[1] + next[2], 1e-6),
58
- )
59
-
60
- if (scale >= 1) return next
61
-
62
- return next.map((radius) => radius * scale) as [number, number, number, number]
63
- }
64
-
65
- function isSameRadiusTuple(
66
- current: [number, number, number, number],
67
- next: [number, number, number, number],
68
- ) {
69
- return current.every((value, index) => Math.abs(value - (next[index] ?? 0)) < 1e-6)
70
- }
71
-
72
- const windowTypeOptions: Array<{ label: string; value: WindowNode['windowType'] }> = [
73
- { label: 'Fixed', value: 'fixed' },
74
- { label: 'Sliding', value: 'sliding' },
75
- { label: 'Casement', value: 'casement' },
76
- { label: 'Awning', value: 'awning' },
77
- { label: 'Single Hung', value: 'single-hung' },
78
- { label: 'Double Hung', value: 'double-hung' },
79
- { label: 'Bay', value: 'bay' },
80
- { label: 'Bow', value: 'bow' },
81
- { label: 'Louvered', value: 'louvered' },
82
- ]
83
-
84
- const rectangleOnlyWindowTypes = new Set<WindowNode['windowType']>([
85
- 'sliding',
86
- 'single-hung',
87
- 'double-hung',
88
- 'bay',
89
- 'bow',
90
- ])
91
-
92
- export function WindowPanel() {
93
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
94
- const setSelection = useViewer((s) => s.setSelection)
95
- const updateNode = useScene((s) => s.updateNode)
96
- const deleteNode = useScene((s) => s.deleteNode)
97
- const setMovingNode = useEditor((s) => s.setMovingNode)
98
- const previewRef = useRef<{
99
- id: AnyNodeId
100
- key: keyof WindowNode
101
- value: unknown
102
- } | null>(null)
103
-
104
- const adapter = usePresetsAdapter()
105
-
106
- const node = useScene((s) =>
107
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as WindowNode | undefined) : undefined,
108
- )
109
-
110
- const handleUpdate = useCallback(
111
- (updates: Partial<WindowNode>) => {
112
- if (!(selectedId && node)) return
113
- const hasChange = Object.entries(updates).some(([key, value]) => {
114
- const currentValue = node[key as keyof WindowNode]
115
- return !isSameWindowValue(currentValue, value)
116
- })
117
- if (!hasChange) return
118
-
119
- updateNode(selectedId as AnyNode['id'], updates)
120
- useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
121
- },
122
- [selectedId, node, updateNode],
123
- )
124
-
125
- const previewWindowUpdate = useCallback(
126
- <K extends keyof WindowNode>(key: K, value: WindowNode[K]) => {
127
- if (!selectedId) return
128
- const liveNode = useScene.getState().nodes[selectedId as AnyNodeId]
129
- if (liveNode?.type !== 'window') return
130
-
131
- if (
132
- !(
133
- previewRef.current &&
134
- previewRef.current.id === selectedId &&
135
- previewRef.current.key === key
136
- )
137
- ) {
138
- previewRef.current = {
139
- id: selectedId as AnyNodeId,
140
- key,
141
- value: liveNode[key],
142
- }
143
- }
144
-
145
- if (isSameWindowValue(liveNode[key], value)) return
146
-
147
- ;(liveNode as WindowNode)[key] = value
148
- useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
149
- },
150
- [selectedId],
151
- )
152
-
153
- const commitWindowPreview = useCallback(
154
- <K extends keyof WindowNode>(key: K, value: WindowNode[K]) => {
155
- if (!selectedId) return
156
-
157
- const scene = useScene.getState()
158
- const liveNode = scene.nodes[selectedId as AnyNodeId]
159
- const preview = previewRef.current
160
- if (liveNode?.type === 'window' && preview?.id === selectedId && preview.key === key) {
161
- ;(liveNode as WindowNode)[key] = preview.value as WindowNode[K]
162
- scene.dirtyNodes.add(selectedId as AnyNodeId)
163
- }
164
- previewRef.current = null
165
-
166
- updateNode(selectedId as AnyNode['id'], { [key]: value } as Partial<WindowNode>)
167
- scene.dirtyNodes.add(selectedId as AnyNodeId)
168
- },
169
- [selectedId, updateNode],
170
- )
171
-
172
- const handleClose = useCallback(() => {
173
- setSelection({ selectedIds: [] })
174
- }, [setSelection])
175
-
176
- const handleFlip = useCallback(() => {
177
- if (!node) return
178
- handleUpdate({
179
- side: node.side === 'front' ? 'back' : 'front',
180
- rotation: [node.rotation[0], node.rotation[1] + Math.PI, node.rotation[2]],
181
- })
182
- }, [node, handleUpdate])
183
-
184
- const handleMove = useCallback(() => {
185
- if (!node) return
186
- sfxEmitter.emit('sfx:item-pick')
187
- setMovingNode(node)
188
- setSelection({ selectedIds: [] })
189
- }, [node, setMovingNode, setSelection])
190
-
191
- const handleDelete = useCallback(() => {
192
- if (!(selectedId && node)) return
193
- sfxEmitter.emit('sfx:item-delete')
194
- deleteNode(selectedId as AnyNode['id'])
195
- if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
196
- setSelection({ selectedIds: [] })
197
- }, [selectedId, node, deleteNode, setSelection])
198
-
199
- const handleDuplicate = useCallback(() => {
200
- if (!node?.parentId) return
201
- sfxEmitter.emit('sfx:item-pick')
202
- useScene.temporal.getState().pause()
203
- const duplicate = WindowNode.parse({
204
- position: [...node.position] as [number, number, number],
205
- rotation: [...node.rotation] as [number, number, number],
206
- side: node.side,
207
- wallId: node.wallId,
208
- parentId: node.parentId,
209
- width: node.width,
210
- height: node.height,
211
- windowType: node.windowType,
212
- operationState: node.operationState,
213
- awningDirection: node.awningDirection,
214
- casementStyle: node.casementStyle,
215
- hingesSide: node.hingesSide,
216
- frameThickness: node.frameThickness,
217
- frameDepth: node.frameDepth,
218
- openingKind: node.openingKind,
219
- openingShape: node.openingShape,
220
- openingRadiusMode: node.openingRadiusMode ?? 'all',
221
- openingCornerRadii: [...(node.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15])],
222
- cornerRadius: node.cornerRadius,
223
- archHeight: node.archHeight,
224
- openingRevealRadius: node.openingRevealRadius,
225
- columnRatios: [...node.columnRatios],
226
- rowRatios: [...node.rowRatios],
227
- columnDividerThickness: node.columnDividerThickness,
228
- rowDividerThickness: node.rowDividerThickness,
229
- sill: node.sill,
230
- sillDepth: node.sillDepth,
231
- sillThickness: node.sillThickness,
232
- metadata: { isNew: true },
233
- })
234
- useScene.getState().createNode(duplicate, node.parentId as AnyNodeId)
235
- setMovingNode(duplicate)
236
- setSelection({ selectedIds: [] })
237
- }, [node, setMovingNode, setSelection])
238
-
239
- const getWindowPresetData = useCallback(() => {
240
- if (!node) return null
241
- return {
242
- width: node.width,
243
- height: node.height,
244
- windowType: node.windowType,
245
- operationState: node.operationState,
246
- awningDirection: node.awningDirection,
247
- casementStyle: node.casementStyle,
248
- hingesSide: node.hingesSide,
249
- frameThickness: node.frameThickness,
250
- frameDepth: node.frameDepth,
251
- openingKind: node.openingKind,
252
- openingShape: node.openingShape,
253
- openingRadiusMode: node.openingRadiusMode ?? 'all',
254
- openingCornerRadii: node.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15],
255
- cornerRadius: node.cornerRadius,
256
- archHeight: node.archHeight,
257
- openingRevealRadius: node.openingRevealRadius,
258
- columnRatios: node.columnRatios,
259
- rowRatios: node.rowRatios,
260
- columnDividerThickness: node.columnDividerThickness,
261
- rowDividerThickness: node.rowDividerThickness,
262
- sill: node.sill,
263
- sillDepth: node.sillDepth,
264
- sillThickness: node.sillThickness,
265
- }
266
- }, [node])
267
-
268
- const handleSavePreset = useCallback(
269
- async (name: string) => {
270
- const data = getWindowPresetData()
271
- if (!(data && selectedId)) return
272
- const presetId = await adapter.savePreset('window', name, data)
273
- if (presetId) emitter.emit('preset:generate-thumbnail', { presetId, nodeId: selectedId })
274
- },
275
- [getWindowPresetData, selectedId, adapter],
276
- )
277
-
278
- const handleOverwritePreset = useCallback(
279
- async (id: string) => {
280
- const data = getWindowPresetData()
281
- if (!(data && selectedId)) return
282
- await adapter.overwritePreset('window', id, data)
283
- emitter.emit('preset:generate-thumbnail', { presetId: id, nodeId: selectedId })
284
- },
285
- [getWindowPresetData, selectedId, adapter],
286
- )
287
-
288
- const handleApplyPreset = useCallback(
289
- (data: Record<string, unknown>) => {
290
- handleUpdate(data as Partial<WindowNode>)
291
- },
292
- [handleUpdate],
293
- )
294
-
295
- if (!(node && node.type === 'window' && selectedId)) return null
296
-
297
- const numCols = node.columnRatios.length
298
- const numRows = node.rowRatios.length
299
-
300
- const colSum = node.columnRatios.reduce((a, b) => a + b, 0)
301
- const rowSum = node.rowRatios.reduce((a, b) => a + b, 0)
302
- const normCols = node.columnRatios.map((r) => r / colSum)
303
- const normRows = node.rowRatios.map((r) => r / rowSum)
304
- const isOpening = node.openingKind === 'opening'
305
- const openingShape = node.openingShape ?? 'rectangle'
306
- const windowShape =
307
- openingShape === 'arch' || openingShape === 'rounded' ? openingShape : 'rectangle'
308
- const openingRadiusMode = node.openingRadiusMode ?? 'all'
309
- const openingCornerRadii = node.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15]
310
- const cornerRadius = node.cornerRadius ?? 0.15
311
- const archHeight = node.archHeight ?? 0.35
312
- const openingRevealRadius = node.openingRevealRadius ?? 0.025
313
- const maxRoundedRadius = Math.max(0.01, getMaxSharedWindowRadius(node.width, node.height))
314
- const displayedWindowType = node.windowType === 'hopper' ? 'awning' : (node.windowType ?? 'fixed')
315
- const awningDirection = node.windowType === 'hopper' ? 'down' : (node.awningDirection ?? 'up')
316
- const isOperableWindow =
317
- node.windowType === 'sliding' ||
318
- node.windowType === 'casement' ||
319
- node.windowType === 'awning' ||
320
- node.windowType === 'hopper' ||
321
- node.windowType === 'single-hung' ||
322
- node.windowType === 'double-hung' ||
323
- node.windowType === 'louvered'
324
-
325
- const setOperationState = (value: number) => {
326
- useInteractive.getState().cancelWindowAnimation(node.id)
327
- useInteractive.getState().removeWindowOpenState(node.id)
328
- handleUpdate({ operationState: Math.max(0, Math.min(1, value)) })
329
- }
330
-
331
- const getDimensionUpdates = (updates: Partial<Pick<WindowNode, 'width' | 'height'>>) => {
332
- const nextWidth = updates.width ?? node.width
333
- const nextHeight = updates.height ?? node.height
334
- const nextUpdates: Partial<WindowNode> = { ...updates }
335
-
336
- if (openingShape === 'rounded') {
337
- if (openingRadiusMode === 'individual') {
338
- const currentRadii = openingCornerRadii as [number, number, number, number]
339
- const nextRadii = normalizeWindowCornerRadii(
340
- openingCornerRadii as [number, number, number, number],
341
- nextWidth,
342
- nextHeight,
343
- )
344
- if (!isSameRadiusTuple(currentRadii, nextRadii)) {
345
- nextUpdates.openingCornerRadii = nextRadii
346
- }
347
- } else {
348
- const nextRadius = Math.min(
349
- Math.max(cornerRadius, 0),
350
- getMaxSharedWindowRadius(nextWidth, nextHeight),
351
- )
352
- if (Math.abs(nextRadius - cornerRadius) > 1e-6) {
353
- nextUpdates.cornerRadius = nextRadius
354
- }
355
- }
356
- }
357
-
358
- if (openingShape === 'arch') {
359
- const nextArchHeight = Math.min(Math.max(archHeight, 0.05), Math.max(nextHeight, 0.05))
360
- if (Math.abs(nextArchHeight - archHeight) > 1e-6) {
361
- nextUpdates.archHeight = nextArchHeight
362
- }
363
- }
364
-
365
- return nextUpdates
366
- }
367
-
368
- const setOpeningCornerRadius = (index: number, value: number, commit = false) => {
369
- const next = [...openingCornerRadii] as [number, number, number, number]
370
- next[index] = value
371
- if (commit) {
372
- commitWindowPreview('openingCornerRadii', next)
373
- } else {
374
- previewWindowUpdate('openingCornerRadii', next)
375
- }
376
- }
377
-
378
- const setColumnRatio = (index: number, newVal: number) => {
379
- const clamped = Math.max(0.05, Math.min(0.95, newVal))
380
- const neighborIdx = index < numCols - 1 ? index + 1 : index - 1
381
- const delta = clamped - normCols[index]!
382
- const neighborVal = Math.max(0.05, normCols[neighborIdx]! - delta)
383
- const newRatios = normCols.map((v, i) => {
384
- if (i === index) return clamped
385
- if (i === neighborIdx) return neighborVal
386
- return v
387
- })
388
- handleUpdate({ columnRatios: newRatios })
389
- }
390
-
391
- const setRowRatio = (index: number, newVal: number) => {
392
- const clamped = Math.max(0.05, Math.min(0.95, newVal))
393
- const neighborIdx = index < numRows - 1 ? index + 1 : index - 1
394
- const delta = clamped - normRows[index]!
395
- const neighborVal = Math.max(0.05, normRows[neighborIdx]! - delta)
396
- const newRatios = normRows.map((v, i) => {
397
- if (i === index) return clamped
398
- if (i === neighborIdx) return neighborVal
399
- return v
400
- })
401
- handleUpdate({ rowRatios: newRatios })
402
- }
403
-
404
- return (
405
- <PanelWrapper
406
- icon="/icons/window.png"
407
- onClose={handleClose}
408
- title={node.name || 'Window'}
409
- width={320}
410
- >
411
- {/* Presets strip */}
412
- <div className="border-border/30 border-b px-3 pt-2.5 pb-1.5">
413
- <PresetsPopover
414
- isAuthenticated={adapter.isAuthenticated}
415
- onApply={handleApplyPreset}
416
- onDelete={(id) => adapter.deletePreset(id)}
417
- onFetchPresets={(tab) => adapter.fetchPresets('window', tab)}
418
- onOverwrite={handleOverwritePreset}
419
- onRename={(id, name) => adapter.renamePreset(id, name)}
420
- onSave={handleSavePreset}
421
- onToggleCommunity={adapter.togglePresetCommunity}
422
- tabs={adapter.tabs}
423
- type="window"
424
- >
425
- <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">
426
- <BookMarked className="h-3.5 w-3.5 shrink-0" />
427
- <span>Presets</span>
428
- </button>
429
- </PresetsPopover>
430
- </div>
431
-
432
- <PanelSection title="Type">
433
- <SegmentedControl
434
- onChange={(value) =>
435
- handleUpdate({
436
- openingKind: value as WindowNode['openingKind'],
437
- ...(value === 'opening'
438
- ? {
439
- openingShape,
440
- openingRadiusMode,
441
- openingCornerRadii,
442
- cornerRadius,
443
- archHeight,
444
- openingRevealRadius,
445
- }
446
- : {}),
447
- })
448
- }
449
- options={[
450
- { value: 'window', label: 'Window' },
451
- { value: 'opening', label: 'Opening' },
452
- ]}
453
- value={node.openingKind ?? 'window'}
454
- />
455
- </PanelSection>
456
-
457
- {!isOpening && (
458
- <PanelSection title="Window Type">
459
- <div className="grid grid-cols-2 gap-1.5 px-1 pt-1">
460
- {windowTypeOptions.map((option) => {
461
- const isSelected = displayedWindowType === option.value
462
- return (
463
- <button
464
- className={cn(
465
- 'flex min-h-12 items-center rounded-lg border px-2.5 text-left text-xs transition-colors',
466
- isSelected
467
- ? 'border-orange-400/60 bg-orange-400/10 text-foreground'
468
- : 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
469
- )}
470
- key={option.value}
471
- onClick={() =>
472
- handleUpdate({
473
- windowType: option.value,
474
- ...(option.value === 'awning' ? { awningDirection } : {}),
475
- ...(rectangleOnlyWindowTypes.has(option.value)
476
- ? { openingShape: 'rectangle' }
477
- : {}),
478
- ...(option.value === 'bay' || option.value === 'bow' ? { sill: false } : {}),
479
- })
480
- }
481
- type="button"
482
- >
483
- <span className="truncate font-medium">{option.label}</span>
484
- </button>
485
- )
486
- })}
487
- </div>
488
- {displayedWindowType === 'awning' && (
489
- <div className="mt-2">
490
- <SegmentedControl
491
- onChange={(value) =>
492
- handleUpdate({
493
- windowType: 'awning',
494
- awningDirection: value as WindowNode['awningDirection'],
495
- })
496
- }
497
- options={[
498
- { value: 'up', label: 'Up' },
499
- { value: 'down', label: 'Down' },
500
- ]}
501
- value={awningDirection}
502
- />
503
- </div>
504
- )}
505
- {node.windowType === 'casement' && (
506
- <div className="mt-2 space-y-2">
507
- <SegmentedControl
508
- onChange={(value) =>
509
- handleUpdate({ casementStyle: value as WindowNode['casementStyle'] })
510
- }
511
- options={[
512
- { value: 'single', label: 'Single' },
513
- { value: 'french', label: 'French' },
514
- ]}
515
- value={node.casementStyle ?? 'single'}
516
- />
517
- {(node.casementStyle ?? 'single') === 'single' && (
518
- <SegmentedControl
519
- onChange={(value) =>
520
- handleUpdate({ hingesSide: value as WindowNode['hingesSide'] })
521
- }
522
- options={[
523
- { value: 'left', label: 'Left' },
524
- { value: 'right', label: 'Right' },
525
- ]}
526
- value={node.hingesSide ?? 'left'}
527
- />
528
- )}
529
- </div>
530
- )}
531
- {isOperableWindow && (
532
- <div className="mt-2">
533
- <SliderControl
534
- label="Open"
535
- max={1}
536
- min={0}
537
- onChange={setOperationState}
538
- precision={2}
539
- restoreOnCommit={false}
540
- step={0.05}
541
- value={Math.round((node.operationState ?? 0) * 100) / 100}
542
- />
543
- </div>
544
- )}
545
- </PanelSection>
546
- )}
547
-
548
- <PanelSection title="Position">
549
- <SliderControl
550
- label={
551
- <>
552
- X<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
553
- </>
554
- }
555
- onChange={(v) => handleUpdate({ position: [v, node.position[1], node.position[2]] })}
556
- precision={2}
557
- step={0.1}
558
- unit="m"
559
- value={Math.round(node.position[0] * 100) / 100}
560
- />
561
- <SliderControl
562
- label={
563
- <>
564
- Y<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
565
- </>
566
- }
567
- onChange={(v) => handleUpdate({ position: [node.position[0], v, node.position[2]] })}
568
- precision={2}
569
- step={0.1}
570
- unit="m"
571
- value={Math.round(node.position[1] * 100) / 100}
572
- />
573
- {!isOpening && (
574
- <div className="px-1 pt-2 pb-1">
575
- <ActionButton
576
- className="w-full"
577
- icon={<FlipHorizontal2 className="h-4 w-4" />}
578
- label="Flip Side"
579
- onClick={handleFlip}
580
- />
581
- </div>
582
- )}
583
- </PanelSection>
584
-
585
- <PanelSection title="Dimensions">
586
- <SliderControl
587
- label="Width"
588
- min={0}
589
- onChange={(v) => handleUpdate(getDimensionUpdates({ width: v }))}
590
- precision={2}
591
- restoreOnCommit={false}
592
- step={0.1}
593
- unit="m"
594
- value={Math.round(node.width * 100) / 100}
595
- />
596
- <SliderControl
597
- label="Height"
598
- min={0}
599
- onChange={(v) => handleUpdate(getDimensionUpdates({ height: v }))}
600
- precision={2}
601
- restoreOnCommit={false}
602
- step={0.1}
603
- unit="m"
604
- value={Math.round(node.height * 100) / 100}
605
- />
606
- </PanelSection>
607
-
608
- {!(isOpening || rectangleOnlyWindowTypes.has(node.windowType)) && (
609
- <PanelSection title="Corner Shape">
610
- <SegmentedControl
611
- onChange={(value) =>
612
- handleUpdate({
613
- openingShape: value as WindowNode['openingShape'],
614
- ...(value === 'rounded'
615
- ? {
616
- openingRadiusMode,
617
- openingCornerRadii,
618
- cornerRadius: Math.min(cornerRadius, maxRoundedRadius),
619
- openingRevealRadius,
620
- sill: false,
621
- }
622
- : {}),
623
- ...(value === 'arch' ? { archHeight } : {}),
624
- })
625
- }
626
- options={[
627
- { value: 'rectangle', label: 'Rect' },
628
- { value: 'rounded', label: 'Rounded' },
629
- { value: 'arch', label: 'Arch' },
630
- ]}
631
- value={windowShape}
632
- />
633
- {windowShape === 'rounded' && (
634
- <div className="mt-2 flex flex-col gap-1">
635
- <SegmentedControl
636
- onChange={(value) =>
637
- handleUpdate({ openingRadiusMode: value as WindowNode['openingRadiusMode'] })
638
- }
639
- options={[
640
- { value: 'all', label: 'All' },
641
- { value: 'individual', label: 'Individual' },
642
- ]}
643
- value={openingRadiusMode}
644
- />
645
- {openingRadiusMode === 'all' ? (
646
- <SliderControl
647
- label="Corner Radius"
648
- max={maxRoundedRadius}
649
- min={0}
650
- onChange={(value) => previewWindowUpdate('cornerRadius', value)}
651
- onCommit={(value) => commitWindowPreview('cornerRadius', value)}
652
- precision={2}
653
- step={0.05}
654
- unit="m"
655
- value={Math.round(cornerRadius * 100) / 100}
656
- />
657
- ) : (
658
- <>
659
- {[
660
- ['Top Left', 0],
661
- ['Top Right', 1],
662
- ['Bottom Right', 2],
663
- ['Bottom Left', 3],
664
- ].map(([label, index]) => (
665
- <SliderControl
666
- key={label}
667
- label={label}
668
- max={maxRoundedRadius}
669
- min={0}
670
- onChange={(value) => setOpeningCornerRadius(index as number, value)}
671
- onCommit={(value) => setOpeningCornerRadius(index as number, value, true)}
672
- precision={2}
673
- step={0.05}
674
- unit="m"
675
- value={Math.round((openingCornerRadii[index as number] ?? 0) * 100) / 100}
676
- />
677
- ))}
678
- </>
679
- )}
680
- <SliderControl
681
- label="Reveal Radius"
682
- max={0.08}
683
- min={0}
684
- onChange={(value) => previewWindowUpdate('openingRevealRadius', value)}
685
- onCommit={(value) => commitWindowPreview('openingRevealRadius', value)}
686
- precision={3}
687
- step={0.005}
688
- unit="m"
689
- value={Math.round(openingRevealRadius * 1000) / 1000}
690
- />
691
- </div>
692
- )}
693
- {windowShape === 'arch' && (
694
- <div className="mt-2 flex flex-col gap-1">
695
- <SliderControl
696
- label="Arch Height"
697
- max={Math.max(0.05, node.height)}
698
- min={0.05}
699
- onChange={(value) => handleUpdate({ archHeight: value })}
700
- precision={2}
701
- restoreOnCommit={false}
702
- step={0.05}
703
- unit="m"
704
- value={Math.round(archHeight * 100) / 100}
705
- />
706
- </div>
707
- )}
708
- </PanelSection>
709
- )}
710
-
711
- {isOpening && (
712
- <PanelSection title="Opening Shape">
713
- <SegmentedControl
714
- onChange={(value) =>
715
- handleUpdate({ openingShape: value as WindowNode['openingShape'] })
716
- }
717
- options={[
718
- { value: 'rectangle', label: 'Rect' },
719
- { value: 'rounded', label: 'Rounded' },
720
- { value: 'arch', label: 'Arch' },
721
- ]}
722
- value={openingShape}
723
- />
724
- {openingShape === 'rounded' && (
725
- <div className="mt-2 flex flex-col gap-1">
726
- <SegmentedControl
727
- onChange={(value) =>
728
- handleUpdate({ openingRadiusMode: value as WindowNode['openingRadiusMode'] })
729
- }
730
- options={[
731
- { value: 'all', label: 'All' },
732
- { value: 'individual', label: 'Individual' },
733
- ]}
734
- value={openingRadiusMode}
735
- />
736
- {openingRadiusMode === 'all' ? (
737
- <SliderControl
738
- label="Corner Radius"
739
- max={maxRoundedRadius}
740
- min={0}
741
- onChange={(value) => previewWindowUpdate('cornerRadius', value)}
742
- onCommit={(value) => commitWindowPreview('cornerRadius', value)}
743
- precision={2}
744
- step={0.05}
745
- unit="m"
746
- value={Math.round(cornerRadius * 100) / 100}
747
- />
748
- ) : (
749
- <>
750
- {[
751
- ['Top Left', 0],
752
- ['Top Right', 1],
753
- ['Bottom Right', 2],
754
- ['Bottom Left', 3],
755
- ].map(([label, index]) => (
756
- <SliderControl
757
- key={label}
758
- label={label}
759
- max={maxRoundedRadius}
760
- min={0}
761
- onChange={(value) => setOpeningCornerRadius(index as number, value)}
762
- onCommit={(value) => setOpeningCornerRadius(index as number, value, true)}
763
- precision={2}
764
- step={0.05}
765
- unit="m"
766
- value={Math.round((openingCornerRadii[index as number] ?? 0) * 100) / 100}
767
- />
768
- ))}
769
- </>
770
- )}
771
- <SliderControl
772
- label="Reveal Radius"
773
- max={0.08}
774
- min={0}
775
- onChange={(value) => previewWindowUpdate('openingRevealRadius', value)}
776
- onCommit={(value) => commitWindowPreview('openingRevealRadius', value)}
777
- precision={3}
778
- step={0.005}
779
- unit="m"
780
- value={Math.round(openingRevealRadius * 1000) / 1000}
781
- />
782
- </div>
783
- )}
784
- {openingShape === 'arch' && (
785
- <div className="mt-2 flex flex-col gap-1">
786
- <SliderControl
787
- label="Arch Height"
788
- max={Math.max(0.05, node.height)}
789
- min={0.05}
790
- onChange={(value) => handleUpdate({ archHeight: value })}
791
- precision={2}
792
- restoreOnCommit={false}
793
- step={0.05}
794
- unit="m"
795
- value={Math.round(archHeight * 100) / 100}
796
- />
797
- </div>
798
- )}
799
- </PanelSection>
800
- )}
801
-
802
- {!isOpening && (
803
- <>
804
- <PanelSection title="Frame">
805
- <SliderControl
806
- label="Thickness"
807
- min={0}
808
- onChange={(v) => handleUpdate({ frameThickness: v })}
809
- precision={3}
810
- step={0.01}
811
- unit="m"
812
- value={Math.round(node.frameThickness * 1000) / 1000}
813
- />
814
- <SliderControl
815
- label="Depth"
816
- min={0}
817
- onChange={(v) => handleUpdate({ frameDepth: v })}
818
- precision={3}
819
- step={0.01}
820
- unit="m"
821
- value={Math.round(node.frameDepth * 1000) / 1000}
822
- />
823
- </PanelSection>
824
-
825
- <PanelSection title="Grid">
826
- <SliderControl
827
- label="Columns"
828
- max={8}
829
- min={1}
830
- onChange={(v) => {
831
- const n = Math.max(1, Math.min(8, Math.round(v)))
832
- handleUpdate({ columnRatios: Array(n).fill(1 / n) })
833
- }}
834
- precision={0}
835
- step={1}
836
- value={numCols}
837
- />
838
- <SliderControl
839
- label="Rows"
840
- max={8}
841
- min={1}
842
- onChange={(v) => {
843
- const n = Math.max(1, Math.min(8, Math.round(v)))
844
- handleUpdate({ rowRatios: Array(n).fill(1 / n) })
845
- }}
846
- precision={0}
847
- step={1}
848
- value={numRows}
849
- />
850
-
851
- {numCols > 1 && (
852
- <div className="mt-2 flex flex-col gap-1">
853
- <div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
854
- Col Widths
855
- </div>
856
- {normCols.map((ratio, i) => (
857
- <SliderControl
858
- key={`c-${i}`}
859
- label={`C${i + 1}`}
860
- max={95}
861
- min={5}
862
- onChange={(v) => setColumnRatio(i, v / 100)}
863
- precision={1}
864
- step={1}
865
- unit="%"
866
- value={Math.round(ratio * 100 * 10) / 10}
867
- />
868
- ))}
869
- <div className="mt-1 border-border/50 border-t pt-1">
870
- <SliderControl
871
- label="Divider"
872
- max={0.1}
873
- min={0.005}
874
- onChange={(v) => handleUpdate({ columnDividerThickness: v })}
875
- precision={3}
876
- step={0.01}
877
- unit="m"
878
- value={Math.round((node.columnDividerThickness ?? 0.03) * 1000) / 1000}
879
- />
880
- </div>
881
- </div>
882
- )}
883
-
884
- {numRows > 1 && (
885
- <div className="mt-2 flex flex-col gap-1">
886
- <div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
887
- Row Heights
888
- </div>
889
- {normRows.map((ratio, i) => (
890
- <SliderControl
891
- key={`r-${i}`}
892
- label={`R${i + 1}`}
893
- max={95}
894
- min={5}
895
- onChange={(v) => setRowRatio(i, v / 100)}
896
- precision={1}
897
- step={1}
898
- unit="%"
899
- value={Math.round(ratio * 100 * 10) / 10}
900
- />
901
- ))}
902
- <div className="mt-1 border-border/50 border-t pt-1">
903
- <SliderControl
904
- label="Divider"
905
- max={0.1}
906
- min={0.005}
907
- onChange={(v) => handleUpdate({ rowDividerThickness: v })}
908
- precision={3}
909
- step={0.01}
910
- unit="m"
911
- value={Math.round((node.rowDividerThickness ?? 0.03) * 1000) / 1000}
912
- />
913
- </div>
914
- </div>
915
- )}
916
- </PanelSection>
917
-
918
- <PanelSection title="Sill">
919
- <ToggleControl
920
- checked={node.sill}
921
- label="Enable Sill"
922
- onChange={(checked) => handleUpdate({ sill: checked })}
923
- />
924
- {node.sill && (
925
- <div className="mt-1 flex flex-col gap-1">
926
- <SliderControl
927
- label="Depth"
928
- min={0}
929
- onChange={(v) => handleUpdate({ sillDepth: v })}
930
- precision={3}
931
- step={0.01}
932
- unit="m"
933
- value={Math.round(node.sillDepth * 1000) / 1000}
934
- />
935
- <SliderControl
936
- label="Thickness"
937
- min={0}
938
- onChange={(v) => handleUpdate({ sillThickness: v })}
939
- precision={3}
940
- step={0.01}
941
- unit="m"
942
- value={Math.round(node.sillThickness * 1000) / 1000}
943
- />
944
- </div>
945
- )}
946
- </PanelSection>
947
- </>
948
- )}
949
-
950
- <PanelSection title="Actions">
951
- <ActionGroup>
952
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
953
- <ActionButton
954
- icon={<Copy className="h-3.5 w-3.5" />}
955
- label="Duplicate"
956
- onClick={handleDuplicate}
957
- />
958
- <ActionButton
959
- className="hover:bg-red-500/20"
960
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
961
- label="Delete"
962
- onClick={handleDelete}
963
- />
964
- </ActionGroup>
965
- </PanelSection>
966
- </PanelWrapper>
967
- )
968
- }