@ngenux/ngage-whiteboarding 1.0.5 → 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 +172 -78
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +172 -78
- 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/components/Whiteboard/Toolbar.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) => {
|
|
@@ -37149,13 +37237,15 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37149
37237
|
setIsInitialized(true);
|
|
37150
37238
|
}
|
|
37151
37239
|
}, [state]);
|
|
37152
|
-
// Set white as default stroke color when video background is
|
|
37240
|
+
// Set white as default stroke color when video background is first activated (only once)
|
|
37241
|
+
const hasVideoBackgroundRef = React__default.useRef(false);
|
|
37153
37242
|
useEffect(() => {
|
|
37154
|
-
|
|
37155
|
-
|
|
37243
|
+
// Only auto-switch to white on first video activation, and only if currently black
|
|
37244
|
+
if (hasVideoBackground && !hasVideoBackgroundRef.current && state.color === '#000000') {
|
|
37156
37245
|
dispatch({ type: 'SET_COLOR', payload: '#FFFFFF' });
|
|
37157
37246
|
}
|
|
37158
|
-
|
|
37247
|
+
hasVideoBackgroundRef.current = hasVideoBackground;
|
|
37248
|
+
}, [hasVideoBackground, dispatch]);
|
|
37159
37249
|
// Track initial access grant
|
|
37160
37250
|
useEffect(() => {
|
|
37161
37251
|
if (hasToolAccess && !hasEverHadAccess) {
|
|
@@ -37687,15 +37777,15 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37687
37777
|
totalActions: 0,
|
|
37688
37778
|
});
|
|
37689
37779
|
// Throttling configuration - Ultra-optimized for smooth real-time collaboration
|
|
37690
|
-
const THROTTLE_DELAY =
|
|
37691
|
-
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
|
|
37692
37782
|
const MAX_MESSAGE_SIZE = 500; // characters - matches API constraint
|
|
37693
|
-
const MAX_MESSAGES_PER_SECOND =
|
|
37783
|
+
const MAX_MESSAGES_PER_SECOND = 30; // Maximum rate for smoother collaboration
|
|
37694
37784
|
const MAX_PAYLOAD_SIZE = 1024; // bytes (1KB) - matches API constraint
|
|
37695
37785
|
// Drawing-specific throttling for ultra-smooth real-time collaboration
|
|
37696
|
-
const DRAWING_THROTTLE_DELAY =
|
|
37697
|
-
const DRAWING_BATCH_SIZE =
|
|
37698
|
-
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
|
|
37699
37789
|
// Message rate limiting
|
|
37700
37790
|
const messageTimestampsRef = useRef([]);
|
|
37701
37791
|
const isRateLimited = useCallback(() => {
|
|
@@ -37859,7 +37949,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37859
37949
|
let delay = THROTTLE_DELAY;
|
|
37860
37950
|
if (remainingDrawingActions.length > 0) {
|
|
37861
37951
|
delay = remainingDrawingActions.length >= DRAWING_IMMEDIATE_THRESHOLD ?
|
|
37862
|
-
Math.max(DRAWING_THROTTLE_DELAY / 2,
|
|
37952
|
+
Math.max(DRAWING_THROTTLE_DELAY / 2, 2) : // Even faster for multiple actions
|
|
37863
37953
|
DRAWING_THROTTLE_DELAY;
|
|
37864
37954
|
}
|
|
37865
37955
|
throttleTimerRef.current = setTimeout(() => {
|
|
@@ -38002,7 +38092,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38002
38092
|
clearTimeout(throttleTimerRef.current);
|
|
38003
38093
|
throttleTimerRef.current = setTimeout(() => {
|
|
38004
38094
|
transmitRef.current?.();
|
|
38005
|
-
},
|
|
38095
|
+
}, 1); // Ultra-fast transmission - 1ms for maximum responsiveness
|
|
38006
38096
|
}
|
|
38007
38097
|
}
|
|
38008
38098
|
}, [state.canvasSize, state.userId, roomId, callbacks, sendWithConstraints]);
|
|
@@ -38022,7 +38112,11 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38022
38112
|
// Process each action in the batch with duplicate prevention
|
|
38023
38113
|
parsedData.actions.forEach(action => {
|
|
38024
38114
|
// Create unique action ID for deduplication
|
|
38025
|
-
|
|
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
|
+
}
|
|
38026
38120
|
// Skip if we've already processed this action (prevents shape loss from duplicate processing)
|
|
38027
38121
|
if (processedActionsRef.current.has(actionId)) {
|
|
38028
38122
|
return;
|