@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.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
- drawingSessionId: newShape.drawingSessionId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
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
- drawingSessionId: newShape.drawingSessionId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
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 id (which contains timestamp) or any timestamp property
34991
- return a.id.localeCompare(b.id);
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
- // Only apply if the received state has more shapes or we have no shapes
35321
- if (state.shapes.length === 0 || syncState.shapes.length > state.shapes.length) {
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 = syncState.shapes.map((shape, index) => {
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 [imageElement, setImageElement] = React.useState(null);
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
- // Convert canvas to image
35957
- const img = new window.Image();
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
- }, [shapeProps, bounds]); // Re-render when shape or bounds change
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
- return (jsxRuntime.jsx(Image$1, { image: imageElement, x: bounds.minX - padding, y: bounds.minY - padding, draggable: isSelected, onClick: onSelect, onTap: onSelect, onDragEnd: (e) => {
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 newDrawingSessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
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 - just collect points
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
- }, 8); // Optimized to ~120fps for ultra-smooth real-time collaboration
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 - apply erase to intersecting shapes
36665
+ // Handle eraser tool - cleanup on mouse up (erasing already done in mouse move)
36535
36666
  if (state.tool === 'eraser') {
36536
- // Find shapes that intersect with the erase path
36537
- const intersectingShapes = findIntersectingShapes(currentPoints, state.shapes);
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
- switch (shape.type) {
36642
- case 'rectangle':
36643
- return jsxRuntime.jsx(Rectangle, { ...commonProps });
36644
- case 'ellipse':
36645
- return jsxRuntime.jsx(Ellipse, { ...commonProps });
36646
- case 'line':
36647
- return jsxRuntime.jsx(Line, { ...commonProps });
36648
- case 'pencil':
36649
- return jsxRuntime.jsx(FreehandDrawing, { ...commonProps });
36650
- case 'arrow':
36651
- return jsxRuntime.jsx(Arrow, { ...commonProps });
36652
- default:
36653
- return null;
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.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: jsxRuntime.jsxs(Layer, { children: [React.useMemo(() => {
36762
- if (state.backgroundColor === 'transparent')
36763
- return null;
36764
- return (jsxRuntime.jsx(Rect, { x: 0, y: 0, width: size.width, height: size.height, fill: state.backgroundColor, listening: false }));
36765
- }, [state.backgroundColor, size.width, size.height]), renderedShapes, renderedActiveDrawings, renderCurrentShape] }) }) }));
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 = 30; // ms - Reduced delay for faster transmission
37713
- const MAX_ACTIONS_PER_BATCH = 15; // Smaller batches for faster transmission
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 = 15; // Increased rate for smoother collaboration
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 = 8; // ms - 120fps for ultra-smooth drawing actions
37719
- const DRAWING_BATCH_SIZE = 5; // Much smaller batches for immediate transmission
37720
- const DRAWING_IMMEDIATE_THRESHOLD = 2; // Send immediately if we have 2+ drawing actions
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, 4) : // Even faster for multiple actions
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
- }, Math.max(DRAWING_THROTTLE_DELAY / 4, 2)); // Ultra-fast transmission
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
- const actionId = `${action.type}-${parsedData.userId}-${action.timestamp || Date.now()}`;
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 = () => {