@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.js
CHANGED
|
@@ -34777,12 +34777,15 @@ const whiteboardReducer = (state, action) => {
|
|
|
34777
34777
|
// Prevent duplicate shapes during simultaneous drawing
|
|
34778
34778
|
const newShape = action.payload;
|
|
34779
34779
|
const existingShapeIndex = state.shapes.findIndex(shape => shape.id === newShape.id);
|
|
34780
|
+
// Ensure shape has timestamp for proper ordering and stale action filtering
|
|
34781
|
+
const timestamp = newShape.timestamp || Date.now();
|
|
34780
34782
|
// If shape already exists, update it instead of adding duplicate
|
|
34781
34783
|
if (existingShapeIndex >= 0) {
|
|
34782
34784
|
const updatedShapes = [...state.shapes];
|
|
34783
34785
|
updatedShapes[existingShapeIndex] = {
|
|
34784
34786
|
...newShape,
|
|
34785
|
-
|
|
34787
|
+
timestamp,
|
|
34788
|
+
drawingSessionId: newShape.drawingSessionId || `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`
|
|
34786
34789
|
};
|
|
34787
34790
|
return {
|
|
34788
34791
|
...state,
|
|
@@ -34790,10 +34793,11 @@ const whiteboardReducer = (state, action) => {
|
|
|
34790
34793
|
currentDrawingShape: undefined,
|
|
34791
34794
|
};
|
|
34792
34795
|
}
|
|
34793
|
-
// Add new shape with session tracking
|
|
34796
|
+
// Add new shape with session tracking and timestamp
|
|
34794
34797
|
const shapeWithSession = {
|
|
34795
34798
|
...newShape,
|
|
34796
|
-
|
|
34799
|
+
timestamp,
|
|
34800
|
+
drawingSessionId: newShape.drawingSessionId || `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`
|
|
34797
34801
|
};
|
|
34798
34802
|
const newShapes = [...state.shapes, shapeWithSession];
|
|
34799
34803
|
const newHistory = state.history.slice(0, state.historyIndex + 1);
|
|
@@ -34987,11 +34991,13 @@ const whiteboardReducer = (state, action) => {
|
|
|
34987
34991
|
const userShapes = state.shapes
|
|
34988
34992
|
.filter(shape => shape.userId === userId)
|
|
34989
34993
|
.sort((a, b) => {
|
|
34990
|
-
// Sort by
|
|
34991
|
-
|
|
34994
|
+
// Sort by timestamp to ensure LIFO (Last In First Out) order
|
|
34995
|
+
const timestampA = a.timestamp || 0;
|
|
34996
|
+
const timestampB = b.timestamp || 0;
|
|
34997
|
+
return timestampA - timestampB;
|
|
34992
34998
|
});
|
|
34993
34999
|
if (userShapes.length > 0) {
|
|
34994
|
-
// Get the most recent shape
|
|
35000
|
+
// Get the most recent shape (last in sorted array)
|
|
34995
35001
|
const lastUserShape = userShapes[userShapes.length - 1];
|
|
34996
35002
|
// Check if there are multiple shapes that were part of the same drawing session
|
|
34997
35003
|
// Use drawingSessionId if available, otherwise fall back to time-based detection
|
|
@@ -35285,9 +35291,16 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35285
35291
|
// Drawing actions should be filtered if they're older than the last clear
|
|
35286
35292
|
if (action.type === 'start_draw' || action.type === 'continue_draw' || action.type === 'end_draw' || action.type === 'add') {
|
|
35287
35293
|
const actionTimestamp = action.timestamp || 0;
|
|
35288
|
-
if (actionTimestamp <= state.lastClearTimestamp) {
|
|
35294
|
+
if (actionTimestamp > 0 && actionTimestamp <= state.lastClearTimestamp) {
|
|
35289
35295
|
return true;
|
|
35290
35296
|
}
|
|
35297
|
+
// Also check timestamp in the payload if it exists
|
|
35298
|
+
if (typeof action.payload === 'object' && action.payload !== null && !Array.isArray(action.payload)) {
|
|
35299
|
+
const payloadTimestamp = action.payload.timestamp || 0;
|
|
35300
|
+
if (payloadTimestamp > 0 && payloadTimestamp <= state.lastClearTimestamp) {
|
|
35301
|
+
return true;
|
|
35302
|
+
}
|
|
35303
|
+
}
|
|
35291
35304
|
}
|
|
35292
35305
|
return false;
|
|
35293
35306
|
};
|
|
@@ -35317,10 +35330,15 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35317
35330
|
if (syncState.shapes.length === 0) {
|
|
35318
35331
|
return; // Don't apply empty state or keep requesting
|
|
35319
35332
|
}
|
|
35320
|
-
//
|
|
35321
|
-
|
|
35333
|
+
// Filter out shapes that are older than our last clear
|
|
35334
|
+
const validShapes = syncState.shapes.filter(shape => {
|
|
35335
|
+
const shapeTimestamp = shape.timestamp || 0;
|
|
35336
|
+
return shapeTimestamp === 0 || shapeTimestamp > state.lastClearTimestamp;
|
|
35337
|
+
});
|
|
35338
|
+
// Only apply if the received state has valid shapes
|
|
35339
|
+
if (validShapes.length > 0 && (state.shapes.length === 0 || validShapes.length > state.shapes.length)) {
|
|
35322
35340
|
// All shapes from sync_state should have normalized coordinates, denormalize them
|
|
35323
|
-
const denormalizedShapes =
|
|
35341
|
+
const denormalizedShapes = validShapes.map((shape, index) => {
|
|
35324
35342
|
return denormalizeShape(shape);
|
|
35325
35343
|
});
|
|
35326
35344
|
// Apply the synchronized state
|
|
@@ -35338,7 +35356,8 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35338
35356
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35339
35357
|
// Additional check to prevent adding shapes from before the last clear
|
|
35340
35358
|
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35341
|
-
if (shapeTimestamp <= state.lastClearTimestamp) {
|
|
35359
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35360
|
+
console.warn(`[APPLY_ACTION] Skipping stale shape from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
|
|
35342
35361
|
break;
|
|
35343
35362
|
}
|
|
35344
35363
|
dispatch({ type: 'ADD_SHAPE', payload: denormalizedShape });
|
|
@@ -35392,6 +35411,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35392
35411
|
case 'start_draw':
|
|
35393
35412
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35394
35413
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35414
|
+
// Check if this shape is from before the last clear
|
|
35415
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35416
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35417
|
+
break; // Skip stale shapes
|
|
35418
|
+
}
|
|
35395
35419
|
// Only apply collaborative start_draw if it's from another user
|
|
35396
35420
|
if (denormalizedShape.userId !== state.userId) {
|
|
35397
35421
|
// Add to active drawings for real-time collaborative visibility
|
|
@@ -35405,6 +35429,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35405
35429
|
case 'continue_draw':
|
|
35406
35430
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35407
35431
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35432
|
+
// Check if this shape is from before the last clear
|
|
35433
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35434
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35435
|
+
break; // Skip stale shapes
|
|
35436
|
+
}
|
|
35408
35437
|
// Only apply collaborative drawing updates if it's not from the current user
|
|
35409
35438
|
// to avoid interfering with local real-time drawing
|
|
35410
35439
|
if (denormalizedShape.userId !== state.userId) {
|
|
@@ -35419,6 +35448,12 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35419
35448
|
case 'end_draw':
|
|
35420
35449
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35421
35450
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35451
|
+
// Check if this shape is from before the last clear
|
|
35452
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35453
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35454
|
+
console.warn(`[APPLY_ACTION] Skipping stale end_draw from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
|
|
35455
|
+
break; // Skip stale shapes
|
|
35456
|
+
}
|
|
35422
35457
|
// Only apply collaborative end_draw if it's from another user
|
|
35423
35458
|
// Local user's end_draw is handled directly in Board component via ADD_SHAPE
|
|
35424
35459
|
if (denormalizedShape.userId !== state.userId) {
|
|
@@ -35923,7 +35958,7 @@ const Arrow = React.memo(({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
35923
35958
|
});
|
|
35924
35959
|
|
|
35925
35960
|
const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
35926
|
-
const [
|
|
35961
|
+
const [bitmap, setBitmap] = React.useState(null);
|
|
35927
35962
|
const canvasRef = React.useRef(null);
|
|
35928
35963
|
// Memoize bounds calculation to avoid unnecessary recalculations
|
|
35929
35964
|
const bounds = React.useMemo(() => calculateShapeBounds(shapeProps), [shapeProps.points, shapeProps.strokeWidth, shapeProps.erasePaths]);
|
|
@@ -35953,22 +35988,24 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
35953
35988
|
});
|
|
35954
35989
|
}
|
|
35955
35990
|
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();
|
|
35991
|
+
// Use canvas directly as Konva image - no async
|
|
35992
|
+
setBitmap(canvas);
|
|
35966
35993
|
return () => {
|
|
35967
35994
|
if (canvasRef.current) {
|
|
35968
35995
|
canvasRef.current.remove();
|
|
35996
|
+
canvasRef.current = null;
|
|
35969
35997
|
}
|
|
35970
35998
|
};
|
|
35971
|
-
}, [
|
|
35999
|
+
}, [
|
|
36000
|
+
bounds,
|
|
36001
|
+
shapeProps.points,
|
|
36002
|
+
shapeProps.erasePaths,
|
|
36003
|
+
shapeProps.stroke,
|
|
36004
|
+
shapeProps.strokeWidth,
|
|
36005
|
+
shapeProps.opacity,
|
|
36006
|
+
shapeProps.strokeStyle,
|
|
36007
|
+
shapeProps.type
|
|
36008
|
+
]);
|
|
35972
36009
|
// Calculate bounds of the shape including erase paths
|
|
35973
36010
|
function calculateShapeBounds(shape) {
|
|
35974
36011
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
@@ -36087,11 +36124,48 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
|
|
|
36087
36124
|
ctx.stroke();
|
|
36088
36125
|
}
|
|
36089
36126
|
};
|
|
36090
|
-
if (!imageElement) {
|
|
36091
|
-
return null; // Still rendering
|
|
36092
|
-
}
|
|
36093
36127
|
const padding = 50;
|
|
36094
|
-
|
|
36128
|
+
// Render original shape as fallback while bitmap is being created
|
|
36129
|
+
if (!bitmap) {
|
|
36130
|
+
const commonProps = {
|
|
36131
|
+
onClick: onSelect,
|
|
36132
|
+
onTap: onSelect,
|
|
36133
|
+
listening: true,
|
|
36134
|
+
stroke: shapeProps.stroke,
|
|
36135
|
+
strokeWidth: shapeProps.strokeWidth,
|
|
36136
|
+
opacity: shapeProps.opacity,
|
|
36137
|
+
dash: shapeProps.strokeStyle === 'dashed' ? [5, 5] : shapeProps.strokeStyle === 'dotted' ? [2, 3] : undefined,
|
|
36138
|
+
};
|
|
36139
|
+
switch (shapeProps.type) {
|
|
36140
|
+
case 'pencil':
|
|
36141
|
+
return (jsxRuntime.jsx(Line$1, { points: shapeProps.points.flatMap(p => [p.x, p.y]), lineCap: "round", lineJoin: "round", ...commonProps }));
|
|
36142
|
+
case 'line':
|
|
36143
|
+
if (shapeProps.points.length >= 2) {
|
|
36144
|
+
return (jsxRuntime.jsx(Line$1, { points: [
|
|
36145
|
+
shapeProps.points[0].x,
|
|
36146
|
+
shapeProps.points[0].y,
|
|
36147
|
+
shapeProps.points[1].x,
|
|
36148
|
+
shapeProps.points[1].y,
|
|
36149
|
+
], ...commonProps }));
|
|
36150
|
+
}
|
|
36151
|
+
return null;
|
|
36152
|
+
case 'rectangle':
|
|
36153
|
+
if (shapeProps.points.length >= 2) {
|
|
36154
|
+
const [start, end] = shapeProps.points;
|
|
36155
|
+
return (jsxRuntime.jsx(Rect, { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y, ...commonProps }));
|
|
36156
|
+
}
|
|
36157
|
+
return null;
|
|
36158
|
+
case 'ellipse':
|
|
36159
|
+
if (shapeProps.points.length >= 2) {
|
|
36160
|
+
const [start, end] = shapeProps.points;
|
|
36161
|
+
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 }));
|
|
36162
|
+
}
|
|
36163
|
+
return null;
|
|
36164
|
+
default:
|
|
36165
|
+
return null;
|
|
36166
|
+
}
|
|
36167
|
+
}
|
|
36168
|
+
return (jsxRuntime.jsx(Image$1, { image: bitmap, x: bounds.minX - padding, y: bounds.minY - padding, draggable: isSelected, onClick: onSelect, onTap: onSelect, onDragEnd: (e) => {
|
|
36095
36169
|
const newX = e.target.x();
|
|
36096
36170
|
const newY = e.target.y();
|
|
36097
36171
|
const deltaX = newX - (bounds.minX - padding);
|
|
@@ -36131,6 +36205,10 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36131
36205
|
const containerRef = React.useRef(null);
|
|
36132
36206
|
const lastPointerPosition = React.useRef(null);
|
|
36133
36207
|
const mouseMoveThrottleRef = React.useRef(null);
|
|
36208
|
+
// NEW: Eraser preview state for real-time visual feedback
|
|
36209
|
+
const [eraserPreviewPoints, setEraserPreviewPoints] = React.useState([]);
|
|
36210
|
+
const [keepPreviewVisible, setKeepPreviewVisible] = React.useState(false);
|
|
36211
|
+
const [justErasedIds, setJustErasedIds] = React.useState(new Set());
|
|
36134
36212
|
// Find shapes that intersect with the erase path
|
|
36135
36213
|
const findIntersectingShapes = (erasePath, shapes) => {
|
|
36136
36214
|
const eraseRadius = 10; // Half of eraser width
|
|
@@ -36221,8 +36299,20 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36221
36299
|
setCurrentShapeId(null);
|
|
36222
36300
|
setCurrentDrawingSessionId(null);
|
|
36223
36301
|
lastPointerPosition.current = null;
|
|
36302
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36303
|
+
setKeepPreviewVisible(false);
|
|
36304
|
+
setJustErasedIds(new Set());
|
|
36224
36305
|
}
|
|
36225
36306
|
}, [hasToolAccess, state.isDrawing, dispatch]);
|
|
36307
|
+
// Clear justErasedIds after one frame to complete transition
|
|
36308
|
+
React.useEffect(() => {
|
|
36309
|
+
if (justErasedIds.size > 0) {
|
|
36310
|
+
const id = requestAnimationFrame(() => {
|
|
36311
|
+
setJustErasedIds(new Set());
|
|
36312
|
+
});
|
|
36313
|
+
return () => cancelAnimationFrame(id);
|
|
36314
|
+
}
|
|
36315
|
+
}, [justErasedIds]);
|
|
36226
36316
|
// Memoized export functionality for performance
|
|
36227
36317
|
const exportAsImage = React.useCallback((format = 'png') => {
|
|
36228
36318
|
if (!stageRef.current) {
|
|
@@ -36385,12 +36475,14 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36385
36475
|
if (state.tool === 'eraser') {
|
|
36386
36476
|
dispatch({ type: 'SET_DRAWING', payload: true });
|
|
36387
36477
|
setCurrentPoints([pos]);
|
|
36478
|
+
setEraserPreviewPoints([pos]); // NEW: start preview stroke
|
|
36388
36479
|
setCurrentShapeId('erasing'); // Special ID for erasing mode
|
|
36389
36480
|
return;
|
|
36390
36481
|
}
|
|
36391
36482
|
// Create new shape ID for regular drawing tools
|
|
36392
36483
|
const newShapeId = v4();
|
|
36393
|
-
const
|
|
36484
|
+
const timestamp = Date.now();
|
|
36485
|
+
const newDrawingSessionId = `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`;
|
|
36394
36486
|
setCurrentShapeId(newShapeId);
|
|
36395
36487
|
setCurrentDrawingSessionId(newDrawingSessionId);
|
|
36396
36488
|
// Start drawing
|
|
@@ -36407,6 +36499,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36407
36499
|
strokeStyle: state.strokeStyle,
|
|
36408
36500
|
opacity: state.opacity,
|
|
36409
36501
|
drawingSessionId: newDrawingSessionId,
|
|
36502
|
+
timestamp,
|
|
36410
36503
|
// Initialize transformation properties
|
|
36411
36504
|
x: 0,
|
|
36412
36505
|
y: 0,
|
|
@@ -36433,6 +36526,9 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36433
36526
|
setCurrentShapeId(null);
|
|
36434
36527
|
setCurrentDrawingSessionId(null);
|
|
36435
36528
|
lastPointerPosition.current = null;
|
|
36529
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36530
|
+
setKeepPreviewVisible(false);
|
|
36531
|
+
setJustErasedIds(new Set());
|
|
36436
36532
|
}
|
|
36437
36533
|
return;
|
|
36438
36534
|
}
|
|
@@ -36450,10 +36546,41 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36450
36546
|
return; // Ultra-sensitive for maximum smoothness
|
|
36451
36547
|
}
|
|
36452
36548
|
lastPointerPosition.current = pos;
|
|
36453
|
-
// Handle eraser tool -
|
|
36549
|
+
// Handle eraser tool - collect points and stream erase in real-time
|
|
36454
36550
|
if (state.tool === 'eraser') {
|
|
36455
36551
|
const newPoints = [...currentPoints, pos];
|
|
36456
36552
|
setCurrentPoints(newPoints);
|
|
36553
|
+
setEraserPreviewPoints(newPoints); // Live preview
|
|
36554
|
+
// Create segment from last point to current point for real-time streaming
|
|
36555
|
+
const segment = currentPoints.length > 0
|
|
36556
|
+
? [currentPoints[currentPoints.length - 1], pos]
|
|
36557
|
+
: [pos];
|
|
36558
|
+
// Apply segment to intersecting shapes in real-time
|
|
36559
|
+
const intersectingShapes = findIntersectingShapes(segment, state.shapes);
|
|
36560
|
+
if (intersectingShapes.length > 0) {
|
|
36561
|
+
const timestamp = Date.now();
|
|
36562
|
+
intersectingShapes.forEach((shape, index) => {
|
|
36563
|
+
const updatedShape = {
|
|
36564
|
+
...shape,
|
|
36565
|
+
erasePaths: [...(shape.erasePaths || []), segment],
|
|
36566
|
+
};
|
|
36567
|
+
// Update the shape locally
|
|
36568
|
+
dispatch({ type: 'UPDATE_SHAPE', payload: updatedShape });
|
|
36569
|
+
// Queue erase action for real-time collaboration
|
|
36570
|
+
if (queueAction) {
|
|
36571
|
+
const erasePayload = {
|
|
36572
|
+
shapeId: shape.id,
|
|
36573
|
+
erasePath: segment,
|
|
36574
|
+
timestamp: timestamp + index,
|
|
36575
|
+
};
|
|
36576
|
+
queueAction({
|
|
36577
|
+
type: 'erase',
|
|
36578
|
+
payload: erasePayload,
|
|
36579
|
+
timestamp: timestamp + index,
|
|
36580
|
+
});
|
|
36581
|
+
}
|
|
36582
|
+
});
|
|
36583
|
+
}
|
|
36457
36584
|
return;
|
|
36458
36585
|
}
|
|
36459
36586
|
let newPoints;
|
|
@@ -36485,6 +36612,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36485
36612
|
strokeStyle: state.strokeStyle,
|
|
36486
36613
|
opacity: state.opacity,
|
|
36487
36614
|
drawingSessionId: currentDrawingSessionId,
|
|
36615
|
+
timestamp: Date.now(),
|
|
36488
36616
|
// Initialize transformation properties
|
|
36489
36617
|
x: 0,
|
|
36490
36618
|
y: 0,
|
|
@@ -36503,7 +36631,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36503
36631
|
};
|
|
36504
36632
|
queueAction(continueAction);
|
|
36505
36633
|
mouseMoveThrottleRef.current = null;
|
|
36506
|
-
},
|
|
36634
|
+
}, 1); // Maximum frequency for ultra-smooth real-time collaboration
|
|
36507
36635
|
}
|
|
36508
36636
|
}, [hasToolAccess, state.isDrawing, state.tool, state.userId, state.color, state.strokeWidth, state.strokeStyle, state.opacity, currentShapeId, currentDrawingSessionId, currentPoints, queueAction, dispatch]);
|
|
36509
36637
|
const handleMouseUp = React.useCallback(() => {
|
|
@@ -36516,6 +36644,9 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36516
36644
|
setCurrentShapeId(null);
|
|
36517
36645
|
setCurrentDrawingSessionId(null);
|
|
36518
36646
|
lastPointerPosition.current = null;
|
|
36647
|
+
setEraserPreviewPoints([]); // Clear eraser preview
|
|
36648
|
+
setKeepPreviewVisible(false);
|
|
36649
|
+
setJustErasedIds(new Set());
|
|
36519
36650
|
}
|
|
36520
36651
|
return;
|
|
36521
36652
|
}
|
|
@@ -36531,36 +36662,23 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36531
36662
|
setCurrentDrawingSessionId(null);
|
|
36532
36663
|
return;
|
|
36533
36664
|
}
|
|
36534
|
-
// Handle eraser tool -
|
|
36665
|
+
// Handle eraser tool - cleanup on mouse up (erasing already done in mouse move)
|
|
36535
36666
|
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
|
-
});
|
|
36667
|
+
// Stop drawing state
|
|
36668
|
+
dispatch({ type: 'SET_DRAWING', payload: false });
|
|
36559
36669
|
// Reset erasing state
|
|
36560
36670
|
setCurrentPoints([]);
|
|
36561
36671
|
setCurrentShapeId(null);
|
|
36562
36672
|
setCurrentDrawingSessionId(null);
|
|
36563
36673
|
lastPointerPosition.current = null;
|
|
36674
|
+
// Clear preview after a brief delay to ensure smooth transition
|
|
36675
|
+
requestAnimationFrame(() => {
|
|
36676
|
+
requestAnimationFrame(() => {
|
|
36677
|
+
setEraserPreviewPoints([]);
|
|
36678
|
+
setKeepPreviewVisible(false);
|
|
36679
|
+
setJustErasedIds(new Set());
|
|
36680
|
+
});
|
|
36681
|
+
});
|
|
36564
36682
|
return;
|
|
36565
36683
|
}
|
|
36566
36684
|
// Handle regular drawing tools
|
|
@@ -36574,6 +36692,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36574
36692
|
strokeStyle: state.strokeStyle,
|
|
36575
36693
|
opacity: state.opacity,
|
|
36576
36694
|
drawingSessionId: currentDrawingSessionId,
|
|
36695
|
+
timestamp: Date.now(),
|
|
36577
36696
|
// Initialize transformation properties
|
|
36578
36697
|
x: 0,
|
|
36579
36698
|
y: 0,
|
|
@@ -36627,31 +36746,39 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36627
36746
|
if (shape.isEraser) {
|
|
36628
36747
|
return null;
|
|
36629
36748
|
}
|
|
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
36749
|
const commonProps = {
|
|
36636
36750
|
shapeProps: shape,
|
|
36637
36751
|
isSelected: shape.isSelected || false,
|
|
36638
36752
|
onSelect: () => handleShapeClick(shape.id),
|
|
36639
36753
|
onUpdate: handleShapeUpdate,
|
|
36640
36754
|
};
|
|
36641
|
-
|
|
36642
|
-
|
|
36643
|
-
|
|
36644
|
-
|
|
36645
|
-
|
|
36646
|
-
|
|
36647
|
-
|
|
36648
|
-
|
|
36649
|
-
|
|
36650
|
-
|
|
36651
|
-
|
|
36652
|
-
|
|
36653
|
-
|
|
36755
|
+
// Render original shape component
|
|
36756
|
+
const OriginalShape = () => {
|
|
36757
|
+
switch (shape.type) {
|
|
36758
|
+
case 'rectangle':
|
|
36759
|
+
return jsxRuntime.jsx(Rectangle, { ...commonProps });
|
|
36760
|
+
case 'ellipse':
|
|
36761
|
+
return jsxRuntime.jsx(Ellipse, { ...commonProps });
|
|
36762
|
+
case 'line':
|
|
36763
|
+
return jsxRuntime.jsx(Line, { ...commonProps });
|
|
36764
|
+
case 'pencil':
|
|
36765
|
+
return jsxRuntime.jsx(FreehandDrawing, { ...commonProps });
|
|
36766
|
+
case 'arrow':
|
|
36767
|
+
return jsxRuntime.jsx(Arrow, { ...commonProps });
|
|
36768
|
+
default:
|
|
36769
|
+
return null;
|
|
36770
|
+
}
|
|
36771
|
+
};
|
|
36772
|
+
// Use ErasedShape component for shapes that have erase paths applied
|
|
36773
|
+
if (shape.erasePaths && shape.erasePaths.length > 0) {
|
|
36774
|
+
// If this shape just got erased, render both to prevent flicker
|
|
36775
|
+
if (justErasedIds.has(shape.id)) {
|
|
36776
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(OriginalShape, {}), jsxRuntime.jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate })] }));
|
|
36777
|
+
}
|
|
36778
|
+
return (jsxRuntime.jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate }));
|
|
36654
36779
|
}
|
|
36780
|
+
// Use original shape components for shapes without erase operations
|
|
36781
|
+
return jsxRuntime.jsx(OriginalShape, {});
|
|
36655
36782
|
}, (prevProps, nextProps) => {
|
|
36656
36783
|
// Ultra-precise comparison to prevent unnecessary re-renders during simultaneous drawing
|
|
36657
36784
|
const prevShape = prevProps.shape;
|
|
@@ -36758,11 +36885,11 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36758
36885
|
state.tool === 'select' ? 'default' :
|
|
36759
36886
|
state.tool === 'pan' ? 'grab' : 'crosshair'
|
|
36760
36887
|
}), [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
|
-
|
|
36888
|
+
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(() => {
|
|
36889
|
+
if (state.backgroundColor === 'transparent')
|
|
36890
|
+
return null;
|
|
36891
|
+
return (jsxRuntime.jsx(Rect, { x: 0, y: 0, width: size.width, height: size.height, fill: state.backgroundColor, listening: false }));
|
|
36892
|
+
}, [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
36893
|
});
|
|
36767
36894
|
// Memoize the Board component to prevent unnecessary re-renders
|
|
36768
36895
|
const Board = React.memo(BoardComponent, (prevProps, nextProps) => {
|
|
@@ -37709,15 +37836,15 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37709
37836
|
totalActions: 0,
|
|
37710
37837
|
});
|
|
37711
37838
|
// Throttling configuration - Ultra-optimized for smooth real-time collaboration
|
|
37712
|
-
const THROTTLE_DELAY =
|
|
37713
|
-
const MAX_ACTIONS_PER_BATCH =
|
|
37839
|
+
const THROTTLE_DELAY = 10; // ms - Minimal delay for faster transmission
|
|
37840
|
+
const MAX_ACTIONS_PER_BATCH = 8; // Smaller batches for faster transmission
|
|
37714
37841
|
const MAX_MESSAGE_SIZE = 500; // characters - matches API constraint
|
|
37715
|
-
const MAX_MESSAGES_PER_SECOND =
|
|
37842
|
+
const MAX_MESSAGES_PER_SECOND = 30; // Maximum rate for smoother collaboration
|
|
37716
37843
|
const MAX_PAYLOAD_SIZE = 1024; // bytes (1KB) - matches API constraint
|
|
37717
37844
|
// Drawing-specific throttling for ultra-smooth real-time collaboration
|
|
37718
|
-
const DRAWING_THROTTLE_DELAY =
|
|
37719
|
-
const DRAWING_BATCH_SIZE =
|
|
37720
|
-
const DRAWING_IMMEDIATE_THRESHOLD =
|
|
37845
|
+
const DRAWING_THROTTLE_DELAY = 1; // ms - Maximum frequency for ultra-smooth drawing actions
|
|
37846
|
+
const DRAWING_BATCH_SIZE = 2; // Minimal batches for immediate transmission
|
|
37847
|
+
const DRAWING_IMMEDIATE_THRESHOLD = 1; // Send immediately if we have 1+ drawing actions
|
|
37721
37848
|
// Message rate limiting
|
|
37722
37849
|
const messageTimestampsRef = React.useRef([]);
|
|
37723
37850
|
const isRateLimited = React.useCallback(() => {
|
|
@@ -37881,7 +38008,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
37881
38008
|
let delay = THROTTLE_DELAY;
|
|
37882
38009
|
if (remainingDrawingActions.length > 0) {
|
|
37883
38010
|
delay = remainingDrawingActions.length >= DRAWING_IMMEDIATE_THRESHOLD ?
|
|
37884
|
-
Math.max(DRAWING_THROTTLE_DELAY / 2,
|
|
38011
|
+
Math.max(DRAWING_THROTTLE_DELAY / 2, 2) : // Even faster for multiple actions
|
|
37885
38012
|
DRAWING_THROTTLE_DELAY;
|
|
37886
38013
|
}
|
|
37887
38014
|
throttleTimerRef.current = setTimeout(() => {
|
|
@@ -38024,7 +38151,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38024
38151
|
clearTimeout(throttleTimerRef.current);
|
|
38025
38152
|
throttleTimerRef.current = setTimeout(() => {
|
|
38026
38153
|
transmitRef.current?.();
|
|
38027
|
-
},
|
|
38154
|
+
}, 1); // Ultra-fast transmission - 1ms for maximum responsiveness
|
|
38028
38155
|
}
|
|
38029
38156
|
}
|
|
38030
38157
|
}, [state.canvasSize, state.userId, roomId, callbacks, sendWithConstraints]);
|
|
@@ -38044,7 +38171,11 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
|
|
|
38044
38171
|
// Process each action in the batch with duplicate prevention
|
|
38045
38172
|
parsedData.actions.forEach(action => {
|
|
38046
38173
|
// Create unique action ID for deduplication
|
|
38047
|
-
|
|
38174
|
+
// For erase actions, include shapeId to allow multiple erases in same stroke
|
|
38175
|
+
let actionId = `${action.type}-${parsedData.userId}-${action.timestamp || Date.now()}`;
|
|
38176
|
+
if (action.type === 'erase' && action.payload && typeof action.payload === 'object' && 'shapeId' in action.payload) {
|
|
38177
|
+
actionId = `${action.type}-${parsedData.userId}-${action.payload.shapeId}-${action.timestamp || Date.now()}`;
|
|
38178
|
+
}
|
|
38048
38179
|
// Skip if we've already processed this action (prevents shape loss from duplicate processing)
|
|
38049
38180
|
if (processedActionsRef.current.has(actionId)) {
|
|
38050
38181
|
return;
|
|
@@ -42663,6 +42794,28 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42663
42794
|
leaveRoom(roomId);
|
|
42664
42795
|
};
|
|
42665
42796
|
}, [roomId]);
|
|
42797
|
+
// Clear all canvases on component unmount if admin leaves
|
|
42798
|
+
React.useEffect(() => {
|
|
42799
|
+
return () => {
|
|
42800
|
+
// If admin leaves, clear all users' canvases
|
|
42801
|
+
if (isAdmin && queueAction) {
|
|
42802
|
+
const clearTimestamp = Date.now();
|
|
42803
|
+
// Clear local state immediately
|
|
42804
|
+
dispatch({ type: 'CLEAR_CANVAS' });
|
|
42805
|
+
dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
|
|
42806
|
+
// Send clear action to all users
|
|
42807
|
+
queueAction({
|
|
42808
|
+
type: 'clear',
|
|
42809
|
+
payload: {
|
|
42810
|
+
timestamp: clearTimestamp,
|
|
42811
|
+
adminId: userId,
|
|
42812
|
+
},
|
|
42813
|
+
userId: userId,
|
|
42814
|
+
timestamp: clearTimestamp,
|
|
42815
|
+
});
|
|
42816
|
+
}
|
|
42817
|
+
};
|
|
42818
|
+
}, [isAdmin, queueAction, userId, dispatch]);
|
|
42666
42819
|
// Global cleanup on app unmount
|
|
42667
42820
|
React.useEffect(() => {
|
|
42668
42821
|
const handleBeforeUnload = () => {
|