@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.esm.js CHANGED
@@ -35903,7 +35903,7 @@ const Arrow = React__default.memo(({ shapeProps, isSelected, onSelect, onUpdate,
35903
35903
  });
35904
35904
 
35905
35905
  const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
35906
- const [imageElement, setImageElement] = useState(null);
35906
+ const [bitmap, setBitmap] = useState(null);
35907
35907
  const canvasRef = useRef(null);
35908
35908
  // Memoize bounds calculation to avoid unnecessary recalculations
35909
35909
  const bounds = useMemo(() => calculateShapeBounds(shapeProps), [shapeProps.points, shapeProps.strokeWidth, shapeProps.erasePaths]);
@@ -35933,22 +35933,24 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
35933
35933
  });
35934
35934
  }
35935
35935
  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();
35936
+ // Use canvas directly as Konva image - no async
35937
+ setBitmap(canvas);
35946
35938
  return () => {
35947
35939
  if (canvasRef.current) {
35948
35940
  canvasRef.current.remove();
35941
+ canvasRef.current = null;
35949
35942
  }
35950
35943
  };
35951
- }, [shapeProps, bounds]); // Re-render when shape or bounds change
35944
+ }, [
35945
+ bounds,
35946
+ shapeProps.points,
35947
+ shapeProps.erasePaths,
35948
+ shapeProps.stroke,
35949
+ shapeProps.strokeWidth,
35950
+ shapeProps.opacity,
35951
+ shapeProps.strokeStyle,
35952
+ shapeProps.type
35953
+ ]);
35952
35954
  // Calculate bounds of the shape including erase paths
35953
35955
  function calculateShapeBounds(shape) {
35954
35956
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
@@ -36067,11 +36069,48 @@ const ErasedShape = ({ shapeProps, isSelected, onSelect, onUpdate, }) => {
36067
36069
  ctx.stroke();
36068
36070
  }
36069
36071
  };
