@pascal-app/editor 0.9.1 → 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 (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  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 +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  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 +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  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 +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
package/src/index.tsx CHANGED
@@ -11,8 +11,57 @@ export { default as Editor } from './components/editor'
11
11
  // they're referenced throughout the editor's own internals; the public
12
12
  // surface uses the shorter, shell-friendly names from the unified
13
13
  // preset-system spec.
14
+ export { BakeExporter } from './components/editor/bake-exporter'
14
15
  export { FloatingActionMenu as FloatingMenu } from './components/editor/floating-action-menu'
15
- export { formatMeasurement, MeasurementPill } from './components/editor/measurement-pill'
16
+ // Embed surface — the editor's real in-canvas affordances, so a host can mount
17
+ // authentic selection handles, interactive build tools, and the mover on top
18
+ // of a bare `<Viewer>` without the full `<Editor>` shell.
19
+ // - `NodeArrowHandles` renders the selected node's registry resize/rotate/move
20
+ // handles.
21
+ // - `MoveTool` runs the kind-owned mover once a translate handle arms
22
+ // `useEditor.movingNode`.
23
+ // - `ToolManager` mounts the active registry build tool (wall / door / window /
24
+ // …) for interactive placement when `useEditor` is in build mode with a
25
+ // tool, plus the snap/alignment guide layers. Mount it only while a tool is
26
+ // active to avoid its select-mode boundary editors.
27
+ // - `Grid` is the interactive drafting plane: it raycasts the pointer and
28
+ // emits the `grid:move` / `grid:click` events the build tools consume (the
29
+ // wall tool is driven entirely by them; door/window use them for free-follow
30
+ // alongside the viewer's `wall:*` mesh events). Without it the tools mount
31
+ // but their cursor never tracks the pointer. Mount it while a tool is active.
32
+ // All read `useViewer` selection + `useEditor` state, and cooperate with host
33
+ // camera controls via the `useViewer.inputDragging` / `useEditor.movingNode`
34
+ // flags. Tools place onto `useViewer.selection.levelId`, so the host must set a
35
+ // building + level selection first.
36
+ export { Grid } from './components/editor/grid'
37
+ export {
38
+ DimensionPill,
39
+ type DimensionPillPart,
40
+ formatMeasurement,
41
+ MeasurementPill,
42
+ } from './components/editor/measurement-pill'
43
+ // In-world arrow handle primitives (chevron geometry, invisible hit area,
44
+ // shared material, palette + scale constants). Re-exported so kind-owned
45
+ // 3D selection affordances in `@pascal-app/nodes` (duct side-move / height /
46
+ // extend arrows) reuse the same UI family as the wall / fence side handles.
47
+ export {
48
+ ARROW_COLOR,
49
+ ARROW_HOVER_COLOR,
50
+ ARROW_SCALE,
51
+ createArrowHandleGeometry,
52
+ createArrowHitAreaGeometry,
53
+ HandleArrow,
54
+ type HandleArrowInputShape,
55
+ type HandleArrowPlacement,
56
+ type HandleArrowProps,
57
+ InvisibleHandleHitArea,
58
+ NO_RAYCAST,
59
+ NodeArrowHandles,
60
+ swallowNextClick,
61
+ useArrowMaterial,
62
+ useInvisibleHitAreaMaterial,
63
+ } from './components/editor/node-arrow-handles'
64
+ export { QuickMeasurementCard } from './components/editor/quick-measurement-card'
16
65
  export {
17
66
  type SnapshotCameraData,
18
67
  ThumbnailGenerator,
@@ -32,9 +81,11 @@ export {
32
81
  // shared/segment-angle.ts) once every Stage D port is in.
33
82
  export {
34
83
  createFenceOnCurrentLevel,
84
+ createSplineFenceOnCurrentLevel,
35
85
  type FencePlanPoint,
36
86
  snapFenceDraftPoint,
37
87
  } from './components/tools/fence/fence-drafting'
88
+ export { MoveTool } from './components/tools/item/move-tool'
38
89
  // Placement-math helpers — shared by kind-owned placement tools in
39
90
  // `@pascal-app/nodes` (wall curve sagitta snap, door / window placement,
40
91
  // item drop) so kinds don't reach into editor internals.
@@ -62,9 +113,11 @@ export { CursorSphere } from './components/tools/shared/cursor-sphere'
62
113
  export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
63
114
  export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
64
115
  export { useFreshPlacementVisibility } from './components/tools/shared/fresh-placement-visibility'
116
+ export { PlacementBox } from './components/tools/shared/placement-box'
65
117
  // Phase 5 Stage D — PolygonEditor for slab/ceiling boundary + hole editors.
66
118
  export {
67
119
  PolygonEditor,
120
+ type PolygonEditorPlanPointSnapContext,
68
121
  type PolygonEditorProps,
69
122
  } from './components/tools/shared/polygon-editor'
70
123
  export {
@@ -95,19 +148,24 @@ export {
95
148
  DEFAULT_STAIR_TYPE,
96
149
  DEFAULT_STAIR_WIDTH,
97
150
  } from './components/tools/stair/stair-defaults'
151
+ export { ToolManager } from './components/tools/tool-manager'
98
152
  export {
153
+ chainEndJoinsExistingWall,
99
154
  createWallOnCurrentLevel,
100
155
  getSegmentGridStep,
101
156
  isSegmentLongEnough,
157
+ resolveEndpointWallSplit,
102
158
  snapPointToGrid,
103
159
  snapScalarToGrid,
104
160
  snapWallDraftPoint,
105
161
  snapWallDraftPointDetailed,
106
- WALL_FINE_GRID_STEP,
162
+ WALL_CONNECT_SNAP_RADIUS,
107
163
  WALL_GRID_STEP,
164
+ WALL_JOIN_SNAP_RADIUS,
108
165
  type WallDraftSnapKind,
109
166
  type WallDraftSnapResult,
110
167
  type WallPlanPoint,
168
+ type WallSnapRadii,
111
169
  } from './components/tools/wall/wall-drafting'
112
170
  // `ToolbarLeft` / `ToolbarRight` are the headless-spec aliases for the
113
171
  // existing `ViewerToolbarLeft` / `ViewerToolbarRight` exports — the
@@ -164,6 +222,13 @@ export {
164
222
  } from './components/ui/sidebar/panels/settings-panel'
165
223
  export type { SitePanelProps } from './components/ui/sidebar/panels/site-panel'
166
224
  export type { SidebarTab } from './components/ui/sidebar/tab-bar'
225
+ export {
226
+ resolveAssetSnapTarget,
227
+ resolveNodeSnapTarget,
228
+ type SnapTarget,
229
+ SnapTargetBadge,
230
+ SnapTargetIcon,
231
+ } from './components/ui/snap-target-badge'
167
232
  export type { SaveStatus } from './hooks/use-auto-save'
168
233
  // useDragAction is the React-side glue for the registry's DragAction
169
234
  // primitive. Public so registry-driven kinds (Phase 5+ Stage D ports)
@@ -172,8 +237,28 @@ export { type UseDragActionArgs, useDragAction } from './hooks/use-drag-action'
172
237
  // Phase 5 Stage D — extras for kind-owned placement tools (FenceTool etc.).
173
238
  export { markToolCancelConsumed } from './hooks/use-keyboard'
174
239
  export { type Selection, useSelection } from './hooks/use-selection'
240
+ export {
241
+ clearPlacementSurface,
242
+ getPlacementSurface,
243
+ type PlacementSurface,
244
+ publishPlacementSurface,
245
+ } from './lib/active-placement-surface'
246
+ export {
247
+ CEILING_ALIGNMENT_THRESHOLD_M,
248
+ type CeilingPlanSnapInput,
249
+ type CeilingPlanSnapResult,
250
+ clearCeilingSnapFeedback,
251
+ resolveCeilingPlanPointSnap,
252
+ } from './lib/ceiling-plan-snap'
175
253
  export { EDITOR_LAYER } from './lib/constants'
176
254
  // Helper libs used by the kind-owned roof / stair / elevator panels.
255
+ export {
256
+ CONTINUATION_PROFILES,
257
+ type ContinuationContext,
258
+ type ContinuationMode,
259
+ continuationContextOf,
260
+ nextContinuation,
261
+ } from './lib/continuation'
177
262
  export {
178
263
  resolveCurrentBuildingId,
179
264
  resolveElevatorNodeSupportY,
@@ -201,15 +286,55 @@ export {
201
286
  getFloorplanWallThickness,
202
287
  } from './lib/floorplan'
203
288
  export { commitFreshPlacementSubtree } from './lib/fresh-planar-placement'
289
+ export { exportSceneToGlb } from './lib/glb-export'
290
+ export {
291
+ boundaryReshapeScope,
292
+ curveReshapeScope,
293
+ endpointReshapeScope,
294
+ holeEditScope,
295
+ movingNodeOf,
296
+ } from './lib/interaction/scope'
204
297
  export {
205
298
  buildResetSurfaceMaterialUpdates,
206
299
  buildRoofSurfaceMaterialPatch,
207
300
  buildSingleSurfaceMaterialPatch,
208
301
  buildStairSurfaceMaterialPatch,
209
- buildWallSurfaceMaterialPatch,
210
302
  getActivePaintMaterialLabel,
211
303
  hasActivePaintMaterial,
212
304
  } from './lib/material-paint'
305
+ export {
306
+ CREATABLE_MEASUREMENT_KINDS,
307
+ type CreatableMeasurementKind,
308
+ DEFAULT_CREATABLE_MEASUREMENT_KIND,
309
+ isCreatableMeasurementKind,
310
+ normalizeCreatableMeasurementKind,
311
+ } from './lib/measurement-kind'
312
+ export {
313
+ measurementPolygonLabelAnchor,
314
+ triangulateMeasurementPolygon,
315
+ } from './lib/measurement-label'
316
+ export {
317
+ buildMeasurementAngleArcPoints,
318
+ cubicMetersToVolumeUnit,
319
+ formatAreaLabel,
320
+ formatLinearMeasurement,
321
+ formatVolumeLabel,
322
+ getAreaUnitLabel,
323
+ getLinearUnitLabel,
324
+ getVolumeUnitLabel,
325
+ type LinearUnit,
326
+ linearControlValueToMeters,
327
+ linearUnitToMeters,
328
+ MEASUREMENT_ACTIVE_COLOR,
329
+ MEASUREMENT_DANGLING_COLOR,
330
+ MEASUREMENT_FLOORPLAN_COLOR,
331
+ MEASUREMENT_PERSISTENT_COLOR,
332
+ measurementFloorplanPresentationColor,
333
+ measurementPresentationColor,
334
+ metersToLinearUnit,
335
+ squareMetersToAreaUnit,
336
+ } from './lib/measurements'
337
+ export { consumePlacementDragRelease } from './lib/placement-drag-release'
213
338
  export {
214
339
  addFreshPlacementMetadata,
215
340
  getPlacementMetadataRecord,
@@ -221,10 +346,36 @@ export {
221
346
  type PlanarPoint,
222
347
  resolvePlanarCursorPosition,
223
348
  } from './lib/planar-cursor-placement'
349
+ export {
350
+ type EditorHostPanel,
351
+ type EditorHostPanelWorkspace,
352
+ editorHostPanelRegistry,
353
+ registerEditorHostPanel,
354
+ } from './lib/plugin-panels'
355
+ export {
356
+ createQuickMeasurementPointerScheduler,
357
+ quickMeasurementContext,
358
+ resolveQuickMeasurementReport,
359
+ } from './lib/quick-measurement'
224
360
  export { clearRoofDuplicateMetadata, duplicateRoofSubtree } from './lib/roof-duplication'
361
+ // Roof wall-face hit resolution + overlap guard — shared by the
362
+ // kind-owned door / window tools in `@pascal-app/nodes` and the item
363
+ // placement coordinator's roof-wall strategy.
364
+ export { hasRoofFaceChildOverlap, type RoofWallHit, resolveRoofWallHit } from './lib/roof-wall-hit'
225
365
  export type { SceneGraph } from './lib/scene'
226
366
  export { applySceneGraphToEditor } from './lib/scene'
367
+ export { movementSfxStepKey } from './lib/sfx/movement-tick'
227
368
  export { triggerSFX } from './lib/sfx-bus'
369
+ export {
370
+ clearSlabSnapFeedback,
371
+ resolveSlabEdgeBandSnap,
372
+ resolveSlabPlanPointSnap,
373
+ SLAB_ALIGNMENT_THRESHOLD_M,
374
+ type SlabEdgeBandSnapInput,
375
+ type SlabEdgeBandSnapResult,
376
+ type SlabPlanSnapInput,
377
+ type SlabPlanSnapResult,
378
+ } from './lib/slab-plan-snap'
228
379
  export { duplicateStairSubtree } from './lib/stair-duplication'
229
380
  export {
230
381
  getBuildingLevelsForLevel,
@@ -234,13 +385,22 @@ export {
234
385
  resolveStairPlacementLevelId,
235
386
  resolveStairToLevelId,
236
387
  } from './lib/stair-levels'
388
+ export {
389
+ clearSurfacePlanSnapFeedback,
390
+ resolveSurfacePlanPointSnap,
391
+ SURFACE_ALIGNMENT_THRESHOLD_M,
392
+ type SurfacePlanSnapInput,
393
+ type SurfacePlanSnapResult,
394
+ } from './lib/surface-plan-snap'
237
395
  // `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
238
396
  // nodes` so they don't need their own copy / their own tailwind-merge
239
397
  // dependency.
240
398
  export { cn } from './lib/utils'
241
399
  export {
242
400
  getActiveBuildingPose,
401
+ projectAlignmentGuidesWorldToActiveBuildingLocal,
243
402
  resolveAlignmentForActiveBuilding,
403
+ resolveAlignmentForFloorplanView,
244
404
  snapBuildingLocalToWorldGrid,
245
405
  snapWorldXZForActiveBuilding,
246
406
  } from './lib/world-grid-snap'
@@ -248,22 +408,75 @@ export { default as useAlignmentGuides } from './store/use-alignment-guides'
248
408
  export { default as useAudio } from './store/use-audio'
249
409
  export { type CommandAction, useCommandRegistry } from './store/use-command-registry'
250
410
  export type {
411
+ CaptureMode,
251
412
  FloorplanSelectionTool,
252
- MovingFenceEndpoint,
253
- MovingWallEndpoint,
413
+ SnapshotCropMode,
414
+ SnapshotStandardAspect,
254
415
  SplitOrientation,
255
416
  Tool,
256
417
  ToolDefaults,
257
418
  ViewMode,
258
419
  WorkspaceMode,
259
420
  } from './store/use-editor'
260
- export { default as useEditor } from './store/use-editor'
421
+ export {
422
+ default as useEditor,
423
+ getActiveContinuationContext,
424
+ getContinuation,
425
+ isAlignmentGuideActive,
426
+ isAngleSnapActive,
427
+ isGridSnapActive,
428
+ isMagneticSnapActive,
429
+ } from './store/use-editor'
430
+ export { default as useFacingPose, type FacingPose } from './store/use-facing-pose'
431
+ export { default as useFenceCurveDraft } from './store/use-fence-curve-draft'
432
+ export {
433
+ default as useInteractionScope,
434
+ getEditingHole,
435
+ getIsCurveReshape,
436
+ getMovingNode,
437
+ useActiveHandleDrag,
438
+ useEditingHole,
439
+ useEndpointReshape,
440
+ useIsCurveReshape,
441
+ useMovingNode,
442
+ useReshapingNode,
443
+ } from './store/use-interaction-scope'
444
+ export {
445
+ commitMeasurementDraft,
446
+ finishMeasurementDraft,
447
+ handleMeasurementDraftEscape,
448
+ type MeasurementAxis,
449
+ type MeasurementAxisGuide,
450
+ type MeasurementDraftOwner,
451
+ type MeasurementDraftPayload,
452
+ type MeasurementDraftStage,
453
+ type MeasurementKind,
454
+ type MeasurementPoint,
455
+ type MeasurementSurfacePoint,
456
+ measurementPolygonMidpoints,
457
+ useMeasurementDraft,
458
+ } from './store/use-measurement-draft'
459
+ export {
460
+ default as useOpeningGuides,
461
+ type OpeningGuide3D,
462
+ type OpeningGuideVec3,
463
+ } from './store/use-opening-guides'
261
464
  export {
262
465
  type PaletteView,
263
466
  type PaletteViewProps,
264
467
  usePaletteViewRegistry,
265
468
  } from './store/use-palette-view-registry'
266
469
  export { default as usePlacementPreview } from './store/use-placement-preview'
470
+ export {
471
+ activateQuickMeasurementHudSource,
472
+ clearQuickMeasurementHudSource,
473
+ publishQuickMeasurementHudSource,
474
+ type QuickMeasurementHudEntry,
475
+ type QuickMeasurementHudSource,
476
+ selectQuickMeasurementHudEntry,
477
+ useQuickMeasurementHud,
478
+ } from './store/use-quick-measurement-hud'
479
+ export { default as useSegmentDraftChain } from './store/use-segment-draft-chain'
267
480
  export { useUploadStore } from './store/use-upload'
268
481
  export { useWallMoveGhosts, type WallMoveGhostBridge } from './store/use-wall-move-ghosts'
269
482
  export {
@@ -0,0 +1,35 @@
1
+ import { Vector3 } from 'three'
2
+
3
+ // The surface the active placement/move ghost is currently snapped to: a contact
4
+ // point (world space) and the surface's outward unit normal. Published each frame
5
+ // by the placement tools (the item coordinator + the drawn-kind tools) and read
6
+ // by the grid so its snap patch sits at the ghost's height AND orients to the
7
+ // surface — horizontal on a floor / shelf top, vertical in a wall plane.
8
+ //
9
+ // A plain module singleton (not a store): both writer and reader run inside
10
+ // `useFrame`, so reactivity would only add overhead. The vectors are reused, so
11
+ // readers must consume them within the same frame.
12
+ export type PlacementSurface = {
13
+ point: Vector3
14
+ normal: Vector3
15
+ }
16
+
17
+ const surface: PlacementSurface = {
18
+ point: new Vector3(),
19
+ normal: new Vector3(0, 1, 0),
20
+ }
21
+ let active = false
22
+
23
+ export function publishPlacementSurface(point: Vector3, normal: Vector3): void {
24
+ surface.point.copy(point)
25
+ surface.normal.copy(normal)
26
+ active = true
27
+ }
28
+
29
+ export function clearPlacementSurface(): void {
30
+ active = false
31
+ }
32
+
33
+ export function getPlacementSurface(): PlacementSurface | null {
34
+ return active ? surface : null
35
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ clearSurfacePlanSnapFeedback,
3
+ resolveSurfacePlanPointSnap,
4
+ SURFACE_ALIGNMENT_THRESHOLD_M,
5
+ type SurfacePlanSnapInput,
6
+ type SurfacePlanSnapResult,
7
+ } from './surface-plan-snap'
8
+
9
+ const CEILING_SNAP_MOVING_ID = '__ceiling_snap__'
10
+
11
+ export const CEILING_ALIGNMENT_THRESHOLD_M = SURFACE_ALIGNMENT_THRESHOLD_M
12
+ export type CeilingPlanSnapInput = SurfacePlanSnapInput
13
+ export type CeilingPlanSnapResult = SurfacePlanSnapResult
14
+
15
+ export function clearCeilingSnapFeedback() {
16
+ clearSurfacePlanSnapFeedback()
17
+ }
18
+
19
+ export function resolveCeilingPlanPointSnap(input: CeilingPlanSnapInput): CeilingPlanSnapResult {
20
+ return resolveSurfacePlanPointSnap({
21
+ ...input,
22
+ movingId: input.movingId ?? CEILING_SNAP_MOVING_ID,
23
+ })
24
+ }
@@ -0,0 +1,112 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { resolveSelectModeHelpHints } from './contextual-help'
3
+
4
+ describe('resolveSelectModeHelpHints', () => {
5
+ test('stays hidden in idle select mode with no selection', () => {
6
+ expect(
7
+ resolveSelectModeHelpHints({
8
+ selectedCount: 0,
9
+ hasMovableSelection: false,
10
+ hasRotatableSelection: false,
11
+ commandPressed: false,
12
+ shiftPressed: false,
13
+ }),
14
+ ).toEqual([])
15
+ })
16
+
17
+ test('shows multi-select guidance when a modifier is held without selection', () => {
18
+ expect(
19
+ resolveSelectModeHelpHints({
20
+ selectedCount: 0,
21
+ hasMovableSelection: false,
22
+ hasRotatableSelection: false,
23
+ commandPressed: true,
24
+ shiftPressed: false,
25
+ }),
26
+ ).toEqual([
27
+ {
28
+ keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
29
+ label: 'Add or remove objects from the selection',
30
+ active: true,
31
+ },
32
+ ])
33
+ })
34
+
35
+ test('shows direct manipulation tips for selected movable and rotatable nodes', () => {
36
+ const hints = resolveSelectModeHelpHints({
37
+ selectedCount: 1,
38
+ hasMovableSelection: true,
39
+ hasRotatableSelection: true,
40
+ commandPressed: false,
41
+ shiftPressed: false,
42
+ })
43
+
44
+ expect(hints).toContainEqual({
45
+ keys: ['Left click'],
46
+ label: 'Drag selected movable object',
47
+ })
48
+ expect(hints).toContainEqual({
49
+ keys: ['Cmd/Ctrl', 'Right click'],
50
+ label: 'Drag left or right to rotate selected object',
51
+ })
52
+ // Cmd/Ctrl and Shift click both toggle selection membership (3D selection
53
+ // manager and 2D floorplan alike) — advertised as a single or-group row.
54
+ expect(hints).toContainEqual({
55
+ keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
56
+ label: 'Add or remove objects from the selection',
57
+ active: false,
58
+ })
59
+ })
60
+
61
+ test('multi-selection advertises the group move + rotate gestures', () => {
62
+ const hints = resolveSelectModeHelpHints({
63
+ selectedCount: 3,
64
+ hasMovableSelection: true,
65
+ hasRotatableSelection: true,
66
+ commandPressed: false,
67
+ shiftPressed: false,
68
+ })
69
+
70
+ expect(hints).toEqual([
71
+ {
72
+ keys: ['Left click'],
73
+ label: 'Click or drag the selection to move it as one',
74
+ },
75
+ { keys: ['R / T'], label: 'Rotate the selection ±45°' },
76
+ {
77
+ keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
78
+ label: 'Add or remove objects from the selection',
79
+ active: false,
80
+ },
81
+ { keys: ['Esc'], label: 'Clear the selection (or click outside)' },
82
+ ])
83
+ })
84
+
85
+ test('holding a modifier keeps the same rows and only lights the selection one', () => {
86
+ // Guides/snapping are governed by the snapping mode (Shift toggles it),
87
+ // so no modifier-specific "freely / with guides / bypass" variants exist.
88
+ const hints = resolveSelectModeHelpHints({
89
+ selectedCount: 1,
90
+ hasMovableSelection: true,
91
+ hasRotatableSelection: true,
92
+ commandPressed: true,
93
+ shiftPressed: true,
94
+ })
95
+
96
+ expect(hints).toEqual([
97
+ {
98
+ keys: ['Left click'],
99
+ label: 'Drag selected movable object',
100
+ },
101
+ {
102
+ keys: ['Cmd/Ctrl', 'Right click'],
103
+ label: 'Drag left or right to rotate selected object',
104
+ },
105
+ {
106
+ keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
107
+ label: 'Add or remove objects from the selection',
108
+ active: true,
109
+ },
110
+ ])
111
+ })
112
+ })
@@ -0,0 +1,144 @@
1
+ export type ContextualShortcutHint = {
2
+ // A combo of keys pressed together (rendered joined by "+"). An entry may
3
+ // itself be an array of alternatives (rendered joined by "/"), e.g.
4
+ // [['Cmd/Ctrl', 'Shift'], 'Left click'] → "⌘ / ⇧ + click".
5
+ keys: Array<string | string[]>
6
+ label: string
7
+ // Optional secondary line under the label for a terser qualifier
8
+ // (e.g. "disable 15° snap"). The HUD wraps both lines rather than truncating.
9
+ subtitle?: string
10
+ active?: boolean
11
+ }
12
+
13
+ // `activeHandleDrag.label` value a rotate gizmo sets while dragging, so the
14
+ // contextual HUD can surface the Shift = free-rotation toggle for the duration
15
+ // (mirrors how wall drafting advertises Shift). Distinct from resize handles,
16
+ // which route their own measurement label here.
17
+ export const ROTATE_HANDLE_DRAG_LABEL = 'rotate-handle'
18
+
19
+ // `activeHandleDrag.label` a plain resize / radial-resize arrow sets while
20
+ // dragging (when it carries no dimension `measureLabel`). It exists only so the
21
+ // interaction scope is non-idle during a resize, which keeps the idle
22
+ // select-mode hints off-screen — a resize is its own action, not a selection.
23
+ export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
24
+
25
+ // `activeHandleDrag.label`s for the multi-selection group drags: the body
26
+ // drag sessions (2D floorplan + 3D ground plane) and the rotate gizmo. Kept
27
+ // here (not in the session/gizmo modules) so `snapping-mode.ts` can resolve
28
+ // the group move to the 'item' snap context without importing component code.
29
+ export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
30
+ export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
31
+
32
+ // Hints shown while a rotate gizmo is mid-drag: Shift bypasses the angle step
33
+ // (free rotation), the same toggle wall drafting exposes. `active` lights the
34
+ // pill while Shift is held.
35
+ export function resolveRotateHandleHelpHints(shiftPressed: boolean): ContextualShortcutHint[] {
36
+ return [
37
+ {
38
+ keys: [SHIFT_KEY],
39
+ label: shiftPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
40
+ active: shiftPressed,
41
+ },
42
+ ]
43
+ }
44
+
45
+ export type SelectModeHelpContext = {
46
+ selectedCount: number
47
+ hasMovableSelection: boolean
48
+ hasRotatableSelection: boolean
49
+ commandPressed: boolean
50
+ shiftPressed: boolean
51
+ // When a single MEP node is selected its in-world handle rig (click a dot to
52
+ // reveal move arrows) is the real editing path, so the panel leads with the
53
+ // handle-specific hints instead of just the generic Cmd-drag tips.
54
+ mepSelection?: 'run' | 'fitting' | null
55
+ }
56
+
57
+ const COMMAND_KEY = 'Cmd/Ctrl'
58
+ const LEFT_CLICK = 'Left click'
59
+ const RIGHT_CLICK = 'Right click'
60
+ const SHIFT_KEY = 'Shift'
61
+ const CLICK = 'Click'
62
+ const ALT_KEY = 'Alt'
63
+ const ROTATE_KEYS = 'R / T'
64
+ const ESC_KEY = 'Esc'
65
+
66
+ export function resolveSelectModeHelpHints({
67
+ selectedCount,
68
+ hasMovableSelection,
69
+ hasRotatableSelection,
70
+ commandPressed,
71
+ shiftPressed,
72
+ mepSelection = null,
73
+ }: SelectModeHelpContext): ContextualShortcutHint[] {
74
+ const hints: ContextualShortcutHint[] = []
75
+
76
+ if (selectedCount === 0) {
77
+ if (!commandPressed && !shiftPressed) return hints
78
+
79
+ hints.push({
80
+ keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
81
+ label: 'Add or remove objects from the selection',
82
+ active: true,
83
+ })
84
+ return hints
85
+ }
86
+
87
+ // Multi-selection — advertise the group gestures: drag any member to slide
88
+ // the whole selection (2D floor plan body drag + the 3D move gizmo), R / T
89
+ // to step-rotate the group around its bounding-box center.
90
+ if (selectedCount > 1) {
91
+ hints.push({
92
+ keys: [LEFT_CLICK],
93
+ label: 'Click or drag the selection to move it as one',
94
+ })
95
+ hints.push({ keys: [ROTATE_KEYS], label: 'Rotate the selection ±45°' })
96
+ hints.push({
97
+ keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
98
+ label: 'Add or remove objects from the selection',
99
+ active: commandPressed || shiftPressed,
100
+ })
101
+ hints.push({ keys: [ESC_KEY], label: 'Clear the selection (or click outside)' })
102
+ return hints
103
+ }
104
+
105
+ // MEP handle workflow — duct/pipe runs and fittings are edited through the
106
+ // in-world arrow rig that a click on the handle dot reveals, so surface those
107
+ // hints first. A run endpoint's side / up-down arrows swing the run and Alt
108
+ // detaches the joint mid-drag; a fitting's cluster adds rotate arcs, with
109
+ // R / T (and Alt to switch axis) for keyboard rotation.
110
+ if (mepSelection === 'run') {
111
+ hints.push({ keys: [CLICK], label: 'Click a handle dot to show move arrows' })
112
+ hints.push({ keys: [ALT_KEY], label: 'Detach the joint while dragging an arrow' })
113
+ } else if (mepSelection === 'fitting') {
114
+ hints.push({ keys: [CLICK], label: 'Click the handle dot to show move + rotate handles' })
115
+ hints.push({ keys: [ROTATE_KEYS], label: 'Rotate ±45°' })
116
+ hints.push({ keys: [ALT_KEY], label: 'Switch the rotation axis (Y → X → Z)' })
117
+ }
118
+
119
+ // The rows are the same whatever modifier is held — guides/snapping are
120
+ // governed by the snapping mode (Shift toggles it), not by the modifier of
121
+ // this gesture, so there are no modifier-specific variants to advertise.
122
+ // Holding Cmd/Ctrl or Shift just lights the selection row.
123
+ if (hasMovableSelection) {
124
+ hints.push({
125
+ keys: [LEFT_CLICK],
126
+ label: 'Drag selected movable object',
127
+ })
128
+ }
129
+
130
+ if (hasRotatableSelection) {
131
+ hints.push({
132
+ keys: [COMMAND_KEY, RIGHT_CLICK],
133
+ label: 'Drag left or right to rotate selected object',
134
+ })
135
+ }
136
+
137
+ hints.push({
138
+ keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
139
+ label: 'Add or remove objects from the selection',
140
+ active: commandPressed || shiftPressed,
141
+ })
142
+
143
+ return hints
144
+ }