@pascal-app/editor 1.0.0-beta.4 → 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 (283) 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/alignment-3d-guide-layer.tsx +12 -3
  6. package/src/components/editor/bake-exporter.tsx +1 -0
  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 +17 -5
  13. package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
  14. package/src/components/editor/export-manager.tsx +175 -33
  15. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  16. package/src/components/editor/first-person-controls.tsx +182 -57
  17. package/src/components/editor/floating-action-menu.tsx +55 -55
  18. package/src/components/editor/floorplan-background-selection.test.ts +31 -4
  19. package/src/components/editor/floorplan-background-selection.ts +27 -7
  20. package/src/components/editor/floorplan-panel.tsx +81 -57
  21. package/src/components/editor/grid.tsx +12 -7
  22. package/src/components/editor/group-actions.ts +34 -50
  23. package/src/components/editor/group-floating-action-menu.tsx +30 -15
  24. package/src/components/editor/group-move-3d.ts +51 -21
  25. package/src/components/editor/group-transform-shared.test.ts +78 -0
  26. package/src/components/editor/group-transform-shared.ts +74 -7
  27. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  28. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  29. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  30. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  31. package/src/components/editor/handles/resize-snap.ts +10 -1
  32. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  33. package/src/components/editor/index.tsx +252 -46
  34. package/src/components/editor/measurement-pill.tsx +12 -12
  35. package/src/components/editor/node-action-menu.tsx +42 -1
  36. package/src/components/editor/node-arrow-handles.tsx +47 -58
  37. package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
  38. package/src/components/editor/selection-manager.tsx +132 -47
  39. package/src/components/editor/site-edge-labels.tsx +2 -1
  40. package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
  41. package/src/components/editor/snapshot-capture.test.ts +422 -0
  42. package/src/components/editor/snapshot-capture.ts +145 -0
  43. package/src/components/editor/thumbnail-generator.tsx +295 -232
  44. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  45. package/src/components/editor/wall-measurement-label.tsx +3 -2
  46. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  47. package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
  48. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  49. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  50. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  51. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  52. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  53. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  54. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
  57. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  59. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  60. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  61. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  62. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  63. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  64. package/src/components/systems/selection-affordance-services.ts +19 -0
  65. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  66. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  67. package/src/components/tools/fence/fence-drafting.ts +6 -25
  68. package/src/components/tools/item/draft-creation.test.ts +12 -0
  69. package/src/components/tools/item/draft-creation.ts +10 -0
  70. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  71. package/src/components/tools/item/face-host-commit.ts +56 -0
  72. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  73. package/src/components/tools/item/face-host-preview.ts +82 -0
  74. package/src/components/tools/item/move-tool.tsx +4 -3
  75. package/src/components/tools/item/placement-math.test.ts +11 -1
  76. package/src/components/tools/item/placement-math.ts +0 -20
  77. package/src/components/tools/item/placement-strategies.test.ts +494 -0
  78. package/src/components/tools/item/placement-strategies.ts +192 -24
  79. package/src/components/tools/item/placement-surface.test.ts +61 -0
  80. package/src/components/tools/item/placement-surface.ts +24 -0
  81. package/src/components/tools/item/placement-types.ts +6 -0
  82. package/src/components/tools/item/test-face-host.ts +121 -0
  83. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  84. package/src/components/tools/item/use-draft-node.ts +26 -4
  85. package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
  86. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  87. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  88. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  89. package/src/components/tools/registry-tool-context.tsx +30 -0
  90. package/src/components/tools/select/box-select-tool.tsx +3 -1
  91. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  92. package/src/components/tools/select/marquee-footprint.ts +29 -0
  93. package/src/components/tools/shared/placement-box.tsx +63 -8
  94. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  95. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  96. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  97. package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
  98. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  99. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  100. package/src/components/tools/stair/stair-tool.tsx +127 -74
  101. package/src/components/tools/tool-manager.tsx +25 -8
  102. package/src/components/tools/wall/wall-drafting.test.ts +527 -4
  103. package/src/components/tools/wall/wall-drafting.ts +55 -462
  104. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  105. package/src/components/ui/action-menu/index.tsx +11 -1
  106. package/src/components/ui/command-palette/editor-commands.tsx +12 -13
  107. package/src/components/ui/command-palette/index.tsx +7 -2
  108. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  109. package/src/components/ui/controls/material-picker.tsx +4 -4
  110. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  111. package/src/components/ui/controls/segmented-control.tsx +8 -1
  112. package/src/components/ui/controls/slider-control.tsx +19 -3
  113. package/src/components/ui/controls/toggle-control.tsx +18 -5
  114. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  115. package/src/components/ui/floating-level-selector.tsx +23 -6
  116. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  117. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  118. package/src/components/ui/helpers/item-helper.tsx +1 -1
  119. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  120. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  121. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  122. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  123. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  124. package/src/components/ui/panels/multi-field-value.ts +163 -0
  125. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  126. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  127. package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
  128. package/src/components/ui/panels/node-display.ts +4 -0
  129. package/src/components/ui/panels/panel-manager.tsx +77 -3
  130. package/src/components/ui/panels/panel-wrapper.tsx +228 -12
  131. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  132. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  133. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  134. package/src/components/ui/panels/reference-panel.tsx +31 -1
  135. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  136. package/src/components/ui/scene-loader.tsx +35 -0
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
  138. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  139. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
  140. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  142. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  143. package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
  144. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  145. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  146. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  147. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
  148. package/src/components/ui/sidebar/tab-bar.tsx +9 -1
  149. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  150. package/src/components/viewer/floorplan-compass-button.tsx +50 -0
  151. package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
  152. package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
  153. package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
  154. package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
  155. package/src/components/viewer/floorplan-preview.tsx +1069 -0
  156. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
  157. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  158. package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
  159. package/src/components/viewer/viewer-stage-modes.ts +49 -0
  160. package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
  161. package/src/components/viewer/viewer-stage.test.tsx +47 -0
  162. package/src/components/viewer/viewer-stage.tsx +221 -0
  163. package/src/components/viewer-overlay.tsx +13 -30
  164. package/src/components/viewer-zone-system.tsx +6 -2
  165. package/src/components/walkthrough-hud.tsx +19 -10
  166. package/src/hooks/use-auto-save.test.ts +122 -1
  167. package/src/hooks/use-auto-save.ts +119 -13
  168. package/src/hooks/use-ceiling-events.test.ts +122 -0
  169. package/src/hooks/use-drag-action.ts +17 -3
  170. package/src/hooks/use-grid-events.test.ts +240 -0
  171. package/src/hooks/use-grid-events.ts +190 -11
  172. package/src/hooks/use-keyboard.test.ts +244 -0
  173. package/src/hooks/use-keyboard.ts +164 -63
  174. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  175. package/src/hooks/use-save-shortcut.ts +33 -0
  176. package/src/index.tsx +109 -4
  177. package/src/lib/active-placement-surface.test.ts +16 -0
  178. package/src/lib/active-placement-surface.ts +14 -0
  179. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  180. package/src/lib/ceiling-surface-raycast.ts +12 -0
  181. package/src/lib/contextual-help-extension.ts +17 -0
  182. package/src/lib/contextual-help.test.ts +8 -0
  183. package/src/lib/contextual-help.ts +8 -0
  184. package/src/lib/continuation.test.ts +11 -0
  185. package/src/lib/continuation.ts +8 -1
  186. package/src/lib/direct-manipulation.test.ts +132 -0
  187. package/src/lib/direct-manipulation.ts +38 -1
  188. package/src/lib/editor-api.ts +6 -3
  189. package/src/lib/elevation-guides.test.ts +28 -1
  190. package/src/lib/elevation-guides.ts +22 -7
  191. package/src/lib/floating-menu-scale.test.ts +29 -0
  192. package/src/lib/floating-menu-scale.ts +14 -0
  193. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  194. package/src/lib/floorplan/apply-alignment.ts +10 -1
  195. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  196. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  197. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  198. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  199. package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
  200. package/src/lib/floorplan/floorplan-readonly.ts +85 -0
  201. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  202. package/src/lib/floorplan-grid-event-point.ts +13 -0
  203. package/src/lib/glb-export.test.ts +1169 -21
  204. package/src/lib/glb-export.ts +629 -101
  205. package/src/lib/grid-event-presentation.ts +16 -0
  206. package/src/lib/history.test.ts +523 -77
  207. package/src/lib/history.ts +133 -14
  208. package/src/lib/host-tree-children.test.ts +27 -0
  209. package/src/lib/host-tree-children.ts +79 -0
  210. package/src/lib/inspector-card-mode.test.ts +92 -0
  211. package/src/lib/inspector-card-mode.ts +60 -0
  212. package/src/lib/interaction/hot-set.test.ts +10 -1
  213. package/src/lib/interaction/hot-set.ts +3 -1
  214. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  215. package/src/lib/interaction/overlay-policy.ts +16 -0
  216. package/src/lib/interaction/registered-drafting.ts +36 -0
  217. package/src/lib/interaction/scope.ts +31 -0
  218. package/src/lib/level-print-export.test.ts +484 -0
  219. package/src/lib/level-print-export.ts +526 -0
  220. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  221. package/src/lib/local-project-presentation-persistence.ts +335 -0
  222. package/src/lib/material-paint.ts +2 -0
  223. package/src/lib/measurement-parser.ts +3 -0
  224. package/src/lib/measurements.test.ts +4 -0
  225. package/src/lib/model-export.ts +29 -0
  226. package/src/lib/paint-preview-owner.ts +71 -0
  227. package/src/lib/paint-scope.test.ts +58 -2
  228. package/src/lib/paint-scope.ts +8 -2
  229. package/src/lib/planar-cursor-placement.test.ts +178 -1
  230. package/src/lib/planar-cursor-placement.ts +61 -5
  231. package/src/lib/plugin-panels.test.ts +92 -1
  232. package/src/lib/plugin-panels.ts +48 -0
  233. package/src/lib/portable-export.test.ts +366 -0
  234. package/src/lib/portable-export.ts +969 -0
  235. package/src/lib/print-3mf.test.ts +86 -0
  236. package/src/lib/print-3mf.ts +161 -0
  237. package/src/lib/print-content-scope.test.ts +107 -0
  238. package/src/lib/print-content-scope.ts +90 -0
  239. package/src/lib/print-export.test.ts +294 -0
  240. package/src/lib/print-export.ts +735 -0
  241. package/src/lib/print-feature-thickness.test.ts +147 -0
  242. package/src/lib/print-feature-thickness.ts +180 -0
  243. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  244. package/src/lib/print-golden-house.test.ts +249 -0
  245. package/src/lib/print-roof-solids.test.ts +133 -0
  246. package/src/lib/print-roof-solids.ts +630 -0
  247. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  248. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  249. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  250. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  251. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  252. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  253. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  254. package/src/lib/print-shell-compiler.ts +273 -0
  255. package/src/lib/print-wall-solids.test.ts +194 -0
  256. package/src/lib/print-wall-solids.ts +328 -0
  257. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  258. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  259. package/src/lib/scene.ts +22 -8
  260. package/src/lib/selection-routing.test.ts +125 -6
  261. package/src/lib/selection-routing.ts +34 -1
  262. package/src/lib/session-groups.test.ts +88 -0
  263. package/src/lib/session-groups.ts +201 -0
  264. package/src/lib/sfx-bus.ts +4 -0
  265. package/src/lib/sfx-player.ts +8 -0
  266. package/src/lib/snapping-mode.test.ts +32 -4
  267. package/src/lib/snapping-mode.ts +11 -7
  268. package/src/lib/structured-clone-fallback.test.ts +45 -0
  269. package/src/lib/structured-clone-fallback.ts +24 -0
  270. package/src/lib/usdz-export.ts +38 -0
  271. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  272. package/src/store/live-draft-preview-stores.test.ts +3 -0
  273. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  274. package/src/store/tool-mode.test.ts +87 -0
  275. package/src/store/use-camera-hint-focus.ts +48 -0
  276. package/src/store/use-editor.tsx +299 -188
  277. package/src/store/use-floorplan-draft-preview.ts +9 -0
  278. package/src/store/use-interaction-scope.test.ts +23 -1
  279. package/src/store/use-interaction-scope.ts +63 -2
  280. package/src/store/use-placement-preview.ts +56 -3
  281. package/src/store/use-session-groups.ts +103 -0
  282. package/src/components/tools/roof/roof-tool.tsx +0 -656
  283. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -1,9 +1,21 @@
