|
1 | | -/*export function getOutline(grid) { |
2 | | - const outline: number[][] = []; |
3 | | - const rows = grid.length; |
4 | | - const cols = grid[0].length; |
5 | | -
|
6 | | - // Directions: [dx, dy] |
7 | | - const dirs = [ |
8 | | - [0, -1], // up |
9 | | - [1, 0], // right |
10 | | - [0, 1], // down |
11 | | - [-1, 0] // left |
| 1 | +export function getOutline(pixels: number[][], ignoreInside: boolean = false) { |
| 2 | + const moatPixels: number[][] = []; |
| 3 | + const height = pixels.length; |
| 4 | + if (height === 0) return moatPixels; |
| 5 | + const width = pixels[0].length; |
| 6 | + |
| 7 | + const directions = [ |
| 8 | + [0, 1], |
| 9 | + [0, -1], |
| 10 | + [1, 0], |
| 11 | + [-1, 0] |
12 | 12 | ]; |
13 | 13 |
|
14 | | - for (let y = 0; y < rows; y++) { |
15 | | - for (let x = 0; x < cols; x++) { |
16 | | - if (grid[y][x] !== 1) continue; |
| 14 | + // --------------------------------------------------------- |
| 15 | + // STEP 1: If ignoring inside, flood-fill from the outside |
| 16 | + // --------------------------------------------------------- |
| 17 | + let outsideZero: boolean[][] = Array.from({ length: height }, () => |
| 18 | + Array(width).fill(false) |
| 19 | + ); |
17 | 20 |
|
18 | | - // Check if this cell touches the boundary of the shape |
19 | | - let isBoundary = false; |
| 21 | + if (ignoreInside) { |
| 22 | + const queue: [number, number][] = []; |
20 | 23 |
|
21 | | - for (const [dx, dy] of dirs) { |
22 | | - const nx = x + dx; |
23 | | - const ny = y + dy; |
24 | | -
|
25 | | - // If neighbor is out of bounds OR is zero, this is an outline cell |
26 | | - if (ny < 0 || ny >= rows || nx < 0 || nx >= cols || grid[ny][nx] === 0) { |
27 | | - isBoundary = true; |
28 | | - break; |
29 | | - } |
30 | | - } |
31 | | -
|
32 | | - if (isBoundary) { |
33 | | - outline.push([x, y]); |
34 | | - } |
| 24 | + // Seed flood-fill with all border zeros |
| 25 | + for (let x = 0; x < width; x++) { |
| 26 | + if (pixels[0][x] === 0) queue.push([x, 0]); |
| 27 | + if (pixels[height - 1][x] === 0) queue.push([x, height - 1]); |
35 | 28 | } |
36 | | - } |
37 | | -
|
38 | | - return outline; |
39 | | -} */ |
40 | | - |
41 | | -export function getOutline(grid) { |
42 | | - const rows = grid.length; |
43 | | - const cols = grid[0].length; |
44 | | - |
45 | | - // ------------------------------------------------------------ |
46 | | - // 1. Flood-fill background reachable from outside |
47 | | - // ------------------------------------------------------------ |
48 | | - const outside = Array.from({ length: rows }, () => Array(cols).fill(false)); |
49 | | - const queue: number[][] = []; |
50 | | - |
51 | | - // Add border 0-cells |
52 | | - for (let x = 0; x < cols; x++) { |
53 | | - if (grid[0][x] === 0) queue.push([x, 0]); |
54 | | - if (grid[rows - 1][x] === 0) queue.push([x, rows - 1]); |
55 | | - } |
56 | | - for (let y = 0; y < rows; y++) { |
57 | | - if (grid[y][0] === 0) queue.push([0, y]); |
58 | | - if (grid[y][cols - 1] === 0) queue.push([cols - 1, y]); |
59 | | - } |
60 | | - |
61 | | - // BFS flood fill |
62 | | - while (queue.length) { |
63 | | - const [x, y] = queue.shift() as number[]; |
64 | | - if (outside[y][x]) continue; |
65 | | - outside[y][x] = true; |
66 | | - |
67 | | - const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; |
68 | | - for (const [dx, dy] of dirs) { |
69 | | - const nx = x + dx, ny = y + dy; |
70 | | - if ( |
71 | | - nx >= 0 && nx < cols && |
72 | | - ny >= 0 && ny < rows && |
73 | | - grid[ny][nx] === 0 && |
74 | | - !outside[ny][nx] |
75 | | - ) { |
76 | | - queue.push([nx, ny]); |
77 | | - } |
| 29 | + for (let y = 0; y < height; y++) { |
| 30 | + if (pixels[y][0] === 0) queue.push([0, y]); |
| 31 | + if (pixels[y][width - 1] === 0) queue.push([width - 1, y]); |
78 | 32 | } |
79 | | - } |
80 | | - |
81 | | - // ------------------------------------------------------------ |
82 | | - // 2. Collect boundary cells (unordered) |
83 | | - // ------------------------------------------------------------ |
84 | | - const boundary = new Set(); |
85 | | - const key = (x, y) => `${x},${y}`; |
86 | 33 |
|
87 | | - for (let y = 0; y < rows; y++) { |
88 | | - for (let x = 0; x < cols; x++) { |
89 | | - if (grid[y][x] === 0) continue; |
90 | | - |
91 | | - const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; |
92 | | - for (const [dx, dy] of dirs) { |
93 | | - const nx = x + dx, ny = y + dy; |
| 34 | + // BFS flood-fill |
| 35 | + while (queue.length > 0) { |
| 36 | + const [x, y] = queue.shift()!; |
| 37 | + if (outsideZero[y][x]) continue; |
| 38 | + outsideZero[y][x] = true; |
94 | 39 |
|
| 40 | + for (const [dx, dy] of directions) { |
| 41 | + const nx = x + dx; |
| 42 | + const ny = y + dy; |
95 | 43 | if ( |
96 | | - nx < 0 || nx >= cols || |
97 | | - ny < 0 || ny >= rows || |
98 | | - (grid[ny][nx] !== 1 && outside[ny][nx]) |
| 44 | + nx >= 0 && |
| 45 | + nx < width && |
| 46 | + ny >= 0 && |
| 47 | + ny < height && |
| 48 | + pixels[ny][nx] === 0 && |
| 49 | + !outsideZero[ny][nx] |
99 | 50 | ) { |
100 | | - boundary.add(key(x, y)); |
101 | | - break; |
| 51 | + queue.push([nx, ny]); |
102 | 52 | } |
103 | 53 | } |
104 | 54 | } |
105 | 55 | } |
106 | 56 |
|
107 | | - if (boundary.size === 0) return []; |
108 | | - |
109 | | - // ------------------------------------------------------------ |
110 | | - // 3. SAFE OUTLINE WALK (no infinite loops) |
111 | | - // ------------------------------------------------------------ |
112 | | - |
113 | | - // Convert boundary set to a fast lookup |
114 | | - const isBoundary = (x, y) => boundary.has(key(x, y)); |
115 | | - |
116 | | - // Find a starting boundary cell |
117 | | - const [startKey] = boundary; |
118 | | - // @ts-ignore |
119 | | - const [sx, sy] = startKey.split(',').map(Number); |
| 57 | + // --------------------------------------------------------- |
| 58 | + // STEP 2: Collect moat pixels |
| 59 | + // --------------------------------------------------------- |
| 60 | + const moatSet = new Set<string>(); |
120 | 61 |
|
121 | | - const outline: number[][] = []; |
122 | | - const visited = new Set(); |
123 | | - let current = { x: sx, y: sy }; |
| 62 | + for (let y = 0; y < height; y++) { |
| 63 | + for (let x = 0; x < width; x++) { |
| 64 | + if (pixels[y][x] !== 1) continue; |
124 | 65 |
|
125 | | - // Moore-neighborhood directions (8 directions) |
126 | | - const dirs8 = [ |
127 | | - [1,0], [1,1], [0,1], [-1,1], |
128 | | - [-1,0], [-1,-1], [0,-1], [1,-1] |
129 | | - ]; |
| 66 | + for (const [dx, dy] of directions) { |
| 67 | + const nx = x + dx; |
| 68 | + const ny = y + dy; |
130 | 69 |
|
131 | | - const maxSteps = rows * cols * 8; |
| 70 | + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; |
132 | 71 |
|
133 | | - for (let step = 0; step < maxSteps; step++) { |
134 | | - outline.push([current.x, current.y]); |
135 | | - visited.add(key(current.x, current.y)); |
| 72 | + if (pixels[ny][nx] === 0) { |
| 73 | + // If ignoring inside, only count zeros reachable from outside |
| 74 | + if (ignoreInside && !outsideZero[ny][nx]) continue; |
136 | 75 |
|
137 | | - // Try to find next boundary neighbor |
138 | | - let next = null; |
139 | | - for (const [dx, dy] of dirs8) { |
140 | | - const nx = current.x + dx; |
141 | | - const ny = current.y + dy; |
142 | | - if (isBoundary(nx, ny) && !visited.has(key(nx, ny))) { |
143 | | - // @ts-ignore |
144 | | - next = { x: nx, y: ny }; |
145 | | - break; |
| 76 | + const key = `${nx},${ny}`; |
| 77 | + if (!moatSet.has(key)) { |
| 78 | + moatSet.add(key); |
| 79 | + moatPixels.push([nx, ny]); |
| 80 | + } |
| 81 | + } |
146 | 82 | } |
147 | 83 | } |
148 | | - |
149 | | - // If no next step → open contour (C-shape) |
150 | | - if (!next) break; |
151 | | - |
152 | | - // If next is start → closed loop |
153 | | - // @ts-ignore |
154 | | - if (next.x === sx && next.y === sy) { |
155 | | - outline.push([sx, sy]); |
156 | | - break; |
157 | | - } |
158 | | - |
159 | | - current = next; |
160 | 84 | } |
161 | 85 |
|
162 | | - return outline; |
| 86 | + return moatPixels; |
163 | 87 | } |
0 commit comments