@ngenux/ngage-whiteboarding 1.0.6 → 1.0.7
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.
- package/dist/index.esm.js +166 -74
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +166 -74
- package/dist/index.js.map +1 -1
- package/dist/src/components/Shapes/ErasedShape.d.ts.map +1 -1
- package/dist/src/components/Whiteboard/Board.d.ts.map +1 -1
- package/dist/src/hooks/useCollaborativeWhiteboard.d.ts.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -35903,7 +35903,7 @@ const Arrow = React__default.memo(({ shapeProps, isSelected, onSelect, onUpdate,
|
|
|
35903
35903
|
});
|
|
35904
35904
|
|
|
35905
35905
|
const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
35906
|
-
const [
|
|
35906
|
+
const [bitmap, setBitmap] = useState(null);
|
|
35907
35907
|
const canvasRef = useRef(null);
|
|
35908
35908
|
// Memoize bounds calculation to avoid unnecessary recalculations
|
|
35909
35909
|
const bounds = useMemo(() => calculateShapeBounds(shapeProps), [shapeProps.points, shapeProps.strokeWidth, shapeProps.erasePaths]);
|
|
@@ -35933,22 +35933,24 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
35933
35933
|
});
|
|
35934
35934
|
}
|
|
35935
35935
|
ctx.restore();
|
|
35936
|
-
//
|
|
35937
|
-
|
|
35938
|
-
img.onload = () => {
|
|
35939
|
-
setImageElement(img);
|
|
35940
|
-
};
|
|
35941
|
-
img.onerror = () => {
|
|
35942
|
-
console.error('[ErasedShape] Failed to create image from canvas');
|
|
35943
|
-
setImageElement(null);
|
|
35944
|
-
};
|
|
35945
|
-
img.src = canvas.toDataURL();
|
|
35936
|
+
// Use canvas directly as Konva image - no async
|
|
35937
|
+
setBitmap(canvas);
|
|
35946
35938
|
return () => {
|
|
35947
35939
|
if (canvasRef.current) {
|
|
35948
35940
|
canvasRef.current.remove();
|
|
35941
|
+
canvasRef.current = null;
|
|
35949
35942
|
}
|
|
35950
35943
|
};
|
|
35951
|
-
}, [
|
|
35944
|
+
}, [
|
|
35945
|
+
bounds,
|
|
35946
|
+
shapeProps.points,
|
|
35947
|
+
shapeProps.erasePaths,
|
|
35948
|
+
shapeProps.stroke,
|
|
35949
|
+
shapeProps.strokeWidth,
|
|
35950
|
+
shapeProps.opacity,
|
|
35951
|
+
shapeProps.strokeStyle,
|
|
35952
|
+
shapeProps.type
|
|
35953
|
+
]);
|
|
35952
35954
|
// Calculate bounds of the shape including erase paths
|
|
35953
35955
|
function calculateShapeBounds(shape) {
|
|
35954
35956
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
@@ -36067,11 +36069,48 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
36067
36069
|
ctx.stroke();
|
|
36068
36070
|
}
|
|
36069
36071
|
};
|
|
36070
|
-
if (!imageElement) {
|
|
36071
|
-
return null; // Still rendering
|
|
36072
|
-
}
|
|
36073
36072
|
const padding = 50;
|
|
36074
|
-
|
|
36073
|
+
// Render original shape as fallback while bitmap is being created
|
|
36074
|
+
if (!bitmap) {
|
|
36075
|
+
const commonProps = {
|
|
36076
|
+
onClick: onSelect,
|
|
36077
|
+
onTap: onSelect,
|
|
36078
|
+
listening: true,
|
|
36079
|
+
stroke: shapeProps.stroke,
|
|
36080
|
+
strokeWidth: shapeProps.strokeWidth,
|
|
36081
|
+
opacity: shapeProps.opacity,
|
|
36082
|
+
dash: shapeProps.strokeStyle === 'dashed' ? [5, 5] : shapeProps.strokeStyle === 'dotted' ? [2, 3] : undefined,
|
|
36083
|
+
};
|
|
36084
|
+
switch (shapeProps.type) {
|
|
36085
|
+
case 'pencil':
|
|
36086
|
+
return (jsx(Line$1, { points: shapeProps.points.flatMap(p => [p.x, p.y]), lineCap: "round", lineJoin: "round", ...commonProps }));
|
|
36087
|
+
case 'line':
|
|
36088
|
+
if (shapeProps.points.length >= 2) {
|
|
36089
|
+
return (jsx(Line$1, { points: [
|
|
36090
|
+
shapeProps.points[0].x,
|
|
36091
|
+
shapeProps.points[0].y,
|
|
36092
|
+
shapeProps.points[1].x,
|
|
36093
|
+
shapeProps.points[1].y,
|
|
36094
|
+
], ...commonProps }));
|
|
36095
|
+
}
|
|
36096
|
+
return null;
|
|
36097
|
+
case 'rectangle':
|
|
36098
|
+
if (shapeProps.points.length >= 2) {
|
|
36099
|
+
const [start, end] = shapeProps.points;
|
|
36100
|
+
return (jsx(Rect, { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y, ...commonProps }));
|
|
36101
|
+
}
|
|
36102
|
+
return null;
|
|
36103
|
+
case 'ellipse':
|
|
36104
|
+
if (shapeProps.points.length >= 2) {
|
|
36105
|
+
const [start, end] = shapeProps.points;
|
|
36106
|
+
return (jsx(Ellipse$1, { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2, radiusX: Math.abs(end.x - start.x) / 2, radiusY: Math.abs(end.y - start.y) / 2, ...commonProps }));
|
|
36107
|
+
}
|
|
36108
|
+
return null;
|
|
36109
|
+
default:
|
|
36110
|
+
return null;
|
|
36111
|
+
}
|
|
36112
|
+
}
|
|
36113
|
+
return (jsx(Image$1, { image: bitmap, x: bounds.minX - padding, y: bounds.minY - padding, draggable: isSelected, onClick: onSelect, onTap: onSelect, onDragEnd: (e) => {
|
|
36075
36114
|
const newX = e.target.x();
|
|
36076
36115
|
const newY = e.target.y();
|
|
36077
36116
|
const deltaX = newX - (bounds.minX - padding);
|
|
@@ -36111,6 +36150,10 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36111
36150
|
const containerRef = useRef(null);
|
|
36112
36151
|
const lastPointerPosition = useRef(null);
|
|
36113
36152
|
const mouseMoveThrottleRef = useRef(null);
|
|
36153
|
+
// NEW: Eraser preview state for real-time visual feedback
|
|
36154
|
+
const [eraserPreviewPoints, setEraserPreviewPoints] = useState([]);
|
|
36155
|
+
const [keepPreviewVisible, setKeepPreviewVisible] = useState(false);
|
|
36156
|
+
const [justErasedIds, setJustErasedIds] = useState(new Set());
|
|
36114
36157
|
// Find shapes that intersect with the erase path
|
|
36115
36158
|
const findIntersectingShapes = (erasePath, shapes) => {
|
|
36116
36159
|
const eraseRadius = 10; // Half of eraser width
|
|
@@ -36201,8 +36244,20 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36201
36244
|
setCurrentShapeId(null);
|
|
36202
36245
|
setCurrentDrawingSessionId(null);
|
|
36203
36246
|
lastPointerPosition.current = null;
|
|
36247
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36248
|
+
setKeepPreviewVisible(false);
|
|
36249
|
+
setJustErasedIds(new Set());
|
|
36204
36250
|
}
|
|
36205
36251
|
}, [hasToolAccess, state.isDrawing, dispatch]);
|
|
36252
|
+
// Clear justErasedIds after one frame to complete transition
|
|
36253
|
+
useEffect(() => {
|
|
36254
|
+
if (justErasedIds.size > 0) {
|
|
36255
|
+
const id = requestAnimationFrame(() => {
|
|
36256
|
+
setJustErasedIds(new Set());
|
|
36257
|
+
});
|
|
36258
|
+
return () => cancelAnimationFrame(id);
|
|
36259
|
+
}
|
|
36260
|
+
}, [justErasedIds]);
|
|
36206
36261
|
// Memoized export functionality for performance
|
|
36207
36262
|
const exportAsImage = useCallback((format = 'png') => {
|
|
36208
36263
|
if (!stageRef.current) {
|
|
@@ -36365,6 +36420,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36365
36420
|
if (state.tool === 'eraser') {
|
|
36366
36421
|
dispatch({ type: 'SET_DRAWING', payload: true });
|
|
36367
36422
|
setCurrentPoints([pos]);
|
|
36423
|
+
setEraserPreviewPoints([pos]); // NEW: start preview stroke
|
|
36368
36424
|
setCurrentShapeId('erasing'); // Special ID for erasing mode
|
|
36369
36425
|
return;
|
|
36370
36426
|
}
|
|
@@ -36413,6 +36469,9 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36413
36469
|
setCurrentShapeId(null);
|
|
36414
36470
|
setCurrentDrawingSessionId(null);
|
|
36415
36471
|
lastPointerPosition.current = null;
|
|
36472
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36473
|
+
setKeepPreviewVisible(false);
|
|
36474
|
+
setJustErasedIds(new Set());
|
|
36416
36475
|
}
|
|
36417
36476
|
return;
|
|
36418
36477
|
}
|
|
@@ -36430,10 +36489,41 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36430
36489
|
return; // Ultra-sensitive for maximum smoothness
|
|
36431
36490
|
}
|
|
36432
36491
|
lastPointerPosition.current = pos;
|
|
36433
|
-
// Handle eraser tool -
|
|
36492
|
+
// Handle eraser tool - collect points and stream erase in real-time
|
|
36434
36493
|
if (state.tool === 'eraser') {
|
|
36435
36494
|
const newPoints = [...currentPoints, pos];
|
|
36436
36495
|
setCurrentPoints(newPoints);
|
|
36496
|
+
setEraserPreviewPoints(newPoints); // Live preview
|
|
36497
|
+
// Create segment from last point to current point for real-time streaming
|
|
36498
|
+
const segment = currentPoints.length > 0
|
|
36499
|
+
? [currentPoints[currentPoints.length - 1], pos]
|
|
36500
|
+
: [pos];
|
|
36501
|
+
// Apply segment to intersecting shapes in real-time
|
|
36502
|
+
const intersectingShapes = findIntersectingShapes(segment, state.shapes);
|
|
36503
|
+
if (intersectingShapes.length > 0) {
|
|
36504
|
+
const timestamp = Date.now();
|
|
36505
|
+
intersectingShapes.forEach((shape, index) => {
|
|
36506
|
+
const updatedShape = {
|
|
36507
|
+
...shape,
|
|
36508
|
+
erasePaths: [...(shape.erasePaths || []), segment],
|
|
36509
|
+
};
|
|
36510
|
+
// Update the shape locally
|
|
36511
|
+
dispatch({ type: 'UPDATE_SHAPE', payload: updatedShape });
|
|
36512
|
+
// Queue erase action for real-time collaboration
|
|
36513
|
+
if (queueAction) {
|
|
36514
|
+
const erasePayload = {
|
|
36515
|
+
shapeId: shape.id,
|
|
36516
|
+
erasePath: segment,
|
|
36517
|
+
timestamp: timestamp + index,
|
|
36518
|
+
};
|
|
36519
|
+
queueAction({
|
|
36520
|
+
type: 'erase',
|
|
36521
|
+
payload: erasePayload,
|
|
36522
|
+
timestamp: timestamp + index,
|
|
36523
|
+
});
|
|
36524
|
+
}
|
|
36525
|
+
});
|
|
36526
|
+
}
|
|
36437
36527
|
return;
|
|
36438
36528
|
}
|
|
36439
36529
|
let newPoints;
|
|
@@ -36483,7 +36573,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36483
36573
|
};
|
|
36484
36574
|
queueAction(continueAction);
|
|
36485
36575
|
mouseMoveThrottleRef.current = null;
|
|
36486
|
-
},
|
|
36576
|
+
}, 1); // Maximum frequency for ultra-smooth real-time collaboration
|
|
36487
36577
|
}
|
|
36488
36578
|
}, [hasToolAccess, state.isDrawing, state.tool, state.userId, state.color, state.strokeWidth, state.strokeStyle, state.opacity, currentShapeId, currentDrawingSessionId, currentPoints, queueAction, dispatch]);
|
|
36489
36579
|
const handleMouseUp = useCallback(() => {
|
|
@@ -36496,6 +36586,9 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36496
36586
|
setCurrentShapeId(null);
|
|
36497
36587
|
setCurrentDrawingSessionId(null);
|
|
36498
36588
|
lastPointerPosition.current = null;
|
|
36589
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36590
|
+
setKeepPreviewVisible(false);
|
|
36591
|
+
setJustErasedIds(new Set());
|
|
36499
36592
|
}
|
|
36500
36593
|
return;
|
|
36501
36594
|
}
|
|
@@ -36511,36 +36604,23 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36511
36604
|
setCurrentDrawingSessionId(null);
|
|
36512
36605
|
return;
|
|
36513
36606
|
}
|
|
36514
|
-
// Handle eraser tool -
|
|
36607
|
+
// Handle eraser tool - cleanup on mouse up (erasing already done in mouse move)
|
|
36515
36608
|
if (state.tool === 'eraser') {
|
|
36516
|
-
//
|
|
36517
|
-
|
|
36518
|
-
// Apply erase path to each intersecting shape
|
|
36519
|
-
intersectingShapes.forEach(shape => {
|
|
36520
|
-
const updatedShape = {
|
|
36521
|
-
...shape,
|
|
36522
|
-
erasePaths: [...(shape.erasePaths || []), currentPoints],
|
|
36523
|
-
};
|
|
36524
|
-
// Update the shape locally
|
|
36525
|
-
dispatch({ type: 'UPDATE_SHAPE', payload: updatedShape });
|
|
36526
|
-
// Queue erase action for collaboration
|
|
36527
|
-
if (queueAction) {
|
|
36528
|
-
const erasePayload = {
|
|
36529
|
-
shapeId: shape.id,
|
|
36530
|
-
erasePath: currentPoints,
|
|
36531
|
-
timestamp: Date.now(),
|
|
36532
|
-
};
|
|
36533
|
-
queueAction({
|
|
36534
|
-
type: 'erase',
|
|
36535
|
-
payload: erasePayload,
|
|
36536
|
-
});
|
|
36537
|
-
}
|
|
36538
|
-
});
|
|
36609
|
+
// Stop drawing state
|
|
36610
|
+
dispatch({ type: 'SET_DRAWING', payload: false });
|
|
36539
36611
|
// Reset erasing state
|
|
36540
36612
|
setCurrentPoints([]);
|
|
36541
36613
|
setCurrentShapeId(null);
|
|
36542
36614
|
setCurrentDrawingSessionId(null);
|
|
36543
36615
|
lastPointerPosition.current = null;
|
|
36616
|
+
// Clear preview after a brief delay to ensure smooth transition
|
|
36617
|
+
requestAnimationFrame(() => {
|
|
36618
|
+
requestAnimationFrame(() => {
|
|
36619
|
+
setEraserPreviewPoints([]);
|
|
36620
|
+
setKeepPreviewVisible(false);
|
|
36621
|
+
setJustErasedIds(new Set());
|
|
36622
|
+
});
|
|
36623
|
+
});
|
|
36544
36624
|
return;
|
|
36545
36625
|
}
|
|
36546
36626
|
// Handle regular drawing tools
|
|
@@ -36607,31 +36687,39 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36607
36687
|
if (shape.isEraser) {
|
|
36608
36688
|
return null;
|
|
36609
36689
|
}
|
|
36610
|
-
// Use ErasedShape component for shapes that have erase paths applied
|
|
36611
|
-
if (shape.erasePaths && shape.erasePaths.length > 0) {
|
|
36612
|
-
return (jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate }));
|
|
36613
|
-
}
|
|
36614
|
-
// Use original shape components for shapes without erase operations
|
|
36615
36690
|
const commonProps = {
|
|
36616
36691
|
shapeProps: shape,
|
|
36617
36692
|
isSelected: shape.isSelected || false,
|
|
36618
36693
|
onSelect: () => handleShapeClick(shape.id),
|
|
36619
36694
|
onUpdate: handleShapeUpdate,
|
|
36620
36695
|
};
|
|
36621
|
-
|
|
36622
|
-
|
|
36623
|
-
|
|
36624
|
-
|
|
36625
|
-
|
|
36626
|
-
|
|
36627
|
-
|
|
36628
|
-
|
|
36629
|
-
|
|
36630
|
-
|
|
36631
|
-
|
|
36632
|
-
|
|
36633
|
-
|
|
36696
|
+
// Render original shape component
|
|
36697
|
+
const OriginalShape = () => {
|
|
36698
|
+
switch (shape.type) {
|
|
36699
|
+
case 'rectangle':
|
|
36700
|
+
return jsx(Rectangle, { ...commonProps });
|
|
36701
|
+
case 'ellipse':
|
|
36702
|
+
return jsx(Ellipse, { ...commonProps });
|
|
36703
|
+
case 'line':
|
|
36704
|
+
return jsx(Line, { ...commonProps });
|
|
36705
|
+
case 'pencil':
|
|
36706
|
+
return jsx(FreehandDrawing, { ...commonProps });
|
|
36707
|
+
case 'arrow':
|
|
36708
|
+
return jsx(Arrow, { ...commonProps });
|
|
36709
|
+
default:
|
|
36710
|
+
return null;
|
|
36711
|
+
}
|
|
36712
|
+
};
|
|
36713
|
+
// Use ErasedShape component for shapes that have erase paths applied
|
|
36714
|
+
if (shape.erasePaths && shape.erasePaths.length > 0) {
|
|
36715
|
+
// If this shape just got erased, render both to prevent flicker
|
|
36716
|
+
if (justErasedIds.has(shape.id)) {
|
|
36717
|
+
return (jsxs(Fragment, { children: [jsx(OriginalShape, {}), jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate })] }));
|
|
36718
|
+
}
|
|
36719
|
+
return (jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate }));
|
|
36634
36720
|
}
|
|
36721
|
+
// Use original shape components for shapes without erase operations
|
|
36722
|
+
return jsx(OriginalShape, {});
|
|
36635
36723
|
}, (prevProps, nextProps) => {
|
|
36636
36724
|
// Ultra-precise comparison to prevent unnecessary re-renders during simultaneous drawing
|
|
36637
36725
|
const prevShape = prevProps.shape;
|
|
@@ -36738,11 +36826,11 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36738
36826
|
state.tool === 'select' ? 'default' :
|
|
36739
36827
|
state.tool === 'pan' ? 'grab' : 'crosshair'
|
|
36740
36828
|
}), [hasToolAccess, state.tool]);
|
|
36741
|
-
return (jsx("div", { ref: containerRef, className: "w-full h-full relative", style: { backgroundColor: state.backgroundColor }, children:
|
|
36742
|
-
|
|
36743
|
-
|
|
36744
|
-
|
|
36745
|
-
|
|
36829
|
+
return (jsx("div", { ref: containerRef, className: "w-full h-full relative", style: { backgroundColor: state.backgroundColor }, children: jsxs(Stage, { ref: stageRef, width: size.width, height: size.height, onMouseDown: handleMouseDown, onMousemove: handleMouseMove, onMouseup: handleMouseUp, onTouchStart: handleMouseDown, onTouchMove: handleMouseMove, onTouchEnd: handleMouseUp, style: cursorStyle, children: [jsxs(Layer, { children: [useMemo(() => {
|
|
36830
|
+
if (state.backgroundColor === 'transparent')
|
|
36831
|
+
return null;
|
|
36832
|
+
return (jsx(Rect, { x: 0, y: 0, width: size.width, height: size.height, fill: state.backgroundColor, listening: false }));
|
|
36833
|
+
}, [state.backgroundColor, size.width, size.height]), renderedShapes, renderedActiveDrawings, renderCurrentShape] }), jsx(Layer, { listening: false, children: eraserPreviewPoints.length > 0 && (state.isDrawing || keepPreviewVisible) && state.tool === 'eraser' && (jsx(Line$1, { points: eraserPreviewPoints.flatMap(p => [p.x, p.y]), stroke: state.backgroundColor === 'transparent' ? '#FFFFFF' : state.backgroundColor, strokeWidth: 20, lineCap: "round", lineJoin: "round", opacity: 1.0, listening: false, perfectDrawEnabled: false })) })] }) }));
|
|
36746
36834
|
});
|
|
36747
36835
|
// Memoize the Board component to prevent unnecessary re-renders
|
|
36748
36836
|
const Board = React__default.memo(BoardComponent, (prevProps, nextProps) => {
|
|
@@ -37689,15 +37777,15 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37689
37777
|
totalActions: 0,
|
|
37690
37778
|
});
|
|
37691
37779
|
// Throttling configuration - Ultra-optimized for smooth real-time collaboration
|
|
37692
|
-
const THROTTLE_DELAY =
|
|
37693
|
-
const MAX_ACTIONS_PER_BATCH =
|
|
37780
|
+
const THROTTLE_DELAY = 10; // ms - Minimal delay for faster transmission
|
|
37781
|
+
const MAX_ACTIONS_PER_BATCH = 8; // Smaller batches for faster transmission
|
|
37694
37782
|
const MAX_MESSAGE_SIZE = 500; // characters - matches API constraint
|
|
37695
|
-
const MAX_MESSAGES_PER_SECOND =
|
|
37783
|
+
const MAX_MESSAGES_PER_SECOND = 30; // Maximum rate for smoother collaboration
|
|
37696
37784
|
const MAX_PAYLOAD_SIZE = 1024; // bytes (1KB) - matches API constraint
|
|
37697
37785
|
// Drawing-specific throttling for ultra-smooth real-time collaboration
|
|
37698
|
-
const DRAWING_THROTTLE_DELAY =
|
|
37699
|
-
const DRAWING_BATCH_SIZE =
|
|
37700
|
-
const DRAWING_IMMEDIATE_THRESHOLD =
|
|
37786
|
+
const DRAWING_THROTTLE_DELAY = 1; // ms - Maximum frequency for ultra-smooth drawing actions
|
|
37787
|
+
const DRAWING_BATCH_SIZE = 2; // Minimal batches for immediate transmission
|
|
37788
|
+
const DRAWING_IMMEDIATE_THRESHOLD = 1; // Send immediately if we have 1+ drawing actions
|
|
37701
37789
|
// Message rate limiting
|
|
37702
37790
|
const messageTimestampsRef = useRef([]);
|
|
37703
37791
|
const isRateLimited = useCallback(() => {
|
|
@@ -37861,7 +37949,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37861
37949
|
let delay = THROTTLE_DELAY;
|
|
37862
37950
|
if (remainingDrawingActions.length > 0) {
|
|
37863
37951
|
delay = remainingDrawingActions.length >= DRAWING_IMMEDIATE_THRESHOLD ?
|
|
37864
|
-
Math.max(DRAWING_THROTTLE_DELAY / 2,
|
|
37952
|
+
Math.max(DRAWING_THROTTLE_DELAY / 2, 2) : // Even faster for multiple actions
|
|
37865
37953
|
DRAWING_THROTTLE_DELAY;
|
|
37866
37954
|
}
|
|
37867
37955
|
throttleTimerRef.current = setTimeout(() => {
|
|
@@ -38004,7 +38092,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38004
38092
|
clearTimeout(throttleTimerRef.current);
|
|
38005
38093
|
throttleTimerRef.current = setTimeout(() => {
|
|
38006
38094
|
transmitRef.current?.();
|
|
38007
|
-
},
|
|
38095
|
+
}, 1); // Ultra-fast transmission - 1ms for maximum responsiveness
|
|
38008
38096
|
}
|
|
38009
38097
|
}
|
|
38010
38098
|
}, [state.canvasSize, state.userId, roomId, callbacks, sendWithConstraints]);
|
|
@@ -38024,7 +38112,11 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38024
38112
|
// Process each action in the batch with duplicate prevention
|
|
38025
38113
|
parsedData.actions.forEach(action => {
|
|
38026
38114
|
// Create unique action ID for deduplication
|
|
38027
|
-
|
|
38115
|
+
// For erase actions, include shapeId to allow multiple erases in same stroke
|
|
38116
|
+
let actionId = `${action.type}-${parsedData.userId}-${action.timestamp || Date.now()}`;
|
|
38117
|
+
if (action.type === 'erase' && action.payload && typeof action.payload === 'object' && 'shapeId' in action.payload) {
|
|
38118
|
+
actionId = `${action.type}-${parsedData.userId}-${action.payload.shapeId}-${action.timestamp || Date.now()}`;
|
|
38119
|
+
}
|
|
38028
38120
|
// Skip if we've already processed this action (prevents shape loss from duplicate processing)
|
|
38029
38121
|
if (processedActionsRef.current.has(actionId)) {
|
|
38030
38122
|
return;
|