1
1
  'use client'
2
2
 
3
- import { emitter, type FenceNode, isCurvedWall, type WallNode } from '@pascal-app/core'
4
- import { type MouseEvent as ReactMouseEvent, useCallback } from 'react'
3
+ import {
4
+ emitter,
5
+ type FenceNode,
6
+ isCurvedWall,
7
+ nodeRegistry,
8
+ type WallNode,
9
+ } from '@pascal-app/core'
10
+ import {
11
+ type MouseEvent as ReactMouseEvent,
12
+ useCallback,
13
+ useEffect,
14
+ useSyncExternalStore,
15
+ } from 'react'
5
16
  import { resolveCeilingPlanPointSnap } from '../../lib/ceiling-plan-snap'
6
17
  import { alignFloorplanDraftPoint, getPlanPointDistance } from '../../lib/floorplan'
18
+ import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-event-point'
7
19
  import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
8
20
  import useAlignmentGuides from '../../store/use-alignment-guides'
9
21
  import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../store/use-editor'
@@ -12,6 +24,9 @@ import useSegmentDraftChain from '../../store/use-segment-draft-chain'
12
24
  import { snapFenceDraftPoint } from '../tools/fence/fence-drafting'
13
25
  import { getSegmentGridStep, type WallPlanPoint } from '../tools/wall/wall-drafting'
