feat: paint bucket tool
This commit is contained in:
parent
5d8b073a1e
commit
a7f39aa430
3 changed files with 78 additions and 13 deletions
|
|
@ -336,6 +336,13 @@ function Canvas() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
|
// Directions for adjacent blocks (up, down, left, right)
|
||||||
|
const directions = [
|
||||||
|
{ dx: 0, dy: 1 },
|
||||||
|
{ dx: 0, dy: -1 },
|
||||||
|
{ dx: 1, dy: 0 },
|
||||||
|
{ dx: -1, dy: 0 },
|
||||||
|
];
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case "magic-wand": {
|
case "magic-wand": {
|
||||||
const visited = new Set<string>();
|
const visited = new Set<string>();
|
||||||
|
|
@ -353,13 +360,6 @@ function Canvas() {
|
||||||
|
|
||||||
result.push([x, y]);
|
result.push([x, y]);
|
||||||
|
|
||||||
// Directions for adjacent blocks (up, down, left, right)
|
|
||||||
const directions = [
|
|
||||||
{ dx: 0, dy: 1 },
|
|
||||||
{ dx: 0, dy: -1 },
|
|
||||||
{ dx: 1, dy: 0 },
|
|
||||||
{ dx: -1, dy: 0 },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { dx, dy } of directions) {
|
for (const { dx, dy } of directions) {
|
||||||
const newX = x + dx;
|
const newX = x + dx;
|
||||||
|
|
@ -390,6 +390,45 @@ function Canvas() {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "paint-bucket": {
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const startBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
||||||
|
const startName = startBlock ? startBlock.name : "air";
|
||||||
|
|
||||||
|
// If the target area is already the selected block, break
|
||||||
|
if (startName === selectedBlock) break;
|
||||||
|
|
||||||
|
function floodFill(x: number, y: number) {
|
||||||
|
const key = `${x},${y}`;
|
||||||
|
if (visited.has(key)) return;
|
||||||
|
visited.add(key);
|
||||||
|
|
||||||
|
const withinCanvas = x >= canvasSize.minX && x < canvasSize.maxX && y >= canvasSize.minY && y < canvasSize.maxY;
|
||||||
|
if (!withinCanvas) return;
|
||||||
|
|
||||||
|
const block = blocks.find((b) => b.x === x && b.y === y);
|
||||||
|
const currentName = block ? block.name : "air";
|
||||||
|
|
||||||
|
// Only fill if the current block name matches the target block name.
|
||||||
|
if (currentName !== startName) return;
|
||||||
|
|
||||||
|
// Update block name or push new one
|
||||||
|
if (block) {
|
||||||
|
block.name = selectedBlock;
|
||||||
|
} else {
|
||||||
|
blocks.push({ x, y, name: selectedBlock });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive
|
||||||
|
for (const { dx, dy } of directions) {
|
||||||
|
floodFill(x + dx, y + dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
floodFill(mouseCoords.x, mouseCoords.y);
|
||||||
|
setBlocks([...blocks]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "eyedropper": {
|
case "eyedropper": {
|
||||||
const mouseBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
const mouseBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
||||||
if (mouseBlock) setSelectedBlock(mouseBlock.name);
|
if (mouseBlock) setSelectedBlock(mouseBlock.name);
|
||||||
|
|
@ -405,7 +444,7 @@ function Canvas() {
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [tool, holdingAltRef, scale, mouseCoords, blocks, canvasSize, setSelectionCoords, setSelectedBlock, zoom]);
|
}, [tool, holdingAltRef, scale, mouseCoords, blocks, selectedBlock, canvasSize, setSelectionCoords, setBlocks, setSelectedBlock, zoom]);
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyDown = useCallback(
|
||||||
async (e: React.KeyboardEvent) => {
|
async (e: React.KeyboardEvent) => {
|
||||||
|
|
@ -480,9 +519,12 @@ function Canvas() {
|
||||||
setTool("eraser");
|
setTool("eraser");
|
||||||
break;
|
break;
|
||||||
case "8":
|
case "8":
|
||||||
setTool("eyedropper");
|
setTool("paint-bucket");
|
||||||
break;
|
break;
|
||||||
case "9":
|
case "9":
|
||||||
|
setTool("eyedropper");
|
||||||
|
break;
|
||||||
|
case "0":
|
||||||
setTool("zoom");
|
setTool("zoom");
|
||||||
break;
|
break;
|
||||||
case "ArrowRight": {
|
case "ArrowRight": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { EraserIcon, HandIcon, LassoIcon, MousePointer2Icon, PencilIcon, PipetteIcon, SquareDashedIcon, WandIcon, ZoomInIcon } from "lucide-react";
|
import {
|
||||||
|
EraserIcon,
|
||||||
|
HandIcon,
|
||||||
|
LassoIcon,
|
||||||
|
MousePointer2Icon,
|
||||||
|
PaintBucketIcon,
|
||||||
|
PencilIcon,
|
||||||
|
PipetteIcon,
|
||||||
|
SquareDashedIcon,
|
||||||
|
WandIcon,
|
||||||
|
ZoomInIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
@ -105,6 +116,18 @@ function Toolbar() {
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Paint Bucket */}
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<ToggleGroupItem value="paint-bucket" className="!p-0 !h-8 !min-w-8">
|
||||||
|
<PaintBucketIcon />
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={10}>
|
||||||
|
<p>Paint Bucket (8)</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{/* Eyedropper */}
|
{/* Eyedropper */}
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
|
|
@ -113,7 +136,7 @@ function Toolbar() {
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" sideOffset={10}>
|
<TooltipContent side="right" sideOffset={10}>
|
||||||
<p>Eyedropper (8)</p>
|
<p>Eyedropper (9)</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
@ -125,7 +148,7 @@ function Toolbar() {
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" sideOffset={10}>
|
<TooltipContent side="right" sideOffset={10}>
|
||||||
<p>Zoom (9)</p>
|
<p>Zoom (0)</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
||||||
2
src/types.d.ts
vendored
2
src/types.d.ts
vendored
|
|
@ -23,7 +23,7 @@ interface Block extends Position {
|
||||||
|
|
||||||
type CoordinateArray = [number, number][];
|
type CoordinateArray = [number, number][];
|
||||||
|
|
||||||
type Tool = "hand" | "move" | "rectangle-select" | "lasso" | "magic-wand" | "pencil" | "eraser" | "eyedropper" | "zoom";
|
type Tool = "hand" | "move" | "rectangle-select" | "lasso" | "magic-wand" | "pencil" | "eraser" | "paint-bucket" | "eyedropper" | "zoom";
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
grid: boolean;
|
grid: boolean;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue