Skip to content

Commit bc05e1d

Browse files
committed
Flood fill selection.
1 parent 5915f5b commit bc05e1d

4 files changed

Lines changed: 152 additions & 10 deletions

File tree

src/pg/inputPixelEditor/__examples__/basic/basic.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ <h3>Canvas Tools</h3>
3838
<div>
3939
<h3>Selection Tools</h3>
4040
<button part="selectRectangle">Select Rectangle</button>
41-
<button part="selectCircle">Select Circle</button>
41+
<button part="selectEllipse">Select Circle</button>
4242
<button part="selectLasso">Select Lasso</button>
4343
<button part="selectMagic">Select Magic Wand</button>
4444
</div>

src/pg/inputPixelEditor/__examples__/basic/basic.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default class XPgInputPixelEditorBasic extends HTMLElement {
4444
@Part() $modeEllipse: HTMLButtonElement;
4545
@Part() $modeEllipseOutline: HTMLButtonElement;
4646
@Part() $selectRectangle: HTMLButtonElement;
47-
@Part() $selectCircle: HTMLButtonElement;
47+
@Part() $selectEllipse: HTMLButtonElement;
4848
@Part() $selectLasso: HTMLButtonElement;
4949
@Part() $selectMagic: HTMLButtonElement;
5050

@@ -104,6 +104,15 @@ export default class XPgInputPixelEditorBasic extends HTMLElement {
104104
this.$selectRectangle.addEventListener('click', () => {
105105
this.$input.inputModeSelectRectangle();
106106
});
107+
this.$selectEllipse.addEventListener('click', () => {
108+
this.$input.inputModeSelectEllipse();
109+
});
110+
this.$selectLasso.addEventListener('click', () => {
111+
this.$input.inputModeSelectLasso();
112+
});
113+
this.$selectMagic.addEventListener('click', () => {
114+
this.$input.inputModeSelectMagicWand();
115+
});
107116
this.$reset.addEventListener('click', () => {
108117
this.$input.reset();
109118
});

src/pg/inputPixelEditor/inputPixelEditor.ts

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import diffGrid from './utils/diffGrid';
1818
import { getGuides } from './utils/getGuides';
1919
import { getOutline } from './utils/getOutline';
2020
import { getGridColorIndexes } from './utils/getGridColorIndexes';
21+
import { getFloodFill } from './utils/getFloodFill';
2122

2223
type Color = [number, number, number, number];
2324

@@ -297,6 +298,14 @@ export default class PgInputPixelEditor extends HTMLElement {
297298
});
298299
}
299300

301+
clearSelection() {
302+
this.#selectionPixels.forEach(([x, y]) => {
303+
this.#selection[y][x] = 0;
304+
});
305+
this.#selectionPixels.clear();
306+
this.$selectionPath.classList.toggle('hide', true);
307+
}
308+
300309
#setSelectionPixel(x: number, y: number) {
301310
this.#selectionPixels.set(`${x},${y}`, [x, y]);
302311
this.#selection[y][x] = 1;
@@ -531,19 +540,20 @@ export default class PgInputPixelEditor extends HTMLElement {
531540

532541
handleKeyDown(event: KeyboardEvent) {
533542
console.log(event.shiftKey, event.ctrlKey, event.altKey, event.key);
543+
this.#isShift = true;
534544
switch (event.key) {
535545
case ' ':
536546
console.log('space');
537547
break;
538548
case 'Escape':
539549
console.log('escape');
540-
// Cancel editing
550+
this.clearSelection();
541551
break;
542552
}
543553
}
544554

545555
handleKeyUp(event: KeyboardEvent) {
546-
556+
this.#isShift = false;
547557
}
548558

549559
handleContextMenu(event: MouseEvent) {
@@ -618,28 +628,64 @@ export default class PgInputPixelEditor extends HTMLElement {
618628
// return;
619629
//}
620630
// Single Tap
621-
if (newX === this.#startX && newY === this.#startY && this.#startColor === 1) {
631+
if (newX === this.#startX && newY === this.#startY) {
622632
switch (this.#inputMode) {
633+
case InputMode.SelectMagicWand:
634+
if (!event.shiftKey) {
635+
this.clearSelection();
636+
}
637+
const color = this.#data[this.#layer][newY][newX];
638+
console.log(color);
639+
const pixels = getFloodFill(this.#data[this.#layer], newX, newY, [color]);
640+
pixels.forEach(([x, y]) => {
641+
this.#setSelectionPixel(x, y);
642+
});
643+
this.$selectionPathPreview.classList.toggle('hide', true);
644+
this.$selectionPath.classList.toggle('hide', false);
645+
this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size }));
646+
break;
623647
case InputMode.Pixel:
624-
this.#setPixel(newX, newY, 0);
625-
this.#data[this.#layer][newY][newX] = 0;
648+
if (this.#startColor === 1) {
649+
this.#setPixel(newX, newY, 0);
650+
this.#data[this.#layer][newY][newX] = 0;
651+
}
626652
break;
627653
case InputMode.Stamp:
628-
this.#inputStamp.forEach((point) => {
629-
this.#setPixel(newX + point[0], newY + point[1], 0);
630-
});
654+
if (this.#startColor === 1) {
655+
this.#inputStamp.forEach((point) => {
656+
this.#setPixel(newX + point[0], newY + point[1], 0);
657+
});
658+
}
631659
break;
632660
}
633661
} else {
634662
switch (this.#inputMode) {
635663
case InputMode.SelectRectangle:
636664
this.#clearSelectionPreview();
665+
if (!event.shiftKey) {
666+
this.clearSelection();
667+
}
637668
getRectanglePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
638669
this.#setSelectionPixel(x, y);
639670
});
640671
this.$selectionPathPreview.classList.toggle('hide', true);
641672
this.$selectionPath.classList.toggle('hide', false);
642673
this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size }));
674+
break;
675+
case InputMode.SelectEllipse:
676+
this.#clearSelectionPreview();
677+
if (!event.shiftKey) {
678+
this.clearSelection();
679+
}
680+
getEllipsePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
681+
this.#setSelectionPixel(x, y);
682+
});
683+
this.$selectionPathPreview.classList.toggle('hide', true);
684+
this.$selectionPath.classList.toggle('hide', false);
685+
this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size }));
686+
break;
687+
case InputMode.SelectLasso:
688+
643689
break;
644690
case InputMode.Line:
645691
getLinePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
@@ -862,6 +908,10 @@ export default class PgInputPixelEditor extends HTMLElement {
862908
this.#redoHistory = [];
863909
}
864910

