@pixerate/editor 0.272.1 → 0.273.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2A5IKZMU.js +557 -0
- package/dist/chunk-YH6SKC7P.cjs +557 -0
- package/dist/paint-editor/controller.d.cts +86 -0
- package/dist/paint-editor/controller.d.ts +86 -0
- package/dist/paint-editor/document.d.cts +1 -1
- package/dist/paint-editor/document.d.ts +1 -1
- package/dist/paint-editor/index.cjs +559 -166
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +397 -4
- package/dist/paint-editor/presence.d.cts +5 -1
- package/dist/paint-editor/presence.d.ts +5 -1
- package/dist/paint-editor/ui.d.cts +5 -2
- package/dist/paint-editor/ui.d.ts +5 -2
- package/dist/raster/fill.d.cts +85 -0
- package/dist/raster/fill.d.ts +85 -0
- package/dist/raster/index.cjs +9 -1
- package/dist/raster/index.d.cts +1 -0
- package/dist/raster/index.d.ts +1 -0
- package/dist/raster/index.js +8 -0
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +19 -43
- package/skills/pixerate-paint-editor/references/evolution.md +27 -0
- package/skills/pixerate-paint-sketch-to-finish/SKILL.md +16 -5
- package/skills/pixerate-paint-sketch-to-finish/evals/flats-under-ink.json +2 -3
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/raster/fill.ts
|
|
2
|
+
var INF = 1e12;
|
|
3
|
+
function squaredDistanceTransform(binary, w, h) {
|
|
4
|
+
const n = w * h;
|
|
5
|
+
const dist = new Float64Array(n);
|
|
6
|
+
let hasZero = false;
|
|
7
|
+
for (let i = 0; i < n; i++) {
|
|
8
|
+
if (binary[i] === 0) {
|
|
9
|
+
dist[i] = 0;
|
|
10
|
+
hasZero = true;
|
|
11
|
+
} else {
|
|
12
|
+
dist[i] = INF;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (!hasZero) return dist;
|
|
16
|
+
const maxDim = Math.max(w, h);
|
|
17
|
+
const f = new Float64Array(maxDim);
|
|
18
|
+
const d = new Float64Array(maxDim);
|
|
19
|
+
const v = new Int32Array(maxDim);
|
|
20
|
+
const z = new Float64Array(maxDim + 1);
|
|
21
|
+
for (let x = 0; x < w; x++) {
|
|
22
|
+
for (let y = 0; y < h; y++) f[y] = dist[y * w + x];
|
|
23
|
+
edt1d(f, d, v, z, h);
|
|
24
|
+
for (let y = 0; y < h; y++) dist[y * w + x] = d[y];
|
|
25
|
+
}
|
|
26
|
+
for (let y = 0; y < h; y++) {
|
|
27
|
+
const row = y * w;
|
|
28
|
+
for (let x = 0; x < w; x++) f[x] = dist[row + x];
|
|
29
|
+
edt1d(f, d, v, z, w);
|
|
30
|
+
for (let x = 0; x < w; x++) dist[row + x] = d[x];
|
|
31
|
+
}
|
|
32
|
+
return dist;
|
|
33
|
+
}
|
|
34
|
+
function edt1d(f, d, v, z, n) {
|
|
35
|
+
let k = 0;
|
|
36
|
+
v[0] = 0;
|
|
37
|
+
z[0] = -INF;
|
|
38
|
+
z[1] = INF;
|
|
39
|
+
for (let q = 1; q < n; q++) {
|
|
40
|
+
const fq = f[q] + q * q;
|
|
41
|
+
let s = (fq - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
|
|
42
|
+
while (s <= z[k]) {
|
|
43
|
+
k--;
|
|
44
|
+
s = (fq - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
|
|
45
|
+
}
|
|
46
|
+
k++;
|
|
47
|
+
v[k] = q;
|
|
48
|
+
z[k] = s;
|
|
49
|
+
z[k + 1] = INF;
|
|
50
|
+
}
|
|
51
|
+
k = 0;
|
|
52
|
+
for (let q = 0; q < n; q++) {
|
|
53
|
+
while (z[k + 1] < q) k++;
|
|
54
|
+
const dx = q - v[k];
|
|
55
|
+
d[q] = dx * dx + f[v[k]];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function pixelMatches(data, p, sr, sg, sb, sa, tol) {
|
|
59
|
+
const a = data[p + 3];
|
|
60
|
+
if (sa === 0 && a === 0) return true;
|
|
61
|
+
return Math.abs(data[p] - sr) <= tol && Math.abs(data[p + 1] - sg) <= tol && Math.abs(data[p + 2] - sb) <= tol && Math.abs(a - sa) <= tol;
|
|
62
|
+
}
|
|
63
|
+
function floodOneSeed(reference, w, h, sx, sy, tol, closeGap, expand, queue) {
|
|
64
|
+
const n = w * h;
|
|
65
|
+
const marks = new Uint8Array(n);
|
|
66
|
+
if (sx < 0 || sy < 0 || sx >= w || sy >= h) return marks;
|
|
67
|
+
const sp = (sy * w + sx) * 4;
|
|
68
|
+
const sr = reference[sp];
|
|
69
|
+
const sg = reference[sp + 1];
|
|
70
|
+
const sb = reference[sp + 2];
|
|
71
|
+
const sa = reference[sp + 3];
|
|
72
|
+
const open = new Uint8Array(n);
|
|
73
|
+
for (let i = 0; i < n; i++) {
|
|
74
|
+
if (pixelMatches(reference, i * 4, sr, sg, sb, sa, tol)) open[i] = 1;
|
|
75
|
+
}
|
|
76
|
+
const start = sy * w + sx;
|
|
77
|
+
open[start] = 1;
|
|
78
|
+
if (closeGap <= 0) {
|
|
79
|
+
let head = 0;
|
|
80
|
+
let tail = 0;
|
|
81
|
+
marks[start] = 1;
|
|
82
|
+
queue[tail++] = start;
|
|
83
|
+
while (head < tail) {
|
|
84
|
+
const cur = queue[head++];
|
|
85
|
+
const cx = cur % w;
|
|
86
|
+
const cy = (cur - cx) / w;
|
|
87
|
+
if (cx > 0) {
|
|
88
|
+
const nb = cur - 1;
|
|
89
|
+
if (open[nb] === 1 && marks[nb] === 0) {
|
|
90
|
+
marks[nb] = 1;
|
|
91
|
+
queue[tail++] = nb;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (cx + 1 < w) {
|
|
95
|
+
const nb = cur + 1;
|
|
96
|
+
if (open[nb] === 1 && marks[nb] === 0) {
|
|
97
|
+
marks[nb] = 1;
|
|
98
|
+
queue[tail++] = nb;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (cy > 0) {
|
|
102
|
+
const nb = cur - w;
|
|
103
|
+
if (open[nb] === 1 && marks[nb] === 0) {
|
|
104
|
+
marks[nb] = 1;
|
|
105
|
+
queue[tail++] = nb;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (cy + 1 < h) {
|
|
109
|
+
const nb = cur + w;
|
|
110
|
+
if (open[nb] === 1 && marks[nb] === 0) {
|
|
111
|
+
marks[nb] = 1;
|
|
112
|
+
queue[tail++] = nb;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const distSq = squaredDistanceTransform(open, w, h);
|
|
118
|
+
const r = (closeGap + 1) * 0.5;
|
|
119
|
+
const rSq = r * r;
|
|
120
|
+
const maxClimb = Math.ceil(r) + 2;
|
|
121
|
+
let coreSeed = start;
|
|
122
|
+
let head = 0;
|
|
123
|
+
let tail = 0;
|
|
124
|
+
marks[start] = 1;
|
|
125
|
+
queue[tail++] = start;
|
|
126
|
+
for (let step = 0; step < maxClimb && distSq[coreSeed] <= rSq; step++) {
|
|
127
|
+
const cx = coreSeed % w;
|
|
128
|
+
const cy = (coreSeed - cx) / w;
|
|
129
|
+
let best = coreSeed;
|
|
130
|
+
let bestD = distSq[coreSeed];
|
|
131
|
+
if (cx > 0 && open[coreSeed - 1] === 1 && distSq[coreSeed - 1] > bestD) {
|
|
132
|
+
best = coreSeed - 1;
|
|
133
|
+
bestD = distSq[best];
|
|
134
|
+
}
|
|
135
|
+
if (cx + 1 < w && open[coreSeed + 1] === 1 && distSq[coreSeed + 1] > bestD) {
|
|
136
|
+
best = coreSeed + 1;
|
|
137
|
+
bestD = distSq[best];
|
|
138
|
+
}
|
|
139
|
+
if (cy > 0 && open[coreSeed - w] === 1 && distSq[coreSeed - w] > bestD) {
|
|
140
|
+
best = coreSeed - w;
|
|
141
|
+
bestD = distSq[best];
|
|
142
|
+
}
|
|
143
|
+
if (cy + 1 < h && open[coreSeed + w] === 1 && distSq[coreSeed + w] > bestD) {
|
|
144
|
+
best = coreSeed + w;
|
|
145
|
+
bestD = distSq[best];
|
|
146
|
+
}
|
|
147
|
+
if (best === coreSeed) break;
|
|
148
|
+
coreSeed = best;
|
|
149
|
+
if (marks[coreSeed] === 0) {
|
|
150
|
+
marks[coreSeed] = 1;
|
|
151
|
+
queue[tail++] = coreSeed;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (distSq[coreSeed] > rSq) {
|
|
155
|
+
let scan = 0;
|
|
156
|
+
while (scan < tail) {
|
|
157
|
+
const cur = queue[scan++];
|
|
158
|
+
if (distSq[cur] <= rSq) continue;
|
|
159
|
+
const cx = cur % w;
|
|
160
|
+
const cy = (cur - cx) / w;
|
|
161
|
+
if (cx > 0) {
|
|
162
|
+
const nb = cur - 1;
|
|
163
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] > rSq) {
|
|
164
|
+
marks[nb] = 1;
|
|
165
|
+
queue[tail++] = nb;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (cx + 1 < w) {
|
|
169
|
+
const nb = cur + 1;
|
|
170
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] > rSq) {
|
|
171
|
+
marks[nb] = 1;
|
|
172
|
+
queue[tail++] = nb;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (cy > 0) {
|
|
176
|
+
const nb = cur - w;
|
|
177
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] > rSq) {
|
|
178
|
+
marks[nb] = 1;
|
|
179
|
+
queue[tail++] = nb;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (cy + 1 < h) {
|
|
183
|
+
const nb = cur + w;
|
|
184
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] > rSq) {
|
|
185
|
+
marks[nb] = 1;
|
|
186
|
+
queue[tail++] = nb;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
while (head < tail) {
|
|
191
|
+
const cur = queue[head++];
|
|
192
|
+
const curD = distSq[cur];
|
|
193
|
+
const cx = cur % w;
|
|
194
|
+
const cy = (cur - cx) / w;
|
|
195
|
+
if (cx > 0) {
|
|
196
|
+
const nb = cur - 1;
|
|
197
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] <= rSq && distSq[nb] <= curD) {
|
|
198
|
+
marks[nb] = 1;
|
|
199
|
+
queue[tail++] = nb;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (cx + 1 < w) {
|
|
203
|
+
const nb = cur + 1;
|
|
204
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] <= rSq && distSq[nb] <= curD) {
|
|
205
|
+
marks[nb] = 1;
|
|
206
|
+
queue[tail++] = nb;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (cy > 0) {
|
|
210
|
+
const nb = cur - w;
|
|
211
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] <= rSq && distSq[nb] <= curD) {
|
|
212
|
+
marks[nb] = 1;
|
|
213
|
+
queue[tail++] = nb;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (cy + 1 < h) {
|
|
217
|
+
const nb = cur + w;
|
|
218
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] <= rSq && distSq[nb] <= curD) {
|
|
219
|
+
marks[nb] = 1;
|
|
220
|
+
queue[tail++] = nb;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
const maxLocal = Math.max(64, closeGap * closeGap * 8);
|
|
226
|
+
while (head < tail && tail < maxLocal) {
|
|
227
|
+
const cur = queue[head++];
|
|
228
|
+
const cx = cur % w;
|
|
229
|
+
const cy = (cur - cx) / w;
|
|
230
|
+
if (cx > 0) {
|
|
231
|
+
const nb = cur - 1;
|
|
232
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] <= rSq) {
|
|
233
|
+
marks[nb] = 1;
|
|
234
|
+
queue[tail++] = nb;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (cx + 1 < w) {
|
|
238
|
+
const nb = cur + 1;
|
|
239
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] <= rSq) {
|
|
240
|
+
marks[nb] = 1;
|
|
241
|
+
queue[tail++] = nb;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (cy > 0) {
|
|
245
|
+
const nb = cur - w;
|
|
246
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] <= rSq) {
|
|
247
|
+
marks[nb] = 1;
|
|
248
|
+
queue[tail++] = nb;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (cy + 1 < h) {
|
|
252
|
+
const nb = cur + w;
|
|
253
|
+
if (open[nb] === 1 && marks[nb] === 0 && distSq[nb] <= rSq) {
|
|
254
|
+
marks[nb] = 1;
|
|
255
|
+
queue[tail++] = nb;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (expand !== 0) {
|
|
262
|
+
let any = false;
|
|
263
|
+
for (let i = 0; i < n; i++) {
|
|
264
|
+
if (marks[i] === 1) {
|
|
265
|
+
any = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (any) {
|
|
270
|
+
const expSq = expand * expand;
|
|
271
|
+
if (expand > 0) {
|
|
272
|
+
const inv = new Uint8Array(n);
|
|
273
|
+
for (let i = 0; i < n; i++) inv[i] = marks[i] === 1 ? 0 : 1;
|
|
274
|
+
const dSq = squaredDistanceTransform(inv, w, h);
|
|
275
|
+
for (let i = 0; i < n; i++) marks[i] = dSq[i] <= expSq ? 1 : 0;
|
|
276
|
+
} else {
|
|
277
|
+
const dSq = squaredDistanceTransform(marks, w, h);
|
|
278
|
+
for (let i = 0; i < n; i++) marks[i] = dSq[i] > expSq ? 1 : 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return marks;
|
|
283
|
+
}
|
|
284
|
+
function summarizeMarks(marks, w, h) {
|
|
285
|
+
let minX = w;
|
|
286
|
+
let minY = h;
|
|
287
|
+
let maxX = -1;
|
|
288
|
+
let maxY = -1;
|
|
289
|
+
let pixelCount = 0;
|
|
290
|
+
let leaked = false;
|
|
291
|
+
for (let y = 0; y < h; y++) {
|
|
292
|
+
const row = y * w;
|
|
293
|
+
for (let x = 0; x < w; x++) {
|
|
294
|
+
if (marks[row + x] === 1) {
|
|
295
|
+
pixelCount++;
|
|
296
|
+
if (x < minX) minX = x;
|
|
297
|
+
if (y < minY) minY = y;
|
|
298
|
+
if (x > maxX) maxX = x;
|
|
299
|
+
if (y > maxY) maxY = y;
|
|
300
|
+
if (x === 0 || y === 0 || x === w - 1 || y === h - 1) leaked = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const bounds = pixelCount > 0 ? { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 } : null;
|
|
305
|
+
return { bounds, pixelCount, leaked };
|
|
306
|
+
}
|
|
307
|
+
function fillRegionMask(reference, width, height, options) {
|
|
308
|
+
const n = width * height;
|
|
309
|
+
const tol = Math.max(0, Math.min(255, _nullishCoalesce(options.tolerance, () => ( 32))));
|
|
310
|
+
const closeGap = Math.max(0, Math.min(32, _nullishCoalesce(options.closeGap, () => ( 0))));
|
|
311
|
+
const expand = Math.max(-16, Math.min(16, _nullishCoalesce(options.expand, () => ( 0))));
|
|
312
|
+
const antialias = _nullishCoalesce(options.antialias, () => ( true));
|
|
313
|
+
const combined = new Uint8Array(n);
|
|
314
|
+
const queue = new Int32Array(n);
|
|
315
|
+
const regions = [];
|
|
316
|
+
for (const pt of options.points) {
|
|
317
|
+
const sx = Math.floor(pt.x);
|
|
318
|
+
const sy = Math.floor(pt.y);
|
|
319
|
+
const seedMarks = floodOneSeed(reference, width, height, sx, sy, tol, closeGap, expand, queue);
|
|
320
|
+
const summary = summarizeMarks(seedMarks, width, height);
|
|
321
|
+
regions.push({
|
|
322
|
+
point: { x: pt.x, y: pt.y },
|
|
323
|
+
bounds: summary.bounds,
|
|
324
|
+
pixelCount: summary.pixelCount,
|
|
325
|
+
leaked: summary.leaked
|
|
326
|
+
});
|
|
327
|
+
for (let i = 0; i < n; i++) {
|
|
328
|
+
if (seedMarks[i] === 1) combined[i] = 1;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const total = summarizeMarks(combined, width, height);
|
|
332
|
+
const mask = new Float32Array(n);
|
|
333
|
+
if (total.bounds) {
|
|
334
|
+
const x0 = total.bounds.x;
|
|
335
|
+
const y0 = total.bounds.y;
|
|
336
|
+
const x1 = x0 + total.bounds.width;
|
|
337
|
+
const y1 = y0 + total.bounds.height;
|
|
338
|
+
for (let y = y0; y < y1; y++) {
|
|
339
|
+
const row = y * width;
|
|
340
|
+
for (let x = x0; x < x1; x++) {
|
|
341
|
+
const idx = row + x;
|
|
342
|
+
if (combined[idx] !== 1) continue;
|
|
343
|
+
if (!antialias) {
|
|
344
|
+
mask[idx] = 1;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
let sum = 0;
|
|
348
|
+
let edge = false;
|
|
349
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
350
|
+
const ny = y + dy;
|
|
351
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
352
|
+
const nx = x + dx;
|
|
353
|
+
const v = nx < 0 || ny < 0 || nx >= width || ny >= height ? 1 : combined[ny * width + nx];
|
|
354
|
+
if (v === 0) edge = true;
|
|
355
|
+
sum += v;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
mask[idx] = edge ? 0.5 + 0.5 * (sum / 9) : 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
mask,
|
|
364
|
+
bounds: total.bounds,
|
|
365
|
+
pixelCount: total.pixelCount,
|
|
366
|
+
leaked: total.leaked,
|
|
367
|
+
regions
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function findUnfilledRegionsMask(reference, width, height, options = {}) {
|
|
371
|
+
const n = width * height;
|
|
372
|
+
if (n <= 0) return [];
|
|
373
|
+
const tol = Math.max(0, Math.min(255, _nullishCoalesce(options.tolerance, () => ( 32))));
|
|
374
|
+
const closeGap = Math.max(0, Math.min(32, _nullishCoalesce(options.closeGap, () => ( 0))));
|
|
375
|
+
const minArea = Math.max(1, Math.floor(_nullishCoalesce(options.minArea, () => ( 16))));
|
|
376
|
+
const includeEdge = _nullishCoalesce(options.includeEdge, () => ( false));
|
|
377
|
+
const maxRegions = Math.max(1, Math.floor(_nullishCoalesce(options.maxRegions, () => ( 100))));
|
|
378
|
+
const target = options.target;
|
|
379
|
+
let hasTransparent = false;
|
|
380
|
+
for (let i = 0; i < n; i++) {
|
|
381
|
+
if (reference[i * 4 + 3] <= tol) {
|
|
382
|
+
hasTransparent = true;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const br = reference[0];
|
|
387
|
+
const bg = reference[1];
|
|
388
|
+
const bb = reference[2];
|
|
389
|
+
const ba = reference[3];
|
|
390
|
+
const openRef = new Uint8Array(n);
|
|
391
|
+
for (let i = 0; i < n; i++) {
|
|
392
|
+
const p = i * 4;
|
|
393
|
+
const isRefOpen = hasTransparent ? reference[p + 3] <= tol : pixelMatches(reference, p, br, bg, bb, ba, tol);
|
|
394
|
+
if (isRefOpen) openRef[i] = 1;
|
|
395
|
+
}
|
|
396
|
+
const unfilled = new Uint8Array(n);
|
|
397
|
+
for (let i = 0; i < n; i++) {
|
|
398
|
+
if (openRef[i] === 1 && (!target || target[i * 4 + 3] <= tol)) {
|
|
399
|
+
unfilled[i] = 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const distSq = squaredDistanceTransform(openRef, width, height);
|
|
403
|
+
const r = closeGap > 0 ? (closeGap + 1) * 0.5 : 0;
|
|
404
|
+
const rSq = r * r;
|
|
405
|
+
const labels = new Int32Array(n);
|
|
406
|
+
const queue = new Int32Array(n);
|
|
407
|
+
let nextLabel = 0;
|
|
408
|
+
let coreTail = 0;
|
|
409
|
+
for (let i = 0; i < n; i++) {
|
|
410
|
+
if (unfilled[i] !== 1 || labels[i] !== 0 || distSq[i] <= rSq) continue;
|
|
411
|
+
const label = ++nextLabel;
|
|
412
|
+
labels[i] = label;
|
|
413
|
+
let head = coreTail;
|
|
414
|
+
queue[coreTail++] = i;
|
|
415
|
+
while (head < coreTail) {
|
|
416
|
+
const cur = queue[head++];
|
|
417
|
+
const cx = cur % width;
|
|
418
|
+
const cy = (cur - cx) / width;
|
|
419
|
+
if (cx > 0) {
|
|
420
|
+
const nb = cur - 1;
|
|
421
|
+
if (unfilled[nb] === 1 && labels[nb] === 0 && distSq[nb] > rSq) {
|
|
422
|
+
labels[nb] = label;
|
|
423
|
+
queue[coreTail++] = nb;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (cx + 1 < width) {
|
|
427
|
+
const nb = cur + 1;
|
|
428
|
+
if (unfilled[nb] === 1 && labels[nb] === 0 && distSq[nb] > rSq) {
|
|
429
|
+
labels[nb] = label;
|
|
430
|
+
queue[coreTail++] = nb;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (cy > 0) {
|
|
434
|
+
const nb = cur - width;
|
|
435
|
+
if (unfilled[nb] === 1 && labels[nb] === 0 && distSq[nb] > rSq) {
|
|
436
|
+
labels[nb] = label;
|
|
437
|
+
queue[coreTail++] = nb;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (cy + 1 < height) {
|
|
441
|
+
const nb = cur + width;
|
|
442
|
+
if (unfilled[nb] === 1 && labels[nb] === 0 && distSq[nb] > rSq) {
|
|
443
|
+
labels[nb] = label;
|
|
444
|
+
queue[coreTail++] = nb;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (closeGap > 0 && nextLabel > 0) {
|
|
450
|
+
let head = 0;
|
|
451
|
+
let tail = coreTail;
|
|
452
|
+
while (head < tail) {
|
|
453
|
+
const cur = queue[head++];
|
|
454
|
+
const label = labels[cur];
|
|
455
|
+
const curD = distSq[cur];
|
|
456
|
+
const cx = cur % width;
|
|
457
|
+
const cy = (cur - cx) / width;
|
|
458
|
+
if (cx > 0) {
|
|
459
|
+
const nb = cur - 1;
|
|
460
|
+
if (unfilled[nb] === 1 && labels[nb] === 0 && distSq[nb] <= rSq && distSq[nb] <= curD) {
|
|
461
|
+
labels[nb] = label;
|
|
462
|
+
queue[tail++] = nb;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (cx + 1 < width) {
|
|
466
|
+
const nb = cur + 1;
|
|
467
|
+
if (unfilled[nb] === 1 && labels[nb] === 0 && distSq[nb] <= rSq && distSq[nb] <= curD) {
|
|
468
|
+
labels[nb] = label;
|
|
469
|
+
queue[tail++] = nb;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (cy > 0) {
|
|
473
|
+
const nb = cur - width;
|
|
474
|
+
if (unfilled[nb] === 1 && labels[nb] === 0 && distSq[nb] <= rSq && distSq[nb] <= curD) {
|
|
475
|
+
labels[nb] = label;
|
|
476
|
+
queue[tail++] = nb;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (cy + 1 < height) {
|
|
480
|
+
const nb = cur + width;
|
|
481
|
+
if (unfilled[nb] === 1 && labels[nb] === 0 && distSq[nb] <= rSq && distSq[nb] <= curD) {
|
|
482
|
+
labels[nb] = label;
|
|
483
|
+
queue[tail++] = nb;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (nextLabel === 0) return [];
|
|
489
|
+
const counts = new Int32Array(nextLabel + 1);
|
|
490
|
+
const sumX = new Float64Array(nextLabel + 1);
|
|
491
|
+
const sumY = new Float64Array(nextLabel + 1);
|
|
492
|
+
const minX = new Int32Array(nextLabel + 1).fill(width);
|
|
493
|
+
const minY = new Int32Array(nextLabel + 1).fill(height);
|
|
494
|
+
const maxX = new Int32Array(nextLabel + 1).fill(-1);
|
|
495
|
+
const maxY = new Int32Array(nextLabel + 1).fill(-1);
|
|
496
|
+
const edge = new Uint8Array(nextLabel + 1);
|
|
497
|
+
const bestIdx = new Int32Array(nextLabel + 1).fill(-1);
|
|
498
|
+
const bestDist = new Float64Array(nextLabel + 1).fill(-1);
|
|
499
|
+
for (let y = 0; y < height; y++) {
|
|
500
|
+
const row = y * width;
|
|
501
|
+
for (let x = 0; x < width; x++) {
|
|
502
|
+
const idx = row + x;
|
|
503
|
+
const l = labels[idx];
|
|
504
|
+
if (l === 0) continue;
|
|
505
|
+
counts[l]++;
|
|
506
|
+
sumX[l] += x;
|
|
507
|
+
sumY[l] += y;
|
|
508
|
+
if (x < minX[l]) minX[l] = x;
|
|
509
|
+
if (y < minY[l]) minY[l] = y;
|
|
510
|
+
if (x > maxX[l]) maxX[l] = x;
|
|
511
|
+
if (y > maxY[l]) maxY[l] = y;
|
|
512
|
+
if (x === 0 || y === 0 || x === width - 1 || y === height - 1) edge[l] = 1;
|
|
513
|
+
const d = distSq[idx];
|
|
514
|
+
if (d > bestDist[l]) {
|
|
515
|
+
bestDist[l] = d;
|
|
516
|
+
bestIdx[l] = idx;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const out = [];
|
|
521
|
+
for (let l = 1; l <= nextLabel; l++) {
|
|
522
|
+
const count = counts[l];
|
|
523
|
+
if (count < minArea) continue;
|
|
524
|
+
const touchesEdge = edge[l] === 1;
|
|
525
|
+
if (!includeEdge && touchesEdge) continue;
|
|
526
|
+
const cx = Math.round(sumX[l] / count);
|
|
527
|
+
const cy = Math.round(sumY[l] / count);
|
|
528
|
+
const cIdx = cy * width + cx;
|
|
529
|
+
let px = cx;
|
|
530
|
+
let py = cy;
|
|
531
|
+
if (cx < 0 || cy < 0 || cx >= width || cy >= height || labels[cIdx] !== l || distSq[cIdx] <= rSq) {
|
|
532
|
+
const bi = bestIdx[l];
|
|
533
|
+
px = bi % width;
|
|
534
|
+
py = (bi - px) / width;
|
|
535
|
+
}
|
|
536
|
+
out.push({
|
|
537
|
+
point: { x: px, y: py },
|
|
538
|
+
centroid: { x: cx, y: cy },
|
|
539
|
+
bounds: {
|
|
540
|
+
x: minX[l],
|
|
541
|
+
y: minY[l],
|
|
542
|
+
width: maxX[l] - minX[l] + 1,
|
|
543
|
+
height: maxY[l] - minY[l] + 1
|
|
544
|
+
},
|
|
545
|
+
pixelCount: count,
|
|
546
|
+
touchesEdge
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
out.sort((a, b) => b.pixelCount - a.pixelCount || a.bounds.y - b.bounds.y || a.bounds.x - b.bounds.x);
|
|
550
|
+
return out.slice(0, maxRegions);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
exports.squaredDistanceTransform = squaredDistanceTransform; exports.fillRegionMask = fillRegionMask; exports.findUnfilledRegionsMask = findUnfilledRegionsMask;
|
|
@@ -18,6 +18,7 @@ import { type LayerLocks } from '../raster/layers.cjs';
|
|
|
18
18
|
import { type LayerEffect, type LayerEffectKind, type LayerEffects } from '../raster/effects/model.cjs';
|
|
19
19
|
import type { EffectPattern } from '../raster/effects/patterns.cjs';
|
|
20
20
|
import { TiledSurface, type RasterRect, type Tile } from '../raster/surface.cjs';
|
|
21
|
+
import { type FillRegionPoint, type FillRegionSummary, type UnfilledRegion } from '../raster/fill.cjs';
|
|
21
22
|
import { type PaintBrushInput, type PaintBrush } from './brushes.cjs';
|
|
22
23
|
import { type BrushLibrary, type BrushLibraryFile } from '../brushes/library.cjs';
|
|
23
24
|
import { type BrushPatch, type UnsettableBrushFacet } from '../brushes/patch.cjs';
|
|
@@ -297,6 +298,75 @@ export interface FillLayerOptions {
|
|
|
297
298
|
pressure?: number;
|
|
298
299
|
blend?: BlendMode;
|
|
299
300
|
}
|
|
301
|
+
export type FillReference = 'layer' | 'all' | 'composite' | (string & {});
|
|
302
|
+
export interface FillRegionOptions {
|
|
303
|
+
/** Seed x in document pixels (pass `x` and `y`, or `points`). */
|
|
304
|
+
x?: number;
|
|
305
|
+
/** Seed y in document pixels (pass `x` and `y`, or `points`). */
|
|
306
|
+
y?: number;
|
|
307
|
+
/** Multiple seed points in document pixels (filled together as one undo step). */
|
|
308
|
+
points?: ReadonlyArray<FillRegionPoint>;
|
|
309
|
+
/** Target layer id from `listLayers` (default the active layer). */
|
|
310
|
+
layerId?: string;
|
|
311
|
+
/**
|
|
312
|
+
* Which pixels define the region boundaries:
|
|
313
|
+
* - `'layer'` (default): the target layer itself.
|
|
314
|
+
* - `'all'` or `'composite'`: all visible layers composited (without paper).
|
|
315
|
+
* - a layer id from `listLayers`: that layer (e.g. an ink line-art layer above a flats layer).
|
|
316
|
+
*/
|
|
317
|
+
reference?: FillReference;
|
|
318
|
+
/** Fill colour #rrggbb (default the editor's paint colour). */
|
|
319
|
+
color?: string;
|
|
320
|
+
/** Opacity 0..1 (default 1). */
|
|
321
|
+
opacity?: number;
|
|
322
|
+
/** Blend mode (default `'normal'`). */
|
|
323
|
+
blend?: BlendMode;
|
|
324
|
+
/** Alias for `blend`. */
|
|
325
|
+
blendMode?: BlendMode;
|
|
326
|
+
/** Colour/alpha similarity tolerance 0..255 (default 32). */
|
|
327
|
+
tolerance?: number;
|
|
328
|
+
/** Close small gaps in line art up to this width in pixels, 0..32 (default 0). */
|
|
329
|
+
closeGap?: number;
|
|
330
|
+
/** Expand (> 0, bleed under lines) or contract (< 0) the filled region in pixels, -16..16 (default 0). */
|
|
331
|
+
expand?: number;
|
|
332
|
+
/** Soften boundary edges (default true). */
|
|
333
|
+
antialias?: boolean;
|
|
334
|
+
/** Paper grain response 0..1 (default 0: flat fill). */
|
|
335
|
+
grain?: number;
|
|
336
|
+
/** With grain: pressure 0..1, how far the fill reaches into the valleys (default 0.5). */
|
|
337
|
+
pressure?: number;
|
|
338
|
+
/** Pour a wet watercolour wash into the region on the layer's wet layer (default false). */
|
|
339
|
+
wet?: boolean;
|
|
340
|
+
}
|
|
341
|
+
export interface FillRegionResult {
|
|
342
|
+
layerId: string;
|
|
343
|
+
bounds: RasterRect | null;
|
|
344
|
+
pixelCount: number;
|
|
345
|
+
leaked: boolean;
|
|
346
|
+
regions: FillRegionSummary[];
|
|
347
|
+
wet?: boolean;
|
|
348
|
+
}
|
|
349
|
+
export interface FindUnfilledRegionsOptions {
|
|
350
|
+
/** Target layer id from `listLayers` (when given, regions already painted on it are excluded). */
|
|
351
|
+
layerId?: string;
|
|
352
|
+
/**
|
|
353
|
+
* Which pixels define the line-art barriers:
|
|
354
|
+
* - `'all'` or `'composite'` (default): all visible layers composited (without paper).
|
|
355
|
+
* - `'layer'`: the target (or active) layer itself.
|
|
356
|
+
* - a layer id from `listLayers`: that layer (e.g. an ink line-art layer).
|
|
357
|
+
*/
|
|
358
|
+
reference?: FillReference;
|
|
359
|
+
/** Alpha/colour threshold 0..255 above which a reference pixel is treated as line art (default 32). */
|
|
360
|
+
tolerance?: number;
|
|
361
|
+
/** Close small gaps in line art up to this width in pixels, 0..32 (default 0). */
|
|
362
|
+
closeGap?: number;
|
|
363
|
+
/** Ignore regions smaller than this many pixels (default 16). */
|
|
364
|
+
minArea?: number;
|
|
365
|
+
/** Include regions that touch the canvas border (default false: only enclosed shapes). */
|
|
366
|
+
includeEdge?: boolean;
|
|
367
|
+
/** Maximum number of regions to return, sorted largest first (default 100). */
|
|
368
|
+
maxRegions?: number;
|
|
369
|
+
}
|
|
300
370
|
/** Pixels to add on each side of the canvas (whole numbers, 0 or more; missing = 0). */
|
|
301
371
|
export interface PaintCanvasExtension {
|
|
302
372
|
left?: number;
|
|
@@ -653,6 +723,22 @@ export declare class PaintEditorController {
|
|
|
653
723
|
layerId: string;
|
|
654
724
|
bounds: RasterRect;
|
|
655
725
|
};
|
|
726
|
+
/**
|
|
727
|
+
* Flood-fills one or more contiguous regions on a pixel layer (one undo step), sampling boundaries
|
|
728
|
+
* from the layer itself (`reference: 'layer'`), all visible layers (`'all'` / `'composite'`), or a
|
|
729
|
+
* specific reference layer id (e.g. an ink line-art layer above a flats layer). Closes line-art gaps
|
|
730
|
+
* up to `closeGap` px and expands (`> 0`, bleeding under lines) or contracts (`< 0`) by `expand` px.
|
|
731
|
+
* With `wet: true`, pours a watercolour wash into the region on the layer's wet layer.
|
|
732
|
+
*/
|
|
733
|
+
fillRegion(options: FillRegionOptions): FillRegionResult;
|
|
734
|
+
/**
|
|
735
|
+
* Finds enclosed unfilled regions from a reference layer or the visible composite (without paper),
|
|
736
|
+
* closing line-art gaps up to `closeGap` px and returning each region's interior seed `point`,
|
|
737
|
+
* `centroid`, `bounds`, `pixelCount`, and `touchesEdge` flag.
|
|
738
|
+
*/
|
|
739
|
+
findUnfilledRegions(options?: FindUnfilledRegionsOptions): {
|
|
740
|
+
regions: UnfilledRegion[];
|
|
741
|
+
};
|
|
656
742
|
private applyCoverage;
|
|
657
743
|
/** The registered effects provider, or `null`. */
|
|
658
744
|
get effectsProvider(): EffectProvider | null;
|