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

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 (245) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +13 -7
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +1 -0
  6. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  7. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  8. package/src/components/editor/capture-camera-rig.tsx +59 -0
  9. package/src/components/editor/custom-camera-controls.tsx +20 -11
  10. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  11. package/src/components/editor/editor-layout-v2.tsx +9 -2
  12. package/src/components/editor/export-manager.tsx +175 -33
  13. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  14. package/src/components/editor/first-person-controls.tsx +180 -12
  15. package/src/components/editor/floating-action-menu.tsx +42 -51
  16. package/src/components/editor/floorplan-panel.tsx +76 -12
  17. package/src/components/editor/grid.tsx +12 -7
  18. package/src/components/editor/group-actions.ts +34 -50
  19. package/src/components/editor/group-move-3d.ts +51 -21
  20. package/src/components/editor/group-transform-shared.test.ts +78 -0
  21. package/src/components/editor/group-transform-shared.ts +74 -7
  22. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  23. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  24. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  25. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  26. package/src/components/editor/handles/resize-snap.ts +10 -1
  27. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  28. package/src/components/editor/index.tsx +140 -21
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +47 -58
  31. package/src/components/editor/selection-manager.tsx +104 -42
  32. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  33. package/src/components/editor/snapshot-capture.test.ts +422 -0
  34. package/src/components/editor/snapshot-capture.ts +145 -0
  35. package/src/components/editor/thumbnail-generator.tsx +284 -237
  36. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  37. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  38. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  39. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  41. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  42. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  43. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  44. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  45. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  46. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
  47. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  48. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  49. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  51. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  52. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  53. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  54. package/src/components/systems/selection-affordance-services.ts +19 -0
  55. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  56. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  57. package/src/components/tools/fence/fence-drafting.ts +6 -25
  58. package/src/components/tools/item/draft-creation.test.ts +12 -0
  59. package/src/components/tools/item/draft-creation.ts +10 -0
  60. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  61. package/src/components/tools/item/face-host-commit.ts +56 -0
  62. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  63. package/src/components/tools/item/face-host-preview.ts +82 -0
  64. package/src/components/tools/item/move-tool.tsx +4 -3
  65. package/src/components/tools/item/placement-math.test.ts +11 -1
  66. package/src/components/tools/item/placement-math.ts +0 -20
  67. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  68. package/src/components/tools/item/placement-strategies.ts +135 -2
  69. package/src/components/tools/item/placement-surface.test.ts +61 -0
  70. package/src/components/tools/item/placement-surface.ts +24 -0
  71. package/src/components/tools/item/placement-types.ts +6 -0
  72. package/src/components/tools/item/test-face-host.ts +121 -0
  73. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  74. package/src/components/tools/item/use-draft-node.ts +26 -4
  75. package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
  76. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  79. package/src/components/tools/registry-tool-context.tsx +30 -0
  80. package/src/components/tools/select/box-select-tool.tsx +3 -1
  81. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  82. package/src/components/tools/select/marquee-footprint.ts +29 -0
  83. package/src/components/tools/shared/placement-box.tsx +57 -7
  84. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  85. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  86. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  87. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  88. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  89. package/src/components/tools/stair/stair-tool.tsx +127 -74
  90. package/src/components/tools/tool-manager.tsx +25 -8
  91. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  92. package/src/components/tools/wall/wall-drafting.ts +23 -114
  93. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  94. package/src/components/ui/action-menu/index.tsx +11 -1
  95. package/src/components/ui/command-palette/editor-commands.tsx +9 -12
  96. package/src/components/ui/command-palette/index.tsx +7 -2
  97. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  98. package/src/components/ui/controls/material-picker.tsx +4 -4
  99. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  100. package/src/components/ui/controls/segmented-control.tsx +8 -1
  101. package/src/components/ui/controls/slider-control.tsx +19 -3
  102. package/src/components/ui/controls/toggle-control.tsx +18 -5
  103. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  104. package/src/components/ui/floating-level-selector.tsx +22 -5
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  106. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  107. package/src/components/ui/helpers/item-helper.tsx +1 -1
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  109. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  110. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  111. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  112. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  113. package/src/components/ui/panels/multi-field-value.ts +163 -0
  114. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  115. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  116. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  117. package/src/components/ui/panels/node-display.ts +4 -0
  118. package/src/components/ui/panels/panel-manager.tsx +77 -3
  119. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  120. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  121. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  122. package/src/components/ui/panels/reference-panel.tsx +31 -1
  123. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  124. package/src/components/ui/scene-loader.tsx +35 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  127. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  128. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  129. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  130. package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
  131. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  132. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  134. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
  135. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  136. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  137. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  138. package/src/components/viewer/viewer-stage.tsx +4 -1
  139. package/src/components/viewer-overlay.tsx +1 -22
  140. package/src/components/viewer-zone-system.tsx +6 -2
  141. package/src/components/walkthrough-hud.tsx +19 -10
  142. package/src/hooks/use-auto-save.test.ts +73 -1
  143. package/src/hooks/use-auto-save.ts +76 -5
  144. package/src/hooks/use-ceiling-events.test.ts +122 -0
  145. package/src/hooks/use-drag-action.ts +17 -3
  146. package/src/hooks/use-grid-events.test.ts +240 -0
  147. package/src/hooks/use-grid-events.ts +190 -11
  148. package/src/hooks/use-keyboard.test.ts +244 -0
  149. package/src/hooks/use-keyboard.ts +129 -62
  150. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  151. package/src/hooks/use-save-shortcut.ts +33 -0
  152. package/src/index.tsx +82 -4
  153. package/src/lib/active-placement-surface.test.ts +16 -0
  154. package/src/lib/active-placement-surface.ts +14 -0
  155. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  156. package/src/lib/ceiling-surface-raycast.ts +12 -0
  157. package/src/lib/contextual-help-extension.ts +17 -0
  158. package/src/lib/continuation.test.ts +11 -0
  159. package/src/lib/continuation.ts +8 -1
  160. package/src/lib/direct-manipulation.test.ts +132 -0
  161. package/src/lib/direct-manipulation.ts +38 -1
  162. package/src/lib/editor-api.ts +6 -3
  163. package/src/lib/elevation-guides.test.ts +28 -1
  164. package/src/lib/elevation-guides.ts +22 -7
  165. package/src/lib/floating-menu-scale.test.ts +29 -0
  166. package/src/lib/floating-menu-scale.ts +14 -0
  167. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  168. package/src/lib/floorplan/apply-alignment.ts +10 -1
  169. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  170. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  171. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  172. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  173. package/src/lib/floorplan/floorplan-readonly.ts +1 -0
  174. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  175. package/src/lib/floorplan-grid-event-point.ts +13 -0
  176. package/src/lib/glb-export.test.ts +1169 -21
  177. package/src/lib/glb-export.ts +629 -101
  178. package/src/lib/grid-event-presentation.ts +16 -0
  179. package/src/lib/history.test.ts +528 -147
  180. package/src/lib/history.ts +81 -7
  181. package/src/lib/host-tree-children.test.ts +27 -0
  182. package/src/lib/host-tree-children.ts +79 -0
  183. package/src/lib/interaction/hot-set.test.ts +10 -1
  184. package/src/lib/interaction/hot-set.ts +3 -1
  185. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  186. package/src/lib/interaction/overlay-policy.ts +16 -0
  187. package/src/lib/interaction/registered-drafting.ts +36 -0
  188. package/src/lib/interaction/scope.ts +31 -0
  189. package/src/lib/level-print-export.test.ts +484 -0
  190. package/src/lib/level-print-export.ts +526 -0
  191. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  192. package/src/lib/local-project-presentation-persistence.ts +335 -0
  193. package/src/lib/material-paint.ts +2 -0
  194. package/src/lib/model-export.ts +29 -0
  195. package/src/lib/paint-preview-owner.ts +71 -0
  196. package/src/lib/planar-cursor-placement.test.ts +178 -1
  197. package/src/lib/planar-cursor-placement.ts +61 -5
  198. package/src/lib/plugin-panels.test.ts +92 -1
  199. package/src/lib/plugin-panels.ts +48 -0
  200. package/src/lib/portable-export.test.ts +366 -0
  201. package/src/lib/portable-export.ts +969 -0
  202. package/src/lib/print-3mf.test.ts +86 -0
  203. package/src/lib/print-3mf.ts +161 -0
  204. package/src/lib/print-content-scope.test.ts +107 -0
  205. package/src/lib/print-content-scope.ts +90 -0
  206. package/src/lib/print-export.test.ts +294 -0
  207. package/src/lib/print-export.ts +735 -0
  208. package/src/lib/print-feature-thickness.test.ts +147 -0
  209. package/src/lib/print-feature-thickness.ts +180 -0
  210. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  211. package/src/lib/print-golden-house.test.ts +249 -0
  212. package/src/lib/print-roof-solids.test.ts +133 -0
  213. package/src/lib/print-roof-solids.ts +630 -0
  214. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  215. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  216. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  217. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  218. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  219. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  220. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  221. package/src/lib/print-shell-compiler.ts +273 -0
  222. package/src/lib/print-wall-solids.test.ts +194 -0
  223. package/src/lib/print-wall-solids.ts +328 -0
  224. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  225. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  226. package/src/lib/scene.ts +22 -8
  227. package/src/lib/selection-routing.test.ts +105 -1
  228. package/src/lib/selection-routing.ts +19 -0
  229. package/src/lib/sfx-bus.ts +4 -0
  230. package/src/lib/sfx-player.ts +8 -0
  231. package/src/lib/snapping-mode.test.ts +32 -4
  232. package/src/lib/snapping-mode.ts +11 -7
  233. package/src/lib/usdz-export.ts +38 -0
  234. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  235. package/src/store/live-draft-preview-stores.test.ts +3 -0
  236. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  237. package/src/store/tool-mode.test.ts +87 -0
  238. package/src/store/use-camera-hint-focus.ts +48 -0
  239. package/src/store/use-editor.tsx +299 -188
  240. package/src/store/use-floorplan-draft-preview.ts +9 -0
  241. package/src/store/use-interaction-scope.test.ts +23 -1
  242. package/src/store/use-interaction-scope.ts +63 -2
  243. package/src/store/use-placement-preview.ts +56 -3
  244. package/src/components/tools/roof/roof-tool.tsx +0 -656
  245. 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,7 @@ export {
132
137
  type PlacementCoordinatorConfig,
133
138
  usePlacementCoordinator,
134
139
  } from './components/tools/item/use-placement-coordinator'
140
+ export { useRegistryToolContext } from './components/tools/registry-tool-context'
135
141
  export { CursorSphere } from './components/tools/shared/cursor-sphere'
136
142
  export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
137
143
  export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
@@ -144,6 +150,7 @@ export {
144
150
  resolveLevelConstructionPlane,
145
151
  } from './components/tools/shared/horizontal-construction-plane'
146
152
  export { PlacementBox } from './components/tools/shared/placement-box'
153
+ export { PlacementDimensionGuides } from './components/tools/shared/placement-dimension-guides'
147
154
  // Pointer-decided support surface (deck top vs floor underneath) — the
148
155
  // draw tools (wall / fence) ride their grid plane and commit cap on it.
149
156
  export {
@@ -234,6 +241,7 @@ export { SegmentedControl } from './components/ui/controls/segmented-control'
234
241
  export { SliderControl } from './components/ui/controls/slider-control'
235
242
  export { TerrainSculptPanel } from './components/ui/controls/terrain-sculpt-panel'
236
243
  export { ToggleControl } from './components/ui/controls/toggle-control'
244
+ export { ToolOptionsPanel } from './components/ui/controls/tool-options-panel'
237
245
  export { FloatingLevelSelector } from './components/ui/floating-level-selector'
238
246
  export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items'
239
247
  // Item collections UI — used by the kind-owned ItemPanel in nodes/.
@@ -254,6 +262,10 @@ export {
254
262
  DropdownMenuSubTrigger,
255
263
  DropdownMenuTrigger,
256
264
  } from './components/ui/primitives/dropdown-menu'
265
+ export {
266
+ ShortcutToken,
267
+ shortcutDisplayValue,
268
+ } from './components/ui/primitives/shortcut-token'
257
269
  export { useSidebarStore } from './components/ui/primitives/sidebar'
258
270
  export { Slider } from './components/ui/primitives/slider'
259
271
  export { SceneLoader } from './components/ui/scene-loader'
@@ -333,6 +345,12 @@ export {
333
345
  resolveCeilingPlanPointSnap,
334
346
  } from './lib/ceiling-plan-snap'
335
347
  export { EDITOR_LAYER } from './lib/constants'
348
+ export type { ContextualShortcutHint } from './lib/contextual-help'
349
+ export {
350
+ CONTEXTUAL_HELP_NODE_EXTENSION_KEY,
351
+ type ContextualHelpNodeExtension,
352
+ getContextualHelpNodeExtension,
353
+ } from './lib/contextual-help-extension'
336
354
  // Helper libs used by the kind-owned roof / stair / elevator panels.
337
355
  export {
338
356
  CONTINUATION_PROFILES,
@@ -341,6 +359,7 @@ export {
341
359
  continuationContextOf,
342
360
  nextContinuation,
343
361
  } from './lib/continuation'
362
+ export { createEditorApi } from './lib/editor-api'
344
363
  export {
345
364
  clearStructuralElevationGuide,
346
365
  collectElevationSnapTargets,
@@ -348,6 +367,7 @@ export {
348
367
  type ElevationGuideSource,
349
368
  type ElevationSnapMatch,
350
369
  type ElevationSnapTarget,
370
+ publishResolvedElevationGuide,
351
371
  publishStructuralElevationGuide,
352
372
  resolveElevationSnapMatch,
353
373
  resolveStructuralElevationSnap,
@@ -358,6 +378,7 @@ export {
358
378
  resolveElevatorSupportLevelId,
359
379
  resolveElevatorSupportY,
360
380
  } from './lib/elevator-support'
381
+ export { getFloatingMenuScale } from './lib/floating-menu-scale'
361
382
  // Floor-plan stair helpers — the cumulative-transform walk
362
383
  // (`computeFloorplanStairSegmentTransforms`) and the rich segment-entry
363
384
  // builder (`buildFloorplanStairEntry`) used by the kind-owned stair
@@ -382,6 +403,10 @@ export type {
382
403
  FloorplanAnnotationCategory,
383
404
  FloorplanAnnotationVisibility,
384
405
  } from './lib/floorplan/annotation-visibility'
406
+ export {
407
+ exportFloorplanPdf,
408
+ type FloorplanExportScope,
409
+ } from './lib/floorplan/floorplan-export'
385
410
  export {
386
411
  createFloorplanContextExtensions,
387
412
  FLOORPLAN_CONTEXT_EXTENSION_KEY,
@@ -407,8 +432,16 @@ export {
407
432
  type FloorplanMode,
408
433
  isFloorplanToolAvailableInMode,
409
434
  } from './lib/floorplan/floorplan-mode'
410
- export { commitFreshPlacementSubtree } from './lib/fresh-planar-placement'
411
- export { exportSceneToGlb } from './lib/glb-export'
435
+ export {
436
+ commitFreshPlacementSubtree,
437
+ createFreshPlacementSubtree,
438
+ } from './lib/fresh-planar-placement'
439
+ export { exportSceneToGlb, type GlbExportOptions } from './lib/glb-export'
440
+ export {
441
+ type EditorGridEvent,
442
+ type GridEventScreenProjection,
443
+ getGridEventScreenProjection,
444
+ } from './lib/grid-event-presentation'
412
445
  export {
413
446
  getHistoryCommandState,
414
447
  type HistoryCommandDelegate,
@@ -419,15 +452,27 @@ export {
419
452
  runUndo,
420
453
  subscribeHistoryCommandState,
421
454
  } from './lib/history'
455
+ export {
456
+ type EditorHostTreeChildren,
457
+ type EditorHostTreeChildrenProps,
458
+ editorHostTreeChildrenRegistry,
459
+ registerEditorHostTreeChildren,
460
+ } from './lib/host-tree-children'
461
+ export {
462
+ DRAFTING_SURFACE_EXTENSION_KEY,
463
+ type DraftingSurfaceExtension,
464
+ } from './lib/interaction/registered-drafting'
422
465
  export {
423
466
  boundaryReshapeScope,
424
467
  curveReshapeScope,
425
468
  endpointReshapeScope,
426
469
  holeEditScope,
470
+ meshEditScope,
427
471
  movingNodeOf,
428
472
  scopeNodeId,
429
473
  } from './lib/interaction/scope'
430
474
  export {
475
+ type ActivePaintMaterial,
431
476
  buildResetSurfaceMaterialUpdates,
432
477
  buildRoofSurfaceMaterialPatch,
433
478
  buildSingleSurfaceMaterialPatch,
@@ -446,6 +491,14 @@ export {
446
491
  measurementPolygonLabelAnchor,
447
492
  triangulateMeasurementPolygon,
448
493
  } from './lib/measurement-label'
494
+ export {
495
+ type LingoUnitSpec,
496
+ lingoUnitSpec,
497
+ type MeasurementHintOptions,
498
+ measurementHint,
499
+ type ParseMeasurementOptions,
500
+ parseMeasurement,
501
+ } from './lib/measurement-parser'
449
502
  export {
450
503
  buildMeasurementAngleArcPoints,
451
504
  cubicMetersToVolumeUnit,
@@ -467,6 +520,12 @@ export {
467
520
  metersToLinearUnit,
468
521
  squareMetersToAreaUnit,
469
522
  } from './lib/measurements'
523
+ export type {
524
+ ModelExport,
525
+ ModelExportArtifact,
526
+ ModelExportFormat,
527
+ ModelExportOptions,
528
+ } from './lib/model-export'
470
529
  export { consumePlacementDragRelease } from './lib/placement-drag-release'
471
530
  export {
472
531
  addFreshPlacementMetadata,
@@ -510,6 +569,13 @@ export {
510
569
  type SlabPlanSnapInput,
511
570
  type SlabPlanSnapResult,
512
571
  } from './lib/slab-plan-snap'
572
+ export {
573
+ getSnappingModeLabel,
574
+ resolveSnapFlags,
575
+ type SnapContext,
576
+ type SnapFlags,
577
+ type SnappingMode,
578
+ } from './lib/snapping-mode'
513
579
  export { duplicateStairSubtree } from './lib/stair-duplication'
514
580
  export {
515
581
  getBuildingLevelsForLevel,
@@ -533,6 +599,7 @@ export {
533
599
  resolveFlattenTarget,
534
600
  sculptFieldForSite,
535
601
  } from './lib/terrain-sculpt'
602
+ export { exportSceneToUsdz, type UsdzExportOptions } from './lib/usdz-export'
536
603
  // `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
537
604
  // nodes` so they don't need their own copy / their own tailwind-merge
538
605
  // dependency.
@@ -548,6 +615,7 @@ export {
548
615
  export { subscribeCameraPose } from './store/camera-pose-store'
549
616
  export { default as useAlignmentGuides } from './store/use-alignment-guides'
550
617
  export { default as useAudio } from './store/use-audio'
618
+ export { type CameraHintAction, useCameraHintFocus } from './store/use-camera-hint-focus'
551
619
  export { type CommandAction, useCommandRegistry } from './store/use-command-registry'
552
620
  export {
553
621
  DRAWING_TYPE_OPTIONS,
@@ -556,17 +624,24 @@ export {
556
624
  export type {
557
625
  CaptureMode,
558
626
  FloorplanSelectionTool,
627
+ Mode,
559
628
  SnapshotCropMode,
560
629
  SnapshotStandardAspect,
561
630
  SplitOrientation,
631
+ StructureTool,
562
632
  Tool,
563
633
  ToolDefaults,
634
+ ToolMode,
564
635
  ViewMode,
565
636
  WorkspaceMode,
566
637
  } from './store/use-editor'
567
638
  export {
639
+ armMaterialPaint,
640
+ armToolMode,
568
641
  default as useEditor,
569
642
  getActiveContinuationContext,
643
+ getActiveSnapContext,
644
+ getActiveSnappingMode,
570
645
  getContinuation,
571
646
  isAlignmentGuideActive,
572
647
  isAngleSnapActive,
@@ -623,7 +698,10 @@ export {
623
698
  type PathDraftPoint,
624
699
  usePathDraftPreview,
625
700
  } from './store/use-path-draft-preview'
626
- export { default as usePlacementPreview } from './store/use-placement-preview'
701
+ export {
702
+ default as usePlacementPreview,
703
+ type PlacementPreviewDimension,
704
+ } from './store/use-placement-preview'
627
705
  export {
628
706
  activateQuickMeasurementHudSource,
629
707
  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 {
@@ -1,6 +1,7 @@
1
- import type { AnyNode, EditorApi } from '@pascal-app/core'
1
+ import { type AnyNode, type EditorApi, useScene } from '@pascal-app/core'
2
2
  import useEditor from '../store/use-editor'
3
3
  import useInteractionScope from '../store/use-interaction-scope'
4
+ import { resolveDirectManipulationNode, resolveMoveActionNode } from './direct-manipulation'
4
5
  import {
5
6
  controlPointReshapeScope,
6
7
  endpointReshapeScope,
@@ -25,14 +26,16 @@ export function createEditorApi(): EditorApi {
25
26
  // (every concrete kind enumerated). Descriptors pass any node; the
26
27
  // cast lets registry-driven move kinds through without forcing a
27
28
  // schema-level type widening.
28
- editor.setMovingNode(node as Parameters<typeof editor.setMovingNode>[0])
29
+ const target = resolveMoveActionNode(node, useScene.getState().nodes)
30
+ editor.setMovingNode(target as Parameters<typeof editor.setMovingNode>[0])
29
31
  },
30
32
  engageMoveDrag(node: AnyNode) {
31
33
  const editor = useEditor.getState()
32
34
  // Flag drag mode BEFORE mounting the move tool so the coordinator reads
33
35
  // it at setup and wires its commit-on-release listener.
34
36
  editor.setPlacementDragMode(true)
35
- editor.setMovingNode(node as Parameters<typeof editor.setMovingNode>[0])
37
+ const target = resolveDirectManipulationNode(node, useScene.getState().nodes)
38
+ editor.setMovingNode(target as Parameters<typeof editor.setMovingNode>[0])
36
39
  },
37
40
  engageEndpointMove(node: AnyNode, endpoint: 'start' | 'end') {
38
41
  // Endpoint reshape is kind-agnostic: the scope carries the node id + which