feat: revert canvas changes to latest

This commit is contained in:
RohitR311
2025-01-08 22:23:45 +05:30
parent 8666c5e932
commit e907615f9b

View File

@@ -1,147 +1,21 @@
import React, { useCallback, useEffect, useRef, useMemo, Suspense } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { getMappedCoordinates } from "../../helpers/inputHelpers";
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { useActionContext } from '../../context/browserActions'; import { useActionContext } from '../../context/browserActions';
const DatePicker = React.lazy(() => import('./DatePicker')); import DatePicker from './DatePicker';
const Dropdown = React.lazy(() => import('./Dropdown')); import Dropdown from './Dropdown';
const TimePicker = React.lazy(() => import('./TimePicker')); import TimePicker from './TimePicker';
const DateTimeLocalPicker = React.lazy(() => import('./DateTimeLocalPicker')); import DateTimeLocalPicker from './DateTimeLocalPicker';
class RAFScheduler { interface CreateRefCallback {
private queue: Set<() => void> = new Set(); (ref: React.RefObject<HTMLCanvasElement>): void;
private isProcessing: boolean = false;
private frameId: number | null = null;
schedule(callback: () => void): void {
this.queue.add(callback);
if (!this.isProcessing) {
this.process();
}
}
private process = (): void => {
this.isProcessing = true;
this.frameId = requestAnimationFrame(() => {
const callbacks = Array.from(this.queue);
this.queue.clear();
callbacks.forEach(callback => {
try {
callback();
} catch (error) {
console.error('RAF Scheduler error:', error);
}
});
this.isProcessing = false;
this.frameId = null;
if (this.queue.size > 0) {
this.process();
}
});
}
clear(): void {
this.queue.clear();
if (this.frameId !== null) {
cancelAnimationFrame(this.frameId);
this.frameId = null;
}
this.isProcessing = false;
}
}
class EventDebouncer {
private highPriorityQueue: Array<() => void> = [];
private lowPriorityQueue: Array<() => void> = [];
private processing: boolean = false;
private scheduler: RAFScheduler;
constructor(scheduler: RAFScheduler) {
this.scheduler = scheduler;
}
add(callback: () => void, highPriority: boolean = false): void {
if (highPriority) {
this.highPriorityQueue.push(callback);
} else {
this.lowPriorityQueue.push(callback);
}
if (!this.processing) {
this.process();
}
}
private process(): void {
this.processing = true;
this.scheduler.schedule(() => {
while (this.highPriorityQueue.length > 0) {
const callback = this.highPriorityQueue.shift();
callback?.();
}
if (this.lowPriorityQueue.length > 0) {
const callback = this.lowPriorityQueue.shift();
callback?.();
if (this.lowPriorityQueue.length > 0) {
this.process();
}
}
this.processing = false;
});
}
clear(): void {
this.highPriorityQueue = [];
this.lowPriorityQueue = [];
this.processing = false;
}
}
// Optimized measurement cache with LRU
class MeasurementCache {
private cache: Map<HTMLElement, DOMRect>;
private maxSize: number;
constructor(maxSize: number = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(element: HTMLElement): DOMRect | undefined {
const cached = this.cache.get(element);
if (cached) {
// Refresh the entry
this.cache.delete(element);
this.cache.set(element, cached);
}
return cached;
}
set(element: HTMLElement, rect: DOMRect): void {
if (this.cache.size >= this.maxSize) {
// Remove oldest entry
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(element, rect);
}
clear(): void {
this.cache.clear();
}
} }
interface CanvasProps { interface CanvasProps {
width: number; width: number;
height: number; height: number;
onCreateRef: (ref: React.RefObject<HTMLCanvasElement>) => void; onCreateRef: CreateRefCallback;
} }
/** /**
@@ -152,229 +26,225 @@ export interface Coordinates {
y: number; y: number;
}; };
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => { const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { setLastAction, lastAction } = useGlobalInfoStore(); const { setLastAction, lastAction } = useGlobalInfoStore();
const { getText, getList } = useActionContext(); const { getText, getList } = useActionContext();
const getTextRef = useRef(getText);
const getListRef = useRef(getList);
const scheduler = useRef(new RAFScheduler()); const [datePickerInfo, setDatePickerInfo] = React.useState<{
const debouncer = useRef(new EventDebouncer(scheduler.current)); coordinates: Coordinates;
const measurementCache = useRef(new MeasurementCache(50)); selector: string;
//const performanceMonitor = useRef(new FrontendPerformanceMonitor()); } | null>(null);
const refs = useRef({ const [dropdownInfo, setDropdownInfo] = React.useState<{
getText, coordinates: Coordinates;
getList, selector: string;
lastMousePosition: { x: 0, y: 0 }, options: Array<{
lastFrameTime: 0, value: string;
context: null as CanvasRenderingContext2D | null, text: string;
}); disabled: boolean;
selected: boolean;
}>;
} | null>(null);
const [state, dispatch] = React.useReducer((state: any, action: any) => { const [timePickerInfo, setTimePickerInfo] = React.useState<{
switch (action.type) { coordinates: Coordinates;
case 'BATCH_UPDATE': selector: string;
return { ...state, ...action.payload }; } | null>(null);
default:
return state; const [dateTimeLocalInfo, setDateTimeLocalInfo] = React.useState<{
coordinates: Coordinates;
selector: string;
} | null>(null);
const notifyLastAction = (action: string) => {
if (lastAction !== action) {
setLastAction(action);
} }
}, { };
datePickerInfo: null,
dropdownInfo: null,
timePickerInfo: null,
dateTimeLocalInfo: null
});
const getEventCoordinates = useCallback((event: MouseEvent): { x: number; y: number } => { const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 });
if (!canvasRef.current) return { x: 0, y: 0 };
let rect = measurementCache.current.get(canvasRef.current); useEffect(() => {
if (!rect) { getTextRef.current = getText;
rect = canvasRef.current.getBoundingClientRect(); getListRef.current = getList;
measurementCache.current.set(canvasRef.current, rect); }, [getText, getList]);
useEffect(() => {
if (socket) {
socket.on('showDatePicker', (info: {coordinates: Coordinates, selector: string}) => {
setDatePickerInfo(info);
});
socket.on('showDropdown', (info: {
coordinates: Coordinates,
selector: string,
options: Array<{
value: string;
text: string;
disabled: boolean;
selected: boolean;
}>;
}) => {
setDropdownInfo(info);
});
socket.on('showTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
setTimePickerInfo(info);
});
socket.on('showDateTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
setDateTimeLocalInfo(info);
});
return () => {
socket.off('showDatePicker');
socket.off('showDropdown');
socket.off('showTimePicker');
socket.off('showDateTimePicker');
};
} }
}, [socket]);
return { const onMouseEvent = useCallback((event: MouseEvent) => {
x: event.clientX - rect.left, if (socket && canvasRef.current) {
y: event.clientY - rect.top // Get the canvas bounding rectangle
}; const rect = canvasRef.current.getBoundingClientRect();
}, []); const clickCoordinates = {
x: event.clientX - rect.left, // Use relative x coordinate
y: event.clientY - rect.top, // Use relative y coordinate
};
const handleMouseEvent = useCallback((event: MouseEvent) => { switch (event.type) {
if (!socket || !canvasRef.current) return; case 'mousedown':
if (getTextRef.current === true) {
//performanceMonitor.current.measureEventLatency(event);
const coordinates = getEventCoordinates(event);
switch (event.type) {
case 'mousedown':
debouncer.current.add(() => {
if (refs.current.getText) {
console.log('Capturing Text...'); console.log('Capturing Text...');
} else if (refs.current.getList) { } else if (getListRef.current === true) {
console.log('Capturing List...'); console.log('Capturing List...');
} else { } else {
socket.emit('input:mousedown', coordinates); socket.emit('input:mousedown', clickCoordinates);
} }
setLastAction('click'); notifyLastAction('click');
}, true); // High priority break;
break; case 'mousemove':
if (lastMousePosition.current.x !== clickCoordinates.x ||
case 'mousemove': lastMousePosition.current.y !== clickCoordinates.y) {
if (refs.current.lastMousePosition.x !== coordinates.x || lastMousePosition.current = {
refs.current.lastMousePosition.y !== coordinates.y) { x: clickCoordinates.x,
debouncer.current.add(() => { y: clickCoordinates.y,
refs.current.lastMousePosition = coordinates; };
socket.emit('input:mousemove', coordinates); socket.emit('input:mousemove', {
setLastAction('move'); x: clickCoordinates.x,
}); y: clickCoordinates.y,
} });
break; notifyLastAction('move');
}
case 'wheel': break;
const wheelEvent = event as WheelEvent; case 'wheel':
debouncer.current.add(() => { const wheelEvent = event as WheelEvent;
socket.emit('input:wheel', { const deltas = {
deltaX: Math.round(wheelEvent.deltaX), deltaX: Math.round(wheelEvent.deltaX),
deltaY: Math.round(wheelEvent.deltaY) deltaY: Math.round(wheelEvent.deltaY),
}); };
setLastAction('scroll'); socket.emit('input:wheel', deltas);
}); notifyLastAction('scroll');
break; break;
default:
console.log('Default mouseEvent registered');
return;
}
} }
}, [socket, getEventCoordinates]); }, [socket]);
const handleKeyboardEvent = useCallback((event: KeyboardEvent) => { const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
if (!socket) return; if (socket) {
debouncer.current.add(() => {
switch (event.type) { switch (event.type) {
case 'keydown': case 'keydown':
socket.emit('input:keydown', { socket.emit('input:keydown', { key: event.key, coordinates: lastMousePosition.current });
key: event.key, notifyLastAction(`${event.key} pressed`);
coordinates: refs.current.lastMousePosition
});
setLastAction(`${event.key} pressed`);
break; break;
case 'keyup': case 'keyup':
socket.emit('input:keyup', event.key); socket.emit('input:keyup', event.key);
break; break;
default:
console.log('Default keyEvent registered');
return;
} }
}, event.type === 'keydown'); // High priority for keydown }
}, [socket]); }, [socket]);
// Setup and cleanup
useEffect(() => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
refs.current.context = canvas.getContext('2d', {
alpha: false,
desynchronized: true
});
onCreateRef(canvasRef);
const options = { passive: true };
canvas.addEventListener('mousedown', handleMouseEvent, options);
canvas.addEventListener('mousemove', handleMouseEvent, options);
canvas.addEventListener('wheel', handleMouseEvent, options);
canvas.addEventListener('keydown', handleKeyboardEvent, options);
canvas.addEventListener('keyup', handleKeyboardEvent, options);
return () => {
canvas.removeEventListener('mousedown', handleMouseEvent);
canvas.removeEventListener('mousemove', handleMouseEvent);
canvas.removeEventListener('wheel', handleMouseEvent);
canvas.removeEventListener('keydown', handleKeyboardEvent);
canvas.removeEventListener('keyup', handleKeyboardEvent);
scheduler.current.clear();
debouncer.current.clear();
measurementCache.current.clear();
};
}, [handleMouseEvent, handleKeyboardEvent, onCreateRef]);
// Performance monitoring
// useEffect(() => {
// const intervalId = setInterval(() => {
// console.log('Performance Report:', performanceMonitor.current.getPerformanceReport());
// }, 20000);
// return () => clearInterval(intervalId);
// }, []);
useEffect(() => { useEffect(() => {
if (!socket) return; if (canvasRef.current) {
onCreateRef(canvasRef);
canvasRef.current.addEventListener('mousedown', onMouseEvent);
canvasRef.current.addEventListener('mousemove', onMouseEvent);
canvasRef.current.addEventListener('wheel', onMouseEvent, { passive: true });
canvasRef.current.addEventListener('keydown', onKeyboardEvent);
canvasRef.current.addEventListener('keyup', onKeyboardEvent);
const handlers = { return () => {
showDatePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { datePickerInfo: info } }), if (canvasRef.current) {
showDropdown: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dropdownInfo: info } }), canvasRef.current.removeEventListener('mousedown', onMouseEvent);
showTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { timePickerInfo: info } }), canvasRef.current.removeEventListener('mousemove', onMouseEvent);
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } }) canvasRef.current.removeEventListener('wheel', onMouseEvent);
}; canvasRef.current.removeEventListener('keydown', onKeyboardEvent);
canvasRef.current.removeEventListener('keyup', onKeyboardEvent);
}
Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler)); };
return () => { } else {
Object.keys(handlers).forEach(event => socket.off(event)); console.log('Canvas not initialized');
}; }
}, [socket]);
const memoizedDimensions = useMemo(() => ({ }, [onMouseEvent]);
width: width || 900,
height: height || 400
}), [width, height]);
return ( return (
<div className="relative bg-white rounded-b-md overflow-hidden"> <div style={{ borderRadius: '0px 0px 5px 5px', overflow: 'hidden', backgroundColor: 'white' }}>
<canvas <canvas
tabIndex={0} tabIndex={0}
ref={canvasRef} ref={canvasRef}
height={memoizedDimensions.height} height={400}
width={memoizedDimensions.width} width={900}
className="block" style={{ display: 'block' }}
/> />
<Suspense fallback={null}> {datePickerInfo && (
{state.datePickerInfo && ( <DatePicker
<DatePicker coordinates={datePickerInfo.coordinates}
coordinates={state.datePickerInfo.coordinates} selector={datePickerInfo.selector}
selector={state.datePickerInfo.selector} onClose={() => setDatePickerInfo(null)}
onClose={() => dispatch({ />
type: 'BATCH_UPDATE', )}
payload: { datePickerInfo: null } {dropdownInfo && (
})} <Dropdown
/> coordinates={dropdownInfo.coordinates}
)} selector={dropdownInfo.selector}
{state.dropdownInfo && ( options={dropdownInfo.options}
<Dropdown onClose={() => setDropdownInfo(null)}
coordinates={state.dropdownInfo.coordinates} />
selector={state.dropdownInfo.selector} )}
options={state.dropdownInfo.options} {timePickerInfo && (
onClose={() => dispatch({ <TimePicker
type: 'BATCH_UPDATE', coordinates={timePickerInfo.coordinates}
payload: { dropdownInfo: null } selector={timePickerInfo.selector}
})} onClose={() => setTimePickerInfo(null)}
/> />
)} )}
{state.timePickerInfo && ( {dateTimeLocalInfo && (
<TimePicker <DateTimeLocalPicker
coordinates={state.timePickerInfo.coordinates} coordinates={dateTimeLocalInfo.coordinates}
selector={state.timePickerInfo.selector} selector={dateTimeLocalInfo.selector}
onClose={() => dispatch({ type: 'SET_TIME_PICKER', payload: null })} onClose={() => setDateTimeLocalInfo(null)}
/> />
)} )}
{state.dateTimeLocalInfo && (
<DateTimeLocalPicker
coordinates={state.dateTimeLocalInfo.coordinates}
selector={state.dateTimeLocalInfo.selector}
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
/>
)}
</Suspense>
</div> </div>
); );
});
Canvas.displayName = 'Canvas'; };
export default Canvas; export default Canvas;