36070
- if (!imageElement) {
36071
- return null; // Still rendering
36072
- }
36073
36072
  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) => {
36073
+ // Render original shape as fallback while bitmap is being created
36074
+ if (!bitmap) {
36075
+ const commonProps = {
36076
+ onClick: onSelect,
36077
+ onTap: onSelect,
36078
+ listening: true,
36079
+ stroke: shapeProps.stroke,
36080
+ strokeWidth: shapeProps.strokeWidth,
36081
+ opacity: shapeProps.opacity,
36082
+ dash: shapeProps.strokeStyle === 'dashed' ? [5, 5] : shapeProps.strokeStyle === 'dotted' ? [2, 3] : undefined,
36083
+ };
36084
+ switch (shapeProps.type) {
36085
+ case 'pencil':
36086
+ return (jsx(Line$1, { points: shapeProps.points.flatMap(p => [p.x, p.y]), lineCap: "round", lineJoin: "round", ...commonProps }));
36087
+ case 'line':
36088
+ if (shapeProps.points.length >= 2) {
36089
+ return (jsx(Line$1, { points: [
36090
+ shapeProps.points[0].x,
36091
+ shapeProps.points[0].y,
36092
+ shapeProps.points[1].x,
36093
+ shapeProps.points[1].y,
36094
+ ], ...commonProps }));
36095
+ }
36096
+ return null;
36097
+ case 'rectangle':
36098
+ if (shapeProps.points.length >= 2) {
36099
+ const [start, end] = shapeProps.points;
36100
+ return (jsx(Rect, { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y, ...commonProps }));
36101
+ }
36102
+ return null;
36103
+ case 'ellipse':
36104
+ if (shapeProps.points.length >= 2) {
36105
+ const [start, end] = shapeProps.points;
36106
+ 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 }));
36107
+ }
36108
+ return null;
36109
+ default:
36110
+ return null;
36111
+ }
36112
+ }
36113
+ return (jsx(Image$1, { image: bitmap, x: bounds.minX - padding, y: bounds.minY - padding, draggable: isSelected, onClick: onSelect, onTap: onSelect, onDragEnd: (e) => {
36075
36114
  const newX = e.target.x();
36076
36115
  const newY = e.target.y();
36077
36116
  const deltaX = newX - (bounds.minX - padding);
@@ -36111,6 +36150,10 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36111
36150
  const containerRef = useRef(null);
36112
36151
  const lastPointerPosition = useRef(null);
36113
36152
  const mouseMoveThrottleRef = useRef(null);
36153
+ // NEW: Eraser preview state for real-time visual feedback
36154
+ const [eraserPreviewPoints, setEraserPreviewPoints] = useState([]);
36155
+ const [keepPreviewVisible, setKeepPreviewVisible] = useState(false);
36156
+ const [justErasedIds, setJustErasedIds] = useState(new Set());
36114
36157
  // Find shapes that intersect with the erase path
36115
36158
  const findIntersectingShapes = (erasePath, shapes) => {
36116
36159
  const eraseRadius = 10; // Half of eraser width
@@ -36201,8 +36244,20 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36201
36244
  setCurrentShapeId(null);
36202
36245
  setCurrentDrawingSessionId(null);
36203
36246
  lastPointerPosition.current = null;
36247
+ setEraserPreviewPoints([]); // Clear eraser preview
36248
+ setKeepPreviewVisible(false);
36249
+ setJustErasedIds(new Set());
36204
36250
  }
36205
36251
  }, [hasToolAccess, state.isDrawing, dispatch]);
36252
+ // Clear justErasedIds after one frame to complete transition
36253
+ useEffect(() => {
36254
+ if (justErasedIds.size > 0) {
36255
+ const id = requestAnimationFrame(() => {
36256
+ setJustErasedIds(new Set());
36257
+ });
36258
+ return () => cancelAnimationFrame(id);
36259
+ }
36260
+ }, [justErasedIds]);
36206
36261
  // Memoized export functionality for performance
36207
36262
  const exportAsImage = useCallback((format = 'png') => {
36208
36263
  if (!stageRef.current) {
@@ -36365,6 +36420,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36365
36420
  if (state.tool === 'eraser') {
36366
36421
  dispatch({ type: 'SET_DRAWING', payload: true });
36367
36422
  setCurrentPoints([pos]);
36423
+ setEraserPreviewPoints([pos]); // NEW: start preview stroke
36368
36424
  setCurrentShapeId('erasing'); // Special ID for erasing mode
36369
36425
  return;
36370
36426
  }
@@ -36413,6 +36469,9 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36413
36469
  setCurrentShapeId(null);
36414
36470
  setCurrentDrawingSessionId(null);
36415
36471
  lastPointerPosition.current = null;
36472
+ setEraserPreviewPoints([]); // Clear eraser preview
36473
+ setKeepPreviewVisible(false);
36474
+ setJustErasedIds(new Set());
36416
36475
  }
36417
36476
  return;
36418
36477
  }
@@ -36430,10 +36489,41 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36430
36489
  return; // Ultra-sensitive for maximum smoothness
36431
36490
  }
36432
36491
  lastPointerPosition.current = pos;
36433
- // Handle eraser tool - just collect points
36492
+ // Handle eraser tool - collect points and stream erase in real-time
36434
36493
  if (state.tool === 'eraser') {
36435
36494
  const newPoints = [...currentPoints, pos];
36436
36495
  setCurrentPoints(newPoints);
36496
+ setEraserPreviewPoints(newPoints); // Live preview
36497
+ // Create segment from last point to current point for real-time streaming
36498
+ const segment = currentPoints.length > 0
36499
+ ? [currentPoints[currentPoints.length - 1], pos]
36500
+ : [pos];
36501
+ // Apply segment to intersecting shapes in real-time
36502
+ const intersectingShapes = findIntersectingShapes(segment, state.shapes);
36503
+ if (intersectingShapes.length > 0) {
36504
+ const timestamp = Date.now();
36505
+ intersectingShapes.forEach((shape, index) => {
36506
+ const updatedShape = {
36507
+ ...shape,
36508
+ erasePaths: [...(shape.erasePaths || []), segment],
36509
+ };
36510
+ // Update the shape locally
36511
+ dispatch({ type: 'UPDATE_SHAPE', payload: updatedShape });
36512
+ // Queue erase action for real-time collaboration
36513
+ if (queueAction) {
36514
+ const erasePayload = {
36515
+ shapeId: shape.id,
36516
+ erasePath: segment,
36517
+ timestamp: timestamp + index,
36518
+ };
36519
+ queueAction({
36520
+ type: 'erase',
36521
+ payload: erasePayload,
36522
+ timestamp: timestamp + index,
36523
+ });
36524
+ }
36525
+ });
36526
+ }
36437
36527
  return;
36438
36528
  }
36439
36529
  let newPoints;
@@ -36483,7 +36573,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36483
36573
  };
36484
36574
  queueAction(continueAction);
36485
36575
  mouseMoveThrottleRef.current = null;
36486
- }, 8); // Optimized to ~120fps for ultra-smooth real-time collaboration
36576
+ }, 1); // Maximum frequency for ultra-smooth real-time collaboration
36487
36577
  }
