@pascal-app/editor 1.0.0-beta.5 → 1.0.1

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 (297) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +5 -0
  6. package/src/components/editor/bake-thumbnail.tsx +4 -1
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +9 -2
  13. package/src/components/editor/export-manager.tsx +175 -33
  14. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  15. package/src/components/editor/first-person-controls.tsx +180 -12
  16. package/src/components/editor/floating-action-menu.tsx +55 -51
  17. package/src/components/editor/floorplan-panel.tsx +99 -15
  18. package/src/components/editor/grid.tsx +12 -7
  19. package/src/components/editor/group-actions.ts +34 -50
  20. package/src/components/editor/group-move-3d.ts +51 -21
  21. package/src/components/editor/group-transform-shared.test.ts +78 -0
  22. package/src/components/editor/group-transform-shared.ts +74 -7
  23. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  24. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  25. package/src/components/editor/handles/handle-placement.test.ts +111 -0
  26. package/src/components/editor/handles/handle-placement.ts +69 -0
  27. package/src/components/editor/handles/linear-resize-drag.ts +78 -0
  28. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  29. package/src/components/editor/handles/resize-snap.ts +10 -1
  30. package/src/components/editor/handles/use-handle-drag.ts +23 -3
  31. package/src/components/editor/index.tsx +151 -24
  32. package/src/components/editor/node-action-menu.tsx +14 -1
  33. package/src/components/editor/node-arrow-handles.tsx +67 -119
  34. package/src/components/editor/selection-manager.tsx +153 -42
  35. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  36. package/src/components/editor/snapshot-capture.test.ts +422 -0
  37. package/src/components/editor/snapshot-capture.ts +145 -0
  38. package/src/components/editor/thumbnail-generator.tsx +295 -239
  39. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  40. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  41. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  42. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  43. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
  44. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  45. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  46. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  47. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  48. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  49. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  50. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
  51. package/src/components/procedural-items/inspector.tsx +223 -0
  52. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  53. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  54. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  55. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  56. package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
  57. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  58. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  59. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  60. package/src/components/systems/selection-affordance-services.ts +19 -0
  61. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  62. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  63. package/src/components/tools/fence/fence-drafting.ts +6 -25
  64. package/src/components/tools/item/draft-creation.test.ts +12 -0
  65. package/src/components/tools/item/draft-creation.ts +10 -0
  66. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  67. package/src/components/tools/item/face-host-commit.ts +56 -0
  68. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  69. package/src/components/tools/item/face-host-preview.ts +82 -0
  70. package/src/components/tools/item/item-surface-placement.test.ts +175 -0
  71. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
  72. package/src/components/tools/item/move-tool.tsx +4 -3
  73. package/src/components/tools/item/placement-math.test.ts +11 -1
  74. package/src/components/tools/item/placement-math.ts +0 -20
  75. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  76. package/src/components/tools/item/placement-strategies.ts +304 -171
  77. package/src/components/tools/item/placement-surface.test.ts +61 -0
  78. package/src/components/tools/item/placement-surface.ts +24 -0
  79. package/src/components/tools/item/placement-types.ts +10 -0
  80. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
  81. package/src/components/tools/item/test-face-host.ts +121 -0
  82. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  83. package/src/components/tools/item/use-draft-node.ts +116 -44
  84. package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
  85. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  86. package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
  87. package/src/components/tools/registry/item-surface-move.test.ts +501 -0
  88. package/src/components/tools/registry/item-surface-move.ts +441 -0
  89. package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
  90. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  91. package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
  92. package/src/components/tools/registry-tool-context.tsx +30 -0
  93. package/src/components/tools/select/box-select-tool.tsx +3 -1
  94. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  95. package/src/components/tools/select/marquee-footprint.ts +29 -0
  96. package/src/components/tools/shared/placement-box.tsx +57 -7
  97. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  98. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  99. package/src/components/tools/shared/pointer-support-cap.ts +56 -17
  100. package/src/components/tools/shared/shelf-stickiness.ts +54 -0
  101. package/src/components/tools/shared/surface-hit.test.ts +56 -0
  102. package/src/components/tools/shared/surface-hit.ts +23 -0
  103. package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
  104. package/src/components/tools/shared/surface-rejection.tsx +29 -0
  105. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  106. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  107. package/src/components/tools/stair/stair-tool.tsx +127 -74
  108. package/src/components/tools/tool-manager.tsx +25 -8
  109. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  110. package/src/components/tools/wall/wall-drafting.ts +23 -114
  111. package/src/components/tools/zone/zone-tool.tsx +13 -4
  112. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  113. package/src/components/ui/action-menu/index.tsx +11 -1
  114. package/src/components/ui/command-palette/editor-commands.tsx +63 -13
  115. package/src/components/ui/command-palette/index.tsx +7 -2
  116. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  117. package/src/components/ui/controls/material-picker.tsx +4 -4
  118. package/src/components/ui/controls/metric-control.tsx +29 -13
  119. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  120. package/src/components/ui/controls/segmented-control.tsx +8 -1
  121. package/src/components/ui/controls/slider-control.tsx +44 -21
  122. package/src/components/ui/controls/toggle-control.tsx +18 -5
  123. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  124. package/src/components/ui/floating-level-selector.tsx +103 -10
  125. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  126. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  127. package/src/components/ui/helpers/item-helper.tsx +1 -1
  128. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  129. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  130. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  131. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  132. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  133. package/src/components/ui/panels/multi-field-value.ts +163 -0
  134. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  135. package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
  136. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  137. package/src/components/ui/panels/node-display.ts +4 -0
  138. package/src/components/ui/panels/panel-manager.tsx +77 -3
  139. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  140. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  141. package/src/components/ui/panels/parametric-inspector.tsx +35 -164
  142. package/src/components/ui/panels/reference-panel.tsx +34 -4
  143. package/src/components/ui/primitives/checkbox.tsx +34 -0
  144. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  145. package/src/components/ui/scene-loader.tsx +35 -0
  146. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
  147. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  148. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
  149. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
  150. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
  151. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  152. package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
  153. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
  154. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  155. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  156. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
  157. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  158. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
  159. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
  160. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
  161. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
  162. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  163. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  164. package/src/components/ui/snap-target-badge.tsx +2 -0
  165. package/src/components/viewer/viewer-scene-header.tsx +5 -1
  166. package/src/components/viewer/viewer-stage.tsx +4 -1
  167. package/src/components/viewer/viewer-units-panel.tsx +116 -0
  168. package/src/components/viewer-overlay.tsx +1 -22
  169. package/src/components/viewer-zone-system.tsx +6 -2
  170. package/src/components/walkthrough-hud.tsx +19 -10
  171. package/src/hooks/use-auto-save.test.ts +73 -1
  172. package/src/hooks/use-auto-save.ts +76 -5
  173. package/src/hooks/use-ceiling-events.test.ts +144 -0
  174. package/src/hooks/use-ceiling-events.ts +5 -1
  175. package/src/hooks/use-drag-action.ts +17 -3
  176. package/src/hooks/use-grid-events.test.ts +240 -0
  177. package/src/hooks/use-grid-events.ts +190 -11
  178. package/src/hooks/use-keyboard.test.ts +244 -0
  179. package/src/hooks/use-keyboard.ts +132 -63
  180. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  181. package/src/hooks/use-save-shortcut.ts +33 -0
  182. package/src/index.tsx +87 -4
  183. package/src/lib/active-placement-surface.test.ts +16 -0
  184. package/src/lib/active-placement-surface.ts +14 -0
  185. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  186. package/src/lib/ceiling-surface-raycast.ts +12 -0
  187. package/src/lib/contextual-help-extension.ts +17 -0
  188. package/src/lib/continuation.test.ts +11 -0
  189. package/src/lib/continuation.ts +8 -1
  190. package/src/lib/direct-manipulation.test.ts +132 -0
  191. package/src/lib/direct-manipulation.ts +38 -1
  192. package/src/lib/editor-api.ts +6 -3
  193. package/src/lib/elevation-guides.test.ts +28 -1
  194. package/src/lib/elevation-guides.ts +22 -7
  195. package/src/lib/export-texture-utils.test.ts +167 -0
  196. package/src/lib/export-texture-utils.ts +121 -0
  197. package/src/lib/export-user-data.test.ts +48 -0
  198. package/src/lib/export-user-data.ts +69 -0
  199. package/src/lib/floating-menu-scale.test.ts +29 -0
  200. package/src/lib/floating-menu-scale.ts +14 -0
  201. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  202. package/src/lib/floorplan/apply-alignment.ts +10 -1
  203. package/src/lib/floorplan/floorplan-export.test.ts +536 -2
  204. package/src/lib/floorplan/floorplan-export.tsx +120 -19
  205. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  206. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  207. package/src/lib/floorplan/floorplan-readonly.ts +5 -0
  208. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  209. package/src/lib/floorplan-grid-event-point.ts +13 -0
  210. package/src/lib/fresh-planar-placement.ts +13 -5
  211. package/src/lib/glb-export-serialize.test.ts +280 -0
  212. package/src/lib/glb-export.test.ts +1375 -21
  213. package/src/lib/glb-export.ts +781 -106
  214. package/src/lib/grid-event-presentation.ts +16 -0
  215. package/src/lib/history.test.ts +528 -147
  216. package/src/lib/history.ts +81 -7
  217. package/src/lib/host-tree-children.test.ts +27 -0
  218. package/src/lib/host-tree-children.ts +79 -0
  219. package/src/lib/interaction/hot-set.test.ts +10 -1
  220. package/src/lib/interaction/hot-set.ts +3 -1
  221. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  222. package/src/lib/interaction/overlay-policy.ts +16 -0
  223. package/src/lib/interaction/registered-drafting.ts +36 -0
  224. package/src/lib/interaction/scope.ts +31 -0
  225. package/src/lib/level-duplication.test.ts +73 -0
  226. package/src/lib/level-duplication.ts +32 -2
  227. package/src/lib/level-print-export.test.ts +484 -0
  228. package/src/lib/level-print-export.ts +526 -0
  229. package/src/lib/linear-display.test.ts +59 -0
  230. package/src/lib/linear-display.ts +39 -0
  231. package/src/lib/local-guide-image.test.ts +24 -0
  232. package/src/lib/local-guide-image.ts +34 -1
  233. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  234. package/src/lib/local-project-presentation-persistence.ts +335 -0
  235. package/src/lib/material-paint.ts +2 -0
  236. package/src/lib/model-export.ts +29 -0
  237. package/src/lib/node-action-movement.ts +21 -0
  238. package/src/lib/paint-preview-owner.ts +71 -0
  239. package/src/lib/planar-cursor-placement.test.ts +178 -1
  240. package/src/lib/planar-cursor-placement.ts +61 -5
  241. package/src/lib/plugin-panels.test.ts +92 -1
  242. package/src/lib/plugin-panels.ts +48 -0
  243. package/src/lib/portable-export.test.ts +455 -0
  244. package/src/lib/portable-export.ts +1042 -0
  245. package/src/lib/print-3mf.test.ts +86 -0
  246. package/src/lib/print-3mf.ts +161 -0
  247. package/src/lib/print-content-scope.test.ts +107 -0
  248. package/src/lib/print-content-scope.ts +90 -0
  249. package/src/lib/print-export.test.ts +294 -0
  250. package/src/lib/print-export.ts +735 -0
  251. package/src/lib/print-feature-thickness.test.ts +147 -0
  252. package/src/lib/print-feature-thickness.ts +180 -0
  253. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  254. package/src/lib/print-golden-house.test.ts +249 -0
  255. package/src/lib/print-roof-solids.test.ts +133 -0
  256. package/src/lib/print-roof-solids.ts +630 -0
  257. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  258. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  259. package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
  260. package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
  261. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  262. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  263. package/src/lib/print-shell-compiler-protocol.ts +38 -0
  264. package/src/lib/print-shell-compiler.ts +273 -0
  265. package/src/lib/print-wall-solids.test.ts +194 -0
  266. package/src/lib/print-wall-solids.ts +328 -0
  267. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  268. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  269. package/src/lib/scene-clipboard.ts +1 -1
  270. package/src/lib/scene.ts +22 -8
  271. package/src/lib/selection-routing.test.ts +105 -1
  272. package/src/lib/selection-routing.ts +19 -0
  273. package/src/lib/send-to-app.test.ts +108 -0
  274. package/src/lib/send-to-app.ts +121 -0
  275. package/src/lib/sfx/movement-tick.ts +37 -0
  276. package/src/lib/sfx-bus.ts +4 -0
  277. package/src/lib/sfx-player.ts +8 -0
  278. package/src/lib/snapping-mode.test.ts +32 -4
  279. package/src/lib/snapping-mode.ts +11 -7
  280. package/src/lib/surface-attachment.ts +77 -0
  281. package/src/lib/units.ts +254 -0
  282. package/src/lib/usdz-export.ts +65 -0
  283. package/src/lib/use-linear-display.ts +8 -32
  284. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  285. package/src/store/live-draft-preview-stores.test.ts +3 -0
  286. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  287. package/src/store/tool-mode.test.ts +87 -0
  288. package/src/store/use-camera-hint-focus.ts +48 -0
  289. package/src/store/use-editor.tsx +299 -188
  290. package/src/store/use-floorplan-draft-preview.ts +9 -0
  291. package/src/store/use-handle-group.ts +4 -0
  292. package/src/store/use-interaction-scope.test.ts +23 -1
  293. package/src/store/use-interaction-scope.ts +63 -2
  294. package/src/store/use-placement-preview.ts +56 -3
  295. package/src/store/use-send-to-app.ts +21 -0
  296. package/src/components/tools/roof/roof-tool.tsx +0 -656
  297. package/src/components/ui/helpers/roof-helper.tsx +0 -14
