@ngenux/ngage-whiteboarding 1.0.6 → 1.0.7

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