36488
36578
  }, [hasToolAccess, state.isDrawing, state.tool, state.userId, state.color, state.strokeWidth, state.strokeStyle, state.opacity, currentShapeId, currentDrawingSessionId, currentPoints, queueAction, dispatch]);
36489
36579
  const handleMouseUp = useCallback(() => {
@@ -36496,6 +36586,9 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36496
36586
  setCurrentShapeId(null);
36497
36587
  setCurrentDrawingSessionId(null);
36498
36588
  lastPointerPosition.current = null;
36589
+ setEraserPreviewPoints([]); // Clear eraser preview
36590
+ setKeepPreviewVisible(false);
36591
+ setJustErasedIds(new Set());
36499
36592
  }
36500
36593
  return;
36501
36594
  }
@@ -36511,36 +36604,23 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36511
36604
  setCurrentDrawingSessionId(null);
36512
36605
  return;
36513
36606
  }
36514
- // Handle eraser tool - apply erase to intersecting shapes
36607
+ // Handle eraser tool - cleanup on mouse up (erasing already done in mouse move)
36515
36608
  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
- });
36609
+ // Stop drawing state
36610
+ dispatch({ type: 'SET_DRAWING', payload: false });
36539
36611
  // Reset erasing state
36540
36612
  setCurrentPoints([]);
36541
36613
  setCurrentShapeId(null);
36542
36614
  setCurrentDrawingSessionId(null);
36543
36615
  lastPointerPosition.current = null;
36616
+ // Clear preview after a brief delay to ensure smooth transition
36617
+ requestAnimationFrame(() => {
36618
+ requestAnimationFrame(() => {
36619
+ setEraserPreviewPoints([]);
36620
+ setKeepPreviewVisible(false);
36621
+ setJustErasedIds(new Set());
36622
+ });
36623
+ });
36544
36624
  return;
36545
36625
  }
36546
36626
  // Handle regular drawing tools
@@ -36607,31 +36687,39 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36607
36687
  if (shape.isEraser) {
36608
36688
  return null;
36609
36689
  }
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
36690
  const commonProps = {
36616
36691
  shapeProps: shape,
36617
36692
  isSelected: shape.isSelected || false,
36618
36693
  onSelect: () => handleShapeClick(shape.id),
36619
36694
  onUpdate: handleShapeUpdate,
36620
36695
  };
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;
36696
+ // Render original shape component
36697
+ const OriginalShape = () => {
36698
+ switch (shape.type) {
36699
+ case 'rectangle':
36700
+ return jsx(Rectangle, { ...commonProps });
36701
+ case 'ellipse':
36702
+ return jsx(Ellipse, { ...commonProps });
36703
+ case 'line':
36704
+ return jsx(Line, { ...commonProps });
36705
+ case 'pencil':
36706
+ return jsx(FreehandDrawing, { ...commonProps });
36707
+ case 'arrow':
36708
+ return jsx(Arrow, { ...commonProps });
36709
+ default:
36710
+ return null;
36711
+ }
36712
+ };
36713
+ // Use ErasedShape component for shapes that have erase paths applied
36714
+ if (shape.erasePaths && shape.erasePaths.length > 0) {
36715
+ // If this shape just got erased, render both to prevent flicker
36716
+ if (justErasedIds.has(shape.id)) {
36717
+ return (jsxs(Fragment, { children: [jsx(OriginalShape, {}), jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate })] }));
36718
+ }
36719
+ return (jsx(ErasedShape, { shapeProps: shape, isSelected: shape.isSelected || false, onSelect: () => handleShapeClick(shape.id), onUpdate: handleShapeUpdate }));
36634
36720
  }
36721
+ // Use original shape components for shapes without erase operations
36722
+ return jsx(OriginalShape, {});
36635
36723
  }, (prevProps, nextProps) => {
36636
36724
  // Ultra-precise comparison to prevent unnecessary re-renders during simultaneous drawing
36637
36725
  const prevShape = prevProps.shape;
@@ -36738,11 +36826,11 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36738
36826
  state.tool === 'select' ? 'default' :
36739
36827
  state.tool === 'pan' ? 'grab' : 'crosshair'
36740
36828
  }), [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] }) }) }));