14
26
 
27
+ const NOOP_SUBSCRIBE = () => () => {}
28
+ const DEFAULT_ROOF_FOOTPRINT_CHOICE = () => 'draw'
29
+
15
30
  type UseFloorplanBackgroundPlacementArgs = {
16
31
  activePolygonDraftPoints: WallPlanPoint[]
17
32
  ceilingDraftPoints: WallPlanPoint[]
@@ -55,6 +70,7 @@ type UseFloorplanBackgroundPlacementArgs = {
55
70
  isWallBuildActive: boolean
56
71
  isZoneBuildActive: boolean
57
72
  levelId: string | null
73
+ registryToolOwnsSnapping: boolean
58
74
  roofDraftStart: WallPlanPoint | null
59
75
  setCursorPoint: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
60
76
  setFenceDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
@@ -113,6 +129,7 @@ export function useFloorplanBackgroundPlacement({
113
129
  isWallBuildActive,
114
130
  isZoneBuildActive,
115
131
  levelId,
132
+ registryToolOwnsSnapping,
116
133
  roofDraftStart,
117
134
  setCursorPoint,
118
135
  setFenceDraftEnd,
@@ -125,6 +142,26 @@ export function useFloorplanBackgroundPlacement({
125
142
  walls,
126
143
  worldGridSnap,
127
144
  }: UseFloorplanBackgroundPlacementArgs) {
145
+ // Read the roof's footprint-source option through the registry, not
146
+ // `@pascal-app/nodes`: this file lands in the nodes package's program via
147
+ // its editor imports, so a direct nodes import would cycle onto nodes' own
148
+ // dist output.
149
+ const roofFootprintOption = nodeRegistry
150
+ .get('roof')
151
+ ?.toolOptions?.find((option) => option.id === 'footprintSource')
152
+ const roofFootprintChoice = useSyncExternalStore(
153
+ roofFootprintOption?.subscribe ?? NOOP_SUBSCRIBE,
154
+ roofFootprintOption?.value ?? DEFAULT_ROOF_FOOTPRINT_CHOICE,
155
+ roofFootprintOption?.value ?? DEFAULT_ROOF_FOOTPRINT_CHOICE,
156
+ )
157
+ // Conical always builds from a curved wall pick, regardless of the choice.
158
+ const roofIsConical = useEditor((state) => state.toolDefaults.roof?.roofType === 'conical')
159
+ const roofFootprintSource = roofIsConical ? 'walls' : roofFootprintChoice
160
+
161
+ useEffect(() => {
162
+ if (isRoofBuildActive && roofFootprintSource !== 'draw') clearRoofPlacementDraft()
163
+ }, [clearRoofPlacementDraft, isRoofBuildActive, roofFootprintSource])
164
+
128
165
  const handleBackgroundPlacementClick = useCallback(
129
166
  (
130
167
  planPoint: WallPlanPoint,
@@ -188,6 +225,11 @@ export function useFloorplanBackgroundPlacement({
188
225
  emitFloorplanGridEvent('click', snappedPoint, event)
189
226
  setCursorPoint(snappedPoint)
190
227
 
228
+ if (roofFootprintSource !== 'draw') {
229
+ clearRoofPlacementDraft()
230
+ return true
231
+ }
232
+
191
233
  if (roofDraftStart) {
192
234
  clearRoofPlacementDraft()
193
235
  } else {
@@ -357,9 +399,13 @@ export function useFloorplanBackgroundPlacement({
357
399
  // local floor-plan draft handler (column / spawn / shelf / etc.).
358
400
  // The tool's `grid:click` subscriber owns the placement.
359
401
  if (isFloorplanGridInteractionActive) {
360
- const snappedPoint = getSnappedFloorplanPoint(planPoint)
361
- emitFloorplanGridEvent('click', snappedPoint, event)
362
- setCursorPoint(snappedPoint)
402
+ const eventPoint = resolveGenericFloorplanGridEventPoint({
403
+ point: planPoint,
404
+ registryToolOwnsSnapping,
405
+ snap: getSnappedFloorplanPoint,
406
+ })
407
+ emitFloorplanGridEvent('click', eventPoint, event)
408
+ setCursorPoint(eventPoint)
363
409
  return true
364
410
  }
365
411
 
@@ -392,6 +438,8 @@ export function useFloorplanBackgroundPlacement({
392
438
  isZoneBuildActive,
393
439
  levelId,
394
440
  roofDraftStart,
441
+ registryToolOwnsSnapping,
442
+ roofFootprintSource,
395
443
  setCursorPoint,
396
444
  setFenceDraftEnd,
397
445
  setFenceDraftStart,
@@ -510,6 +510,7 @@ function SelectedMeasurementAnnotation({ node }: { node: WallNode | ItemNode })
510
510
  function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
511
511
  const nodes = useScene((state) => state.nodes)
512
512
  const unit = useViewer((state) => state.unit)
513
+ const metricNotation = useViewer((state) => state.metricNotation)
513
514
  const isNight = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
514
515
  const color = isNight ? '#ffffff' : '#111111'
515
516
  const shadowColor = isNight ? '#111111' : '#ffffff'
@@ -528,9 +529,9 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
528
529
  }
529
530
  return total
530
531
  }, [guide, wall])
531
- const label = formatLinearMeasurement(length, unit)
532
+ const label = formatLinearMeasurement(length, unit, metricNotation)
532
533
  const height = useMemo(() => getWallEffectiveHeightForNodes(wall, nodes), [nodes, wall])
533
- const heightLabel = `H ${formatLinearMeasurement(height, unit)}`
534
+ const heightLabel = `H ${formatLinearMeasurement(height, unit, metricNotation)}`
534
535
 
535
536
  if (!(guide && Number.isFinite(length) && length >= 0.01)) return null
536
537
 
@@ -4,8 +4,11 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  type FenceNode,
7
+ getFenceCenterlineFrameAt,
8
+ getFenceCenterlineLength,
7
9
  getWallBaseElevationForNodes,
8
10
  getWallCurveFrameAt,
11
+ getWallCurveLength,
9
12
  getWallEffectiveHeightForNodes,
10
13
  getWallThickness,
11
14
  isCurvedWall,
@@ -16,6 +19,7 @@ import {
16
19
  type WallNode,
17
20
  } from '@pascal-app/core'
18
21
  import { useViewer } from '@pascal-app/viewer'
22
+ import { Html } from '@react-three/drei'
19
23
  import { createPortal, type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
20
24
  import { useEffect, useMemo, useRef, useState } from 'react'
21
25
  import {
@@ -29,6 +33,7 @@ import {
29
33
  OrthographicCamera,
30
34
  Plane,
31
35
  Quaternion,
36
+ Ray,
32
37
  Shape,
33
38
  Vector2,
34
39
  Vector3,
@@ -37,6 +42,7 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
37
42
  import { MeshBasicNodeMaterial } from 'three/webgpu'
38
43
  import {
39
44
  clearStructuralElevationGuide,
45
+ getFenceBaseElevationForNodes,
40
46
  publishStructuralElevationGuide,
41
47
  resolveStructuralElevationSnap,
42
48
  } from '../../lib/elevation-guides'
@@ -52,6 +58,7 @@ import useInteractionScope, {
52
58
  import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
53
59
  import { resolveResizeSnapValue } from './handles/resize-snap'
54
60
  import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
61
+ import { MeasurementPill } from './measurement-pill'
55
62
  import {
56
63
  createArrowHitAreaGeometry,
57
64
  createEndpointHitAreaGeometry,
@@ -76,6 +83,8 @@ const CORNER_DASH_SIZE = 0.1
76
83
  const CORNER_GAP_SIZE = 0.07
77
84
  const CORNER_DASH_THICKNESS = 0.006
78
85
  const CORNER_FLOOR_OFFSET = 0.01
86
+ const THICKNESS_HANDLE_RADIUS = 0.075
87
+ const MIN_WALL_THICKNESS = 0.05
79
88
 
80
89
  type WallMoveHandle = {
81
90
  key: string
@@ -140,14 +149,11 @@ export function WallMoveSideHandles() {
140
149
  const isCurveReshape = useIsCurveReshape()
141
150
 
142
151
  const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
143
- // Fence side-move / height / corner-pickers now flow through the
144
- // registry handle path (see packages/nodes/src/fence/definition.ts).
145
- // Only walls still need the legacy renderer here — the registry path
146
- // didn't render correctly for walls specifically and was reverted in
147
- // commit 0e207a7f; revisit once that's diagnosed.
152
+ // Walls still use this legacy handle renderer. Fences retain their registry
153
+ // handles and mount only the matching thickness dots from this component.
148
154
  const selectedNode = useScene((state) => {
149
155
  const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null
150
- return node?.type === 'wall' ? node : null
156
+ return node?.type === 'wall' || node?.type === 'fence' ? node : null
151
157
  })
152
158
 
153
159
  const shouldRender =
@@ -160,7 +166,11 @@ export function WallMoveSideHandles() {
160
166
 
161
167
  if (!shouldRender || !selectedNode) return null
162
168
 
163
- return <WallMoveSideHandlesForWall wall={selectedNode} />
169
+ return selectedNode.type === 'wall' ? (
170
+ <WallMoveSideHandlesForWall wall={selectedNode} />
171
+ ) : (
172
+ <FenceThicknessHandles fence={selectedNode} />
173
+ )
164
174
  }
165
175
 
166
176
  function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
@@ -220,6 +230,18 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
220
230
  <WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
221
231
  ))}
222
232
  <WallHeightArrowHandle wall={effectiveWall} />
233
+ <StructureThicknessHandle
234
+ baseElevation={baseElevation}
235
+ levelObject={levelObject}
236
+ node={effectiveWall}
237
+ side={1}
238
+ />
239
+ <StructureThicknessHandle
240
+ baseElevation={baseElevation}
241
+ levelObject={levelObject}
242
+ node={effectiveWall}
243
+ side={-1}
244
+ />
223
245
  <WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
224
246
  <WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
225
247
  </group>
@@ -233,6 +255,276 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
233
255
  )
234
256
  }
235
257
 
258
+ function closestAxisParameterToRay(axisOrigin: Vector3, axisDirection: Vector3, ray: Ray) {
259
+ const originToRay = new Vector3().subVectors(axisOrigin, ray.origin)
260
+ const directionDot = axisDirection.dot(ray.direction)
261
+ const axisDot = axisDirection.dot(originToRay)
262
+ const rayDot = ray.direction.dot(originToRay)
263
+ const denominator = 1 - directionDot * directionDot
264
+ if (Math.abs(denominator) < 1e-6) return -axisDot
265
+
266
+ const axisParameter = (directionDot * rayDot - axisDot) / denominator
267
+ const rayParameter = rayDot + directionDot * axisParameter
268
+ return rayParameter < 0 ? -axisDot : axisParameter
269
+ }
270
+
271
+ function StructureThicknessHandle({
272
+ node,
273
+ side,
274
+ baseElevation,
275
+ levelObject,
276
+ }: {
277
+ node: WallNode | FenceNode
278
+ side: 1 | -1
279
+ baseElevation: number
280
+ levelObject: Object3D
281
+ }) {
282
+ const [isHovered, setIsHovered] = useState(false)
283
+ const [isDragging, setIsDragging] = useState(false)
284
+ const { camera } = useThree()
285
+ const unit = useViewer((state) => state.unit)
286
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
287
+ const frame =
288
+ node.type === 'fence' ? getFenceCenterlineFrameAt(node, 0.5) : getWallCurveFrameAt(node, 0.5)
289
+ const thickness = node.type === 'fence' ? (node.thickness ?? 0.08) : getWallThickness(node)
290
+ const structureHeight =
291
+ node.type === 'fence'
292
+ ? (node.height ?? 1.8)
293
+ : getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
294
+ const outward = new Vector2(frame.normal.x * side, frame.normal.y * side)
295
+ const faceOffset = thickness / 2 + 0.006
296
+ const position: [number, number, number] = [
297
+ frame.point.x + outward.x * faceOffset,
298
+ structureHeight / 2,
299
+ frame.point.y + outward.y * faceOffset,
300
+ ]
301
+ const rotationY = Math.atan2(outward.x, outward.y)
302
+ const isActive = isHovered || isDragging
303
+ const scale = zoom * (isActive ? 1.18 : 1)
304
+ const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(0.13), [])
305
+ const hitMaterial = useInvisibleHitAreaMaterial()
306
+ const dotMaterial = useMemo(
307
+ () =>
308
+ new MeshBasicNodeMaterial({
309
+ color: new Color(ARROW_COLOR),
310
+ side: DoubleSide,
311
+ depthTest: false,
312
+ depthWrite: false,
313
+ }),
314
+ [],
315
+ )
316
+ const ringMaterial = useMemo(
317
+ () =>
318
+ new MeshBasicNodeMaterial({
319
+ color: new Color(ARROW_HOVER_COLOR),
320
+ side: DoubleSide,
321
+ depthTest: false,
322
+ depthWrite: false,
323
+ }),
324
+ [],
325
+ )
326
+ const dragControls = useMemo<HandleDragControls>(
327
+ () => ({
328
+ onStart: () => {},
329
+ onEnd: () => {},
330
+ }),
331
+ [],
332
+ )
333
+
334
+ useEffect(() => {
335
+ dotMaterial.color.set(isActive ? ARROW_HOVER_COLOR : ARROW_COLOR)
336
+ }, [dotMaterial, isActive])
337
+ useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
338
+ useEffect(() => () => dotMaterial.dispose(), [dotMaterial])
339
+ useEffect(() => () => ringMaterial.dispose(), [ringMaterial])
340
+ useEffect(
341
+ () => () => {
342
+ if (document.body.style.cursor === 'ew-resize') document.body.style.cursor = ''
343
+ },
344
+ [],
345
+ )
346
+
347
+ const activateThicknessResize = useHandleDrag({
348
+ kind: 'drag',
349
+ cursor: 'ew-resize',
350
+ dragControls,
351
+ handleIndex: side > 0 ? 0 : 1,
352
+ node,
353
+ rideObject: levelObject,
354
+ setIsDragging,
355
+ onStart: ({ event, getPointerRay, initialNode, nodeId, rideObject }) => {
356
+ if (initialNode.type !== 'wall' && initialNode.type !== 'fence') return null
357
+
358
+ rideObject.updateWorldMatrix(true, false)
359
+ const initialFrame =
360
+ initialNode.type === 'fence'
361
+ ? getFenceCenterlineFrameAt(initialNode, 0.5)
362
+ : getWallCurveFrameAt(initialNode, 0.5)
363
+ const localAxis = new Vector3(initialFrame.normal.x * side, 0, initialFrame.normal.y * side)
364
+ const initialStructureHeight =
365
+ initialNode.type === 'fence'
366
+ ? (initialNode.height ?? 1.8)
367
+ : getWallEffectiveHeightForNodes(initialNode, useScene.getState().nodes)
368
+ const initialStructureThickness =
369
+ initialNode.type === 'fence'
370
+ ? (initialNode.thickness ?? 0.08)
371
+ : getWallThickness(initialNode)
372
+ const minimumThickness = initialNode.type === 'fence' ? 0.03 : MIN_WALL_THICKNESS
373
+ const localOrigin = new Vector3(
374
+ initialFrame.point.x + localAxis.x * (initialStructureThickness / 2 + 0.006),
375
+ baseElevation + initialStructureHeight / 2,
376
+ initialFrame.point.y + localAxis.z * (initialStructureThickness / 2 + 0.006),
377
+ )
378
+ const worldOrigin = localOrigin.clone().applyMatrix4(rideObject.matrixWorld)
379
+ const worldAxisEnd = localOrigin.clone().add(localAxis).applyMatrix4(rideObject.matrixWorld)
380
+ const worldAxis = worldAxisEnd.sub(worldOrigin)
381
+ const axisScale = worldAxis.length()
382
+ if (axisScale < 1e-6) return null
383
+ worldAxis.normalize()
384
+
385
+ const pointerRay = new Ray()
386
+ const initialPointer =
387
+ closestAxisParameterToRay(
388
+ worldOrigin,
389
+ worldAxis,
390
+ getPointerRay(event.nativeEvent.clientX, event.nativeEvent.clientY, pointerRay),
391
+ ) / axisScale
392
+ let lastThickness = initialStructureThickness
393
+
394
+ return {
395
+ onBegin: () => {
396
+ useInteractionScope.getState().begin({
397
+ kind: 'handle-drag',
398
+ nodeId,
399
+ handle: 'thickness',
400
+ })
401
+ },
402
+ onEnd: () => {
403
+ useInteractionScope.getState().endIf((scope) => scope.kind === 'handle-drag')
404
+ },
405
+ move: ({ event: moveEvent, getPointerRay: getMovePointerRay }) => {
406
+ const currentPointer =
407
+ closestAxisParameterToRay(
408
+ worldOrigin,
409
+ worldAxis,
410
+ getMovePointerRay(moveEvent.clientX, moveEvent.clientY, pointerRay),
411
+ ) / axisScale
412
+ const rawThickness = initialStructureThickness + (currentPointer - initialPointer) * 2
413
+ const nextThickness = Math.max(
414
+ minimumThickness,
415
+ resolveResizeSnapValue({
416
+ rawValue: rawThickness,
417
+ fallbackValue: lastThickness,
418
+ gridSnapEnabled: true,
419
+ gridSnapActive: isGridSnapActive(),
420
+ gridSnapStep: useEditor.getState().gridSnapStep,
421
+ magneticSnapActive: false,
422
+ }),
423
+ )
424
+ if (nextThickness !== lastThickness) sfxEmitter.emit('sfx:resize')
425
+ lastThickness = nextThickness
426
+ return { thickness: nextThickness }
427
+ },
428
+ }
429
+ },
430
+ })
431
+
432
+ return (
433
+ <>
434
+ <group position={position} rotation={[0, rotationY, 0]} scale={scale}>
435
+ <InvisibleHandleHitArea
436
+ geometry={hitGeometry}
437
+ material={hitMaterial}
438
+ onPointerDown={activateThicknessResize}
439
+ onPointerEnter={(event) => {
440
+ event.stopPropagation()
441
+ setIsHovered(true)
442
+ document.body.style.cursor = 'ew-resize'
443
+ }}
444
+ onPointerLeave={(event) => {
445
+ event.stopPropagation()
446
+ setIsHovered(false)
447
+ if (!isDragging && document.body.style.cursor === 'ew-resize') {
448
+ document.body.style.cursor = ''
449
+ }
450
+ }}
451
+ scale={1}
452
+ />
453
+ <mesh material={dotMaterial} raycast={NO_RAYCAST} renderOrder={1003}>
454
+ <circleGeometry args={[THICKNESS_HANDLE_RADIUS, 32]} />
455
+ </mesh>
456
+ <mesh material={ringMaterial} raycast={NO_RAYCAST} renderOrder={1002}>
457
+ <ringGeometry args={[THICKNESS_HANDLE_RADIUS, THICKNESS_HANDLE_RADIUS * 1.18, 32]} />
458
+ </mesh>
459
+ </group>
460
+ {isDragging ? (
461
+ <Html
462
+ center
463
+ position={[position[0], position[1] + 0.22, position[2]]}
464
+ style={{ pointerEvents: 'none', userSelect: 'none' }}
465
+ zIndexRange={[25, 0]}
466
+ >
467
+ <MeasurementPill
468
+ height={structureHeight}
469
+ length={
470
+ node.type === 'fence' ? getFenceCenterlineLength(node) : getWallCurveLength(node)
471
+ }
472
+ primary="thickness"
473
+ thickness={thickness}
474
+ unit={unit}
475
+ />
476
+ </Html>
477
+ ) : null}
478
+ </>
479
+ )
480
+ }
481
+
482
+ function FenceThicknessHandles({ fence }: { fence: FenceNode }) {
483
+ const nodes = useScene((state) => state.nodes)
484
+ const liveOverride = useLiveNodeOverrides((state) => state.overrides.get(fence.id))
485
+ const effectiveFence = useMemo(
486
+ () => (liveOverride ? ({ ...fence, ...liveOverride } as FenceNode) : fence),
487
+ [fence, liveOverride],
488
+ )
489
+ const [levelObject, setLevelObject] = useState<Object3D | null>(() =>
490
+ fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null,
491
+ )
492
+
493
+ useEffect(() => {
494
+ let frameId = 0
495
+ const resolveLevelObject = () => {
496
+ const next = fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null
497
+ setLevelObject(next)
498
+ if (!next) frameId = window.requestAnimationFrame(resolveLevelObject)
499
+ }
500
+ resolveLevelObject()
501
+ return () => {
502
+ if (frameId) window.cancelAnimationFrame(frameId)
503
+ }
504
+ }, [fence.parentId])
505
+
506
+ if (!levelObject) return null
507
+
508
+ const baseElevation = getFenceBaseElevationForNodes(effectiveFence, nodes)
509
+ return createPortal(
510
+ <group position={[0, baseElevation, 0]}>
511
+ <StructureThicknessHandle
512
+ baseElevation={baseElevation}
513
+ levelObject={levelObject}
514
+ node={effectiveFence}
515
+ side={1}
516
+ />
517
+ <StructureThicknessHandle
518
+ baseElevation={baseElevation}
519
+ levelObject={levelObject}
520
+ node={effectiveFence}
521
+ side={-1}
522
+ />
523
+ </group>,
524
+ levelObject,
525
+ )
526
+ }
527
+
236
528
  function buildDashedVerticalGeometry(height: number) {
237
529
  if (!(Number.isFinite(height) && height > 0)) return new BufferGeometry()
238
530
 
@@ -1,11 +1,17 @@
1
1
  'use client'
2
2
 
3
+ import { useScene } from '@pascal-app/core'
3
4
  import { useViewer } from '@pascal-app/viewer'
4
- import { useEffect, useState } from 'react'
5
+ import { useEffect, useMemo, useState } from 'react'
5
6
  import { createPortal } from 'react-dom'
6
7
  import { isActive } from '../../lib/interaction/scope'
8
+ import { canCreateSessionGroup, selectionIntersectsSessionGroup } from '../../lib/session-groups'
7
9
  import useEditor from '../../store/use-editor'
8
10
  import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
11
+ import useSessionGroups, {
12
+ groupCurrentSelection,
13
+ ungroupCurrentSelection,
14
+ } from '../../store/use-session-groups'
9
15
  import {
10
16
  deleteSelection,
11
17
  duplicateSelectionAndPickUp,
@@ -14,25 +20,27 @@ import {
14
20
  import { NodeActionMenu } from '../editor/node-action-menu'
15
21
 
16
22
  /**
17
- * Floating Move / Duplicate / Delete pill for a MULTI-selection in the 2D
18
- * floor plan — the group sibling of `FloorplanRegistryActionMenu` (which is
19
- * sole-selection only). Anchored above the dashed group selection box; every
20
- * action targets the whole selection: Move picks the group up (it rides the
21
- * cursor until a click places it), Duplicate clones the selection and picks
22
- * the clones up, Delete removes everything selected.
23
- *
24
- * Gated on floorplan hover so it never coexists with the 3D group menu in
25
- * split view (that one hides while the floor plan is hovered), and hidden
26
- * during any active interaction so it never competes with a live drag.
23
+ * Floating multi-select pill on the floor plan: Move, Group, Ungroup, Duplicate, Delete.
27
24
  */
28
25
  export function FloorplanGroupActionMenu() {
29
- const isMultiSelect = useViewer((s) => s.selection.selectedIds.length > 1)
26
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
27
+ const isMultiSelect = selectedIds.length > 1
30
28
  const movingNode = useMovingNode()
31
29
  const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
32
30
  const scopeActive = useInteractionScope((s) => isActive(s.scope))
31
+ const sessionGroups = useSessionGroups((s) => s.groups)
32
+ const sceneNodes = useScene((s) => s.nodes)
33
+ const liveIds = useMemo(() => new Set(Object.keys(sceneNodes)), [sceneNodes])
34
+ const showGroup = useMemo(
35
+ () => canCreateSessionGroup(sessionGroups, selectedIds, liveIds),
36
+ [sessionGroups, selectedIds, liveIds],
37
+ )
38
+ const showUngroup = useMemo(
39
+ () => selectionIntersectsSessionGroup(sessionGroups, selectedIds, liveIds),
40
+ [sessionGroups, selectedIds, liveIds],
41
+ )
33
42
 
34
43
  const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
35
-
36
44
  const isVisible = isMultiSelect && !movingNode && isFloorplanHovered && !scopeActive
37
45
 
38
46
  useEffect(() => {
@@ -43,9 +51,6 @@ export function FloorplanGroupActionMenu() {
43
51
  let raf = 0
44
52
  const tick = () => {
45
53
  raf = requestAnimationFrame(tick)
46
- // The dashed group box exists exactly while the multi-selection has
47
- // transformable participants — anchor to its top edge. Only publish
48
- // actual changes so the idle poll doesn't re-render every frame.
49
54
  const box = document.querySelector('[data-group-selection-box]') as SVGGElement | null
50
55
  if (!box) {
51
56
  setPosition((prev) => (prev === null ? prev : null))
@@ -77,9 +82,11 @@ export function FloorplanGroupActionMenu() {
77
82
  <NodeActionMenu
78
83
  onDelete={() => deleteSelection()}
79
84
  onDuplicate={() => duplicateSelectionAndPickUp()}
85
+ onGroup={showGroup ? () => groupCurrentSelection() : undefined}
80
86
  onMove={() => startGroupPickUp()}
81
87
  onPointerDown={(event) => event.stopPropagation()}
82
88
  onPointerUp={(event) => event.stopPropagation()}
89
+ onUngroup={showUngroup ? () => ungroupCurrentSelection() : undefined}
83
90
  />
84
91
  </div>,
85
92
  document.body,