@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
@@ -0,0 +1,91 @@
1
+ import type { IconRef, LazyComponent } from '@pascal-app/core'
2
+
3
+ export type EditorHostPanelWorkspace = string & {}
4
+
5
+ export type EditorHostPanel = {
6
+ id: string
7
+ label: string
8
+ icon: IconRef
9
+ component: LazyComponent
10
+ kinds?: readonly string[]
11
+ workspaces?: readonly EditorHostPanelWorkspace[]
12
+ pluginId?: string
13
+ description?: string
14
+ creator?: {
15
+ name: string
16
+ url?: string
17
+ }
18
+ pluginUrl?: string
19
+ defaultInstalled?: boolean
20
+ }
21
+
22
+ function isDevMode(): boolean {
23
+ try {
24
+ const meta = import.meta as { env?: { DEV?: boolean } }
25
+ if (typeof meta?.env?.DEV === 'boolean') return meta.env.DEV
26
+ } catch {
27
+ // import.meta unavailable in some CJS contexts — fall through.
28
+ }
29
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV) {
30
+ return process.env.NODE_ENV !== 'production'
31
+ }
32
+ return false
33
+ }
34
+
35
+ class EditorHostPanelRegistryImpl {
36
+ private readonly panels = new Map<string, EditorHostPanel>()
37
+ private readonly listeners = new Set<() => void>()
38
+ private cached: EditorHostPanel[] = []
39
+
40
+ subscribe = (onChange: () => void): (() => void) => {
41
+ this.listeners.add(onChange)
42
+ return () => {
43
+ this.listeners.delete(onChange)
44
+ }
45
+ }
46
+
47
+ getSnapshot = (): EditorHostPanel[] => this.cached
48
+
49
+ panelForKind = (kind: string): string | undefined =>
50
+ this.cached.find((panel) => panel.kinds?.includes(kind))?.id
51
+
52
+ getDefaultInstalledPluginIds = (): string[] =>
53
+ Array.from(
54
+ new Set(
55
+ this.cached
56
+ .filter((panel) => panel.pluginId && panel.defaultInstalled)
57
+ .map((panel) => panel.pluginId as string),
58
+ ),
59
+ )
60
+
61
+ reset(): void {
62
+ this.panels.clear()
63
+ this.emit()
64
+ }
65
+
66
+ registerPanel(panel: EditorHostPanel): void {
67
+ if (typeof panel.id !== 'string' || panel.id.length === 0) {
68
+ throw new Error('[editor:host-panels] panel id must be a non-empty string')
69
+ }
70
+ if (this.panels.has(panel.id)) {
71
+ if (isDevMode()) {
72
+ console.warn(`[editor:host-panels] re-registering panel "${panel.id}" (HMR)`)
73
+ } else {
74
+ throw new Error(`[editor:host-panels] duplicate panel id: "${panel.id}" already registered`)
75
+ }
76
+ }
77
+ this.panels.set(panel.id, panel)
78
+ this.emit()
79
+ }
80
+
81
+ private emit(): void {
82
+ this.cached = Array.from(this.panels.values())
83
+ for (const listener of this.listeners) listener()
84
+ }
85
+ }
86
+
87
+ export const editorHostPanelRegistry = new EditorHostPanelRegistryImpl()
88
+
89
+ export function registerEditorHostPanel(panel: EditorHostPanel): void {
90
+ editorHostPanelRegistry.registerPanel(panel)
91
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { createQuickMeasurementPointerScheduler } from './quick-measurement'
3
+
4
+ function pointer(clientX: number, clientY: number): PointerEvent {
5
+ return { clientX, clientY } as PointerEvent
6
+ }
7
+
8
+ function createFrameDriver() {
9
+ let nextId = 1
10
+ const callbacks = new Map<number, FrameRequestCallback>()
11
+ return {
12
+ driver: {
13
+ request: (callback: FrameRequestCallback) => {
14
+ const id = nextId++
15
+ callbacks.set(id, callback)
16
+ return id
17
+ },
18
+ cancel: (frameId: number) => callbacks.delete(frameId),
19
+ },
20
+ pending: () => callbacks.size,
21
+ runNext: (timestamp: number) => {
22
+ const next = callbacks.entries().next().value as [number, FrameRequestCallback] | undefined
23
+ if (!next) throw new Error('No animation frame scheduled')
24
+ callbacks.delete(next[0])
25
+ next[1](timestamp)
26
+ },
27
+ }
28
+ }
29
+
30
+ describe('createQuickMeasurementPointerScheduler', () => {
31
+ test('keeps only the latest pointer event and caps expensive work', () => {
32
+ const frames = createFrameDriver()
33
+ const processed: PointerEvent[] = []
34
+ const scheduler = createQuickMeasurementPointerScheduler(
35
+ (event) => processed.push(event),
36
+ frames.driver,
37
+ )
38
+
39
+ scheduler.enqueue(pointer(1, 1))
40
+ scheduler.enqueue(pointer(8, 4))
41
+ expect(frames.pending()).toBe(1)
42
+ frames.runNext(0)
43
+ expect(processed.map((event) => [event.clientX, event.clientY])).toEqual([[8, 4]])
44
+
45
+ scheduler.enqueue(pointer(12, 4))
46
+ frames.runNext(16)
47
+ expect(processed).toHaveLength(1)
48
+ expect(frames.pending()).toBe(1)
49
+
50
+ scheduler.enqueue(pointer(20, 7))
51
+ frames.runNext(32)
52
+ expect(processed.map((event) => [event.clientX, event.clientY])).toEqual([
53
+ [8, 4],
54
+ [20, 7],
55
+ ])
56
+ })
57
+
58
+ test('ignores sub-pixel jitter and cancels queued work', () => {
59
+ const frames = createFrameDriver()
60
+ const processed: PointerEvent[] = []
61
+ const scheduler = createQuickMeasurementPointerScheduler(
62
+ (event) => processed.push(event),
63
+ frames.driver,
64
+ )
65
+
66
+ scheduler.enqueue(pointer(10, 10))
67
+ frames.runNext(0)
68
+ scheduler.enqueue(pointer(10.5, 10.5))
69
+ frames.runNext(32)
70
+ expect(processed).toHaveLength(1)
71
+
72
+ scheduler.enqueue(pointer(20, 20))
73
+ scheduler.clear()
74
+ expect(frames.pending()).toBe(0)
75
+ expect(processed).toHaveLength(1)
76
+ })
77
+ })
@@ -0,0 +1,109 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ type GeometryContext,
5
+ nodeRegistry,
6
+ type QuickMeasurementReport,
7
+ } from '@pascal-app/core'
8
+
9
+ const QUICK_MEASUREMENT_QUERY_INTERVAL_MS = 30
10
+ const QUICK_MEASUREMENT_MIN_DISTANCE_SQ = 1
11
+
12
+ export function createQuickMeasurementPointerScheduler(
13
+ onPointerMove: (event: PointerEvent) => void,
14
+ frameDriver: {
15
+ request: (callback: FrameRequestCallback) => number
16
+ cancel: (frameId: number) => void
17
+ } = {
18
+ request: (callback) => requestAnimationFrame(callback),
19
+ cancel: (frameId) => cancelAnimationFrame(frameId),
20
+ },
21
+ ): {
22
+ enqueue: (event: PointerEvent) => void
23
+ clear: () => void
24
+ } {
25
+ let latestEvent: PointerEvent | null = null
26
+ let frameId: number | null = null
27
+ let lastProcessedAt = Number.NEGATIVE_INFINITY
28
+ let lastClientX = Number.NaN
29
+ let lastClientY = Number.NaN
30
+
31
+ const schedule = () => {
32
+ if (frameId === null) frameId = frameDriver.request(flush)
33
+ }
34
+ const flush = (timestamp: number) => {
35
+ frameId = null
36
+ if (!latestEvent) return
37
+ if (timestamp - lastProcessedAt < QUICK_MEASUREMENT_QUERY_INTERVAL_MS) {
38
+ schedule()
39
+ return
40
+ }
41
+
42
+ const event = latestEvent
43
+ latestEvent = null
44
+ const deltaX = event.clientX - lastClientX
45
+ const deltaY = event.clientY - lastClientY
46
+ if (
47
+ Number.isFinite(lastClientX) &&
48
+ deltaX * deltaX + deltaY * deltaY < QUICK_MEASUREMENT_MIN_DISTANCE_SQ
49
+ ) {
50
+ return
51
+ }
52
+
53
+ lastClientX = event.clientX
54
+ lastClientY = event.clientY
55
+ lastProcessedAt = timestamp
56
+ onPointerMove(event)
57
+ if (latestEvent) schedule()
58
+ }
59
+
60
+ return {
61
+ enqueue: (event) => {
62
+ latestEvent = event
63
+ schedule()
64
+ },
65
+ clear: () => {
66
+ if (frameId !== null) frameDriver.cancel(frameId)
67
+ latestEvent = null
68
+ frameId = null
69
+ lastProcessedAt = Number.NEGATIVE_INFINITY
70
+ lastClientX = Number.NaN
71
+ lastClientY = Number.NaN
72
+ },
73
+ }
74
+ }
75
+
76
+ export function quickMeasurementContext(
77
+ node: AnyNode,
78
+ nodes: Record<AnyNodeId, AnyNode>,
79
+ ): GeometryContext {
80
+ const resolve: GeometryContext['resolve'] = <N = AnyNode>(id: AnyNodeId) =>
81
+ nodes[id] as N | undefined
82
+ const childIds =
83
+ 'children' in node && Array.isArray(node.children) ? (node.children as AnyNodeId[]) : []
84
+ const children = childIds
85
+ .map((id) => nodes[id])
86
+ .filter((child): child is AnyNode => child !== undefined)
87
+ const parent = node.parentId ? (nodes[node.parentId as AnyNodeId] ?? null) : null
88
+ const siblings =
89
+ parent && 'children' in parent && Array.isArray(parent.children)
90
+ ? (parent.children as AnyNodeId[])
91
+ .map((id) => nodes[id])
92
+ .filter(
93
+ (sibling): sibling is AnyNode => sibling !== undefined && sibling.type === node.type,
94
+ )
95
+ : []
96
+
97
+ return { resolve, children, parent, siblings }
98
+ }
99
+
100
+ export function resolveQuickMeasurementReport(
101
+ nodeId: string | null,
102
+ nodes: Record<AnyNodeId, AnyNode>,
103
+ ): QuickMeasurementReport | null {
104
+ if (!nodeId) return null
105
+ const node = nodes[nodeId as AnyNodeId]
106
+ if (!node || node.visible === false) return null
107
+ const quickMeasure = nodeRegistry.get(node.type)?.measurement?.quickMeasure
108
+ return quickMeasure ? quickMeasure(node, quickMeasurementContext(node, nodes)) : null
109
+ }
@@ -80,7 +80,7 @@ export function duplicateRoofSubtree(
80
80
  const scene = useScene.getState()
81
81
  const sourceRoof = scene.nodes[sourceRoofId]
82
82
 
83
- if (!sourceRoof || sourceRoof.type !== 'roof') {
83
+ if (sourceRoof?.type !== 'roof') {
84
84
  throw new Error(`Node "${sourceRoofId}" is not a roof`)
85
85
  }
86
86
 
@@ -105,7 +105,7 @@ export function duplicateRoofSubtree(
105
105
  const segmentClones: RoofSegmentNode[] = []
106
106
  for (const childId of sourceRoof.children ?? []) {
107
107
  const childNode = scene.nodes[childId as AnyNodeId]
108
- if (!childNode || childNode.type !== 'roof-segment') {
108
+ if (childNode?.type !== 'roof-segment') {
109
109
  continue
110
110
  }
111
111
 
@@ -125,7 +125,7 @@ export function duplicateRoofSubtree(
125
125
 
126
126
  const nextScene = useScene.getState()
127
127
  const createdRoof = nextScene.nodes[roofClone.id as AnyNodeId]
128
- if (!createdRoof || createdRoof.type !== 'roof') {
128
+ if (createdRoof?.type !== 'roof') {
129
129
  throw new Error(`Duplicated roof "${roofClone.id}" was not created`)
130
130
  }
131
131
 
@@ -151,7 +151,7 @@ export function duplicateRoofSubtree(
151
151
 
152
152
  const invalidSegment = segmentIds.find((segmentId) => {
153
153
  const segment = nextScene.nodes[segmentId as AnyNodeId]
154
- return !segment || segment.type !== 'roof-segment' || segment.parentId !== createdRoof.id
154
+ return segment?.type !== 'roof-segment' || segment.parentId !== createdRoof.id
155
155
  })
156
156
  if (invalidSegment) {
157
157
  throw new Error(
@@ -175,11 +175,11 @@ export function duplicateRoofSubtree(
175
175
 
176
176
  export function clearRoofDuplicateMetadata(
177
177
  roofId: AnyNodeId,
178
- updates: Partial<Pick<RoofNode, 'position' | 'rotation' | 'metadata'>> = {},
178
+ updates: Partial<Pick<RoofNode, 'position' | 'rotation' | 'metadata' | 'visible'>> = {},
179
179
  ) {
180
180
  const scene = useScene.getState()
181
181
  const roofNode = scene.nodes[roofId]
182
- if (!roofNode || roofNode.type !== 'roof') {
182
+ if (roofNode?.type !== 'roof') {
183
183
  return
184
184
  }
185
185
 
@@ -198,7 +198,7 @@ export function clearRoofDuplicateMetadata(
198
198
 
199
199
  for (const childId of roofNode.children ?? []) {
200
200
  const childNode = scene.nodes[childId as AnyNodeId]
201
- if (!childNode || childNode.type !== 'roof-segment') {
201
+ if (childNode?.type !== 'roof-segment') {
202
202
  continue
203
203
  }
204
204
 
@@ -0,0 +1,22 @@
1
+ import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
2
+ import type * as THREE from 'three'
3
+
4
+ export const HOVERED_ROOF_SEGMENT_OUTLINE_PROXY_NAME = '__roof-hover-outline-proxy__'
5
+
6
+ function hoveredRoofSegmentOutlineProxyName(segmentId: string) {
7
+ return `${HOVERED_ROOF_SEGMENT_OUTLINE_PROXY_NAME}:${segmentId}`
8
+ }
9
+
10
+ export function getHoveredRoofSegmentOutlineProxy(segmentId: string): THREE.Object3D | null {
11
+ const segment = useScene.getState().nodes[segmentId as AnyNodeId]
12
+ if (!(segment?.type === 'roof-segment' && segment.parentId)) return null
13
+ return (
14
+ sceneRegistry.nodes
15
+ .get(segment.parentId)
16
+ ?.getObjectByName(hoveredRoofSegmentOutlineProxyName(segmentId)) ?? null
17
+ )
18
+ }
19
+
20
+ export function getHoveredRoofSegmentOutlineProxyName(segmentId: string): string {
21
+ return hoveredRoofSegmentOutlineProxyName(segmentId)
22
+ }
@@ -0,0 +1,164 @@
1
+ import {
2
+ type AnyNodeId,
3
+ getRoofSegmentWallFaces,
4
+ getScaledDimensions,
5
+ type ItemNode,
6
+ type RoofNode,
7
+ type RoofSegmentNode,
8
+ type RoofSegmentWallFace,
9
+ type RoofWallFaceId,
10
+ sceneRegistry,
11
+ segmentPointToRoofWallFace,
12
+ useScene,
13
+ } from '@pascal-app/core'
14
+ import * as THREE from 'three'
15
+
16
+ const worldPoint = new THREE.Vector3()
17
+ const worldNormal = new THREE.Vector3()
18
+ const localPoint = new THREE.Vector3()
19
+ const localNormal = new THREE.Vector3()
20
+ const inverseMatrix = new THREE.Matrix4()
21
+
22
+ export type RoofWallHit = {
23
+ segment: RoofSegmentNode
24
+ face: RoofSegmentWallFace
25
+ /** Face coords of the hit (u along the face, v above the segment base). */
26
+ u: number
27
+ v: number
28
+ }
29
+
30
+ /** Pointer hits more than this far off the wall plane are not wall hits. */
31
+ const PLANE_TOLERANCE = 0.06
32
+ /** Reject faces whose normal disagrees with the hit normal (slope / soffit). */
33
+ const NORMAL_ALIGNMENT = 0.7
34
+ /** A wall face is vertical; slope faces on low pitches have |ny| ≫ 0. */
35
+ const MAX_NORMAL_Y = 0.4
36
+
37
+ /**
38
+ * Resolve a pointer hit on a roof to one of its segments' vertical wall
39
+ * faces (base walls under the roof + the coplanar gable/shed/gambrel end
40
+ * faces). Counterpart of `resolveRoofSegmentHit`, which resolves to the
41
+ * sloped top surface instead.
42
+ *
43
+ * `normal` must be the raw `NodeEvent.normal` (hit-object-local) together
44
+ * with the `object` it came from — roof events can originate from the
45
+ * merged-roof mesh (roof-local frame) or a painted segment mesh
46
+ * (segment-local frame), so the normal is normalised through world space
47
+ * here instead of trusting the event frame.
48
+ *
49
+ * Lives in `@pascal-app/editor` because both the kind-owned door/window
50
+ * tools (in `@pascal-app/nodes`, which depends on editor) and the item
51
+ * placement coordinator (in editor itself) consume it.
52
+ */
53
+ export function resolveRoofWallHit(
54
+ roof: RoofNode,
55
+ position: [number, number, number],
56
+ normal: [number, number, number] | undefined,
57
+ object: THREE.Object3D | undefined,
58
+ ): RoofWallHit | null {
59
+ if (!normal || !object) return null
60
+
61
+ worldPoint.set(position[0], position[1], position[2])
62
+ worldNormal.set(normal[0], normal[1], normal[2])
63
+ object.updateWorldMatrix(true, false)
64
+ worldNormal.transformDirection(object.matrixWorld)
65
+
66
+ const state = useScene.getState()
67
+ let best: { hit: RoofWallHit; score: number } | null = null
68
+
69
+ for (const childId of roof.children ?? []) {
70
+ const segment = state.nodes[childId as AnyNodeId] as RoofSegmentNode | undefined
71
+ if (segment?.type !== 'roof-segment') continue
72
+ const segObj = sceneRegistry.nodes.get(segment.id)
73
+ if (!segObj) continue
74
+ segObj.updateWorldMatrix(true, false)
75
+
76
+ localPoint.copy(worldPoint)
77
+ segObj.worldToLocal(localPoint)
78
+ inverseMatrix.copy(segObj.matrixWorld).invert()
79
+ localNormal.copy(worldNormal).transformDirection(inverseMatrix)
80
+
81
+ if (Math.abs(localNormal.y) > MAX_NORMAL_Y) continue
82
+
83
+ for (const face of getRoofSegmentWallFaces(segment)) {
84
+ const alignment =
85
+ localNormal.x * face.normal[0] +
86
+ localNormal.y * face.normal[1] +
87
+ localNormal.z * face.normal[2]
88
+ if (alignment < NORMAL_ALIGNMENT) continue
89
+
90
+ const { u, v, dist } = segmentPointToRoofWallFace(segment, face.id, [
91
+ localPoint.x,
92
+ localPoint.y,
93
+ localPoint.z,
94
+ ])
95
+ if (Math.abs(dist) > PLANE_TOLERANCE) continue
96
+ if (u < -PLANE_TOLERANCE || u > face.length + PLANE_TOLERANCE) continue
97
+ if (v < -PLANE_TOLERANCE) continue
98
+
99
+ const score = Math.abs(dist)
100
+ if (!best || score < best.score) {
101
+ best = { hit: { segment, face, u, v }, score }
102
+ }
103
+ }
104
+ }
105
+
106
+ return best?.hit ?? null
107
+ }
108
+
109
+ /**
110
+ * Overlap guard for nodes sharing a roof-segment wall face — the
111
+ * roof-host analogue of `hasWallChildOverlap`. Hosted children store
112
+ * FACE-LOCAL coords + an explicit `roofFace`, so siblings compare
113
+ * directly: doors/windows are center-anchored in v, wall items
114
+ * bottom-anchored.
115
+ */
116
+ export function hasRoofFaceChildOverlap(
117
+ segment: RoofSegmentNode,
118
+ faceId: RoofWallFaceId,
119
+ u: number,
120
+ v: number,
121
+ width: number,
122
+ height: number,
123
+ ignoreId?: string,
124
+ ): boolean {
125
+ const nodes = useScene.getState().nodes
126
+ const newLeft = u - width / 2
127
+ const newRight = u + width / 2
128
+ const newBottom = v - height / 2
129
+ const newTop = v + height / 2
130
+
131
+ for (const childId of segment.children ?? []) {
132
+ if (childId === ignoreId) continue
133
+ const child = nodes[childId as AnyNodeId]
134
+ if (!child) continue
135
+ if ((child as { roofFace?: RoofWallFaceId }).roofFace !== faceId) continue
136
+ const position = (child as { position?: [number, number, number] }).position
137
+ if (!position) continue
138
+
139
+ let childW: number
140
+ let childBottom: number
141
+ let childTop: number
142
+ if (child.type === 'door' || child.type === 'window') {
143
+ const opening = child as { width: number; height: number }
144
+ childW = opening.width
145
+ childBottom = position[1] - opening.height / 2
146
+ childTop = position[1] + opening.height / 2
147
+ } else if (child.type === 'item') {
148
+ const item = child as ItemNode
149
+ if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side') continue
150
+ const [w, h] = getScaledDimensions(item)
151
+ childW = w
152
+ // Items anchor position[1] at their bottom edge.
153
+ childBottom = position[1]
154
+ childTop = position[1] + h
155
+ } else {
156
+ continue
157
+ }
158
+
159
+ const xOverlap = newLeft < position[0] + childW / 2 && newRight > position[0] - childW / 2
160
+ const yOverlap = newBottom < childTop && newTop > childBottom
161
+ if (xOverlap && yOverlap) return true
162
+ }
163
+ return false
164
+ }