@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,441 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { type AnyNode, DoorNode, registerNode, sceneRegistry } from '@pascal-app/core'
3
+ import { buildDoorPreviewMesh } from '@pascal-app/viewer'
4
+ import * as THREE from 'three'
5
+ import { prepareSceneForExport } from './glb-export'
6
+
7
+ afterEach(() => {
8
+ sceneRegistry.clear()
9
+ })
10
+
11
+ function nodeMaterial(overrides: Record<string, unknown> = {}) {
12
+ // Duck-typed stand-in for the viewer's MeshStandard/LambertNodeMaterial:
13
+ // the exporter keys off `isNodeMaterial` and reads plain PBR props.
14
+ return {
15
+ isNodeMaterial: true,
16
+ name: 'painted',
17
+ color: new THREE.Color('#cc3300'),
18
+ roughness: 0.3,
19
+ metalness: 0.7,
20
+ transparent: false,
21
+ opacity: 1,
22
+ side: THREE.FrontSide,
23
+ alphaTest: 0,
24
+ depthWrite: true,
25
+ depthTest: true,
26
+ vertexColors: false,
27
+ toneMapped: true,
28
+ ...overrides,
29
+ } as unknown as THREE.Material
30
+ }
31
+
32
+ function meshWithNodeMaterial(material: THREE.Material): THREE.Mesh {
33
+ const geometry = new THREE.BoxGeometry(1, 1, 1)
34
+ return new THREE.Mesh(geometry, material)
35
+ }
36
+
37
+ describe('prepareSceneForExport', () => {
38
+ test('converts NodeMaterials to classic glTF-standard materials', () => {
39
+ const root = new THREE.Group()
40
+ root.name = 'scene-renderer'
41
+ const mesh = meshWithNodeMaterial(nodeMaterial())
42
+ root.add(mesh)
43
+
44
+ const { scene } = prepareSceneForExport(root, {})
45
+
46
+ const exported = scene.children[0] as THREE.Mesh
47
+ const material = exported.material as THREE.MeshStandardMaterial
48
+ expect(material.isMeshStandardMaterial).toBe(true)
49
+ expect(material.roughness).toBeCloseTo(0.3)
50
+ expect(material.metalness).toBeCloseTo(0.7)
51
+ expect(material.color.getHexString()).toBe('cc3300')
52
+ })
53
+
54
+ test('shared NodeMaterial instances convert to a single shared material', () => {
55
+ const root = new THREE.Group()
56
+ const shared = nodeMaterial()
57
+ root.add(meshWithNodeMaterial(shared), meshWithNodeMaterial(shared))
58
+
59
+ const { scene } = prepareSceneForExport(root, {})
60
+
61
+ const meshes = scene.children as THREE.Mesh[]
62
+ expect(meshes[0]!.material).toBe(meshes[1]!.material)
63
+ })
64
+
65
+ test('strips editor overlays that live off the scene layer', () => {
66
+ const root = new THREE.Group()
67
+ const realMesh = meshWithNodeMaterial(nodeMaterial())
68
+ const overlay = meshWithNodeMaterial(nodeMaterial())
69
+ overlay.layers.set(1) // OVERLAY_LAYER / EDITOR_LAYER — off scene layer 0
70
+ root.add(realMesh, overlay)
71
+
72
+ const { scene } = prepareSceneForExport(root, {})
73
+
74
+ const meshes: THREE.Mesh[] = []
75
+ scene.traverse((o) => {
76
+ if ((o as THREE.Mesh).isMesh) meshes.push(o as THREE.Mesh)
77
+ })
78
+ expect(meshes).toHaveLength(1)
79
+ })
80
+
81
+ test('neutralises an invisible hitbox root but keeps its visible children', () => {
82
+ // Door/window roots are selection hitboxes: a box geometry with an invisible
83
+ // material (object stays visible). Left intact it would plug the wall opening.
84
+ const root = new THREE.Group()
85
+ const hitbox = new THREE.Mesh(
86
+ new THREE.BoxGeometry(1, 2, 0.2),
87
+ new THREE.MeshBasicMaterial({ visible: false }),
88
+ )
89
+ const leaf = meshWithNodeMaterial(nodeMaterial())
90
+ hitbox.add(leaf)
91
+ root.add(hitbox)
92
+
93
+ const doorId = 'door_hitbox'
94
+ sceneRegistry.nodes.set(doorId, hitbox)
95
+ const nodes: Record<string, AnyNode> = {
96
+ [doorId]: { object: 'node', id: doorId, type: 'door' } as unknown as AnyNode,
97
+ }
98
+
99
+ const { scene } = prepareSceneForExport(root, nodes)
100
+
101
+ const exported = scene.getObjectByProperty('name', doorId) as THREE.Mesh
102
+ expect(exported).toBeDefined()
103
+ // Geometry emptied -> GLTFExporter emits a plain node, no solid block.
104
+ expect(exported.geometry.getAttribute('position')).toBeUndefined()
105
+ // The visible leaf survives as a child.
106
+ const visibleChildren = exported.children.filter((c) => (c as THREE.Mesh).isMesh)
107
+ expect(visibleChildren).toHaveLength(1)
108
+ })
109
+
110
+ test('stamps identity from the scene registry and strips other userData', () => {
111
+ const root = new THREE.Group()
112
+ const doorGroup = new THREE.Group()
113
+ const leaf = new THREE.Group()
114
+ leaf.userData.pascalSwingLeaf = { axis: 'y', openRotationY: Math.PI / 2 }
115
+ leaf.add(meshWithNodeMaterial(nodeMaterial()))
116
+ doorGroup.add(leaf)
117
+ root.add(doorGroup)
118
+
119
+ const doorId = 'door_test'
120
+ sceneRegistry.nodes.set(doorId, doorGroup)
121
+ const nodes: Record<string, AnyNode> = {
122
+ [doorId]: {
123
+ object: 'node',
124
+ id: doorId,
125
+ type: 'door',
126
+ name: 'Front door',
127
+ } as unknown as AnyNode,
128
+ }
129
+
130
+ const { scene } = prepareSceneForExport(root, nodes)
131
+
132
+ const exportedDoor = scene.getObjectByProperty('name', doorId)
133
+ expect(exportedDoor).toBeDefined()
134
+ expect(exportedDoor?.userData).toEqual({
135
+ pascalId: doorId,
136
+ kind: 'door',
137
+ label: 'Front door',
138
+ openable: true,
139
+ clips: ['door_test: open'],
140
+ })
141
+
142
+ // The swing-leaf marker must not survive into glTF extras.
143
+ let leafMarkerSurvived = false
144
+ scene.traverse((object) => {
145
+ if (object.userData.pascalSwingLeaf) leafMarkerSurvived = true
146
+ })
147
+ expect(leafMarkerSurvived).toBe(false)
148
+ })
149
+
150
+ test('does not flag a door/window openable when no open clip bakes', () => {
151
+ // A cased opening (no swing leaf) / fixed window (no operable sash) builds
152
+ // no movable part, so no clip bakes and the node must not claim openable.
153
+ const root = new THREE.Group()
154
+ const openingGroup = new THREE.Group()
155
+ openingGroup.add(meshWithNodeMaterial(nodeMaterial()))
156
+ root.add(openingGroup)
157
+
158
+ const openingId = 'door_opening'
159
+ sceneRegistry.nodes.set(openingId, openingGroup)
160
+ const nodes: Record<string, AnyNode> = {
161
+ [openingId]: {
162
+ object: 'node',
163
+ id: openingId,
164
+ type: 'door',
165
+ name: 'Cased opening',
166
+ } as unknown as AnyNode,
167
+ }
168
+
169
+ const { scene, animations } = prepareSceneForExport(root, nodes)
170
+
171
+ expect(animations).toHaveLength(0)
172
+ const exported = scene.getObjectByProperty('name', openingId)
173
+ expect(exported?.userData).toEqual({
174
+ pascalId: openingId,
175
+ kind: 'door',
176
+ label: 'Cased opening',
177
+ })
178
+ expect(exported?.userData.openable).toBeUndefined()
179
+ expect(exported?.userData.clips).toBeUndefined()
180
+ })
181
+
182
+ test('keeps the zone identity node with its polygon and strips the fill mesh', () => {
183
+ const root = new THREE.Group()
184
+ const zoneGroup = new THREE.Group()
185
+ const fill = meshWithNodeMaterial(nodeMaterial())
186
+ fill.layers.set(2) // ZONE_LAYER
187
+ zoneGroup.add(fill)
188
+ zoneGroup.visible = false // the editor often hides zones at export time
189
+ root.add(zoneGroup)
190
+
191
+ const zoneId = 'zone_living'
192
+ const polygon: [number, number][] = [
193
+ [0, 0],
194
+ [4, 0],
195
+ [4, 3],
196
+ ]
197
+ sceneRegistry.nodes.set(zoneId, zoneGroup)
198
+ const nodes: Record<string, AnyNode> = {
199
+ [zoneId]: {
200
+ object: 'node',
201
+ id: zoneId,
202
+ type: 'zone',
203
+ name: 'Living Room',
204
+ polygon,
205
+ color: '#ff0000',
206
+ } as unknown as AnyNode,
207
+ }
208
+
209
+ const { scene } = prepareSceneForExport(root, nodes)
210
+
211
+ const exported = scene.getObjectByProperty('name', zoneId)
212
+ expect(exported).toBeDefined()
213
+ // Forced visible so GLTFExporter's onlyVisible keeps the metadata node.
214
+ expect(exported?.visible).toBe(true)
215
+ expect(exported?.userData).toEqual({
216
+ pascalId: zoneId,
217
+ kind: 'zone',
218
+ label: 'Living Room',
219
+ polygon,
220
+ color: '#ff0000',
221
+ })
222
+ // The ZONE_LAYER fill mesh must not survive (rebuilt in /viewer instead).
223
+ let hasMesh = false
224
+ exported?.traverse((o) => {
225
+ if ((o as THREE.Mesh).isMesh) hasMesh = true
226
+ })
227
+ expect(hasMesh).toBe(false)
228
+ })
229
+
230
+ test('bakes a swing door into an open quaternion clip', () => {
231
+ const root = new THREE.Group()
232
+ const doorGroup = new THREE.Group()
233
+ const leaf = new THREE.Group()
234
+ leaf.userData.pascalSwingLeaf = { axis: 'y', openRotationY: Math.PI / 2 }
235
+ leaf.add(meshWithNodeMaterial(nodeMaterial()))
236
+ doorGroup.add(leaf)
237
+ root.add(doorGroup)
238
+
239
+ const doorId = 'door_swing'
240
+ sceneRegistry.nodes.set(doorId, doorGroup)
241
+ const nodes: Record<string, AnyNode> = {
242
+ [doorId]: { object: 'node', id: doorId, type: 'door', name: 'Door' } as unknown as AnyNode,
243
+ }
244
+
245
+ const { scene, animations } = prepareSceneForExport(root, nodes)
246
+
247
+ expect(animations).toHaveLength(1)
248
+ const clip = animations[0]!
249
+ expect(clip.name).toBe('door_swing: open')
250
+ expect(clip.duration).toBe(1)
251
+ // Playback intent carried in extras so consumers can play once and hold.
252
+ expect(clip.userData).toEqual({ loop: false })
253
+
254
+ const track = clip.tracks[0]!
255
+ expect(track).toBeInstanceOf(THREE.QuaternionKeyframeTrack)
256
+ expect(track.name.endsWith('.quaternion')).toBe(true)
257
+ expect(Array.from(track.times)).toEqual([0, 1])
258
+
259
+ // The track must target an object that exists in the exported tree.
260
+ const targetUuid = track.name.replace('.quaternion', '')
261
+ const target = scene.getObjectByProperty('uuid', targetUuid)
262
+ expect(target).toBeDefined()
263
+
264
+ // Rest pose is closed: the first keyframe is the identity rotation.
265
+ const closed = new THREE.Quaternion().fromArray(Array.from(track.values).slice(0, 4))
266
+ expect(closed.angleTo(new THREE.Quaternion())).toBeCloseTo(0)
267
+ })
268
+
269
+ test('bakes registry-owned open clips and stamps the node openable', () => {
270
+ const root = new THREE.Group()
271
+ const nodeGroup = new THREE.Group()
272
+ const movingPart = new THREE.Group()
273
+ movingPart.add(meshWithNodeMaterial(nodeMaterial()))
274
+ nodeGroup.add(movingPart)
275
+ root.add(nodeGroup)
276
+
277
+ const kind = `test-openable-${crypto.randomUUID()}`
278
+ const nodeId = 'registry_openable'
279
+ registerNode({
280
+ kind,
281
+ schemaVersion: 1,
282
+ category: 'fixtures',
283
+ defaults: () => ({}),
284
+ capabilities: {},
285
+ exportAnimation: ({ node, object }: { node: AnyNode; object: THREE.Object3D }) => {
286
+ const target = object.children[0]!
287
+ const clip = new THREE.AnimationClip(`${node.id}: open`, 1, [
288
+ new THREE.VectorKeyframeTrack(`${target.uuid}.position`, [0, 1], [0, 0, 0, 0, 0, 1]),
289
+ ])
290
+ clip.userData = { loop: false }
291
+ return clip
292
+ },
293
+ } as never)
294
+ sceneRegistry.nodes.set(nodeId, nodeGroup)
295
+
296
+ const { scene, animations } = prepareSceneForExport(root, {
297
+ [nodeId]: {
298
+ object: 'node',
299
+ id: nodeId,
300
+ type: kind,
301
+ name: 'Custom openable',
302
+ } as unknown as AnyNode,
303
+ })
304
+
305
+ expect(animations).toHaveLength(1)
306
+ expect(animations[0]!.name).toBe('registry_openable: open')
307
+ const exported = scene.getObjectByProperty('name', nodeId)
308
+ expect(exported?.userData).toMatchObject({
309
+ pascalId: nodeId,
310
+ kind,
311
+ label: 'Custom openable',
312
+ openable: true,
313
+ clips: ['registry_openable: open'],
314
+ })
315
+ })
316
+
317
+ test('bakes a sliding door into a sampled position clip', () => {
318
+ // Operation doors build their moving parts in a named group posed by
319
+ // `poseDoorMovingParts`; the exporter samples it into keyframes. The active
320
+ // panel group slides along x.
321
+ const root = new THREE.Group()
322
+ const doorGroup = new THREE.Group()
323
+ const activePanel = new THREE.Group()
324
+ activePanel.name = 'door-sliding-active'
325
+ activePanel.add(meshWithNodeMaterial(nodeMaterial()))
326
+ doorGroup.add(activePanel)
327
+ root.add(doorGroup)
328
+
329
+ const doorId = 'door_sliding'
330
+ sceneRegistry.nodes.set(doorId, doorGroup)
331
+ const nodes: Record<string, AnyNode> = {
332
+ [doorId]: {
333
+ object: 'node',
334
+ id: doorId,
335
+ type: 'door',
336
+ name: 'Slider',
337
+ doorType: 'sliding',
338
+ slideDirection: 'left',
339
+ width: 1,
340
+ height: 2.1,
341
+ frameThickness: 0.05,
342
+ } as unknown as AnyNode,
343
+ }
344
+
345
+ const { scene, animations } = prepareSceneForExport(root, nodes)
346
+
347
+ expect(animations).toHaveLength(1)
348
+ const clip = animations[0]!
349
+ expect(clip.name).toBe('door_sliding: open')
350
+ expect(clip.duration).toBe(1)
351
+ expect(clip.userData).toEqual({ loop: false })
352
+
353
+ const track = clip.tracks[0]!
354
+ expect(track).toBeInstanceOf(THREE.VectorKeyframeTrack)
355
+ expect(track.name.endsWith('.position')).toBe(true)
356
+ // 16 segments -> 17 keyframes, evenly spaced over the 1s clip.
357
+ expect(track.times.length).toBe(17)
358
+ expect(track.times[0]).toBeCloseTo(0)
359
+ expect(track.times[track.times.length - 1]!).toBeCloseTo(1)
360
+
361
+ // Rest pose is closed (first keyframe centred); the panel slides off-centre.
362
+ expect(track.values[0]!).toBeCloseTo(0)
363
+ expect(track.values[1]!).toBeCloseTo(0)
364
+ expect(track.values[2]!).toBeCloseTo(0)
365
+ const lastX = track.values[track.values.length - 3]!
366
+ expect(Math.abs(lastX)).toBeGreaterThan(0.1)
367
+
368
+ const target = scene.getObjectByProperty('uuid', track.name.replace('.position', ''))
369
+ expect(target).toBeDefined()
370
+
371
+ const exported = scene.getObjectByProperty('name', doorId)
372
+ expect(exported?.userData.openable).toBe(true)
373
+ expect(exported?.userData.clips).toEqual(['door_sliding: open'])
374
+ })
375
+
376
+ test('bakes a roll-up curtain into a sampled scale clip', () => {
377
+ // Roll-up geometry can't vanish in a glTF clip, so the bake scales the
378
+ // curtain group up into the lintel instead.
379
+ const root = new THREE.Group()
380
+ const doorGroup = new THREE.Group()
381
+ const curtain = new THREE.Group()
382
+ curtain.name = 'door-rollup-curtain'
383
+ curtain.add(meshWithNodeMaterial(nodeMaterial()))
384
+ doorGroup.add(curtain)
385
+ root.add(doorGroup)
386
+
387
+ const doorId = 'door_rollup'
388
+ sceneRegistry.nodes.set(doorId, doorGroup)
389
+ const nodes: Record<string, AnyNode> = {
390
+ [doorId]: {
391
+ object: 'node',
392
+ id: doorId,
393
+ type: 'door',
394
+ name: 'Roll-up',
395
+ doorType: 'garage-rollup',
396
+ width: 2.4,
397
+ height: 2.2,
398
+ frameThickness: 0.05,
399
+ } as unknown as AnyNode,
400
+ }
401
+
402
+ const { animations } = prepareSceneForExport(root, nodes)
403
+
404
+ expect(animations).toHaveLength(1)
405
+ const scaleTrack = animations[0]!.tracks.find((t) => t.name.endsWith('.scale'))
406
+ expect(scaleTrack).toBeInstanceOf(THREE.VectorKeyframeTrack)
407
+ // Rest pose is closed (full curtain, scale 1); it shrinks toward the header.
408
+ expect(Array.from(scaleTrack!.values).slice(0, 3)).toEqual([1, 1, 1])
409
+ const lastScaleY = scaleTrack!.values[scaleTrack!.values.length - 2]!
410
+ expect(lastScaleY).toBeLessThan(0.1)
411
+ })
412
+
413
+ // Regression: a folding door saved in an open state (|fold angle| > π/2) used
414
+ // to bake a 180°-flipped rest pose. The export clones + decomposes the door
415
+ // matrix, which re-derives a gimbal-flipped euler (x=z=π) for the wide Y
416
+ // rotation; the pose reset must zero the full euler triple, not just `.y`.
417
+ test('bakes an identity rest pose for an open folding door', () => {
418
+ const node = DoorNode.parse({
419
+ id: 'door_folding',
420
+ doorType: 'folding',
421
+ leafCount: 4,
422
+ operationState: 0.65,
423
+ })
424
+ const mesh = buildDoorPreviewMesh(node)
425
+ const root = new THREE.Group()
426
+ root.add(mesh)
427
+ sceneRegistry.nodes.set(node.id, mesh)
428
+
429
+ const { scene, animations } = prepareSceneForExport(root, {
430
+ [node.id]: node as unknown as AnyNode,
431
+ })
432
+
433
+ expect(animations).toHaveLength(1)
434
+ for (let index = 0; index < 4; index++) {
435
+ const panel = scene.getObjectByName(`door-fold-${index}`)
436
+ expect(panel).toBeDefined()
437
+ // Rest quaternion must be identity — no residual π on any axis.
438
+ expect(panel!.quaternion.angleTo(new THREE.Quaternion())).toBeLessThan(1e-4)
439
+ }
440
+ })
441
+ })