@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/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
- drawingSessionId: newShape.drawingSessionId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
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
- drawingSessionId: newShape.drawingSessionId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
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 id (which contains timestamp) or any timestamp property
34971
- return a.id.localeCompare(b.id);
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
- // Only apply if the received state has more shapes or we have no shapes
35301
- if (state.shapes.length === 0 || syncState.shapes.length > state.shapes.length) {
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 = syncState.shapes.map((shape, index) => {
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 [imageElement, setImageElement] = useState(null);
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
- // Convert canvas to image
35937
- const img = new window.Image();
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
- }, [shapeProps, bounds]); // Re-render when shape or bounds change
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
- return (jsx(Image$1, { image: imageElement, x: bounds.minX - padding, y: bounds.minY - padding, draggable: isSelected, onClick: onSelect, onTap: onSelect, onDragEnd: (e) => {
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 newDrawingSessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
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 - just collect points
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
- }, 8); // Optimized to ~120fps for ultra-smooth real-time collaboration
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 - apply erase to intersecting shapes
36645
+ // Handle eraser tool - cleanup on mouse up (erasing already done in mouse move)
36515
36646
  if (state.tool === 'eraser') {
36516
- // Find shapes that intersect with the erase path
36517
- const intersectingShapes = findIntersectingShapes(currentPoints, state.shapes);
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
- switch (shape.type) {
36622
- case 'rectangle':
36623
- return jsx(Rectangle, { ...commonProps });
36624
- case 'ellipse':
36625
- return jsx(Ellipse, { ...commonProps });
36626
- case 'line':
36627
- return jsx(Line, { ...commonProps });
36628
- case 'pencil':
36629
- return jsx(FreehandDrawing, { ...commonProps });
36630
- case 'arrow':
36631
- return jsx(Arrow, { ...commonProps });
36632
- default:
36633
- return null;
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: jsx(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(() => {
36742
- if (state.backgroundColor === 'transparent')
36743
- return null;
36744
- return (jsx(Rect, { x: 0, y: 0, width: size.width, height: size.height, fill: state.backgroundColor, listening: false }));
36745
- }, [state.backgroundColor, size.width, size.height]), renderedShapes, renderedActiveDrawings, renderCurrentShape] }) }) }));
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 = 30; // ms - Reduced delay for faster transmission
37693
- const MAX_ACTIONS_PER_BATCH = 15; // Smaller batches for faster transmission
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 = 15; // Increased rate for smoother collaboration
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 = 8; // ms - 120fps for ultra-smooth drawing actions
37699
- const DRAWING_BATCH_SIZE = 5; // Much smaller batches for immediate transmission
37700
- const DRAWING_IMMEDIATE_THRESHOLD = 2; // Send immediately if we have 2+ drawing actions
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, 4) : // Even faster for multiple actions
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
- }, Math.max(DRAWING_THROTTLE_DELAY / 4, 2)); // Ultra-fast transmission
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
- const actionId = `${action.type}-${parsedData.userId}-${action.timestamp || Date.now()}`;
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 = () => {