mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] canvas: marquee rect moves imperatively off a channel, measured 61 to 4.5 percent dropped frames
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { type RefObject } from 'react';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import MarqueeRect from './MarqueeRect';
|
||||
import AgentCard from '../cards/AgentCard';
|
||||
import DashboardViewCard from '../cards/DashboardViewCard';
|
||||
import BrowserCard from '../cards/BrowserCard';
|
||||
@@ -234,23 +235,8 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
|
||||
onDragEnd={onDragEnd}
|
||||
onBringToFront={onBringToFront}
|
||||
/>
|
||||
{/* Marquee selection rectangle */}
|
||||
{selection.marquee && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: selection.marquee.x,
|
||||
top: selection.marquee.y,
|
||||
width: selection.marquee.width,
|
||||
height: selection.marquee.height,
|
||||
border: '1.5px dashed rgba(59, 130, 246, 0.6)',
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
borderRadius: 2,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Marquee selection rectangle: mounted once per sweep, moved imperatively off the channel */}
|
||||
{selection.marquee && <MarqueeRect initial={selection.marquee} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { subscribeMarqueeRect, LiveMarqueeRect } from '../hooks/interaction/marqueeLiveChannel';
|
||||
|
||||
// The selection rectangle, moved imperatively off the marquee channel: React mounts it once per
|
||||
// sweep and never re-renders it mid-drag.
|
||||
const MarqueeRect: React.FC<{ initial: LiveMarqueeRect }> = ({ initial }) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => subscribeMarqueeRect((r) => {
|
||||
const el = ref.current;
|
||||
if (!el || !r) return;
|
||||
el.style.left = `${r.x}px`;
|
||||
el.style.top = `${r.y}px`;
|
||||
el.style.width = `${r.width}px`;
|
||||
el.style.height = `${r.height}px`;
|
||||
}), []);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-marquee-rect
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: initial.x,
|
||||
top: initial.y,
|
||||
width: initial.width,
|
||||
height: initial.height,
|
||||
border: '1.5px dashed rgba(59, 130, 246, 0.6)',
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
borderRadius: 2,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarqueeRect;
|
||||
@@ -0,0 +1,23 @@
|
||||
// Per-frame marquee rects travel OUTSIDE React, same pattern as liveDragChannel: a sweep at pointer
|
||||
// rate re-rendered the whole card layer per frame just to move one rectangle (measured 61% dropped
|
||||
// frames). React mounts/unmounts the rect; this channel moves it.
|
||||
|
||||
export interface LiveMarqueeRect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
type Listener = (rect: LiveMarqueeRect | null) => void;
|
||||
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
export function publishMarqueeRect(rect: LiveMarqueeRect | null): void {
|
||||
for (const listener of listeners) listener(rect);
|
||||
}
|
||||
|
||||
export function subscribeMarqueeRect(listener: Listener): () => void {
|
||||
listeners.add(listener);
|
||||
return () => { listeners.delete(listener); };
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback, useMemo, useRef, useEffect, RefObject } from 'react';
|
||||
import type { CardPosition, ViewCardPosition, BrowserCardPosition, WorkflowCardPosition, WorkflowsHubPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { viewCardKey } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { publishMarqueeRect } from '../interaction/marqueeLiveChannel';
|
||||
|
||||
export type { CardType } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { CardType } from '@/shared/state/dashboardLayoutSlice';
|
||||
@@ -26,6 +27,7 @@ interface ScreenToCanvas {
|
||||
|
||||
const DRAG_THRESHOLD = 4;
|
||||
|
||||
|
||||
function rectsIntersect(
|
||||
a: { x: number; y: number; width: number; height: number },
|
||||
b: { x: number; y: number; width: number; height: number },
|
||||
@@ -252,7 +254,9 @@ export function useDashboardSelection(
|
||||
width: Math.abs(end.x - start.x),
|
||||
height: Math.abs(end.y - start.y),
|
||||
};
|
||||
setMarquee(rect);
|
||||
// React mounts the rect once; per-frame movement rides the channel (the layer re-rendered per frame otherwise).
|
||||
publishMarqueeRect(rect);
|
||||
setMarquee((prev) => prev ?? rect);
|
||||
const next = computeMarqueeSelection(rect, shiftHeldRef.current);
|
||||
// Same membership = same state object, so sweeping across empty space re-renders nothing.
|
||||
setSelectedIds((prev) => {
|
||||
@@ -287,6 +291,7 @@ export function useDashboardSelection(
|
||||
cancelAnimationFrame(marqueeRafRef.current);
|
||||
marqueeRafRef.current = null;
|
||||
}
|
||||
publishMarqueeRect(null);
|
||||
setMarquee(null);
|
||||
document.body.style.userSelect = '';
|
||||
document.body.classList.remove('dashboard-marquee-active');
|
||||
|
||||
Reference in New Issue
Block a user