Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9153a67
feat(editor): group R/T — keyboard rotate for multi-selections
wass08 Jul 8, 2026
0f059c6
feat(editor): polygon participant kind — slabs/ceilings/zones join gr…
wass08 Jul 8, 2026
acd4c94
feat(editor): 2D floorplan group drag — Photoshop-style multi-selecti…
wass08 Jul 8, 2026
8e2b11f
feat(editor): group manipulation polish — helper text + shortcuts dialog
wass08 Jul 8, 2026
5d57758
fix(editor): restore zone renderer useLiveNodeOverrides import
wass08 Jul 8, 2026
4a71b82
feat(editor): tag the 2D group selection box for drivability
wass08 Jul 8, 2026
7819670
feat(editor): multi-selection shows highlight only — hide per-node ed…
wass08 Jul 9, 2026
7b10c52
feat(editor): 3D body-drag group move replaces the move gizmo cross
wass08 Jul 9, 2026
cdc2a00
feat(editor): group action menu — move / duplicate / delete the whole…
wass08 Jul 9, 2026
a4478b7
fix(editor): group transforms no longer re-create auto rooms
wass08 Jul 9, 2026
75cf086
fix(editor): polygon hosts carry their attached items in group transf…
wass08 Jul 9, 2026
749d530
feat(editor): click picks up the group; the 2D dashed box is the drag…
wass08 Jul 9, 2026
df8fba7
feat(editor): 3D dashed group selection box doubles as the drag handle
wass08 Jul 9, 2026
d63f3ed
feat(editor): mid-move R/T, 2D corner rotate, realtime opening symbol…
wass08 Jul 9, 2026
b73fc84
feat(editor): rotate cursor + spinning box on 2D corner rotate; Delet…
wass08 Jul 9, 2026
1f690df
fix(editor): per-node direct manipulation stands down for multi-selec…
wass08 Jul 9, 2026
74782ff
fix(editor): group gestures own their ending pointerup — no synthesiz…
wass08 Jul 9, 2026
96f5f56
fix(editor): screen-rect marquee tests projected oriented bounds, not…
wass08 Jul 9, 2026
c51699e
fix(editor): keep box select alive after group gestures
wass08 Jul 9, 2026
1a4051a
fix(editor): marquee membership by plan footprint; 2D item highlight;…
wass08 Jul 9, 2026
1fbfcc5
fix(editor): 2D marquee plan-footprint membership; item marquee previ…
wass08 Jul 9, 2026
d1c4303
fix(editor): dim the actual site boundary line during multi-selections
wass08 Jul 9, 2026
d55e464
Merge remote-tracking branch 'origin/main' into feat/group-manipulation
wass08 Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client'

import { useViewer } from '@pascal-app/viewer'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { isActive } from '../../lib/interaction/scope'
import useEditor from '../../store/use-editor'
import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
import {
deleteSelection,
duplicateSelectionAndPickUp,
startGroupPickUp,
} from '../editor/group-actions'
import { NodeActionMenu } from '../editor/node-action-menu'

/**
* Floating Move / Duplicate / Delete pill for a MULTI-selection in the 2D
* floor plan — the group sibling of `FloorplanRegistryActionMenu` (which is
* sole-selection only). Anchored above the dashed group selection box; every
* action targets the whole selection: Move picks the group up (it rides the
* cursor until a click places it), Duplicate clones the selection and picks
* the clones up, Delete removes everything selected.
*
* Gated on floorplan hover so it never coexists with the 3D group menu in
* split view (that one hides while the floor plan is hovered), and hidden
* during any active interaction so it never competes with a live drag.
*/
export function FloorplanGroupActionMenu() {
const isMultiSelect = useViewer((s) => s.selection.selectedIds.length > 1)
const movingNode = useMovingNode()
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
const scopeActive = useInteractionScope((s) => isActive(s.scope))

const [position, setPosition] = useState<{ left: number; top: number } | null>(null)

const isVisible = isMultiSelect && !movingNode && isFloorplanHovered && !scopeActive

useEffect(() => {
if (!isVisible) {
setPosition(null)
return
}
let raf = 0
const tick = () => {
raf = requestAnimationFrame(tick)
// The dashed group box exists exactly while the multi-selection has
// transformable participants — anchor to its top edge. Only publish
// actual changes so the idle poll doesn't re-render every frame.
const box = document.querySelector('[data-group-selection-box]') as SVGGElement | null
if (!box) {
setPosition((prev) => (prev === null ? prev : null))
return
}
const rect = box.getBoundingClientRect()
const next = { left: rect.left + rect.width / 2, top: rect.top }
setPosition((prev) =>
prev && Math.abs(prev.left - next.left) < 0.5 && Math.abs(prev.top - next.top) < 0.5
? prev
: next,
)
}
raf = requestAnimationFrame(tick)
return () => cancelAnimationFrame(raf)
}, [isVisible])

if (!(isVisible && position)) return null

return createPortal(
<div
className="pointer-events-none fixed z-30"
style={{
left: position.left,
top: position.top,
transform: 'translate(-50%, calc(-100% - 12px))',
}}
>
<NodeActionMenu
onDelete={() => deleteSelection()}
onDuplicate={() => duplicateSelectionAndPickUp()}
onMove={() => startGroupPickUp()}
onPointerDown={(event) => event.stopPropagation()}
onPointerUp={(event) => event.stopPropagation()}
/>
</div>,
document.body,
)
}
Loading
Loading