@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.js
CHANGED
|
@@ -35923,7 +35923,7 @@ const Arrow = React.memo(({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
35923
35923
|
});
|
|
35924
35924
|
|
|
35925
35925
|
const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
35926
|
-
const [
|
|
35926
|
+
const [bitmap, setBitmap] = React.useState(null);
|
|
35927
35927
|
const canvasRef = React.useRef(null);
|
|
35928
35928
|
// Memoize bounds calculation to avoid unnecessary recalculations
|
|
35929
35929
|
const bounds = React.useMemo(() => calculateShapeBounds(shapeProps), [shapeProps.points, shapeProps.strokeWidth, shapeProps.erasePaths]);
|
|
@@ -35953,22 +35953,24 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
35953
35953
|
});
|
|
35954
35954
|
}
|
|
35955
35955
|
ctx.restore();
|
|
35956
|
-
//
|
|
35957
|
-
|
|
35958
|
-
img.onload = () => {
|
|
35959
|
-
setImageElement(img);
|
|
35960
|
-
};
|
|
35961
|
-
img.onerror = () => {
|
|
35962
|
-
console.error('[ErasedShape] Failed to create image from canvas');
|
|
35963
|
-
setImageElement(null);
|
|
35964
|
-
};
|
|
35965
|
-
img.src = canvas.toDataURL();
|
|
35956
|
+
// Use canvas directly as Konva image - no async
|
|
35957
|
+
setBitmap(canvas);
|
|
35966
35958
|
return () => {
|
|
35967
35959
|
if (canvasRef.current) {
|
|
35968
35960
|
canvasRef.current.remove();
|
|
35961
|
+
canvasRef.current = null;
|
|
35969
35962
|
}
|
|
35970
35963
|
};
|
|
35971
|
-
}, [
|
|
35964
|
+
}, [
|
|
35965
|
+
bounds,
|
|
35966
|
+
shapeProps.points,
|
|
35967
|
+
shapeProps.erasePaths,
|
|
35968
|
+
shapeProps.stroke,
|
|
35969
|
+
shapeProps.strokeWidth,
|
|
35970
|
+
shapeProps.opacity,
|
|
35971
|
+
shapeProps.strokeStyle,
|
|
35972
|
+
shapeProps.type
|
|
35973
|
+
]);
|
|
35972
35974
|
// Calculate bounds of the shape including erase paths
|
|
35973
35975
|
function calculateShapeBounds(shape) {
|
|
35974
35976
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
@@ -36087,11 +36089,48 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
36087
36089
|
ctx.stroke();
|
|
36088
36090
|
}
|
|
36089
36091
|
};
|
|
36090
|
-
if (!imageElement) {
|
|
36091
|
-
return null; // Still rendering
|
|
36092
|
-
}
|
|
36093
36092
|
const padding = 50;
|
|
36094
|
-
|
|
36093
|
+
// Render original shape as fallback while bitmap is being created
|
|
36094
|
+
if (!bitmap) {
|
|
36095
|
+
const commonProps = {
|
|
36096
|
+
onClick: onSelect,
|
|
36097
|
+
onTap: onSelect,
|
|
36098
|
+
listening: true,
|
|
36099
|
+
stroke: shapeProps.stroke,
|
|
36100
|
+
strokeWidth: shapeProps.strokeWidth,
|
|
36101
|
+
opacity: shapeProps.opacity,
|
|
36102
|
+
dash: shapeProps.strokeStyle === 'dashed' ? [5, 5] : shapeProps.strokeStyle === 'dotted' ? [2, 3] : undefined,
|
|
36103
|
+
};
|
|
36104
|
+
switch (shapeProps.type) {
|
|
36105
|
+
case 'pencil':
|
|
36106
|
+
return (jsxRuntime.jsx(Line$1, { points: shapeProps.points.flatMap(p => [p.x, p.y]), lineCap: "round", lineJoin: "round", ...commonProps }));
|
|
36107
|
+
case 'line':
|
|
36108
|
+
if (shapeProps.points.length >= 2) {
|
|
36109
|
+
return (jsxRuntime.jsx(Line$1, { points: [
|
|
36110
|
+
shapeProps.points[0].x,
|
|
36111
|
+
shapeProps.points[0].y,
|
|
36112
|
+
shapeProps.points[1].x,
|
|
36113
|
+
shapeProps.points[1].y,
|
|
36114
|
+
], ...commonProps }));
|
|
36115
|
+
}
|
|
36116
|
+
return null;
|
|
36117
|
+
case 'rectangle':
|
|
36118
|
+
if (shapeProps.points.length >= 2) {
|
|
36119
|
+
const [start, end] = shapeProps.points;
|
|
36120
|
+
return (jsxRuntime.jsx(Rect, { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y, ...commonProps }));
|
|
36121
|
+
}
|
|
36122
|
+
return null;
|
|
36123
|
+
case 'ellipse':
|
|
36124
|
+
if (shapeProps.points.length >= 2) {
|
|
36125
|
+
const [start, end] = shapeProps.points;
|
|
36126
|
+
return (jsxRuntime.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 }));
|
|
36127
|
+
}
|
|
36128
|
+
return null;
|
|
36129
|
+
default:
|
|
36130
|
+
return null;
|
|
36131
|
+
}
|
|
36132
|
+
}
|
|
36133
|
+
return (jsxRuntime.jsx(Image$1, { image: bitmap, x: bounds.minX - padding, y: bounds.minY - padding, draggable: isSelected, onClick: onSelect, onTap: onSelect, onDragEnd: (e) => {
|
|
36095
36134
|
const newX = e.target.x();
|
|
36096
36135
|
const newY = e.target.y();
|
|
36097
36136
|
const deltaX = newX - (bounds.minX - padding);
|
|
@@ -36131,6 +36170,10 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36131
36170
|
const containerRef = React.useRef(null);
|
|
36132
36171
|
const lastPointerPosition = React.useRef(null);
|
|
36133
36172
|
const mouseMoveThrottleRef = React.useRef(null);
|
|
36173
|
+
// NEW: Eraser preview state for real-time visual feedback
|
|
36174
|
+
const [eraserPreviewPoints, setEraserPreviewPoints] = React.useState([]);
|
|
36175
|
+
const [keepPreviewVisible, setKeepPreviewVisible] = React.useState(false);
|
|
36176
|
+
const [justErasedIds, setJustErasedIds] = React.useState(new Set());
|
|
36134
36177
|
// Find shapes that intersect with the erase path
|
|
36135
36178
|
const findIntersectingShapes = (erasePath, shapes) => {
|
|
36136
36179
|
const eraseRadius = 10; // Half of eraser width
|
|
@@ -36221,8 +36264,20 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36221
36264
|
setCurrentShapeId(null);
|
|
36222
36265
|
setCurrentDrawingSessionId(null);
|
|
36223
36266
|
lastPointerPosition.current = null;
|
|
36267
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36268
|
+
setKeepPreviewVisible(false);
|
|
36269
|
+
setJustErasedIds(new Set());
|
|
36224
36270
|
}
|
|
36225
36271
|
}, [hasToolAccess, state.isDrawing, dispatch]);
|
|
36272
|
+
// Clear justErasedIds after one frame to complete transition
|
|
36273
|
+
React.useEffect(() => {
|
|
36274
|
+
if (justErasedIds.size > 0) {
|
|
36275
|
+
const id = requestAnimationFrame(() => {
|
|
36276
|
+
setJustErasedIds(new Set());
|
|
36277
|
+
});
|
|
36278
|
+
return () => cancelAnimationFrame(id);
|
|
36279
|
+
}
|
|
36280
|
+
}, [justErasedIds]);
|
|
36226
36281
|
// Memoized export functionality for performance
|
|
36227
36282
|
const exportAsImage = React.useCallback((format = 'png') => {
|
|
36228
36283
|
if (!stageRef.current) {
|
|
@@ -36385,6 +36440,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36385
36440
|
if (state.tool === 'eraser') {
|
|
36386
36441
|
dispatch({ type: 'SET_DRAWING', payload: true });
|
|
36387
36442
|
setCurrentPoints([pos]);
|
|
36443
|
+
setEraserPreviewPoints([pos]); // NEW: start preview stroke
|
|
36388
36444
|
setCurrentShapeId('erasing'); // Special ID for erasing mode
|
|
36389
36445
|
return;
|
|
36390
36446
|
}
|
|
@@ -36433,6 +36489,9 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36433
36489
|
setCurrentShapeId(null);
|
|
36434
36490
|
setCurrentDrawingSessionId(null);
|
|
36435
36491
|
lastPointerPosition.current = null;
|
|
36492
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36493
|
+
setKeepPreviewVisible(false);
|
|
36494
|
+
setJustErasedIds(new Set());
|
|
36436
36495
|
}
|
|
36437
36496
|
return;
|
|
36438
36497
|
}
|
|
@@ -36450,10 +36509,41 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36450
36509
|
return; // Ultra-sensitive for maximum smoothness
|
|
36451
36510
|
}
|
|
36452
36511
|
lastPointerPosition.current = pos;
|
|
36453
|
-
// Handle eraser tool -
|
|
36512
|
+
// Handle eraser tool - collect points and stream erase in real-time
|
|
36454
36513
|
if (state.tool === 'eraser') {
|
|
36455
36514
|
const newPoints = [...currentPoints, pos];
|
|
36456
36515
|
setCurrentPoints(newPoints);
|
|
36516
|
+
setEraserPreviewPoints(newPoints); // Live preview
|
|
36517
|
+
// Create segment from last point to current point for real-time streaming
|
|
36518
|
+
const segment = currentPoints.length > 0
|
|
36519
|
+
? [currentPoints[currentPoints.length - 1], pos]
|
|
36520
|
+
: [pos];
|
|
36521
|
+
// Apply segment to intersecting shapes in real-time
|
|
36522
|
+
const intersectingShapes = findIntersectingShapes(segment, state.shapes);
|
|
36523
|
+
if (intersectingShapes.length > 0) {
|
|
36524
|
+
const timestamp = Date.now();
|
|
36525
|
+
intersectingShapes.forEach((shape, index) => {
|
|
36526
|
+
const updatedShape = {
|
|
36527
|
+
...shape,
|
|
36528
|
+
erasePaths: [...(shape.erasePaths || []), segment],
|
|
36529
|
+
};
|
|
36530
|
+
// Update the shape locally
|
|
36531
|
+
dispatch({ type: 'UPDATE_SHAPE', payload: updatedShape });
|
|
36532
|
+
// Queue erase action for real-time collaboration
|
|
36533
|
+
if (queueAction) {
|
|
36534
|
+
const erasePayload = {
|
|
36535
|
+
shapeId: shape.id,
|
|
36536
|
+
erasePath: segment,
|
|
36537
|
+
timestamp: timestamp + index,
|
|
36538
|
+
};
|
|
36539
|
+
queueAction({
|
|
36540
|
+
type: 'erase',
|
|
36541
|
+
payload: erasePayload,
|
|
36542
|
+
timestamp: timestamp + index,
|
|
36543
|
+
});
|
|
36544
|
+
}
|
|
36545
|
+
});
|
|
36546
|
+
}
|
|
36457
36547
|
return;
|
|
36458
36548
|
}
|
|
36459
36549
|
let newPoints;
|
|
@@ -36503,7 +36593,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36503
36593
|
};
|
|
36504
36594
|
queueAction(continueAction);
|
|
36505
36595
|
mouseMoveThrottleRef.current = null;
|
|
36506
|
-
},
|
|
36596
|
+
}, 1); // Maximum frequency for ultra-smooth real-time collaboration
|
|
36507
36597
|
}
|
|
36508
36598
|
}, [hasToolAccess, state.isDrawing, state.tool, state.userId, state.color, state.strokeWidth, state.strokeStyle, state.opacity, currentShapeId, currentDrawingSessionId, currentPoints, queueAction, dispatch]);
|
|
36509
36599
|
const handleMouseUp = React.useCallback(() => {
|
|
@@ -36516,6 +36606,9 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36516
36606
|
setCurrentShapeId(null);
|
|
36517
36607
|
setCurrentDrawingSessionId(null);
|
|
36518
36608
|
lastPointerPosition.current = null;
|
|
36609
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36610
|
+
setKeepPreviewVisible(false);
|
|
36611
|
+
setJustErasedIds(new Set());
|
|
36519
36612
|
}
|
|
36520
36613
|
return;
|
|
36521
36614
|
}
|
|
@@ -36531,36 +36624,23 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36531
36624
|
setCurrentDrawingSessionId(null);
|
|
36532
36625
|
return;
|
|
36533
36626
|
}
|
|
36534
|
-
// Handle eraser tool -
|
|
36627
|
+
// Handle eraser tool - cleanup on mouse up (erasing already done in mouse move)
|
|
36535
36628
|
if (state.tool === 'eraser') {
|
|
36536
|
-
//
|
|
36537
|
-
|
|
36538
|
-
// Apply erase path to each intersecting shape
|
|
36539
|
-
intersectingShapes.forEach(shape => {
|
|
36540
|
-
const updatedShape = {
|
|
36541
|
-
...shape,
|
|
36542
|
-
erasePaths: [...(shape.erasePaths || []), currentPoints],
|
|
36543
|
-
};
|
|
36544
|
-
// Update the shape locally
|
|
36545
|
-
dispatch({ type: 'UPDATE_SHAPE', payload: updatedShape });
|
|
36546
|
-
// Queue erase action for collaboration
|
|
36547
|
-
if (queueAction) {
|
|
36548
|
-
const erasePayload = {
|
|
36549
|
-
shapeId: shape.id,
|
|
36550
|
-
erasePath: currentPoints,
|
|
36551
|
-
timestamp: Date.now(),
|
|
36552
|
-
};
|
|
36553
|
-
queueAction({
|
|
36554
|
-
type: 'erase',
|
|
36555
|
-
payload: erasePayload,
|
|
36556
|
-
});
|
|
36557
|
-
}
|
|
36558
|
-
});
|
|
36629
|
+
// Stop drawing state
|
|
36630
|
+
dispatch({ type: 'SET_DRAWING', payload: false });
|
|
36559
36631
|
// Reset erasing state
|
|
36560
36632
|
setCurrentPoints([]);
|
|
36561
36633
|
setCurrentShapeId(null);
|
|
36562
36634
|
setCurrentDrawingSessionId(null);
|
|
36563
36635
|
lastPointerPosition.current = null;
|
|
36636
|
+
// Clear preview after a brief delay to ensure smooth transition
|
|
36637
|
+
requestAnimationFrame(() => {
|
|
36638
|
+
requestAnimationFrame(() => {
|
|
36639
|
+
setEraserPreviewPoints([]);
|
|
36640
|
+
setKeepPreviewVisible(false);
|
|
36641
|
+
setJustErasedIds(new Set());
|
|
36642
|
+
});
|
|
36643
|
+
});
|
|
36564
36644
|
return;
|
|
36565
36645
|
}
|
|
36566
36646
|
// Handle regular drawing tools
|
|
@@ -36627,31 +36707,39 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36627
36707
|
if (shape.isEraser) {
|
|
36628
36708
|
return null;
|
|
36629
36709
|
}
|
|
36630
|
-
// Use ErasedShape component for shapes that have erase paths applied
|
|
36631
|
-
if (shape.erasePaths && shape.erasePaths.length > 0) {
|
|
36632
|
-
return (jsxRuntime.jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate }));
|
|
36633
|
-
}
|
|
36634
|
-
// Use original shape components for shapes without erase operations
|
|
36635
36710
|
const commonProps = {
|
|
36636
36711
|
shapeProps: shape,
|
|
36637
36712
|
isSelected: shape.isSelected || false,
|
|
36638
36713
|
onSelect: () => handleShapeClick(shape.id),
|
|
36639
36714
|
onUpdate: handleShapeUpdate,
|
|
36640
36715
|
};
|
|
36641
|
-
|
|
36642
|
-
|
|
36643
|
-
|
|
36644
|
-
|
|
36645
|
-
|
|
36646
|
-
|
|
36647
|
-
|
|
36648
|
-
|
|
36649
|
-
|
|
36650
|
-
|
|
36651
|
-
|
|
36652
|
-
|
|
36653
|
-
|
|
36716
|
+
// Render original shape component
|
|
36717
|
+
const OriginalShape = () => {
|
|
36718
|
+
switch (shape.type) {
|
|
36719
|
+
case 'rectangle':
|
|
36720
|
+
return jsxRuntime.jsx(Rectangle, { ...commonProps });
|
|
36721
|
+
case 'ellipse':
|
|
36722
|
+
return jsxRuntime.jsx(Ellipse, { ...commonProps });
|
|
36723
|
+
case 'line':
|
|
36724
|
+
return jsxRuntime.jsx(Line, { ...commonProps });
|
|
36725
|
+
case 'pencil':
|
|
36726
|
+
return jsxRuntime.jsx(FreehandDrawing, { ...commonProps });
|
|
36727
|
+
case 'arrow':
|
|
36728
|
+
return jsxRuntime.jsx(Arrow, { ...commonProps });
|
|
36729
|
+
default:
|
|
36730
|
+
return null;
|
|
36731
|
+
}
|
|
36732
|
+
};
|
|
36733
|
+
// Use ErasedShape component for shapes that have erase paths applied
|
|
36734
|
+
if (shape.erasePaths && shape.erasePaths.length > 0) {
|
|
36735
|
+
// If this shape just got erased, render both to prevent flicker
|
|
36736
|
+
if (justErasedIds.has(shape.id)) {
|
|
36737
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(OriginalShape, {}), jsxRuntime.jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate })] }));
|
|
36738
|
+
}
|
|
36739
|
+
return (jsxRuntime.jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate }));
|
|
36654
36740
|
}
|
|
36741
|
+
// Use original shape components for shapes without erase operations
|
|
36742
|
+
return jsxRuntime.jsx(OriginalShape, {});
|
|
36655
36743
|
}, (prevProps, nextProps) => {
|
|
36656
36744
|
// Ultra-precise comparison to prevent unnecessary re-renders during simultaneous drawing
|
|
36657
36745
|
const prevShape = prevProps.shape;
|
|
@@ -36758,11 +36846,11 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36758
36846
|
state.tool === 'select' ? 'default' :
|
|
36759
36847
|
state.tool === 'pan' ? 'grab' : 'crosshair'
|
|
36760
36848
|
}), [hasToolAccess, state.tool]);
|
|
36761
|
-
return (jsxRuntime.jsx("div", { ref: containerRef, className: "w-full h-full relative", style: { backgroundColor: state.backgroundColor }, children: jsxRuntime.
|
|
36762
|
-
|
|
36763
|
-
|
|
36764
|
-
|
|
36765
|
-
|
|
36849
|
+
return (jsxRuntime.jsx("div", { ref: containerRef, className: "w-full h-full relative", style: { backgroundColor: state.backgroundColor }, children: jsxRuntime.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: [jsxRuntime.jsxs(Layer, { children: [React.useMemo(() => {
|
|
36850
|
+
if (state.backgroundColor === 'transparent')
|
|
36851
|
+
return null;
|
|
36852
|
+
return (jsxRuntime.jsx(Rect, { x: 0, y: 0, width: size.width, height: size.height, fill: state.backgroundColor, listening: false }));
|
|
36853
|
+
}, [state.backgroundColor, size.width, size.height]), renderedShapes, renderedActiveDrawings, renderCurrentShape] }), jsxRuntime.jsx(Layer, { listening: false, children: eraserPreviewPoints.length > 0 && (state.isDrawing || keepPreviewVisible) && state.tool === 'eraser' && (jsxRuntime.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 })) })] }) }));
|
|
36766
36854
|
});
|
|
36767
36855
|
// Memoize the Board component to prevent unnecessary re-renders
|
|
36768
36856
|
const Board = React.memo(BoardComponent, (prevProps, nextProps) => {
|
|
@@ -37169,13 +37257,15 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37169
37257
|
setIsInitialized(true);
|
|
37170
37258
|
}
|
|
37171
37259
|
}, [state]);
|
|
37172
|
-
// Set white as default stroke color when video background is
|
|
37260
|
+
// Set white as default stroke color when video background is first activated (only once)
|
|
37261
|
+
const hasVideoBackgroundRef = React.useRef(false);
|
|
37173
37262
|
React.useEffect(() => {
|
|
37174
|
-
|
|
37175
|
-
|
|
37263
|
+
// Only auto-switch to white on first video activation, and only if currently black
|
|
37264
|
+
if (hasVideoBackground && !hasVideoBackgroundRef.current && state.color === '#000000') {
|
|
37176
37265
|
dispatch({ type: 'SET_COLOR', payload: '#FFFFFF' });
|
|
37177
37266
|
}
|
|
37178
|
-
|
|
37267
|
+
hasVideoBackgroundRef.current = hasVideoBackground;
|
|
37268
|
+
}, [hasVideoBackground, dispatch]);
|
|
37179
37269
|
// Track initial access grant
|
|
37180
37270
|
React.useEffect(() => {
|
|
37181
37271
|
if (hasToolAccess && !hasEverHadAccess) {
|
|
@@ -37707,15 +37797,15 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37707
37797
|
totalActions: 0,
|
|
37708
37798
|
});
|
|
37709
37799
|
// Throttling configuration - Ultra-optimized for smooth real-time collaboration
|
|
37710
|
-
const THROTTLE_DELAY =
|
|
37711
|
-
const MAX_ACTIONS_PER_BATCH =
|
|
37800
|
+
const THROTTLE_DELAY = 10; // ms - Minimal delay for faster transmission
|
|
37801
|
+
const MAX_ACTIONS_PER_BATCH = 8; // Smaller batches for faster transmission
|
|
37712
37802
|
const MAX_MESSAGE_SIZE = 500; // characters - matches API constraint
|
|
37713
|
-
const MAX_MESSAGES_PER_SECOND =
|
|
37803
|
+
const MAX_MESSAGES_PER_SECOND = 30; // Maximum rate for smoother collaboration
|
|
37714
37804
|
const MAX_PAYLOAD_SIZE = 1024; // bytes (1KB) - matches API constraint
|
|
37715
37805
|
// Drawing-specific throttling for ultra-smooth real-time collaboration
|
|
37716
|
-
const DRAWING_THROTTLE_DELAY =
|
|
37717
|
-
const DRAWING_BATCH_SIZE =
|
|
37718
|
-
const DRAWING_IMMEDIATE_THRESHOLD =
|
|
37806
|
+
const DRAWING_THROTTLE_DELAY = 1; // ms - Maximum frequency for ultra-smooth drawing actions
|
|
37807
|
+
const DRAWING_BATCH_SIZE = 2; // Minimal batches for immediate transmission
|
|
37808
|
+
const DRAWING_IMMEDIATE_THRESHOLD = 1; // Send immediately if we have 1+ drawing actions
|
|
37719
37809
|
// Message rate limiting
|
|
37720
37810
|
const messageTimestampsRef = React.useRef([]);
|
|
37721
37811
|
const isRateLimited = React.useCallback(() => {
|
|
@@ -37879,7 +37969,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37879
37969
|
let delay = THROTTLE_DELAY;
|
|
37880
37970
|
if (remainingDrawingActions.length > 0) {
|
|
37881
37971
|
delay = remainingDrawingActions.length >= DRAWING_IMMEDIATE_THRESHOLD ?
|
|
37882
|
-
Math.max(DRAWING_THROTTLE_DELAY / 2,
|
|
37972
|
+
Math.max(DRAWING_THROTTLE_DELAY / 2, 2) : // Even faster for multiple actions
|
|
37883
37973
|
DRAWING_THROTTLE_DELAY;
|
|
37884
37974
|
}
|
|
37885
37975
|
throttleTimerRef.current = setTimeout(() => {
|
|
@@ -38022,7 +38112,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38022
38112
|
clearTimeout(throttleTimerRef.current);
|
|
38023
38113
|
throttleTimerRef.current = setTimeout(() => {
|
|
38024
38114
|
transmitRef.current?.();
|
|
38025
|
-
},
|
|
38115
|
+
}, 1); // Ultra-fast transmission - 1ms for maximum responsiveness
|
|
38026
38116
|
}
|
|
38027
38117
|
}
|
|
38028
38118
|
}, [state.canvasSize, state.userId, roomId, callbacks, sendWithConstraints]);
|
|
@@ -38042,7 +38132,11 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38042
38132
|
// Process each action in the batch with duplicate prevention
|
|
38043
38133
|
parsedData.actions.forEach(action => {
|
|
38044
38134
|
// Create unique action ID for deduplication
|
|
38045
|
-
|
|
38135
|
+
// For erase actions, include shapeId to allow multiple erases in same stroke
|
|
38136
|
+
let actionId = `${action.type}-${parsedData.userId}-${action.timestamp || Date.now()}`;
|
|
38137
|
+
if (action.type === 'erase' && action.payload && typeof action.payload === 'object' && 'shapeId' in action.payload) {
|
|
38138
|
+
actionId = `${action.type}-${parsedData.userId}-${action.payload.shapeId}-${action.timestamp || Date.now()}`;
|
|
38139
|
+
}
|
|
38046
38140
|
// Skip if we've already processed this action (prevents shape loss from duplicate processing)
|
|
38047
38141
|
if (processedActionsRef.current.has(actionId)) {
|
|
38048
38142
|
return;
|