drawing-model / src /app /page.js
qtitpc's picture
Upload 23 files
91c8a81 verified
Raw
History Blame Contribute Delete
16.5 kB
'use client'
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import rough from "roughjs/bundled/rough.esm";
import DrawingBoard from './DrawingBoard';
import {
createElement,
drawElement,
} from './element-utils';
import { createMouseHandlers } from './handleMouse';
const captureRegionToPNG = (canvas, region, elements, scale, panOffset, scaleOffset) => {
const tempCanvas = document.createElement('canvas');
const ctx = tempCanvas.getContext('2d');
const width = (region.bounds.x2 - region.bounds.x1);
const height = (region.bounds.y2 - region.bounds.y1);
tempCanvas.width = width * scale;
tempCanvas.height = height * scale;
// Fill white background
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
ctx.save();
ctx.scale(scale, scale);
ctx.translate(-region.bounds.x1, -region.bounds.y1);
const mainCanvas = canvas.getContext('2d');
const roughCanvas = rough.canvas(tempCanvas);
ctx.translate(panOffset.x, panOffset.y);
elements.forEach(element => {
if (isElementInRegion(element, region.bounds)) {
drawElement(roughCanvas, ctx, element);
}
});
ctx.restore();
return tempCanvas.toDataURL('image/png');
};
// Update the downloadRegionImage function:
const downloadRegionImage = (dataUrl, regionId) => {
const link = document.createElement('a');
link.href = dataUrl;
link.download = `region-${regionId}.png`; // Changed extension to .png
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const isElementInRegion = (element, bounds) => {
if (element.type === 'pencil') {
return element.points.some(point =>
point.x >= bounds.x1 && point.x <= bounds.x2 &&
point.y >= bounds.y1 && point.y <= bounds.y2
);
} else {
const elementBounds = {
minX: Math.min(element.x1, element.x2),
maxX: Math.max(element.x1, element.x2),
minY: Math.min(element.y1, element.y2),
maxY: Math.max(element.y1, element.y2)
};
return !(elementBounds.maxX < bounds.x1 ||
elementBounds.minX > bounds.x2 ||
elementBounds.maxY < bounds.y1 ||
elementBounds.minY > bounds.y2);
}
};
const downloadAllRegions = (canvas, regions, elements, scale, panOffset, scaleOffset) => {
regions.forEach(region => {
const dataUrl = captureRegionToPNG(canvas, region, elements, scale, panOffset, scaleOffset);
downloadRegionImage(dataUrl, region.id);
});
};
const isPixelNonTransparent = (pixel) => {
return pixel.a > 0;
};
const isWithinBounds = (x, y, width, height) => {
return x >= 0 && x < width && y >= 0 && y < height;
};
const getPixel = (imageData, x, y) => {
const index = (y * imageData.width + x) * 4;
return imageData.data[index + 3] > 0; // Only check alpha channel for performance
};
// Use Set for faster lookups and Int32Array for coordinates
const floodFill = (imageData, startX, startY, visited) => {
const width = imageData.width;
const height = imageData.height;
const queue = new Int32Array(width * height * 2); // Pre-allocate queue
let queueStart = 0;
let queueEnd = 2;
queue[0] = startX;
queue[1] = startY;
const region = {
points: [],
minX: startX,
maxX: startX,
minY: startY,
maxY: startY
};
// Optimize bounds checking
const isWithinBounds = (x, y) => x >= 0 && x < width && y >= 0 && y < height;
// Pre-calculate neighbor offsets
const neighborOffsets = [
[1, 0], [-1, 0],
[0, 1], [0, -1],
[1, 1], [-1, -1],
[1, -1], [-1, 1]
];
while (queueStart < queueEnd) {
const x = queue[queueStart];
const y = queue[queueStart + 1];
queueStart += 2;
const key = `${x},${y}`;
if (visited.has(key)) continue;
if (!getPixel(imageData, x, y)) continue;
visited.add(key);
region.points.push({ x, y });
// Use Math.min/max for bounds tracking
region.minX = Math.min(region.minX, x);
region.maxX = Math.max(region.maxX, x);
region.minY = Math.min(region.minY, y);
region.maxY = Math.max(region.maxY, y);
// Check neighbors using pre-calculated offsets
for (const [dx, dy] of neighborOffsets) {
const nx = x + dx;
const ny = y + dy;
if (isWithinBounds(nx, ny) && !visited.has(`${nx},${ny}`)) {
queue[queueEnd] = nx;
queue[queueEnd + 1] = ny;
queueEnd += 2;
}
}
}
return region;
};
// Optimize distance calculation
const calculateRegionDistance = (region1, region2) => {
// Quick overlap check
const xOverlap = !(region1.maxX < region2.minX || region1.minX > region2.maxX);
const yOverlap = !(region1.maxY < region2.minY || region1.minY > region2.maxY);
if (xOverlap && yOverlap) return 0;
// Calculate distance only when necessary
const dx = !xOverlap ? Math.min(
Math.abs(region1.maxX - region2.minX),
Math.abs(region1.minX - region2.maxX)
) : 0;
const dy = !yOverlap ? Math.min(
Math.abs(region1.maxY - region2.minY),
Math.abs(region1.minY - region2.maxY)
) : 0;
return Math.sqrt(dx * dx + dy * dy);
};
// Optimize region merging using Set
const mergeRegions = (region1, region2) => ({
points: [...region1.points, ...region2.points],
minX: Math.min(region1.minX, region2.minX),
maxX: Math.max(region1.maxX, region2.maxX),
minY: Math.min(region1.minY, region2.minY),
maxY: Math.max(region1.maxY, region2.maxY)
});
const detectRegions = (canvas, elements, panOffset, scale, scaleOffset, minRegionSize = 100, groupingDistance = 80) => {
// Calculate bounds only once
const bounds = elements.reduce((acc, element) => {
if (element.type === 'pencil') {
element.points.forEach(point => {
acc.minX = Math.min(acc.minX, point.x);
acc.minY = Math.min(acc.minY, point.y);
acc.maxX = Math.max(acc.maxX, point.x);
acc.maxY = Math.max(acc.maxY, point.y);
});
} else {
const { x1, y1, x2, y2 } = element;
acc.minX = Math.min(acc.minX, x1, x2);
acc.minY = Math.min(acc.minY, y1, y2);
acc.maxX = Math.max(acc.maxX, x1, x2);
acc.maxY = Math.max(acc.maxY, y1, y2);
}
return acc;
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
// Add padding
const padding = 100;
bounds.minX -= padding;
bounds.minY -= padding;
bounds.maxX += padding;
bounds.maxY += padding;
// Create optimized temporary canvas
const tempCanvas = document.createElement("canvas");
const width = Math.max(bounds.maxX - bounds.minX, canvas.width);
const height = Math.max(bounds.maxY - bounds.minY, canvas.height);
tempCanvas.width = width * scale;
tempCanvas.height = height * scale;
// Use OffscreenCanvas when available for better performance
const ctx = tempCanvas.getContext("2d", { alpha: true });
const roughCanvas = rough.canvas(tempCanvas);
// Draw elements with transformation
ctx.save();
ctx.translate(-bounds.minX * scale, -bounds.minY * scale);
ctx.scale(scale, scale);
elements.forEach(element => drawElement(roughCanvas, ctx, element));
ctx.restore();
// Process image data
const imageData = ctx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const visited = new Set();
const initialRegions = [];
// Optimize pixel scanning with stride
const stride = 4; // Check every 4th pixel initially
for (let y = 0; y < tempCanvas.height; y += stride) {
for (let x = 0; x < tempCanvas.width; x += stride) {
const key = `${x},${y}`;
if (visited.has(key)) continue;
if (!getPixel(imageData, x, y)) continue;
const region = floodFill(imageData, x, y, visited);
if (region.points.length >= minRegionSize) {
// Transform coordinates back
const transformedRegion = {
...region,
minX: region.minX / scale + bounds.minX,
maxX: region.maxX / scale + bounds.minX,
minY: region.minY / scale + bounds.minY,
maxY: region.maxY / scale + bounds.minY,
points: region.points.map(point => ({
x: point.x / scale + bounds.minX,
y: point.y / scale + bounds.minY
}))
};
initialRegions.push(transformedRegion);
}
}
}
// Optimize region merging
const mergedRegions = [];
const used = new Set();
for (let i = 0; i < initialRegions.length; i++) {
if (used.has(i)) continue;
let currentRegion = initialRegions[i];
used.add(i);
let merged;
do {
merged = false;
for (let j = 0; j < initialRegions.length; j++) {
if (used.has(j)) continue;
const distance = calculateRegionDistance(currentRegion, initialRegions[j]);
if (distance <= groupingDistance) {
currentRegion = mergeRegions(currentRegion, initialRegions[j]);
used.add(j);
merged = true;
}
}
} while (merged);
mergedRegions.push(currentRegion);
}
// Return final regions
return mergedRegions.map((region, index) => ({
id: index + 1,
bounds: {
x1: region.minX,
y1: region.minY,
x2: region.maxX,
y2: region.maxY
},
elements: []
}));
};
const useHistory = initialState => {
const [index, setIndex] = useState(0);
const [history, setHistory] = useState([initialState]);
const setState = (action, overwrite = false) => {
const newState = typeof action === "function" ? action(history[index]) : action;
if (overwrite) {
const historyCopy = [...history];
historyCopy[index] = newState;
setHistory(historyCopy);
} else {
const updatedState = [...history].slice(0, index + 1);
setHistory([...updatedState, newState]);
setIndex(prevState => prevState + 1);
}
};
const undo = () => index > 0 && setIndex(prevState => prevState - 1);
const redo = () => index < history.length - 1 && setIndex(prevState => prevState + 1);
return [history[index], setState, undo, redo];
};
const usePressedKeys = () => {
const [pressedKeys, setPressedKeys] = useState(new Set());
useEffect(() => {
const handleKeyDown = event => {
setPressedKeys(prevKeys => new Set(prevKeys).add(event.key));
};
const handleKeyUp = event => {
setPressedKeys(prevKeys => {
const updatedKeys = new Set(prevKeys);
updatedKeys.delete(event.key);
return updatedKeys;
});
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
return pressedKeys;
};
const drawBoundingBoxes = (context, regions, scale) => {
context.save();
regions.forEach(region => {
const { bounds, id } = region;
// Set styling for bounding box
context.strokeStyle = "#FF5733"; // Custom color for better visibility
context.lineWidth = 1 / scale;
context.setLineDash([5 / scale, 5 / scale]); // Dotted lines for bounding box
// Draw bounding box
context.strokeRect(
bounds.x1,
bounds.y1,
bounds.x2 - bounds.x1,
bounds.y2 - bounds.y1
);
// Display region ID inside bounding box
context.setLineDash([]); // Reset line dash for text
context.font = `${16 / scale}px sans-serif`;
context.fillStyle = "#FF5733";
context.fillText(
`#${id}`,
bounds.x1 + 5 / scale,
bounds.y1 + 20 / scale
);
});
context.restore();
};
const App = () => {
const [elements, setElements, undo, redo] = useHistory([]);
const [action, setAction] = useState("none");
const [tool, setTool] = useState("rectangle");
const [selectedElement, setSelectedElement] = useState(null);
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
const [startPanMousePosition, setStartPanMousePosition] = useState({ x: 0, y: 0 });
const [scale, setScale] = useState(1);
const [scaleOffset, setScaleOffset] = useState({ x: 0, y: 0 });
const [captureArea, setCaptureArea] = useState(null);
const [drawingRegions, setDrawingRegions] = useState([]);
const textAreaRef = useRef();
const pressedKeys = usePressedKeys();
const [pencilSize, setPencilSize] = useState(3);
const [isDrawing, setIsDrawing] = useState(false);
const handleDetectRegions = () => {
const canvas = document.getElementById("canvas");
if (!canvas) return;
const newRegions = detectRegions(canvas, elements, panOffset, scale, scaleOffset);
setDrawingRegions(newRegions);
// Notify user
if (newRegions.length > 0) {
console.log(`Phát hiện ${newRegions.length} vùng vẽ!`);
alert(`Đã phát hiện ${newRegions.length} vùng vẽ!`);
} else {
alert("Không phát hiện được vùng vẽ nào!");
}
};
useLayoutEffect(() => {
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
const roughCanvas = rough.canvas(canvas);
// Clear canvas for redrawing
context.clearRect(0, 0, canvas.width, canvas.height);
// Calculate scaled dimensions
const scaledWidth = canvas.width * scale;
const scaledHeight = canvas.height * scale;
const scaleOffsetX = (scaledWidth - canvas.width) / 2;
const scaleOffsetY = (scaledHeight - canvas.height) / 2;
setScaleOffset({ x: scaleOffsetX, y: scaleOffsetY });
context.save();
context.translate(panOffset.x * scale - scaleOffsetX, panOffset.y * scale - scaleOffsetY);
context.scale(scale, scale);
// Draw elements
elements.forEach(element => {
if (action === "writing" && selectedElement?.id === element.id) return;
drawElement(roughCanvas, context, element);
});
// Call drawBoundingBoxes to render detected regions
drawBoundingBoxes(context, drawingRegions, scale);
context.restore();
}, [elements, action, selectedElement, panOffset, scale, drawingRegions]);
useEffect(() => {
const undoRedoFunction = event => {
if ((event.metaKey || event.ctrlKey) && event.key === "z") {
if (event.shiftKey) {
redo();
} else {
undo();
}
}
};
document.addEventListener("keydown", undoRedoFunction);
return () => {
document.removeEventListener("keydown", undoRedoFunction);
};
}, [undo, redo]);
useEffect(() => {
const panOrZoomFunction = event => {
if (pressedKeys.has("Meta") || pressedKeys.has("Control")) {
onZoom(event.deltaY * -0.01);
} else {
setPanOffset(prevState => ({
x: prevState.x - event.deltaX,
y: prevState.y - event.deltaY,
}));
}
};
document.addEventListener("wheel", panOrZoomFunction);
return () => {
document.removeEventListener("wheel", panOrZoomFunction);
};
}, [pressedKeys]);
const onZoom = delta => {
setScale(prevState => Math.min(Math.max(prevState + delta, 0.1), 2));
};
const handleDownloadRegions = () => {
const canvas = document.getElementById("canvas");
if (!canvas || drawingRegions.length === 0) {
alert("Không có vùng vẽ nào để tải xuống!");
return;
}
downloadAllRegions(canvas, drawingRegions, elements, scale, panOffset, scaleOffset);
alert(`Đã tải xuống ${drawingRegions.length} vùng vẽ!`);
};
const {
handleMouseDown,
handleMouseMove,
handleMouseUp,
handleBlur
} = createMouseHandlers({
action,
setAction,
tool,
setTool,
elements,
setElements,
selectedElement,
setSelectedElement,
panOffset,
setPanOffset,
scale,
scaleOffset,
startPanMousePosition,
setStartPanMousePosition,
pressedKeys,
pencilSize,
setCaptureArea,
isDrawing,
setIsDrawing
});
return (
<DrawingBoard
tool={tool}
setTool={setTool}
elements={elements}
pencilSize={pencilSize}
setPencilSize={setPencilSize}
scale={scale}
setScale={setScale}
onZoom={onZoom}
undo={undo}
redo={redo}
action={action}
selectedElement={selectedElement}
panOffset={panOffset}
scaleOffset={scaleOffset}
handleMouseDown={handleMouseDown}
handleMouseMove={handleMouseMove}
handleMouseUp={handleMouseUp}
handleBlur={handleBlur}
handleDetectRegions={handleDetectRegions}
handleDownloadRegions={handleDownloadRegions}
/>
);
};
export default App;