From 4dc915d5c93dcce7ab6b3cc7035bd3c6fbb1500e Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Fri, 13 Jan 2023 17:45:20 -0600 Subject: [PATCH] fix(gui): prevent mask canvas from going into a save loop --- gui/src/components/MaskCanvas.tsx | 114 +++++++++++++++++------------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/gui/src/components/MaskCanvas.tsx b/gui/src/components/MaskCanvas.tsx index f2a2ca61..334bafd9 100644 --- a/gui/src/components/MaskCanvas.tsx +++ b/gui/src/components/MaskCanvas.tsx @@ -21,6 +21,12 @@ export const THRESHOLDS = { upper: 224, }; +export const MASK_STATE = { + clean: 'clean', + painting: 'painting', + dirty: 'dirty', +}; + export function floodBelow(n: number): number { if (n < THRESHOLDS.upper) { return COLORS.black; @@ -82,46 +88,57 @@ export function MaskCanvas(props: MaskCanvasProps) { } ctx.putImageData(image, 0, 0); - - // eslint-disable-next-line @typescript-eslint/no-floating-promises save(); } - function saveMask(): Promise { + function saveMask(): void { // eslint-disable-next-line no-console console.log('starting canvas save'); - return new Promise((res, _rej) => { - if (doesExist(canvasRef.current)) { - canvasRef.current.toBlob((blob) => { - // eslint-disable-next-line no-console - console.log('finishing canvas save'); - - props.onSave(mustExist(blob)); - res(); - }); - } else { - res(); + if (doesExist(canvasRef.current)) { + if (state.current === MASK_STATE.clean) { + // eslint-disable-next-line no-console + console.log('attempting to save a clean canvas'); + return; } - }); + + canvasRef.current.toBlob((blob) => { + // eslint-disable-next-line no-console + console.log('finishing canvas save'); + + state.current = MASK_STATE.clean; + props.onSave(mustExist(blob)); + }); + } else { + // eslint-disable-next-line no-console + console.log('canvas no longer exists'); + } } - function drawSource(file: Blob): Promise { + function drawCircle(ctx: CanvasRenderingContext2D, point: Point): void { + ctx.beginPath(); + ctx.arc(point.x, point.y, brushSize, 0, FULL_CIRCLE); + ctx.fill(); + } + + function drawSource(file: Blob): void { const image = new Image(); - return new Promise((res, _rej) => { - image.onload = () => { - const canvas = mustExist(canvasRef.current); - const ctx = mustExist(canvas.getContext('2d')); - ctx.drawImage(image, 0, 0); - URL.revokeObjectURL(src); + image.onload = () => { + const canvas = mustExist(canvasRef.current); + const ctx = mustExist(canvas.getContext('2d')); + ctx.drawImage(image, 0, 0); + URL.revokeObjectURL(src); + }; - // putting a save call here has a tendency to go into an infinite loop - res(); - }; + const src = URL.createObjectURL(file); + image.src = src; + } - const src = URL.createObjectURL(file); - image.src = src; - }); + function finishPainting() { + if (state.current === MASK_STATE.painting) { + state.current = MASK_STATE.dirty; + save(); + } } const save = useMemo(() => throttle(saveMask, SAVE_TIME), []); @@ -130,21 +147,19 @@ export function MaskCanvas(props: MaskCanvasProps) { const canvasRef = useRef(null); // painting state + const state = useRef(MASK_STATE.clean); const [clicks, setClicks] = useState>([]); - const [painting, setPainting] = useState(false); const [brushColor, setBrushColor] = useState(DEFAULT_BRUSH.color); const [brushSize, setBrushSize] = useState(DEFAULT_BRUSH.size); useEffect(() => { // including clicks.length prevents the initial render from saving a blank canvas - if (doesExist(canvasRef.current) && clicks.length > 0) { + if (doesExist(canvasRef.current) && state.current === MASK_STATE.painting && clicks.length > 0) { const ctx = mustExist(canvasRef.current.getContext('2d')); ctx.fillStyle = grayToRGB(brushColor); for (const click of clicks) { - ctx.beginPath(); - ctx.arc(click.x, click.y, brushSize, 0, FULL_CIRCLE); - ctx.fill(); + drawCircle(ctx, click); } clicks.length = 0; @@ -152,15 +167,15 @@ export function MaskCanvas(props: MaskCanvasProps) { }, [clicks.length]); useEffect(() => { - if (painting === false) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises + // eslint-disable-next-line no-console + console.log('state hook called', state.current); + if (state.current === MASK_STATE.dirty) { save(); } - }, [painting]); + }, [state.current]); useEffect(() => { if (doesExist(canvasRef.current) && doesExist(source)) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises drawSource(source); } }, [source]); @@ -177,26 +192,25 @@ export function MaskCanvas(props: MaskCanvasProps) { onClick={(event) => { const canvas = mustExist(canvasRef.current); const bounds = canvas.getBoundingClientRect(); + const ctx = mustExist(canvas.getContext('2d')); + ctx.fillStyle = grayToRGB(brushColor); - setClicks([...clicks, { + drawCircle(ctx, { x: event.clientX - bounds.left, y: event.clientY - bounds.top, - }]); + }); + + state.current = MASK_STATE.dirty; + save(); }} onMouseDown={() => { - setPainting(true); - }} - onMouseLeave={() => { - setPainting(false); - }} - onMouseOut={() => { - setPainting(false); - }} - onMouseUp={() => { - setPainting(false); + state.current = MASK_STATE.painting; }} + onMouseLeave={finishPainting} + onMouseOut={finishPainting} + onMouseUp={finishPainting} onMouseMove={(event) => { - if (painting) { + if (state.current === MASK_STATE.painting) { const canvas = mustExist(canvasRef.current); const bounds = canvas.getBoundingClientRect();