36829
+ 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(() => {
36830
+ if (state.backgroundColor === 'transparent')
36831
+ return null;
36832
+ return (jsx(Rect, { x: 0, y: 0, width: size.width, height: size.height, fill: state.backgroundColor, listening: false }));
36833
+ }, [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
36834
  });
36747
36835
  // Memoize the Board component to prevent unnecessary re-renders
36748
36836
  const Board = React__default.memo(BoardComponent, (prevProps, nextProps) => {
@@ -37689,15 +37777,15 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
37689
37777
  totalActions: 0,
37690
37778
  });
37691
37779
  // 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
37780
+ const THROTTLE_DELAY = 10; // ms - Minimal delay for faster transmission
37781
+ const MAX_ACTIONS_PER_BATCH = 8; // Smaller batches for faster transmission
37694
37782
  const MAX_MESSAGE_SIZE = 500; // characters - matches API constraint
37695
- const MAX_MESSAGES_PER_SECOND = 15; // Increased rate for smoother collaboration
37783
+ const MAX_MESSAGES_PER_SECOND = 30; // Maximum rate for smoother collaboration
37696
37784
  const MAX_PAYLOAD_SIZE = 1024; // bytes (1KB) - matches API constraint
37697
37785
  // 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
37786
+ const DRAWING_THROTTLE_DELAY = 1; // ms - Maximum frequency for ultra-smooth drawing actions
37787
+ const DRAWING_BATCH_SIZE = 2; // Minimal batches for immediate transmission
37788
+ const DRAWING_IMMEDIATE_THRESHOLD = 1; // Send immediately if we have 1+ drawing actions
37701
37789
  // Message rate limiting
37702
37790
  const messageTimestampsRef = useRef([]);
37703
37791
  const isRateLimited = useCallback(() => {
@@ -37861,7 +37949,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
37861
37949
  let delay = THROTTLE_DELAY;
37862
37950
  if (remainingDrawingActions.length > 0) {
37863
37951
  delay = remainingDrawingActions.length >= DRAWING_IMMEDIATE_THRESHOLD ?
37864
- Math.max(DRAWING_THROTTLE_DELAY / 2, 4) : // Even faster for multiple actions
37952
+ Math.max(DRAWING_THROTTLE_DELAY / 2, 2) : // Even faster for multiple actions
37865
37953
  DRAWING_THROTTLE_DELAY;
37866
37954
  }
37867
37955
  throttleTimerRef.current = setTimeout(() => {
@@ -38004,7 +38092,7 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
38004
38092
  clearTimeout(throttleTimerRef.current);
38005
38093
  throttleTimerRef.current = setTimeout(() => {
38006
38094
  transmitRef.current?.();
38007
- }, Math.max(DRAWING_THROTTLE_DELAY / 4, 2)); // Ultra-fast transmission
38095
+ }, 1); // Ultra-fast transmission - 1ms for maximum responsiveness
38008
38096
  }
38009
38097
  }
38010
38098
  }, [state.canvasSize, state.userId, roomId, callbacks, sendWithConstraints]);
@@ -38024,7 +38112,11 @@ const useCollaborativeWhiteboard = (roomId, callbacks) => {
38024
38112
  // Process each action in the batch with duplicate prevention
38025
38113
  parsedData.actions.forEach(action => {
38026
38114
  // Create unique action ID for deduplication
38027
- const actionId = `${action.type}-${parsedData.userId}-${action.timestamp || Date.now()}`;
38115
+ // For erase actions, include shapeId to allow multiple erases in same stroke
38116
+ let actionId = `${action.type}-${parsedData.userId}-${action.timestamp || Date.now()}`;
38117
+ if (action.type === 'erase' && action.payload && typeof action.payload === 'object' && 'shapeId' in action.payload) {
38118
+ actionId = `${action.type}-${parsedData.userId}-${action.payload.shapeId}-${action.timestamp || Date.now()}`;
38119
+ }
38028
38120
  // Skip if we've already processed this action (prevents shape loss from duplicate processing)
38029
38121
  if (processedActionsRef.current.has(actionId)) {
38030
38122
  return;