From 93a71679db94f2227b8ec80e4a3030f651f5af1f Mon Sep 17 00:00:00 2001 From: Sujong Kwak Date: Mon, 2 Dec 2024 15:45:05 +0900 Subject: [PATCH] fix: Remove duplicate points in CRDT (#177) * fix: Remove duplicate pixel strokes * fix: Record intermediate points between the start and end of drawing strokes * feat: Add logic to calculate remaining ink pixel count --- client/src/hooks/canvas/useDrawing.ts | 89 +++++++++++-------- .../src/hooks/canvas/useDrawingOperation.ts | 2 + 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/client/src/hooks/canvas/useDrawing.ts b/client/src/hooks/canvas/useDrawing.ts index c92eaea5..1154ef32 100644 --- a/client/src/hooks/canvas/useDrawing.ts +++ b/client/src/hooks/canvas/useDrawing.ts @@ -1,4 +1,4 @@ -import { RefObject, useCallback } from 'react'; +import { RefObject, useCallback, useRef } from 'react'; import { Point, CRDTMessage, @@ -85,12 +85,15 @@ export const useDrawing = ( ) => { const state = useDrawingState(options); const operation = useDrawingOperation(canvasRef, state); + const currentDrawingPoints = useRef([]); const startDrawing = useCallback( (point: Point): CRDTUpdateMessage | null => { if (state.checkInkAvailability() === false || !state.crdtRef.current) return null; state.currentStrokeIdsRef.current = []; + currentDrawingPoints.current = [point]; + const drawingData = state.drawingMode === DRAWING_MODE.FILL ? operation.floodFill(Math.floor(point.x), Math.floor(point.y)) @@ -118,27 +121,29 @@ export const useDrawing = ( const continueDrawing = useCallback( (point: Point): CRDTUpdateMessage | null => { - if (!state.crdtRef.current || state.inkRemaining <= 0) return null; + if (!state.crdtRef.current || currentDrawingPoints.current.length === 0 || state.inkRemaining <= 0) return null; if (state.drawingMode === DRAWING_MODE.FILL) return null; - const lastStrokeId = state.currentStrokeIdsRef.current[state.currentStrokeIdsRef.current.length - 1]; - const lastStroke = state.crdtRef.current.strokes.find((s) => s.id === lastStrokeId); - if (!lastStroke) return null; - - const updatedDrawing = { - ...lastStroke.stroke, - points: [...lastStroke.stroke.points, point], - }; + const lastPoint = currentDrawingPoints.current[currentDrawingPoints.current.length - 1]; + if (lastPoint.x === point.x && lastPoint.y === point.y) return null; - const lastPoint = lastStroke.stroke.points[lastStroke.stroke.points.length - 1]; const pixelsUsed = Math.ceil( Math.sqrt(Math.pow(point.x - lastPoint.x, 2) + Math.pow(point.y - lastPoint.y, 2)) * state.brushSize, ); state.setInkRemaining((prev: number) => Math.max(0, prev - pixelsUsed)); - const strokeId = state.crdtRef.current.addStroke(updatedDrawing); + currentDrawingPoints.current.push(point); + + const drawingData = { + points: [...currentDrawingPoints.current], + style: operation.getCurrentStyle(), + }; + + const strokeId = state.crdtRef.current.addStroke(drawingData); state.currentStrokeIdsRef.current.push(strokeId); - operation.drawStroke(updatedDrawing); + operation.drawStroke(drawingData); + + currentDrawingPoints.current = [point]; return { type: CRDTMessageTypes.UPDATE, @@ -152,24 +157,35 @@ export const useDrawing = ( ); const stopDrawing = useCallback(() => { - if (!state.crdtRef.current || state.currentStrokeIdsRef.current.length === 0) return; + if (!state.crdtRef.current || !currentDrawingPoints.current || state.currentStrokeIdsRef.current.length === 0) + return; if (state.historyPointerRef.current < state.strokeHistoryRef.current.length - 1) { state.strokeHistoryRef.current = state.strokeHistoryRef.current.slice(0, state.historyPointerRef.current + 1); } - const lastStrokeId = state.currentStrokeIdsRef.current[state.currentStrokeIdsRef.current.length - 1]; - const lastStroke = state.crdtRef.current.strokes.find((s) => s.id === lastStrokeId); + const allPoints: Point[] = []; + state.currentStrokeIdsRef.current.forEach((strokeId) => { + const stroke = state.crdtRef.current!.strokes.find((s) => s.id === strokeId); + if (stroke) { + allPoints.push(...stroke.stroke.points); + } + }); - if (!lastStroke) return; + const drawingData = { + points: allPoints, + style: operation.getCurrentStyle(), + }; state.strokeHistoryRef.current.push({ strokeIds: [...state.currentStrokeIdsRef.current], isLocal: true, - drawingData: lastStroke.stroke, + drawingData, }); + state.historyPointerRef.current = state.strokeHistoryRef.current.length - 1; + currentDrawingPoints.current = []; state.currentStrokeIdsRef.current = []; state.updateHistoryState(); }, [state]); @@ -217,26 +233,25 @@ export const useDrawing = ( nextEntry = state.strokeHistoryRef.current[state.historyPointerRef.current + 1]; } - if (!nextEntry?.isLocal) return null; + if (!nextEntry?.isLocal || !nextEntry.drawingData) return null; - const updates = nextEntry.strokeIds.map((): CRDTUpdateMessage => { - const strokeId = state.crdtRef.current!.addStroke(nextEntry.drawingData); - return { - type: CRDTMessageTypes.UPDATE, - state: { - key: strokeId, - register: state.crdtRef.current!.state[strokeId], - }, - }; - }); + const strokeId = state.crdtRef.current.addStroke(nextEntry.drawingData); - nextEntry.strokeIds = updates.map((update) => update.state.key); + const update: CRDTUpdateMessage = { + type: CRDTMessageTypes.UPDATE, + state: { + key: strokeId, + register: state.crdtRef.current.state[strokeId], + }, + }; + + nextEntry.strokeIds = [strokeId]; state.historyPointerRef.current++; state.updateHistoryState(); operation.redrawCanvas(); - return updates; + return [update]; }, [state, operation]); const applyDrawing = useCallback( @@ -258,11 +273,15 @@ export const useDrawing = ( const isLocalUpdate = peerId === state.currentPlayerId; if (!state.crdtRef.current.mergeRegister(key, register) || isLocalUpdate) return; - operation.redrawCanvas(); const stroke = register[2]; - // stroke가 null이 아닌 경우 (삭제되지 않은 경우) 히스토리 추가 - if (!stroke) return; + if (!stroke) { + operation.redrawCanvas(); + return; + } + + operation.drawStroke(stroke); + if (state.historyPointerRef.current < state.strokeHistoryRef.current.length - 1) { state.strokeHistoryRef.current = state.strokeHistoryRef.current.slice(0, state.historyPointerRef.current + 1); } @@ -276,7 +295,7 @@ export const useDrawing = ( state.updateHistoryState(); } }, - [state.currentPlayerId, operation.redrawCanvas, state.updateHistoryState, roomStatus], + [state.currentPlayerId, operation, roomStatus], ); const getAllDrawingData = useCallback((): CRDTSyncMessage | null => { diff --git a/client/src/hooks/canvas/useDrawingOperation.ts b/client/src/hooks/canvas/useDrawingOperation.ts index 7549ac3c..b570aaeb 100644 --- a/client/src/hooks/canvas/useDrawingOperation.ts +++ b/client/src/hooks/canvas/useDrawingOperation.ts @@ -82,6 +82,8 @@ export const useDrawingOperation = ( ctx.fillStyle = style.color; ctx.lineWidth = style.width; ctx.beginPath(); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; if (points.length === 1) { const point = points[0];