feat: paint bucket tool

This commit is contained in:
trafficlunar 2025-02-05 17:02:18 +00:00
parent 5d8b073a1e
commit a7f39aa430
3 changed files with 78 additions and 13 deletions

View file

@ -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": {

View file

@ -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
View file

@ -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;