Skip to content

Commit 0924152

Browse files
committed
Implement lasso
1 parent 608a029 commit 0924152

2 files changed

Lines changed: 89 additions & 1 deletion

File tree

src/components/pg/inputPixelEditor/inputPixelEditor.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import getEllipseOutlinePixels from './utils/getEllipseOutlinePixels';
99
import { WHITE, Pixel } from './utils/constants';
1010
import getLinePixels from './utils/getLinePixels';
1111
import getRectanglePixels from './utils/getRectanglePixels';
12+
import getLassoPixels from './utils/getLassoPixels';
1213
import getRectangleOutlinePixels from './utils/getRectangleOutlinePixels';
1314
import fillGrid from './utils/fillGrid';
1415
import iterateGrid from './utils/interateGrid';
@@ -147,6 +148,9 @@ export default class PgInputPixelEditor extends HTMLElement {
147148
#startColor: number = -1;
148149
#startX: number = -1;
149150
#startY: number = -1;
151+
#lassoPath: [number, number][] = [];
152+
#lassoOutlinePixels: Set<string> = new Set();
153+
#lassoOutlineList: { x: number; y: number }[] = [];
150154
#x: number = -1;
151155
#y: number = -1;
152156
#layer: number[] = [0];
@@ -784,6 +788,11 @@ export default class PgInputPixelEditor extends HTMLElement {
784788
}
785789
}));
786790
break;
791+
case InputMode.SelectLasso:
792+
this.#lassoPath = [[newX, newY]];
793+
this.#lassoOutlinePixels = new Set();
794+
this.#lassoOutlineList = [];
795+
break;
787796
case InputMode.Pixel:
788797
this.#setPixel(newX, newY, color);
789798
break;
@@ -878,7 +887,16 @@ export default class PgInputPixelEditor extends HTMLElement {
878887
this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size })[0]);
879888
break;
880889
case InputMode.SelectLasso:
881-
890+
this.#clearSelectionPreview();
891+
if (!event.shiftKey) {
892+
this.clearSelection();
893+
}
894+
getLassoPixels(this.#lassoPath).forEach(({ x, y }) => {
895+
this.#setSelectionPixel(x, y);
896+
});
897+
this.$selectionPathPreview.classList.toggle('hide', true);
898+
this.$selectionPath.classList.toggle('hide', false);
899+
this.$selectionPath.setAttribute('d', bitmaskToPath(this.#selection, { scale: this.size })[0]);
882900
break;
883901
case InputMode.Line:
884902
getLinePixels(this.#startX, this.#startY, newX, newY).forEach(({ x, y }) => {
@@ -969,6 +987,25 @@ export default class PgInputPixelEditor extends HTMLElement {
969987
case InputMode.SelectEllipse:
970988
this.#setSelectionPreview(getEllipsePixels(startX, startY, lastX, lastY));
971989
break;
990+
case InputMode.SelectLasso: {
991+
const [sx, sy] = this.#lassoPath[0];
992+
for (const [px, py] of points) {
993+
const [prevX, prevY] = this.#lassoPath.at(-1)!;
994+
for (const { x: lx, y: ly } of getLinePixels(prevX, prevY, px, py)) {
995+
const key = `${lx},${ly}`;
996+
if (!this.#lassoOutlinePixels.has(key)) {
997+
this.#lassoOutlinePixels.add(key);
998+
this.#lassoOutlineList.push({ x: lx, y: ly });
999+
}
1000+
}
1001+
this.#lassoPath.push([px, py]);
1002+
}
1003+
const [cx, cy] = this.#lassoPath.at(-1)!;
1004+
const closing = getLinePixels(cx, cy, sx, sy)
1005+
.filter(({ x: lx, y: ly }) => !this.#lassoOutlinePixels.has(`${lx},${ly}`));
1006+
this.#setSelectionPreview([...this.#lassoOutlineList, ...closing]);
1007+
break;
1008+
}
9721009
case InputMode.Pixel:
9731010
for (var point of points) {
9741011
this.#setPixel(point[0], point[1], color);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import getLinePixels from './getLinePixels';
2+
import { Pixel } from './constants';
3+
4+
/**
5+
* Returns all pixels inside (and on the border of) a freehand lasso path.
6+
*
7+
* The path is a sequence of grid-cell coordinates visited during the drag.
8+
* Consecutive points are connected with Bresenham lines to form the outline,
9+
* and the interior is filled with an even-odd scanline rule.
10+
*/
11+
export default function getLassoPixels(path: [number, number][]): Pixel[] {
12+
if (path.length < 3) { return []; }
13+
14+
const result = new Map<string, Pixel>();
15+
const n = path.length;
16+
17+
// Outline: Bresenham lines between consecutive path points, closing back to start
18+
for (let i = 0; i < n; i++) {
19+
const [x0, y0] = path[i];
20+
const [x1, y1] = path[(i + 1) % n];
21+
for (const { x, y } of getLinePixels(x0, y0, x1, y1)) {
22+
result.set(`${x},${y}`, { x, y });
23+
}
24+
}
25+
26+
// Scanline interior fill — test at row + 0.5 to avoid vertex ambiguities
27+
const minY = Math.min(...path.map(([, y]) => y));
28+
const maxY = Math.max(...path.map(([, y]) => y));
29+
30+
for (let row = minY; row <= maxY; row++) {
31+
const intersections: number[] = [];
32+
for (let i = 0; i < n; i++) {
33+
const [x0, y0] = path[i];
34+
const [x1, y1] = path[(i + 1) % n];
35+
if ((y0 <= row && y1 > row) || (y1 <= row && y0 > row)) {
36+
const t = (row + 0.5 - y0) / (y1 - y0);
37+
intersections.push(x0 + t * (x1 - x0));
38+
}
39+
}
40+
intersections.sort((a, b) => a - b);
41+
for (let i = 0; i + 1 < intersections.length; i += 2) {
42+
const xStart = Math.ceil(intersections[i]);
43+
const xEnd = Math.floor(intersections[i + 1]);
44+
for (let x = xStart; x <= xEnd; x++) {
45+
result.set(`${x},${row}`, { x, y: row });
46+
}
47+
}
48+
}
49+
50+
return Array.from(result.values());
51+
}

0 commit comments

Comments
 (0)