package/src/index.tsx CHANGED
@@ -56,6 +56,7 @@ export {
56
56
  formatMeasurement,
57
57
  MeasurementPill,
58
58
  } from './components/editor/measurement-pill'
59
+ export { NodeActionMenu } from './components/editor/node-action-menu'
59
60
  // In-world arrow handle primitives (chevron geometry, invisible hit area,
60
61
  // shared material, palette + scale constants). Re-exported so kind-owned
61
62
  // 3D selection affordances in `@pascal-app/nodes` (duct side-move / height /
@@ -98,6 +99,11 @@ export {
98
99
  buildSvgArrowHeadPoints,
99
100
  getArcPlanPoint,
100
101
  } from './components/editor-2d/svg-paths'
102
+ export type {
103
+ SelectionAffordanceHistoryApi,
104
+ SelectionAffordanceInteractionApi,
105
+ SelectionAffordanceProps,
106
+ } from './components/systems/selection-affordance-services'
101
107
  // Phase 5 Stage D transitional exports — pure drafting / angle helpers
102
108
  // consumed by kind-owned drag actions in @pascal-app/nodes. Stage F
103
109
  // cleanup moves these into @pascal-app/nodes (fence/drafting.ts +
@@ -113,7 +119,6 @@ export { MoveTool } from './components/tools/item/move-tool'
113
119
  // `@pascal-app/nodes` (wall curve sagitta snap, door / window placement,
