@ngenux/ngage-whiteboarding 1.0.6 → 1.0.8
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/README.md +344 -344
- package/dist/index.esm.js +239 -86
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +239 -86
- 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/index.d.ts.map +1 -1
- package/dist/src/context/WhiteboardContext.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/src/utils/video-coordinates.d.ts +0 -36
- package/dist/src/utils/video-coordinates.d.ts.map +0 -1
package/dist/index.esm.js
CHANGED
|
@@ -34757,12 +34757,15 @@ const whiteboardReducer = (state, action) => {
|
|
|
34757
34757
|
// Prevent duplicate shapes during simultaneous drawing
|
|
34758
34758
|
const newShape = action.payload;
|
|
34759
34759
|
const existingShapeIndex = state.shapes.findIndex(shape => shape.id === newShape.id);
|
|
34760
|
+
// Ensure shape has timestamp for proper ordering and stale action filtering
|
|
34761
|
+
const timestamp = newShape.timestamp || Date.now();
|
|
34760
34762
|
// If shape already exists, update it instead of adding duplicate
|
|
34761
34763
|
if (existingShapeIndex >= 0) {
|
|
34762
34764
|
const updatedShapes = [...state.shapes];
|
|
34763
34765
|
updatedShapes[existingShapeIndex] = {
|
|
34764
34766
|
...newShape,
|
|
34765
|
-
|
|
34767
|
+
timestamp,
|
|
34768
|
+
drawingSessionId: newShape.drawingSessionId || `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`
|
|
34766
34769
|
};
|
|
34767
34770
|
return {
|
|
34768
34771
|
...state,
|
|
@@ -34770,10 +34773,11 @@ const whiteboardReducer = (state, action) => {
|
|
|
34770
34773
|
currentDrawingShape: undefined,
|
|
34771
34774
|
};
|
|
34772
34775
|
}
|
|
34773
|
-
// Add new shape with session tracking
|
|
34776
|
+
// Add new shape with session tracking and timestamp
|
|
34774
34777
|
const shapeWithSession = {
|
|
34775
34778
|
...newShape,
|
|
34776
|
-
|
|
34779
|
+
timestamp,
|
|
34780
|
+
drawingSessionId: newShape.drawingSessionId || `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`
|
|
34777
34781
|
};
|
|
34778
34782
|
const newShapes = [...state.shapes, shapeWithSession];
|
|
34779
34783
|
const newHistory = state.history.slice(0, state.historyIndex + 1);
|
|
@@ -34967,11 +34971,13 @@ const whiteboardReducer = (state, action) => {
|
|
|
34967
34971
|
const userShapes = state.shapes
|
|
34968
34972
|
.filter(shape => shape.userId === userId)
|
|
34969
34973
|
.sort((a, b) => {
|
|
34970
|
-
// Sort by
|
|
34971
|
-
|
|
34974
|
+
// Sort by timestamp to ensure LIFO (Last In First Out) order
|
|
34975
|
+
const timestampA = a.timestamp || 0;
|
|
34976
|
+
const timestampB = b.timestamp || 0;
|
|
34977
|
+
return timestampA - timestampB;
|
|
34972
34978
|
});
|
|
34973
34979
|
if (userShapes.length > 0) {
|
|
34974
|
-
// Get the most recent shape
|
|
34980
|
+
// Get the most recent shape (last in sorted array)
|
|
34975
34981
|
const lastUserShape = userShapes[userShapes.length - 1];
|
|
34976
34982
|
// Check if there are multiple shapes that were part of the same drawing session
|
|
34977
34983
|
// Use drawingSessionId if available, otherwise fall back to time-based detection
|
|
@@ -35265,9 +35271,16 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35265
35271
|
// Drawing actions should be filtered if they're older than the last clear
|
|
35266
35272
|
if (action.type === 'start_draw' || action.type === 'continue_draw' || action.type === 'end_draw' || action.type === 'add') {
|
|
35267
35273
|
const actionTimestamp = action.timestamp || 0;
|
|
35268
|
-
if (actionTimestamp <= state.lastClearTimestamp) {
|
|
35274
|
+
if (actionTimestamp > 0 && actionTimestamp <= state.lastClearTimestamp) {
|
|
35269
35275
|
return true;
|
|
35270
35276
|
}
|
|
35277
|
+
// Also check timestamp in the payload if it exists
|
|
35278
|
+
if (typeof action.payload === 'object' && action.payload !== null && !Array.isArray(action.payload)) {
|
|
35279
|
+
const payloadTimestamp = action.payload.timestamp || 0;
|
|
35280
|
+
if (payloadTimestamp > 0 && payloadTimestamp <= state.lastClearTimestamp) {
|
|
35281
|
+
return true;
|
|
35282
|
+
}
|
|
35283
|
+
}
|
|
35271
35284
|
}
|
|
35272
35285
|
return false;
|
|
35273
35286
|
};
|
|
@@ -35297,10 +35310,15 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35297
35310
|
if (syncState.shapes.length === 0) {
|
|
35298
35311
|
return; // Don't apply empty state or keep requesting
|
|
35299
35312
|
}
|
|
35300
|
-
//
|
|
35301
|
-
|
|
35313
|
+
// Filter out shapes that are older than our last clear
|
|
35314
|
+
const validShapes = syncState.shapes.filter(shape => {
|
|
35315
|
+
const shapeTimestamp = shape.timestamp || 0;
|
|
35316
|
+
return shapeTimestamp === 0 || shapeTimestamp > state.lastClearTimestamp;
|
|
35317
|
+
});
|
|
35318
|
+
// Only apply if the received state has valid shapes
|
|
35319
|
+
if (validShapes.length > 0 && (state.shapes.length === 0 || validShapes.length > state.shapes.length)) {
|
|
35302
35320
|
// All shapes from sync_state should have normalized coordinates, denormalize them
|
|
35303
|
-
const denormalizedShapes =
|
|
35321
|
+
const denormalizedShapes = validShapes.map((shape, index) => {
|
|
35304
35322
|
return denormalizeShape(shape);
|
|
35305
35323
|
});
|
|
35306
35324
|
// Apply the synchronized state
|
|
@@ -35318,7 +35336,8 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35318
35336
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35319
35337
|
// Additional check to prevent adding shapes from before the last clear
|
|
35320
35338
|
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35321
|
-
if (shapeTimestamp <= state.lastClearTimestamp) {
|
|
35339
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35340
|
+
console.warn(`[APPLY_ACTION] Skipping stale shape from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
|
|
35322
35341
|
break;
|
|
35323
35342
|
}
|
|
35324
35343
|
dispatch({ type: 'ADD_SHAPE', payload: denormalizedShape });
|
|
@@ -35372,6 +35391,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35372
35391
|
case 'start_draw':
|
|
35373
35392
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35374
35393
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35394
|
+
// Check if this shape is from before the last clear
|
|
35395
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35396
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35397
|
+
break; // Skip stale shapes
|
|
35398
|
+
}
|
|
35375
35399
|
// Only apply collaborative start_draw if it's from another user
|
|
35376
35400
|
if (denormalizedShape.userId !== state.userId) {
|
|
35377
35401
|
// Add to active drawings for real-time collaborative visibility
|
|
@@ -35385,6 +35409,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35385
35409
|
case 'continue_draw':
|
|
35386
35410
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35387
35411
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35412
|
+
// Check if this shape is from before the last clear
|
|
35413
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35414
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35415
|
+
break; // Skip stale shapes
|
|
35416
|
+
}
|
|
35388
35417
|
// Only apply collaborative drawing updates if it's not from the current user
|
|
35389
35418
|
// to avoid interfering with local real-time drawing
|
|
35390
35419
|
if (denormalizedShape.userId !== state.userId) {
|
|
@@ -35399,6 +35428,12 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35399
35428
|
case 'end_draw':
|
|
35400
35429
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35401
35430
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35431
|
+
// Check if this shape is from before the last clear
|
|
35432
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35433
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35434
|
+
console.warn(`[APPLY_ACTION] Skipping stale end_draw from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
|
|
35435
|
+
break; // Skip stale shapes
|
|
35436
|
+
}
|
|
35402
35437
|
// Only apply collaborative end_draw if it's from another user
|
|
35403
35438
|
// Local user's end_draw is handled directly in Board component via ADD_SHAPE
|
|
35404
35439
|
if (denormalizedShape.userId !== state.userId) {
|
|
@@ -35903,7 +35938,7 @@ const Arrow = React__default.memo(({ shapeProps, isSelected, onSelect, onUpdate,
|
|
|
35903
35938
|
});
|
|
35904
35939
|
|
|
35905
35940
|
const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
35906
|
-
const [
|
|
35941
|
+
const [bitmap, setBitmap] = useState(null);
|
|
35907
35942
|
const canvasRef = useRef(null);
|
|
35908
35943
|
// Memoize bounds calculation to avoid unnecessary recalculations
|
|
35909
35944
|
const bounds = useMemo(() => calculateShapeBounds(shapeProps), [shapeProps.points, shapeProps.strokeWidth, shapeProps.erasePaths]);
|
|
@@ -35933,22 +35968,24 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
35933
35968
|
});
|
|
35934
35969
|
}
|
|
35935
35970
|
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();
|
|
35971
|
+
// Use canvas directly as Konva image - no async
|
|
35972
|
+
setBitmap(canvas);
|
|
35946
35973
|
return () => {
|
|
35947
35974
|
if (canvasRef.current) {
|
|
35948
35975
|
canvasRef.current.remove();
|
|
35976
|
+
canvasRef.current = null;
|
|
35949
35977
|
}
|
|
35950
35978
|
};
|
|
35951
|
-
}, [
|
|
35979
|
+
}, [
|
|
35980
|
+
bounds,
|
|
35981
|
+
shapeProps.points,
|
|
35982
|
+
shapeProps.erasePaths,
|
|
35983
|
+
shapeProps.stroke,
|
|
35984
|
+
shapeProps.strokeWidth,
|
|
35985
|
+
shapeProps.opacity,
|
|
35986
|
+
shapeProps.strokeStyle,
|
|
35987
|
+
shapeProps.type
|
|
35988
|
+
]);
|
|
35952
35989
|
// Calculate bounds of the shape including erase paths
|
|
35953
35990
|
function calculateShapeBounds(shape) {
|
|
35954
35991
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
@@ -36067,11 +36104,48 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
36067
36104
|
ctx.stroke();
|
|
36068
36105
|
}
|
|
36069
36106
|
};
|
|
36070
|
-
if (!imageElement) {
|
|
36071
|
-
return null; // Still rendering
|
|
36072
|
-
}
|
|
36073
36107
|
const padding = 50;
|
|
36074
|
-
|
|
36108
|
+
// Render original shape as fallback while bitmap is being created
|
|
36109
|
+
if (!bitmap) {
|
|
36110
|
+
const commonProps = {
|
|
36111
|
+
onClick: onSelect,
|
|
36112
|
+
onTap: onSelect,
|
|
36113
|
+
listening: true,
|
|
36114
|
+
stroke: shapeProps.stroke,
|
|
36115
|
+
strokeWidth: shapeProps.strokeWidth,
|
|
36116
|
+
opacity: shapeProps.opacity,
|
|
36117
|
+
dash: shapeProps.strokeStyle === 'dashed' ? [5, 5] : shapeProps.strokeStyle === 'dotted' ? [2, 3] : undefined,
|
|
36118
|
+
};
|
|
36119
|
+
switch (shapeProps.type) {
|
|
36120
|
+
case 'pencil':
|
|
36121
|
+
return (jsx(Line$1, { points: shapeProps.points.flatMap(p => [p.x, p.y]), lineCap: "round", lineJoin: "round", ...commonProps }));
|
|
36122
|
+
case 'line':
|
|
36123
|
+
if (shapeProps.points.length >= 2) {
|
|
36124
|
+
return (jsx(Line$1, { points: [
|
|
36125
|
+
shapeProps.points[0].x,
|
|
36126
|
+
shapeProps.points[0].y,
|
|
36127
|
+
shapeProps.points[1].x,
|
|
36128
|
+
shapeProps.points[1].y,
|
|
36129
|
+
], ...commonProps }));
|
|
36130
|
+
}
|
|
36131
|
+
return null;
|
|
36132
|
+
case 'rectangle':
|
|
36133
|
+
if (shapeProps.points.length >= 2) {
|
|
36134
|
+
const [start, end] = shapeProps.points;
|
|
36135
|
+
return (jsx(Rect, { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y, ...commonProps }));
|
|
36136
|
+
}
|
|
36137
|
+
return null;
|
|
36138
|
+
case 'ellipse':
|
|
36139
|
+
if (shapeProps.points.length >= 2) {
|
|
36140
|
+
const [start, end] = shapeProps.points;
|
|
36141
|
+
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 }));
|
|
36142
|
+
}
|
|
36143
|
+
return null;
|
|
36144
|
+
default:
|
|
36145
|
+
return null;
|
|
36146
|
+
}
|
|
36147
|
+
}
|
|
36148
|
+
return (jsx(Image$1, { image: bitmap, x: bounds.minX - padding, y: bounds.minY - padding, draggable: isSelected, onClick: onSelect, onTap: onSelect, onDragEnd: (e) => {
|
|
36075
36149
|
const newX = e.target.x();
|
|
36076
36150
|
const newY = e.target.y();
|
|
36077
36151
|
const deltaX = newX - (bounds.minX - padding);
|
|
@@ -36111,6 +36185,10 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36111
36185
|
const containerRef = useRef(null);
|
|
36112
36186
|
const lastPointerPosition = useRef(null);
|
|
36113
36187
|
const mouseMoveThrottleRef = useRef(null);
|
|
36188
|
+
// NEW: Eraser preview state for real-time visual feedback
|
|
36189
|
+
const [eraserPreviewPoints, setEraserPreviewPoints] = useState([]);
|
|
36190
|
+
const [keepPreviewVisible, setKeepPreviewVisible] = useState(false);
|
|
36191
|
+
const [justErasedIds, setJustErasedIds] = useState(new Set());
|
|
36114
36192
|
// Find shapes that intersect with the erase path
|
|
36115
36193
|
const findIntersectingShapes = (erasePath, shapes) => {
|
|
36116
36194
|
const eraseRadius = 10; // Half of eraser width
|
|
@@ -36201,8 +36279,20 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36201
36279
|
setCurrentShapeId(null);
|
|
36202
36280
|
setCurrentDrawingSessionId(null);
|
|
36203
36281
|
lastPointerPosition.current = null;
|
|
36282
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36283
|
+
setKeepPreviewVisible(false);
|
|
36284
|
+
setJustErasedIds(new Set());
|
|
36204
36285
|
}
|
|
36205
36286
|
}, [hasToolAccess, state.isDrawing, dispatch]);
|
|
36287
|
+
// Clear justErasedIds after one frame to complete transition
|
|
36288
|
+
useEffect(() => {
|
|
36289
|
+
if (justErasedIds.size > 0) {
|
|
36290
|
+
const id = requestAnimationFrame(() => {
|
|
36291
|
+
setJustErasedIds(new Set());
|
|
36292
|
+
});
|
|
36293
|
+
return () => cancelAnimationFrame(id);
|
|
36294
|
+
}
|
|
36295
|
+
}, [justErasedIds]);
|
|
36206
36296
|
// Memoized export functionality for performance
|
|
36207
36297
|
const exportAsImage = useCallback((format = 'png') => {
|
|
36208
36298
|
if (!stageRef.current) {
|
|
@@ -36365,12 +36455,14 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36365
36455
|
if (state.tool === 'eraser') {
|
|
36366
36456
|
dispatch({ type: 'SET_DRAWING', payload: true });
|
|
36367
36457
|
setCurrentPoints([pos]);
|
|
36458
|
+
setEraserPreviewPoints([pos]); // NEW: start preview stroke
|
|
36368
36459
|
setCurrentShapeId('erasing'); // Special ID for erasing mode
|
|
36369
36460
|
return;
|
|
36370
36461
|
}
|
|
36371
36462
|
// Create new shape ID for regular drawing tools
|
|
36372
36463
|
const newShapeId = v4();
|
|
36373
|
-
const
|
|
36464
|
+
const timestamp = Date.now();
|
|
36465
|
+
const newDrawingSessionId = `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`;
|
|
36374
36466
|
setCurrentShapeId(newShapeId);
|
|
36375
36467
|
setCurrentDrawingSessionId(newDrawingSessionId);
|
|
36376
36468
|
// Start drawing
|
|
@@ -36387,6 +36479,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36387
36479
|
strokeStyle: state.strokeStyle,
|
|
36388
36480
|
opacity: state.opacity,
|
|
36389
36481
|
drawingSessionId: newDrawingSessionId,
|
|
36482
|
+
timestamp,
|
|
36390
36483
|
// Initialize transformation properties
|
|
36391
36484
|
x: 0,
|
|
36392
36485
|
y: 0,
|
|
@@ -36413,6 +36506,9 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36413
36506
|
setCurrentShapeId(null);
|
|
36414
36507
|
setCurrentDrawingSessionId(null);
|
|
36415
36508
|
lastPointerPosition.current = null;
|
|
36509
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36510
|
+
setKeepPreviewVisible(false);
|
|
36511
|
+
setJustErasedIds(new Set());
|
|
36416
36512
|
}
|
|
36417
36513
|
return;
|
|
36418
36514
|
}
|
|
@@ -36430,10 +36526,41 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36430
36526
|
return; // Ultra-sensitive for maximum smoothness
|
|
36431
36527
|
}
|
|
36432
36528
|
lastPointerPosition.current = pos;
|
|
36433
|
-
// Handle eraser tool -
|
|
36529
|
+
// Handle eraser tool - collect points and stream erase in real-time
|
|
36434
36530
|
if (state.tool === 'eraser') {
|
|
36435
36531
|
const newPoints = [...currentPoints, pos];
|
|
36436
36532
|
setCurrentPoints(newPoints);
|
|
36533
|
+
setEraserPreviewPoints(newPoints); // Live preview
|
|
36534
|
+
// Create segment from last point to current point for real-time streaming
|
|
36535
|
+
const segment = currentPoints.length > 0
|
|
36536
|
+
? [currentPoints[currentPoints.length - 1], pos]
|
|
36537
|
+
: [pos];
|
|
36538
|
+
// Apply segment to intersecting shapes in real-time
|
|
36539
|
+
const intersectingShapes = findIntersectingShapes(segment, state.shapes);
|
|
36540
|
+
if (intersectingShapes.length > 0) {
|
|
36541
|
+
const timestamp = Date.now();
|
|
36542
|
+
intersectingShapes.forEach((shape, index) => {
|
|
36543
|
+
const updatedShape = {
|
|
36544
|
+
...shape,
|
|
36545
|
+
erasePaths: [...(shape.erasePaths || []), segment],
|
|
36546
|
+
};
|
|
36547
|
+
// Update the shape locally
|
|
36548
|
+
dispatch({ type: 'UPDATE_SHAPE', payload: updatedShape });
|
|
36549
|
+
// Queue erase action for real-time collaboration
|
|
36550
|
+
if (queueAction) {
|
|
36551
|
+
const erasePayload = {
|
|
36552
|
+
shapeId: shape.id,
|
|
36553
|
+
erasePath: segment,
|
|
36554
|
+
timestamp: timestamp + index,
|
|
36555
|
+
};
|
|
36556
|
+
queueAction({
|
|
36557
|
+
type: 'erase',
|
|
36558
|
+
payload: erasePayload,
|
|
36559
|
+
timestamp: timestamp + index,
|
|
36560
|
+
});
|
|
36561
|
+
}
|
|
36562
|
+
});
|
|
36563
|
+
}
|
|
36437
36564
|
return;
|
|
36438
36565
|
}
|
|
36439
36566
|
let newPoints;
|
|
@@ -36465,6 +36592,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36465
36592
|
strokeStyle: state.strokeStyle,
|
|
36466
36593
|
opacity: state.opacity,
|
|
36467
36594
|
drawingSessionId: currentDrawingSessionId,
|
|
36595
|
+
timestamp: Date.now(),
|
|
36468
36596
|
// Initialize transformation properties
|
|
36469
36597
|
x: 0,
|
|
36470
36598
|
y: 0,
|
|
@@ -36483,7 +36611,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36483
36611
|
};
|
|
36484
36612
|
queueAction(continueAction);
|
|
36485
36613
|
mouseMoveThrottleRef.current = null;
|
|
36486
|
-
},
|
|
36614
|
+
}, 1); // Maximum frequency for ultra-smooth real-time collaboration
|
|
36487
36615
|
}
|
|
36488
36616
|
}, [hasToolAccess, state.isDrawing, state.tool, state.userId, state.color, state.strokeWidth, state.strokeStyle, state.opacity, currentShapeId, currentDrawingSessionId, currentPoints, queueAction, dispatch]);
|
|
36489
36617
|
const handleMouseUp = useCallback(() => {
|
|
@@ -36496,6 +36624,9 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36496
36624
|
setCurrentShapeId(null);
|
|
36497
36625
|
setCurrentDrawingSessionId(null);
|
|
36498
36626
|
lastPointerPosition.current = null;
|
|
36627
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36628
|
+
setKeepPreviewVisible(false);
|
|
36629
|
+
setJustErasedIds(new Set());
|
|
36499
36630
|
}
|
|
36500
36631
|
return;
|
|
36501
36632
|
}
|
|
@@ -36511,36 +36642,23 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36511
36642
|
setCurrentDrawingSessionId(null);
|
|
36512
36643
|
return;
|
|
36513
36644
|
}
|
|
36514
|
-
// Handle eraser tool -
|
|
36645
|
+
// Handle eraser tool - cleanup on mouse up (erasing already done in mouse move)
|
|
36515
36646
|
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
|
-
});
|
|
36647
|
+
// Stop drawing state
|
|
36648
|
+
dispatch({ type: 'SET_DRAWING', payload: false });
|
|
36539
36649
|
// Reset erasing state
|
|
36540
36650
|
setCurrentPoints([]);
|
|
36541
36651
|
setCurrentShapeId(null);
|
|
36542
36652
|
setCurrentDrawingSessionId(null);
|
|
36543
36653
|
lastPointerPosition.current = null;
|
|
36654
|
+
// Clear preview after a brief delay to ensure smooth transition
|
|
36655
|
+
requestAnimationFrame(() => {
|
|
36656
|
+
requestAnimationFrame(() => {
|
|
36657
|
+
setEraserPreviewPoints([]);
|
|
36658
|
+
setKeepPreviewVisible(false);
|
|
36659
|
+
setJustErasedIds(new Set());
|
|
36660
|
+
});
|
|
36661
|
+
});
|
|
36544
36662
|
return;
|
|
36545
36663
|
}
|
|
36546
36664
|
// Handle regular drawing tools
|
|
@@ -36554,6 +36672,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36554
36672
|
strokeStyle: state.strokeStyle,
|
|
36555
36673
|
opacity: state.opacity,
|
|
36556
36674
|
drawingSessionId: currentDrawingSessionId,
|
|
36675
|
+
timestamp: Date.now(),
|
|
36557
36676
|
// Initialize transformation properties
|
|
36558
36677
|
x: 0,
|
|
36559
36678
|
y: 0,
|
|
@@ -36607,31 +36726,39 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36607
36726
|
if (shape.isEraser) {
|
|
36608
36727
|
return null;
|
|
36609
36728
|
}
|
|
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
36729
|
const commonProps = {
|
|
36616
36730
|
shapeProps: shape,
|
|
36617
36731
|
isSelected: shape.isSelected || false,
|
|
36618
36732
|
onSelect: () => handleShapeClick(shape.id),
|
|
36619
36733
|
onUpdate: handleShapeUpdate,
|
|
36620
36734
|
};
|
|
36621
|
-
|
|
36622
|
-
|
|
36623
|
-
|
|
36624
|
-
|
|
36625
|
-
|
|
36626
|
-
|
|
36627
|
-
|
|
36628
|
-
|
|
36629
|
-
|
|
36630
|
-
|
|
36631
|
-
|
|
36632
|
-
|
|
36633
|
-
|
|
36735
|
+
// Render original shape component
|
|
36736
|
+
const OriginalShape = () => {
|
|
36737
|
+
switch (shape.type) {
|
|
36738
|
+
case 'rectangle':
|
|
36739
|
+
return jsx(Rectangle, { ...commonProps });
|
|
36740
|
+
case 'ellipse':
|
|
36741
|
+
return jsx(Ellipse, { ...commonProps });
|
|
36742
|
+
case 'line':
|
|
36743
|
+
return jsx(Line, { ...commonProps });
|
|
36744
|
+
case 'pencil':
|
|
36745
|
+
return jsx(FreehandDrawing, { ...commonProps });
|
|
36746
|
+
case 'arrow':
|
|
36747
|
+
return jsx(Arrow, { ...commonProps });
|
|
36748
|
+
default:
|
|
36749
|
+
return null;
|
|
36750
|
+
}
|
|
36751
|
+
};
|
|
36752
|
+
// Use ErasedShape component for shapes that have erase paths applied
|
|
36753
|
+
if (shape.erasePaths && shape.erasePaths.length > 0) {
|
|
36754
|
+
// If this shape just got erased, render both to prevent flicker
|
|
36755
|
+
if (justErasedIds.has(shape.id)) {
|
|
36756
|
+
return (jsxs(Fragment, { children: [jsx(OriginalShape, {}), jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate })] }));
|
|
36757
|
+
}
|
|
36758
|
+
return (jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate }));
|
|
36634
36759
|
}
|
|
36760
|
+
// Use original shape components for shapes without erase operations
|
|
36761
|
+
return jsx(OriginalShape, {});
|
|
36635
36762
|
}, (prevProps, nextProps) => {
|
|
36636
36763
|
// Ultra-precise comparison to prevent unnecessary re-renders during simultaneous drawing
|
|
36637
36764
|
const prevShape = prevProps.shape;
|
|
@@ -36738,11 +36865,11 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36738
36865
|
state.tool === 'select' ? 'default' :
|
|
36739
36866
|
state.tool === 'pan' ? 'grab' : 'crosshair'
|
|
36740
36867
|
}), [hasToolAccess, state.tool]);
|
|
36741
|
-
return (jsx("div", { ref: containerRef, className: "w-full h-full relative", style: { backgroundColor: state.backgroundColor }, children:
|
|
36742
|
-
|
|
36743
|
-
|
|
36744
|
-
|
|
36745
|
-
|
|
36868
|
+
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(() => {
|
|
36869
|
+
if (state.backgroundColor === 'transparent')
|
|
36870
|
+
return null;
|
|
36871
|
+
return (jsx(Rect, { x: 0, y: 0, width: size.width, height: size.height, fill: state.backgroundColor, listening: false }));
|
|
36872
|
+
}, [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
36873
|
});
|
|
36747
36874
|
// Memoize the Board component to prevent unnecessary re-renders
|
|
36748
36875
|
const Board = React__default.memo(BoardComponent, (prevProps, nextProps) => {
|
|
@@ -37689,15 +37816,15 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37689
37816
|
totalActions: 0,
|
|
37690
37817
|
});
|
|
37691
37818
|
// Throttling configuration - Ultra-optimized for smooth real-time collaboration
|
|
37692
|
-
const THROTTLE_DELAY =
|
|
37693
|
-
const MAX_ACTIONS_PER_BATCH =
|
|
37819
|
+
const THROTTLE_DELAY = 10; // ms - Minimal delay for faster transmission
|
|
37820
|
+
const MAX_ACTIONS_PER_BATCH = 8; // Smaller batches for faster transmission
|
|
37694
37821
|
const MAX_MESSAGE_SIZE = 500; // characters - matches API constraint
|
|
37695
|
-
const MAX_MESSAGES_PER_SECOND =
|
|
37822
|
+
const MAX_MESSAGES_PER_SECOND = 30; // Maximum rate for smoother collaboration
|
|
37696
37823
|
const MAX_PAYLOAD_SIZE = 1024; // bytes (1KB) - matches API constraint
|
|
37697
37824
|
// Drawing-specific throttling for ultra-smooth real-time collaboration
|
|
37698
|
-
const DRAWING_THROTTLE_DELAY =
|
|
37699
|
-
const DRAWING_BATCH_SIZE =
|
|
37700
|
-
const DRAWING_IMMEDIATE_THRESHOLD =
|
|
37825
|
+
const DRAWING_THROTTLE_DELAY = 1; // ms - Maximum frequency for ultra-smooth drawing actions
|
|
37826
|
+
const DRAWING_BATCH_SIZE = 2; // Minimal batches for immediate transmission
|
|
37827
|
+
const DRAWING_IMMEDIATE_THRESHOLD = 1; // Send immediately if we have 1+ drawing actions
|
|
37701
37828
|
// Message rate limiting
|
|
37702
37829
|
const messageTimestampsRef = useRef([]);
|
|
37703
37830
|
const isRateLimited = useCallback(() => {
|
|
@@ -37861,7 +37988,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37861
37988
|
let delay = THROTTLE_DELAY;
|
|
37862
37989
|
if (remainingDrawingActions.length > 0) {
|
|
37863
37990
|
delay = remainingDrawingActions.length >= DRAWING_IMMEDIATE_THRESHOLD ?
|
|
37864
|
-
Math.max(DRAWING_THROTTLE_DELAY / 2,
|
|
37991
|
+
Math.max(DRAWING_THROTTLE_DELAY / 2, 2) : // Even faster for multiple actions
|
|
37865
37992
|
DRAWING_THROTTLE_DELAY;
|
|
37866
37993
|
}
|
|
37867
37994
|
throttleTimerRef.current = setTimeout(() => {
|
|
@@ -38004,7 +38131,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38004
38131
|
clearTimeout(throttleTimerRef.current);
|
|
38005
38132
|
throttleTimerRef.current = setTimeout(() => {
|
|
38006
38133
|
transmitRef.current?.();
|
|
38007
|
-
},
|
|
38134
|
+
}, 1); // Ultra-fast transmission - 1ms for maximum responsiveness
|
|
38008
38135
|
}
|
|
38009
38136
|
}
|
|
38010
38137
|
}, [state.canvasSize, state.userId, roomId, callbacks, sendWithConstraints]);
|
|
@@ -38024,7 +38151,11 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38024
38151
|
// Process each action in the batch with duplicate prevention
|
|
38025
38152
|
parsedData.actions.forEach(action => {
|
|
38026
38153
|
// Create unique action ID for deduplication
|
|
38027
|
-
|
|
38154
|
+
// For erase actions, include shapeId to allow multiple erases in same stroke
|
|
38155
|
+
let actionId = `${action.type}-${parsedData.userId}-${action.timestamp || Date.now()}`;
|
|
38156
|
+
if (action.type === 'erase' && action.payload && typeof action.payload === 'object' && 'shapeId' in action.payload) {
|
|
38157
|
+
actionId = `${action.type}-${parsedData.userId}-${action.payload.shapeId}-${action.timestamp || Date.now()}`;
|
|
38158
|
+
}
|
|
38028
38159
|
// Skip if we've already processed this action (prevents shape loss from duplicate processing)
|
|
38029
38160
|
if (processedActionsRef.current.has(actionId)) {
|
|
38030
38161
|
return;
|
|
@@ -42643,6 +42774,28 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42643
42774
|
leaveRoom(roomId);
|
|
42644
42775
|
};
|
|
42645
42776
|
}, [roomId]);
|
|
42777
|
+
// Clear all canvases on component unmount if admin leaves
|
|
42778
|
+
useEffect(() => {
|
|
42779
|
+
return () => {
|
|
42780
|
+
// If admin leaves, clear all users' canvases
|
|
42781
|
+
if (isAdmin && queueAction) {
|
|
42782
|
+
const clearTimestamp = Date.now();
|
|
42783
|
+
// Clear local state immediately
|
|
42784
|
+
dispatch({ type: 'CLEAR_CANVAS' });
|
|
42785
|
+
dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
|
|
42786
|
+
// Send clear action to all users
|
|
42787
|
+
queueAction({
|
|
42788
|
+
type: 'clear',
|
|
42789
|
+
payload: {
|
|
42790
|
+
timestamp: clearTimestamp,
|
|
42791
|
+
adminId: userId,
|
|
42792
|
+
},
|
|
42793
|
+
userId: userId,
|
|
42794
|
+
timestamp: clearTimestamp,
|
|
42795
|
+
});
|
|
42796
|
+
}
|
|
42797
|
+
};
|
|
42798
|
+
}, [isAdmin, queueAction, userId, dispatch]);
|
|
42646
42799
|
// Global cleanup on app unmount
|
|
42647
42800
|
useEffect(() => {
|
|
42648
42801
|
const handleBeforeUnload = () => {
|