911+
getHistory() {
912+
return this.#undoHistory;
913+
}
914+
865915
applyTemplate(template: number[][]) {
866916
this.#data = [template];
867917
this.#setPixelAll();
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Represents a coordinate in the 2D array.
3+
*/
4+
type Coordinate = [number, number];
5+
6+
/**
7+
* Flood fills a 2D array starting from a specific coordinate to find all connected numbers
8+
* that are within a specified set of target values.
9+
*
10+
* @param grid The 2D array (grid) of numbers.
11+
* @param startX The starting x-coordinate.
12+
* @param startY The starting y-coordinate.
13+
* @param targetValues The set of numbers to search for (e.g., [1, 2, 5]).
14+
* @returns A list of [x, y] coordinates that were found during the flood fill.
15+
*/
16+
export function getFloodFill(
17+
grid: number[][],
18+
startX: number,
19+
startY: number,
20+
targetValues: number[]
21+
): Coordinate[] {
22+
// Use a Set for quick lookups of target values
23+
const targets = new Set(targetValues);
24+
25+
// Check if starting coordinates are valid and if the value at the start is a target
26+
if (
27+
startY < 0 ||
28+
startY >= grid.length ||
29+
startX < 0 ||
30+
startX >= grid[0].length ||
31+
!targets.has(grid[startY][startX])
32+
) {
33+
return [];
34+
}
35+
36+
// Queue for Breadth-First Search (BFS)
37+
const queue: Coordinate[] = [[startX, startY]];
38+
// Set to keep track of visited coordinates to avoid cycles and redundant processing
39+
const visited = new Set<string>();
40+
const foundPixels: Coordinate[] = [];
41+
42+
// Helper to convert [x, y] to a string key for the 'visited' set
43+
const toKey = (x: number, y: number) => `${x},${y}`;
44+
45+
// Mark the starting pixel as visited and add to found list
46+
visited.add(toKey(startX, startY));
47+
foundPixels.push([startX, startY]);
48+
49+
while (queue.length > 0) {
50+
const [currentX, currentY] = queue.shift()!;
51+
const currentValue = grid[currentY][currentX];
52+
53+
// Define potential neighbors (up, down, left, right)
54+
const neighbors: Coordinate[] = [
55+
[currentX + 1, currentY],
56+
[currentX - 1, currentY],
57+
[currentX, currentY + 1],
58+
[currentX, currentY - 1],
59+
];
60+
61+
for (const [nextX, nextY] of neighbors) {
62+
// Check bounds
63+
if (
64+
nextY >= 0 &&
65+
nextY < grid.length &&
66+
nextX >= 0 &&
67+
nextX < grid[0].length
68+
) {
69+
const neighborValue = grid[nextY][nextX];
70+
const key = toKey(nextX, nextY);
71+
72+
// If the neighbor has a target value and has not been visited yet
73+
if (targets.has(neighborValue) && !visited.has(key)) {
74+
visited.add(key);
75+
foundPixels.push([nextX, nextY]);
76+
queue.push([nextX, nextY]);
77+
}
78+
}
79+
}
80+
}
81+
82+
return foundPixels;
83+
}

0 commit comments

Comments
 (0)