114
120
  // item drop) so kinds don't reach into editor internals.
115
121
  export {
116
- calculateCursorRotation,
117
122
  calculateItemRotation,
118
123
  getSideFromNormal,
119
124
  isValidWallSideFace,
@@ -132,6 +137,8 @@ export {
132
137
  type PlacementCoordinatorConfig,
133
138
  usePlacementCoordinator,
134
139
  } from './components/tools/item/use-placement-coordinator'
140
+ export { MoveRegistryNodeTool } from './components/tools/registry/move-registry-node-tool'
141
+ export { useRegistryToolContext } from './components/tools/registry-tool-context'
135
142
  export { CursorSphere } from './components/tools/shared/cursor-sphere'
136
143
  export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
137
144
  export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
@@ -144,6 +151,7 @@ export {
144
151
  resolveLevelConstructionPlane,
145
152
  } from './components/tools/shared/horizontal-construction-plane'
146
153
  export { PlacementBox } from './components/tools/shared/placement-box'
154
+ export { PlacementDimensionGuides } from './components/tools/shared/placement-dimension-guides'
147
155
  // Pointer-decided support surface (deck top vs floor underneath) — the
148
156
  // draw tools (wall / fence) ride their grid plane and commit cap on it.
149
157
  export {
@@ -234,6 +242,7 @@ export { SegmentedControl } from './components/ui/controls/segmented-control'
234
242
  export { SliderControl } from './components/ui/controls/slider-control'
235
243
  export { TerrainSculptPanel } from './components/ui/controls/terrain-sculpt-panel'
236
244
  export { ToggleControl } from './components/ui/controls/toggle-control'
245
+ export { ToolOptionsPanel } from './components/ui/controls/tool-options-panel'
237
246
  export { FloatingLevelSelector } from './components/ui/floating-level-selector'
238
247
  export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items'
239
248
  // Item collections UI — used by the kind-owned ItemPanel in nodes/.
@@ -254,6 +263,10 @@ export {
254
263
  DropdownMenuSubTrigger,
255
264
  DropdownMenuTrigger,
256
265
  } from './components/ui/primitives/dropdown-menu'
266
+ export {
267
+ ShortcutToken,
268
+ shortcutDisplayValue,
269
+ } from './components/ui/primitives/shortcut-token'
257
270
  export { useSidebarStore } from './components/ui/primitives/sidebar'
258
271
  export { Slider } from './components/ui/primitives/slider'
259
272
  export { SceneLoader } from './components/ui/scene-loader'
@@ -305,6 +318,7 @@ export {
305
318
  ViewerStageSwitcher,
306
319
  type ViewerStageSwitcherProps,
307
320
  } from './components/viewer/viewer-stage-switcher'
321
+ export { ViewerUnitsPanel } from './components/viewer/viewer-units-panel'
308
322
  export {
309
323
  WalkthroughHud,
310
324
  type WalkthroughHudProps,
@@ -333,6 +347,12 @@ export {
333
347
  resolveCeilingPlanPointSnap,
334
348
  } from './lib/ceiling-plan-snap'
335
349
  export { EDITOR_LAYER } from './lib/constants'
350
+ export type { ContextualShortcutHint } from './lib/contextual-help'
351
+ export {
352
+ CONTEXTUAL_HELP_NODE_EXTENSION_KEY,
353
+ type ContextualHelpNodeExtension,
354
+ getContextualHelpNodeExtension,
355
+ } from './lib/contextual-help-extension'
336
356
  // Helper libs used by the kind-owned roof / stair / elevator panels.
337
357
  export {
338
358
  CONTINUATION_PROFILES,
@@ -341,6 +361,7 @@ export {
341
361
  continuationContextOf,
342
362
  nextContinuation,
343
363
  } from './lib/continuation'
364
+ export { createEditorApi } from './lib/editor-api'
344
365
  export {
345
366
  clearStructuralElevationGuide,
346
367
  collectElevationSnapTargets,
@@ -348,6 +369,7 @@ export {
348
369
  type ElevationGuideSource,
349
370
  type ElevationSnapMatch,
350
371
  type ElevationSnapTarget,
372
+ publishResolvedElevationGuide,
351
373
  publishStructuralElevationGuide,
352
374
  resolveElevationSnapMatch,
353
375
  resolveStructuralElevationSnap,
@@ -358,6 +380,8 @@ export {
358
380
  resolveElevatorSupportLevelId,
359
381
  resolveElevatorSupportY,
360
382
  } from './lib/elevator-support'
383
+ export type { ExportTextureUtils } from './lib/export-texture-utils'
384
+ export { getFloatingMenuScale } from './lib/floating-menu-scale'
361
385
  // Floor-plan stair helpers — the cumulative-transform walk
362
386
  // (`computeFloorplanStairSegmentTransforms`) and the rich segment-entry
363
387
  // builder (`buildFloorplanStairEntry`) used by the kind-owned stair
@@ -382,6 +406,10 @@ export type {
382
406
  FloorplanAnnotationCategory,
383
407
  FloorplanAnnotationVisibility,
384
408
  } from './lib/floorplan/annotation-visibility'
409
+ export {
410
+ exportFloorplanPdf,
411
+ type FloorplanExportScope,
412
+ } from './lib/floorplan/floorplan-export'
385
413
  export {
386
414
  createFloorplanContextExtensions,
387
415
  FLOORPLAN_CONTEXT_EXTENSION_KEY,
@@ -407,8 +435,16 @@ export {
407
435
  type FloorplanMode,
408
436
  isFloorplanToolAvailableInMode,
409
437
  } from './lib/floorplan/floorplan-mode'
410
- export { commitFreshPlacementSubtree } from './lib/fresh-planar-placement'
411
- export { exportSceneToGlb } from './lib/glb-export'
438
+ export {
439
+ commitFreshPlacementSubtree,
440
+ createFreshPlacementSubtree,
441
+ } from './lib/fresh-planar-placement'
442
+ export { exportSceneToGlb, type GlbExportOptions } from './lib/glb-export'
443
+ export {
444
+ type EditorGridEvent,
445
+ type GridEventScreenProjection,
446
+ getGridEventScreenProjection,
447
+ } from './lib/grid-event-presentation'
412
448
  export {
413
449
  getHistoryCommandState,
414
450
  type HistoryCommandDelegate,
@@ -419,15 +455,27 @@ export {
419
455
  runUndo,
420
456
  subscribeHistoryCommandState,
421
457
  } from './lib/history'
458
+ export {
459
+ type EditorHostTreeChildren,
460
+ type EditorHostTreeChildrenProps,
461
+ editorHostTreeChildrenRegistry,
462
+ registerEditorHostTreeChildren,
463
+ } from './lib/host-tree-children'
464
+ export {
465
+ DRAFTING_SURFACE_EXTENSION_KEY,
466
+ type DraftingSurfaceExtension,
467
+ } from './lib/interaction/registered-drafting'
422
468
  export {
423
469
  boundaryReshapeScope,
424
470
  curveReshapeScope,
425
471
  endpointReshapeScope,
426
472
  holeEditScope,
473
+ meshEditScope,
427
474
  movingNodeOf,
428
475
  scopeNodeId,
429
476
  } from './lib/interaction/scope'
430
477
  export {
478
+ type ActivePaintMaterial,
431
479
  buildResetSurfaceMaterialUpdates,
432
480
  buildRoofSurfaceMaterialPatch,
433
481
  buildSingleSurfaceMaterialPatch,
@@ -446,6 +494,14 @@ export {
446
494
  measurementPolygonLabelAnchor,
447
495
  triangulateMeasurementPolygon,
448
496
  } from './lib/measurement-label'
497
+ export {
498
+ type LingoUnitSpec,
499
+ lingoUnitSpec,
500
+ type MeasurementHintOptions,
501
+ measurementHint,
502
+ type ParseMeasurementOptions,
503
+ parseMeasurement,
504
+ } from './lib/measurement-parser'
449
505
  export {
450
506
  buildMeasurementAngleArcPoints,
451
507
  cubicMetersToVolumeUnit,
@@ -467,6 +523,12 @@ export {
467
523
  metersToLinearUnit,
468
524
  squareMetersToAreaUnit,
469
525
  } from './lib/measurements'
526
+ export type {
527
+ ModelExport,
528
+ ModelExportArtifact,
529
+ ModelExportFormat,
530
+ ModelExportOptions,
531
+ } from './lib/model-export'
470
532
  export { consumePlacementDragRelease } from './lib/placement-drag-release'
471
533
  export {
472
534
  addFreshPlacementMetadata,
@@ -485,6 +547,8 @@ export {
485
547
  editorHostPanelRegistry,
486
548
  registerEditorHostPanel,
487
549
  } from './lib/plugin-panels'
550
+ export { configureManifoldRuntime } from './lib/print-shell-compiler-manifold-worker'
551
+ export type { ManifoldRuntimeOptions } from './lib/print-shell-compiler-protocol'
488
552
  export {
489
553
  createQuickMeasurementPointerScheduler,
490
554
  quickMeasurementContext,
@@ -510,6 +574,13 @@ export {
510
574
  type SlabPlanSnapInput,
511
575
  type SlabPlanSnapResult,
512
576
  } from './lib/slab-plan-snap'
577
+ export {
578
+ getSnappingModeLabel,
579
+ resolveSnapFlags,
580
+ type SnapContext,
581
+ type SnapFlags,
582
+ type SnappingMode,
583
+ } from './lib/snapping-mode'
513
584
  export { duplicateStairSubtree } from './lib/stair-duplication'
514
585
  export {
515
586
  getBuildingLevelsForLevel,
@@ -533,6 +604,7 @@ export {
533
604
  resolveFlattenTarget,
534
605
  sculptFieldForSite,
535
606
  } from './lib/terrain-sculpt'
607
+ export { exportSceneToUsdz, type UsdzExportOptions } from './lib/usdz-export'
536
608
  // `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
537
609
  // nodes` so they don't need their own copy / their own tailwind-merge
538
610
  // dependency.
@@ -548,6 +620,7 @@ export {
548
620
  export { subscribeCameraPose } from './store/camera-pose-store'
549
621
  export { default as useAlignmentGuides } from './store/use-alignment-guides'
550
622
  export { default as useAudio } from './store/use-audio'
623
+ export { type CameraHintAction, useCameraHintFocus } from './store/use-camera-hint-focus'
551
624
  export { type CommandAction, useCommandRegistry } from './store/use-command-registry'
552
625
  export {
553
626
  DRAWING_TYPE_OPTIONS,
@@ -556,17 +629,24 @@ export {
556
629
  export type {
557
630
  CaptureMode,
558
631
  FloorplanSelectionTool,
632
+ Mode,
559
633
  SnapshotCropMode,
560
634
  SnapshotStandardAspect,
561
635
  SplitOrientation,
636
+ StructureTool,
562
637
  Tool,
563
638
  ToolDefaults,
639
+ ToolMode,
564
640
  ViewMode,
565
641
  WorkspaceMode,
566
642
  } from './store/use-editor'
567
643
  export {
644
+ armMaterialPaint,
645
+ armToolMode,
568
646
  default as useEditor,
569
647
  getActiveContinuationContext,
648
+ getActiveSnapContext,
649
+ getActiveSnappingMode,
570
650
  getContinuation,
571
651
  isAlignmentGuideActive,
572
652
  isAngleSnapActive,
@@ -623,7 +703,10 @@ export {
623
703
  type PathDraftPoint,
624
704
  usePathDraftPreview,
625
705
  } from './store/use-path-draft-preview'
626
- export { default as usePlacementPreview } from './store/use-placement-preview'
706
+ export {
707
+ default as usePlacementPreview,
708
+ type PlacementPreviewDimension,
709
+ } from './store/use-placement-preview'
627
710
  export {
628
711
  activateQuickMeasurementHudSource,
629
712
  clearQuickMeasurementHudSource,
@@ -4,6 +4,7 @@ import {
4
4
  clearPlacementSurface,
5
5
  getPlacementSurface,
6
6
  publishPlacementSurface,
7
+ usesOrientedPlacementPlane,
7
8
  } from './active-placement-surface'
8
9
 
9
10
  describe('active placement surface', () => {
@@ -21,4 +22,19 @@ describe('active placement surface', () => {
21
22
 
22
23
  expect(getPlacementSurface()?.projection).toBe('surface')
23
24
  })
25
+
26
+ test('copies a stable lattice anchor when one is provided', () => {
27
+ publishPlacementSurface(
28
+ new Vector3(4, 2, 3),
29
+ new Vector3(0, 0, 1),
30
+ 'surface',
31
+ new Vector3(1, 2, 3),
32
+ )
33
+
34
+ expect(getPlacementSurface()?.anchor?.toArray()).toEqual([1, 2, 3])
35
+ })
36
+
37
+ test('orients the grid to a sloped placement surface', () => {
38
+ expect(usesOrientedPlacementPlane(new Vector3(0, 0.6, 0.8))).toBe(true)
39
+ })
24
40
  })
@@ -11,12 +11,15 @@ import { Vector3 } from 'three'
11
11
  // readers must consume them within the same frame.
12
12
  export type PlacementSurface = {
13
13
  point: Vector3
14
+ /** Optional stable origin for the construction lattice. */
15
+ anchor?: Vector3
14
16
  normal: Vector3
15
17
  projection: 'surface' | 'fixed-plane'
16
18
  }
17
19
 
18
20
  const surface: PlacementSurface = {
19
21
  point: new Vector3(),
22
+ anchor: new Vector3(),
20
23
  normal: new Vector3(0, 1, 0),
21
24
  projection: 'surface',
22
25
  }
@@ -26,8 +29,15 @@ export function publishPlacementSurface(
26
29
  point: Vector3,
27
30
  normal: Vector3,
28
31
  projection: PlacementSurface['projection'] = 'surface',
32
+ anchor?: Vector3,
29
33
  ): void {
30
34
  surface.point.copy(point)
35
+ if (anchor) {
36
+ surface.anchor ??= new Vector3()
37
+ surface.anchor.copy(anchor)
38
+ } else {
39
+ surface.anchor = undefined
40
+ }
31
41
  surface.normal.copy(normal)
32
42
  surface.projection = projection
33
43
  active = true
@@ -40,3 +50,7 @@ export function clearPlacementSurface(): void {
40
50
  export function getPlacementSurface(): PlacementSurface | null {
41
51
  return active ? surface : null
42
52
  }
53
+
54
+ export function usesOrientedPlacementPlane(normal: Vector3): boolean {
55
+ return Math.abs(normal.y) < 0.95
56
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ BackSide,
4
+ Mesh,
5
+ MeshBasicMaterial,
6
+ Path,
7
+ Raycaster,
8
+ Shape,
9
+ ShapeGeometry,
10
+ Vector3,
11
+ } from 'three'
12
+ import { raycastCeilingUnderside } from './ceiling-surface-raycast'
13
+
14
+ function ceilingMesh() {
15
+ const shape = new Shape()
16
+ shape.moveTo(-2, -2)
17
+ shape.lineTo(2, -2)
18
+ shape.lineTo(2, 2)
19
+ shape.lineTo(-2, 2)
20
+ shape.closePath()
21
+ const hole = new Path()
22
+ hole.moveTo(-0.5, -0.5)
23
+ hole.lineTo(-0.5, 0.5)
24
+ hole.lineTo(0.5, 0.5)
25
+ hole.lineTo(0.5, -0.5)
26
+ hole.closePath()
27
+ shape.holes.push(hole)
28
+ const geometry = new ShapeGeometry(shape).rotateX(-Math.PI / 2)
29
+ const mesh = new Mesh(geometry, new MeshBasicMaterial({ side: BackSide }))
30
+ mesh.position.y = 3
31
+ mesh.updateMatrixWorld(true)
32
+ return mesh
33
+ }
34
+
35
+ describe('ceiling drawing surfaces', () => {
36
+ test('lets drawing pass through the transparent top while picking the underside', () => {
37
+ const ceiling = ceilingMesh()
38
+ for (const side of [-1, 1]) {
39
+ const ray = new Raycaster(new Vector3(1, 3 + side * 2, 0), new Vector3(0, -side, 0))
40
+ const hit = raycastCeilingUnderside(ray, ceiling)[0]
41
+ if (side > 0) {
42
+ expect(hit).toBeUndefined()
43
+ } else {
44
+ expect(hit?.point.y).toBeCloseTo(3)
45
+ expect(hit?.object).toBe(ceiling)
46
+ }
47
+ expect(ceiling.material.side).toBe(BackSide)
48
+ }
49
+ ceiling.geometry.dispose()
50
+ ceiling.material.dispose()
51
+ })
52
+
53
+ test('respects ceiling holes and the polygon boundary from either side', () => {
54
+ const ceiling = ceilingMesh()
55
+ for (const side of [-1, 1]) {
56
+ for (const x of [0, 3]) {
57
+ const ray = new Raycaster(new Vector3(x, 3 + side * 2, 0), new Vector3(0, -side, 0))
58
+ expect(raycastCeilingUnderside(ray, ceiling)).toHaveLength(0)
59
+ }
60
+ }
61
+ ceiling.geometry.dispose()
62
+ ceiling.material.dispose()
63
+ })
64
+ })
@@ -0,0 +1,12 @@
1
+ import { BackSide, Mesh, MeshBasicMaterial, type Object3D, type Raycaster } from 'three'
2
+
3
+ const pickingMaterial = new MeshBasicMaterial({ side: BackSide })
4
+
5
+ export function raycastCeilingUnderside(raycaster: Raycaster, ceiling: Object3D) {
6
+ if (!(ceiling instanceof Mesh)) return []
7
+ // Ignore the transparent top and its grid overlay so drawing can reach surfaces below.
8
+ const proxy = new Mesh(ceiling.geometry, pickingMaterial)
9
+ proxy.matrixWorld.copy(ceiling.matrixWorld)
10
+ proxy.layers.mask = ceiling.layers.mask
11
+ return raycaster.intersectObject(proxy, false).map((hit) => ({ ...hit, object: ceiling }))
12
+ }
@@ -0,0 +1,17 @@
1
+ import type { NodeDefinition } from '@pascal-app/core'
2
+ import type { ContextualShortcutHint } from './contextual-help'
3
+
4
+ export const CONTEXTUAL_HELP_NODE_EXTENSION_KEY = 'pascal:editor/contextual-help'
5
+
6
+ export type ContextualHelpNodeExtension = {
7
+ subscribe: (onChange: () => void) => () => void
8
+ getHints: (nodeId: string) => ContextualShortcutHint[]
9
+ }
10
+
11
+ export function getContextualHelpNodeExtension(
12
+ definition: NodeDefinition<any> | undefined,
13
+ ): ContextualHelpNodeExtension | undefined {
14
+ return definition?.extensions?.[CONTEXTUAL_HELP_NODE_EXTENSION_KEY] as
15
+ | ContextualHelpNodeExtension
16
+ | undefined
17
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { CONTINUATION_PROFILES, continuationContextOf, nextContinuation } from './continuation'
3
+
4
+ describe('canopy continuation', () => {
5
+ test('maps the canopy tool to its own single and continuous profile', () => {
6
+ expect(continuationContextOf('lean-to-extension')).toBe('canopy')
7
+ expect(CONTINUATION_PROFILES.canopy.default).toBe('single')
8
+ expect(nextContinuation('canopy', 'single')).toBe('continuous')
9
+ expect(nextContinuation('canopy', 'continuous')).toBe('single')
10
+ })
11
+ })
@@ -1,4 +1,4 @@
1
- export type ContinuationContext = 'wall' | 'fence' | 'point' | 'cabinet'
1
+ export type ContinuationContext = 'wall' | 'fence' | 'point' | 'cabinet' | 'canopy'
2
2
  export type ContinuationMode = string
3
3
 
4
4
  export const CONTINUATION_PROFILES: Record<
@@ -42,6 +42,12 @@ export const CONTINUATION_PROFILES: Record<
42
42
  labels: { single: 'Single cabinet', continuous: 'Continuous run' },
43
43
  icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
44
44
  },
45
+ canopy: {
46
+ options: ['single', 'continuous'],
47
+ default: 'single',
48
+ labels: { single: 'Single canopy', continuous: 'Continuous canopy' },
49
+ icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
50
+ },
45
51
  }
46
52
 
47
53
  const POINT_KINDS = new Set(['item', 'door', 'window', 'shelf', 'column'])
@@ -60,5 +66,6 @@ export function continuationContextOf(kind: string): ContinuationContext | null
60
66
  if (kind === 'wall') return 'wall'
61
67
  if (kind === 'fence') return 'fence'
62
68
  if (kind === 'cabinet') return 'cabinet'
69
+ if (kind === 'lean-to-extension') return 'canopy'
63
70
  return POINT_KINDS.has(kind) ? 'point' : null
64
71
  }
@@ -9,9 +9,12 @@ import {
9
9
  import { z } from 'zod'
10
10
  import {
11
11
  canDirectMoveNode,
12
+ EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY,
13
+ pointerEventHitsEditorHandle,
12
14
  resolveDirectManipulationNode,
13
15
  resolveDirectRotationDragDelta,
14
16
  resolveMoveActionNode,
17
+ shouldStartDirectMoveDrag,
15
18
  snapDirectRotationDelta,
16
19
  } from './direct-manipulation'
17
20
 
@@ -116,6 +119,91 @@ describe('canDirectMoveNode', () => {
116
119
  })
117
120
  })
118
121
 
122
+ describe('shouldStartDirectMoveDrag', () => {
123
+ test('arms a plain drag for a kind that opts into direct dragging', () => {
124
+ expect(
125
+ shouldStartDirectMoveDrag({
126
+ allowPlainDrag: true,
127
+ commandModifier: false,
128
+ handleOwnsPointer: false,
129
+ nodeId: 'cabinet_existing',
130
+ selectedIds: [],
131
+ }),
132
+ ).toBe(true)
133
+ })
134
+
135
+ test('keeps modifier dragging limited to the sole selected node', () => {
136
+ expect(
137
+ shouldStartDirectMoveDrag({
138
+ allowPlainDrag: false,
139
+ commandModifier: true,
140
+ handleOwnsPointer: false,
141
+ nodeId: 'item_selected',
142
+ selectedIds: ['item_selected'],
143
+ }),
144
+ ).toBe(true)
145
+ expect(
146
+ shouldStartDirectMoveDrag({
147
+ allowPlainDrag: false,
148
+ commandModifier: true,
149
+ handleOwnsPointer: false,
150
+ nodeId: 'item_other',
151
+ selectedIds: ['item_selected'],
152
+ }),
153
+ ).toBe(false)
154
+ })
155
+
156
+ test('does not arm body dragging when a resize handle owns the pointer', () => {
157
+ expect(
158
+ shouldStartDirectMoveDrag({
159
+ allowPlainDrag: true,
160
+ commandModifier: false,
161
+ handleOwnsPointer: true,
162
+ nodeId: 'cabinet_selected',
163
+ selectedIds: ['cabinet_selected'],
164
+ }),
165
+ ).toBe(false)
166
+ })
167
+ })
168
+
169
+ describe('pointerEventHitsEditorHandle', () => {
170
+ test('keeps a visible resize handle from falling through to a nearer cabinet body', () => {
171
+ expect(
172
+ pointerEventHitsEditorHandle({
173
+ intersections: [
174
+ { object: { userData: {} } },
175
+ {
176
+ object: {
177
+ userData: { [EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY]: true },
178
+ },
179
+ },
180
+ ],
181
+ }),
182
+ ).toBe(true)
183
+ })
184
+
185
+ test('recognises a handle when it is the nearest R3F intersection', () => {
186
+ expect(
187
+ pointerEventHitsEditorHandle({
188
+ intersections: [
189
+ {
190
+ object: {
191
+ userData: { [EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY]: true },
192
+ },
193
+ },
194
+ { object: { userData: {} } },
195
+ ],
196
+ }),
197
+ ).toBe(true)
198
+ })
199
+
200
+ test('does not claim ordinary scene intersections', () => {
201
+ expect(pointerEventHitsEditorHandle({ intersections: [{ object: { userData: {} } }] })).toBe(
202
+ false,
203
+ )
204
+ })
205
+ })
206
+
119
207
  describe('resolveDirectManipulationNode', () => {
120
208
  test('routes proxied members to their assembly for direct transforms', () => {
121
209
  const group = {
@@ -272,4 +360,48 @@ describe('resolveMoveActionNode', () => {
272
360
  }),
273
361
  ).toBe(child)
274
362
  })
363
+
364
+ test('routes a parent-frame child move to a rotatable assembly parent', () => {
365
+ const parentKind = 'move-action-rotatable-parent-kind-test'
366
+ const childKind = 'move-action-rotatable-child-kind-test'
367
+ registerTestDefinition(parentKind, {
368
+ capabilities: { rotatable: { axes: ['y'], snapAngles: [Math.PI / 4] } },
369
+ })
370
+ registerTestDefinition(childKind, {
371
+ capabilities: {
372
+ movable: {
373
+ axes: ['x', 'z'],
374
+ gridSnap: true,
375
+ parentFrame: {
376
+ resolveParent: (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) =>
377
+ (node.parentId ? nodes[node.parentId] : null) ?? null,
378
+ parentRotationY: () => 0,
379
+ localToPlan: (_parent: AnyNode, local: readonly [number, number, number]) => [
380
+ local[0],
381
+ local[1],
382
+ local[2],
383
+ ],
384
+ planToLocal: (_parent: AnyNode, planX: number, localY: number, planZ: number) => [
385
+ planX,
386
+ localY,
387
+ planZ,
388
+ ],
389
+ },
390
+ },
391
+ },
392
+ })
393
+ const parent = { id: 'move_action_rotatable_run', type: parentKind } as unknown as AnyNode
394
+ const child = {
395
+ id: 'move_action_rotatable_module',
396
+ type: childKind,
397
+ parentId: parent.id,
398
+ } as unknown as AnyNode
399
+
400
+ expect(
401
+ resolveMoveActionNode(child, {
402
+ [parent.id]: parent,
403
+ [child.id]: child,
404
+ }),
405
+ ).toBe(parent)
406
+ })
275
407
  })
@@ -45,6 +45,25 @@ const BESPOKE_SELECTION_MOVE_KINDS = new Set([
45
45
  'liquid-line',
46
46
  ])
47
47
 
48
+ export const EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY = 'editorHandleHitArea'
49
+
50
+ export function pointerEventHitsEditorHandle(event: unknown): boolean {
51
+ if (!event || typeof event !== 'object') return false
52
+ const intersections = (
53
+ event as {
54
+ intersections?: readonly {
55
+ object?: { userData?: Record<string, unknown> }
56
+ }[]
57
+ }
58
+ ).intersections
59
+ return (
60
+ intersections?.some(
61
+ (intersection) =>
62
+ intersection.object?.userData?.[EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY] === true,
63
+ ) ?? false
64
+ )
65
+ }
66
+
48
67
  export function canDirectMoveNode(node: AnyNode): boolean {
49
68
  // These MEP kinds own move through bespoke selection rigs (latch cubes,
50
69
  // directional arrows, grid-driven previews). Sending body drags/clicks
@@ -64,6 +83,24 @@ export function canDirectMoveNode(node: AnyNode): boolean {
64
83
  return isMovable(node)
65
84
  }
66
85
 
86
+ export function shouldStartDirectMoveDrag({
87
+ allowPlainDrag,
88
+ commandModifier,
89
+ handleOwnsPointer,
90
+ nodeId,
91
+ selectedIds,
92
+ }: {
93
+ allowPlainDrag: boolean
94
+ commandModifier: boolean
95
+ handleOwnsPointer: boolean
96
+ nodeId: string
97
+ selectedIds: readonly string[]
98
+ }): boolean {
99
+ if (handleOwnsPointer) return false
100
+ if (commandModifier) return selectedIds.length === 1 && selectedIds[0] === nodeId
101
+ return allowPlainDrag && selectedIds.length < 2
102
+ }
103
+
67
104
  export function resolveDirectManipulationNode(
68
105
  node: AnyNode,
69
106
  nodes: Readonly<Record<string, AnyNode | undefined>>,
@@ -80,7 +117,7 @@ export function resolveMoveActionNode(
80
117
  ): AnyNode {
81
118
  const parentFrame = nodeRegistry.get(node.type)?.capabilities?.movable?.parentFrame
82
119
  const parent = parentFrame?.resolveParent(node, nodes as Readonly<Record<string, AnyNode>>)
83
- return parent?.type === node.type ? parent : node
120
+ return parent && (parent.type === node.type || canDirectRotateNode(parent)) ? parent : node
84
121
  }
85
122
 
86
123
  export function snapDirectRotationDelta(delta: number, free: boolean): number {