From 3d1de5bf69a46cff3fdc2783c0e7bf8568d1d0c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 12:49:02 +0000 Subject: [PATCH] Fix fit-to-screen drag: support non-adjacent and horizontal swaps Previous approach sorted react-grid-layout's post-drag positions to infer order, which broke for non-adjacent and horizontal moves because rgl pushes items down instead of swapping them. New approach: - onDrag records which item is being dragged (draggedItemRef) - onDragStop uses the dragged item's final x/y to compute the target slot in our own grid, then performs a clean swap in the ordered name array - Layout is always fully regenerated from our order array, ignoring rgl's position arithmetic entirely https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy --- web/src/views/live/DraggableGridLayout.tsx | 54 ++++++++++++++++++---- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index fb7b4063b..60cde0aca 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -467,24 +467,61 @@ export default function DraggableGridLayout({ }, [fitToScreen, fitGridParams, cameras, includeBirdseye, birdseyeConfig]); const [fitLayoutOverride, setFitLayoutOverride] = useState(); + const draggedItemRef = useRef(null); useEffect(() => { setFitLayoutOverride(undefined); }, [fitGridParams, cameras, includeBirdseye]); + const handleFitDrag = useCallback( + (_layout: Layout, _oldItem: LayoutItem, layoutItem: LayoutItem) => { + draggedItemRef.current = layoutItem.i; + }, + [], + ); + const handleFitDragStop = useCallback( - (newLayout: Layout) => { + (newLayout: Layout, _oldItem: LayoutItem, layoutItem: LayoutItem) => { if (!fitToScreen || !fitGridParams) return; + const w = fitGridParams.gridUnitsPerCam; const colsPerRow = fitGridParams.colsPerRow; + const draggedId = draggedItemRef.current ?? layoutItem.i; + draggedItemRef.current = null; - const sorted = [...newLayout].sort((a, b) => { - if (a.y !== b.y) return a.y - b.y; - return a.x - b.x; - }); + const currentOrder = fitLayoutOverride ?? fitLayout ?? []; + const orderedNames = [...currentOrder] + .sort((a, b) => { + if (a.y !== b.y) return a.y - b.y; + return a.x - b.x; + }) + .map((item) => item.i); - const normalized = sorted.map((item, index) => ({ - i: item.i, + const dropCenterX = layoutItem.x + w / 2; + const dropCenterY = layoutItem.y + w / 2; + const targetCol = Math.min(Math.floor(dropCenterX / w), colsPerRow - 1); + const targetRow = Math.floor(dropCenterY / w); + const totalRows = Math.ceil(orderedNames.length / colsPerRow); + const clampedRow = Math.min(targetRow, totalRows - 1); + const targetIndex = Math.min( + clampedRow * colsPerRow + targetCol, + orderedNames.length - 1, + ); + + const sourceIndex = orderedNames.indexOf(draggedId); + if (sourceIndex === -1 || sourceIndex === targetIndex) { + setFitLayoutOverride((prev) => prev); + return; + } + + const newOrder = [...orderedNames]; + [newOrder[sourceIndex], newOrder[targetIndex]] = [ + newOrder[targetIndex], + newOrder[sourceIndex], + ]; + + const normalized = newOrder.map((name, index) => ({ + i: name, x: (index % colsPerRow) * w, y: Math.floor(index / colsPerRow) * w, w, @@ -493,7 +530,7 @@ export default function DraggableGridLayout({ setFitLayoutOverride(normalized); }, - [fitToScreen, fitGridParams], + [fitToScreen, fitGridParams, fitLayoutOverride, fitLayout], ); const activeGridLayout = useMemo(() => { @@ -874,6 +911,7 @@ export default function DraggableGridLayout({ dragConfig={{ enabled: isEditMode, }} + onDrag={fitToScreen ? handleFitDrag : undefined} onDragStop={fitToScreen ? handleFitDragStop : handleLayoutChange} onResize={handleResize} onResizeStart={() => setShowCircles(false)}