@pixerate/editor 0.196.0 → 0.197.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.
@@ -2803,6 +2803,589 @@ function magneticAnchorSpacing(frequency = 57) {
2803
2803
  return 8 + (100 - f);
2804
2804
  }
2805
2805
 
2806
+ // src/image-editor/selection/segment/maxflow.ts
2807
+ var TERMINAL = -2;
2808
+ var NONE = -1;
2809
+ var FREE = 0;
2810
+ var SOURCE = 1;
2811
+ var SINK = 2;
2812
+ var GridGraph = class {
2813
+ width;
2814
+ height;
2815
+ /** Residual capacity of the edge leaving node `p` in direction `d`, at `p * 4 + d`. */
2816
+ cap;
2817
+ /** Net terminal capacity: above 0 to the source, below 0 to the sink. */
2818
+ tr;
2819
+ tree;
2820
+ constructor(width, height) {
2821
+ this.width = width;
2822
+ this.height = height;
2823
+ const n = width * height;
2824
+ this.cap = new Float64Array(n * 4);
2825
+ this.tr = new Float64Array(n);
2826
+ this.tree = new Uint8Array(n);
2827
+ }
2828
+ /** Sets the capacity of the edge between `p` and its neighbour in direction `d`, both ways. */
2829
+ setEdge(p, d, capacity) {
2830
+ const q = d === 0 ? p + 1 : p + this.width;
2831
+ this.cap[p * 4 + d] = capacity;
2832
+ this.cap[q * 4 + (d ^ 2)] = capacity;
2833
+ }
2834
+ /** Adds `source` (cost of putting `p` on the sink side) and `sink` (the other way) to `p`. */
2835
+ addTerminal(p, source, sink) {
2836
+ this.tr[p] += source - sink;
2837
+ }
2838
+ /** Whether `p` is on the source side of the minimum cut (after {@link maxflow}). */
2839
+ inSource(p) {
2840
+ return this.tree[p] === SOURCE;
2841
+ }
2842
+ /** Runs max-flow and returns the flow (the cut's cost, less the constant terminal parts). */
2843
+ maxflow() {
2844
+ const { width: w, height: h, cap, tr, tree } = this;
2845
+ const n = w * h;
2846
+ const nb = (p, d) => {
2847
+ const x = p % w;
2848
+ if (d === 0) return x < w - 1 ? p + 1 : -1;
2849
+ if (d === 1) return p + w < n ? p + w : -1;
2850
+ if (d === 2) return x > 0 ? p - 1 : -1;
2851
+ return p - w >= 0 ? p - w : -1;
2852
+ };
2853
+ const parent = new Int8Array(n).fill(NONE);
2854
+ const ts = new Int32Array(n);
2855
+ const dist = new Int32Array(n);
2856
+ const queue = new Int32Array(n + 1);
2857
+ const queued = new Uint8Array(n);
2858
+ let qHead = 0;
2859
+ let qTail = 0;
2860
+ const activate = (p) => {
2861
+ if (queued[p]) return;
2862
+ queued[p] = 1;
2863
+ queue[qTail] = p;
2864
+ qTail = qTail === n ? 0 : qTail + 1;
2865
+ };
2866
+ const nextActive = () => {
2867
+ while (qHead !== qTail) {
2868
+ const p = queue[qHead];
2869
+ qHead = qHead === n ? 0 : qHead + 1;
2870
+ queued[p] = 0;
2871
+ if (tree[p] !== FREE) return p;
2872
+ }
2873
+ return -1;
2874
+ };
2875
+ let orphans = new Int32Array(64);
2876
+ let orphanCount = 0;
2877
+ const orphan = (p) => {
2878
+ parent[p] = NONE;
2879
+ if (orphanCount === orphans.length) {
2880
+ const grown = new Int32Array(orphans.length * 2);
2881
+ grown.set(orphans);
2882
+ orphans = grown;
2883
+ }
2884
+ orphans[orphanCount++] = p;
2885
+ };
2886
+ tree.fill(FREE);
2887
+ for (let p = 0; p < n; p++) {
2888
+ if (tr[p] > 0) tree[p] = SOURCE;
2889
+ else if (tr[p] < 0) tree[p] = SINK;
2890
+ else continue;
2891
+ parent[p] = TERMINAL;
2892
+ dist[p] = 1;
2893
+ activate(p);
2894
+ }
2895
+ let flow = 0;
2896
+ let time = 0;
2897
+ let current = -1;
2898
+ for (; ; ) {
2899
+ if (current < 0 || tree[current] === FREE) {
2900
+ current = nextActive();
2901
+ if (current < 0) break;
2902
+ }
2903
+ const p = current;
2904
+ const side = tree[p];
2905
+ let s = -1;
2906
+ let t = -1;
2907
+ let dir = 0;
2908
+ for (let d = 0; d < 4; d++) {
2909
+ const q = nb(p, d);
2910
+ if (q < 0) continue;
2911
+ if (side === SOURCE ? cap[p * 4 + d] <= 0 : cap[q * 4 + (d ^ 2)] <= 0) continue;
2912
+ if (tree[q] === FREE) {
2913
+ tree[q] = side;
2914
+ parent[q] = d ^ 2;
2915
+ ts[q] = ts[p];
2916
+ dist[q] = dist[p] + 1;
2917
+ activate(q);
2918
+ } else if (tree[q] !== side) {
2919
+ if (side === SOURCE) {
2920
+ s = p;
2921
+ t = q;
2922
+ dir = d;
2923
+ } else {
2924
+ s = q;
2925
+ t = p;
2926
+ dir = d ^ 2;
2927
+ }
2928
+ break;
2929
+ } else if (ts[q] <= ts[p] && dist[q] > dist[p]) {
2930
+ parent[q] = d ^ 2;
2931
+ ts[q] = ts[p];
2932
+ dist[q] = dist[p] + 1;
2933
+ }
2934
+ }
2935
+ if (s < 0) {
2936
+ current = -1;
2937
+ continue;
2938
+ }
2939
+ time++;
2940
+ let f = cap[s * 4 + dir];
2941
+ for (let x = s; ; ) {
2942
+ const a = parent[x];
2943
+ if (a === TERMINAL) {
2944
+ f = Math.min(f, tr[x]);
2945
+ break;
2946
+ }
2947
+ const up = nb(x, a);
2948
+ f = Math.min(f, cap[up * 4 + (a ^ 2)]);
2949
+ x = up;
2950
+ }
2951
+ for (let x = t; ; ) {
2952
+ const a = parent[x];
2953
+ if (a === TERMINAL) {
2954
+ f = Math.min(f, -tr[x]);
2955
+ break;
2956
+ }
2957
+ f = Math.min(f, cap[x * 4 + a]);
2958
+ x = nb(x, a);
2959
+ }
2960
+ cap[s * 4 + dir] -= f;
2961
+ cap[t * 4 + (dir ^ 2)] += f;
2962
+ for (let x = s; ; ) {
2963
+ const a = parent[x];
2964
+ if (a === TERMINAL) {
2965
+ tr[x] -= f;
2966
+ if (tr[x] <= 0) orphan(x);
2967
+ break;
2968
+ }
2969
+ const up = nb(x, a);
2970
+ cap[up * 4 + (a ^ 2)] -= f;
2971
+ cap[x * 4 + a] += f;
2972
+ if (cap[up * 4 + (a ^ 2)] <= 0) orphan(x);
2973
+ x = up;
2974
+ }
2975
+ for (let x = t; ; ) {
2976
+ const a = parent[x];
2977
+ if (a === TERMINAL) {
2978
+ tr[x] += f;
2979
+ if (tr[x] >= 0) orphan(x);
2980
+ break;
2981
+ }
2982
+ const up = nb(x, a);
2983
+ cap[x * 4 + a] -= f;
2984
+ cap[up * 4 + (a ^ 2)] += f;
2985
+ if (cap[x * 4 + a] <= 0) orphan(x);
2986
+ x = up;
2987
+ }
2988
+ flow += f;
2989
+ while (orphanCount) {
2990
+ const o = orphans[--orphanCount];
2991
+ const oSide = tree[o];
2992
+ let best = NONE;
2993
+ let bestDist = Infinity;
2994
+ for (let d = 0; d < 4; d++) {
2995
+ const q = nb(o, d);
2996
+ if (q < 0 || tree[q] !== oSide) continue;
2997
+ if (oSide === SOURCE ? cap[q * 4 + (d ^ 2)] <= 0 : cap[o * 4 + d] <= 0) continue;
2998
+ let k = 0;
2999
+ let x = q;
3000
+ for (; ; ) {
3001
+ if (ts[x] === time) {
3002
+ k += dist[x];
3003
+ break;
3004
+ }
3005
+ const a = parent[x];
3006
+ k++;
3007
+ if (a === TERMINAL) {
3008
+ ts[x] = time;
3009
+ dist[x] = 1;
3010
+ break;
3011
+ }
3012
+ if (a === NONE) {
3013
+ k = Infinity;
3014
+ break;
3015
+ }
3016
+ x = nb(x, a);
3017
+ }
3018
+ if (k === Infinity) continue;
3019
+ if (k < bestDist) {
3020
+ bestDist = k;
3021
+ best = d;
3022
+ }
3023
+ for (let y = q, dd = k; ts[y] !== time; y = nb(y, parent[y]), dd--) {
3024
+ ts[y] = time;
3025
+ dist[y] = dd;
3026
+ }
3027
+ }
3028
+ if (best !== NONE) {
3029
+ parent[o] = best;
3030
+ ts[o] = time;
3031
+ dist[o] = bestDist + 1;
3032
+ continue;
3033
+ }
3034
+ for (let d = 0; d < 4; d++) {
3035
+ const q = nb(o, d);
3036
+ if (q < 0 || tree[q] !== oSide) continue;
3037
+ if (oSide === SOURCE ? cap[q * 4 + (d ^ 2)] > 0 : cap[o * 4 + d] > 0) activate(q);
3038
+ const a = parent[q];
3039
+ if (a !== TERMINAL && a !== NONE && nb(q, a) === o) orphan(q);
3040
+ }
3041
+ tree[o] = FREE;
3042
+ }
3043
+ }
3044
+ return flow;
3045
+ }
3046
+ };
3047
+
3048
+ // src/image-editor/selection/segment/quick.ts
3049
+ var WORK_SIZE = 512;
3050
+ var workCache = /* @__PURE__ */ new WeakMap();
3051
+ function workImage(pixels, maxSide = WORK_SIZE) {
3052
+ const cached = workCache.get(pixels.data);
3053
+ if (cached && cached.maxSide === maxSide && cached.work.sourceWidth === pixels.width && cached.work.sourceHeight === pixels.height) return cached.work;
3054
+ const { width: W, height: H, data } = pixels;
3055
+ const scale = Math.min(1, maxSide / Math.max(W, H));
3056
+ const w = Math.max(1, Math.round(W * scale));
3057
+ const h = Math.max(1, Math.round(H * scale));
3058
+ const sum = new Float64Array(w * h * 3);
3059
+ const count = new Float64Array(w * h);
3060
+ const xs = new Int32Array(W);
3061
+ for (let x = 0; x < W; x++) xs[x] = Math.min(w - 1, Math.floor(x * w / W));
3062
+ for (let y = 0; y < H; y++) {
3063
+ const row = Math.min(h - 1, Math.floor(y * h / H)) * w;
3064
+ for (let x = 0, p = y * W * 4; x < W; x++, p += 4) {
3065
+ const o = row + xs[x];
3066
+ const a = data[p + 3] / 255;
3067
+ const k = o * 3;
3068
+ sum[k] += data[p] * a + 255 * (1 - a);
3069
+ sum[k + 1] += data[p + 1] * a + 255 * (1 - a);
3070
+ sum[k + 2] += data[p + 2] * a + 255 * (1 - a);
3071
+ count[o]++;
3072
+ }
3073
+ }
3074
+ const rgb = new Float32Array(w * h * 3);
3075
+ for (let i = 0; i < w * h; i++) {
3076
+ const c = count[i] || 1;
3077
+ rgb[i * 3] = sum[i * 3] / c;
3078
+ rgb[i * 3 + 1] = sum[i * 3 + 1] / c;
3079
+ rgb[i * 3 + 2] = sum[i * 3 + 2] / c;
3080
+ }
3081
+ const work = { sourceWidth: W, sourceHeight: H, width: w, height: h, scale: w / W, rgb };
3082
+ workCache.set(pixels.data, { maxSide, work });
3083
+ return work;
3084
+ }
3085
+ var BINS = 16;
3086
+ var binOf = (rgb, i) => (Math.min(BINS - 1, rgb[i * 3] >> 4) * BINS + Math.min(BINS - 1, rgb[i * 3 + 1] >> 4)) * BINS + Math.min(BINS - 1, rgb[i * 3 + 2] >> 4);
3087
+ function costTable(hist, total) {
3088
+ let a = hist;
3089
+ for (const stride of [1, BINS, BINS * BINS]) {
3090
+ const b = new Float64Array(a.length);
3091
+ for (let i = 0; i < a.length; i++) {
3092
+ const pos = Math.floor(i / stride) % BINS;
3093
+ const lo = pos > 0 ? a[i - stride] : a[i];
3094
+ const hi = pos < BINS - 1 ? a[i + stride] : a[i];
3095
+ b[i] = (lo + 2 * a[i] + hi) / 4;
3096
+ }
3097
+ a = b;
3098
+ }
3099
+ const out = new Float64Array(a.length);
3100
+ const floor = 1e-4;
3101
+ const denom = Math.max(1, total) * (1 + floor * a.length);
3102
+ for (let i = 0; i < a.length; i++) out[i] = -Math.log((a[i] + floor * Math.max(1, total)) / denom);
3103
+ return out;
3104
+ }
3105
+ var GAMMA = 50;
3106
+ var UNITS = 64;
3107
+ function strokePixels(points, r, w, h) {
3108
+ const mask = new Uint8Array(w * h);
3109
+ const list = [];
3110
+ const add2 = (g) => {
3111
+ if (!mask[g]) {
3112
+ mask[g] = 1;
3113
+ list.push(g);
3114
+ }
3115
+ };
3116
+ const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
3117
+ for (const [a, b] of segs) {
3118
+ const x0 = Math.max(0, Math.floor(Math.min(a.x, b.x) - r));
3119
+ const x1 = Math.min(w - 1, Math.ceil(Math.max(a.x, b.x) + r));
3120
+ const y0 = Math.max(0, Math.floor(Math.min(a.y, b.y) - r));
3121
+ const y1 = Math.min(h - 1, Math.ceil(Math.max(a.y, b.y) + r));
3122
+ const dx = b.x - a.x;
3123
+ const dy = b.y - a.y;
3124
+ const len2 = dx * dx + dy * dy;
3125
+ for (let y = y0; y <= y1; y++) {
3126
+ for (let x = x0; x <= x1; x++) {
3127
+ const px2 = x + 0.5 - a.x;
3128
+ const py = y + 0.5 - a.y;
3129
+ const t = len2 > 0 ? Math.min(1, Math.max(0, (px2 * dx + py * dy) / len2)) : 0;
3130
+ if ((px2 - t * dx) ** 2 + (py - t * dy) ** 2 <= r * r) add2(y * w + x);
3131
+ }
3132
+ }
3133
+ }
3134
+ if (!list.length) {
3135
+ for (const p of points) add2(Math.min(h - 1, Math.max(0, Math.floor(p.y))) * w + Math.min(w - 1, Math.max(0, Math.floor(p.x))));
3136
+ }
3137
+ return { mask, list };
3138
+ }
3139
+ function segmentStroke(work, stroke, grown, start) {
3140
+ const { width: w, height: h, rgb } = work;
3141
+ let roi = start;
3142
+ for (; ; ) {
3143
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3144
+ const n = rw * rh;
3145
+ const fgHist = new Float64Array(BINS ** 3);
3146
+ const bgHist = new Float64Array(BINS ** 3);
3147
+ let fgN = 0;
3148
+ let bgN = 0;
3149
+ const atImageEdge = { l: rx === 0, t: ry === 0, r: rx + rw === w, b: ry + rh === h };
3150
+ for (let y = 0; y < rh; y++) {
3151
+ for (let x = 0; x < rw; x++) {
3152
+ const g = (ry + y) * w + (rx + x);
3153
+ const bin = binOf(rgb, g);
3154
+ bgHist[bin]++;
3155
+ bgN++;
3156
+ if (stroke.mask[g] || grown?.[g]) {
3157
+ fgHist[bin]++;
3158
+ fgN++;
3159
+ }
3160
+ }
3161
+ }
3162
+ const fgCost = costTable(fgHist, fgN);
3163
+ const bgCost = costTable(bgHist, bgN);
3164
+ let sumSq = 0;
3165
+ let pairs = 0;
3166
+ for (let y = 0; y < rh; y++) {
3167
+ for (let x = 0; x < rw; x++) {
3168
+ const g = ((ry + y) * w + rx + x) * 3;
3169
+ if (x + 1 < rw) {
3170
+ sumSq += (rgb[g] - rgb[g + 3]) ** 2 + (rgb[g + 1] - rgb[g + 4]) ** 2 + (rgb[g + 2] - rgb[g + 5]) ** 2;
3171
+ pairs++;
3172
+ }
3173
+ if (y + 1 < rh) {
3174
+ const k = g + w * 3;
3175
+ sumSq += (rgb[g] - rgb[k]) ** 2 + (rgb[g + 1] - rgb[k + 1]) ** 2 + (rgb[g + 2] - rgb[k + 2]) ** 2;
3176
+ pairs++;
3177
+ }
3178
+ }
3179
+ }
3180
+ const beta = 1 / (2 * Math.max(1e-6, sumSq / Math.max(1, pairs)));
3181
+ const graph = new GridGraph(rw, rh);
3182
+ const HARD = 1e9;
3183
+ for (let y = 0; y < rh; y++) {
3184
+ for (let x = 0; x < rw; x++) {
3185
+ const p = y * rw + x;
3186
+ const g = (ry + y) * w + rx + x;
3187
+ if (stroke.mask[g]) graph.addTerminal(p, HARD, 0);
3188
+ else {
3189
+ const bin = binOf(rgb, g);
3190
+ graph.addTerminal(p, Math.round(bgCost[bin] * UNITS), Math.round(fgCost[bin] * UNITS));
3191
+ }
3192
+ const c = g * 3;
3193
+ if (x + 1 < rw) {
3194
+ const d = (rgb[c] - rgb[c + 3]) ** 2 + (rgb[c + 1] - rgb[c + 4]) ** 2 + (rgb[c + 2] - rgb[c + 5]) ** 2;
3195
+ graph.setEdge(p, 0, Math.round(GAMMA * UNITS * Math.exp(-beta * d)));
3196
+ }
3197
+ if (y + 1 < rh) {
3198
+ const k = c + w * 3;
3199
+ const d = (rgb[c] - rgb[k]) ** 2 + (rgb[c + 1] - rgb[k + 1]) ** 2 + (rgb[c + 2] - rgb[k + 2]) ** 2;
3200
+ graph.setEdge(p, 1, Math.round(GAMMA * UNITS * Math.exp(-beta * d)));
3201
+ }
3202
+ }
3203
+ }
3204
+ graph.maxflow();
3205
+ const label = new Uint8Array(n);
3206
+ const stack = [];
3207
+ for (const g of stroke.list) {
3208
+ const x = g % w - rx;
3209
+ const y = Math.floor(g / w) - ry;
3210
+ if (x < 0 || y < 0 || x >= rw || y >= rh) continue;
3211
+ const p = y * rw + x;
3212
+ if (!label[p]) {
3213
+ label[p] = 1;
3214
+ stack.push(p);
3215
+ }
3216
+ }
3217
+ while (stack.length) {
3218
+ const p = stack.pop();
3219
+ const x = p % rw;
3220
+ for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
3221
+ if (q >= 0 && !label[q] && graph.inSource(q)) {
3222
+ label[q] = 1;
3223
+ stack.push(q);
3224
+ }
3225
+ }
3226
+ }
3227
+ let touches = false;
3228
+ for (let y = 0; y < rh; y++) {
3229
+ for (let x = 0; x < rw; x++) {
3230
+ const p = y * rw + x;
3231
+ if (!label[p]) continue;
3232
+ if (!atImageEdge.l && x === 0 || !atImageEdge.t && y === 0 || !atImageEdge.r && x === rw - 1 || !atImageEdge.b && y === rh - 1) touches = true;
3233
+ }
3234
+ }
3235
+ if (!touches || atImageEdge.l && atImageEdge.t && atImageEdge.r && atImageEdge.b) return { roi, label };
3236
+ const nx = Math.max(0, rx - Math.ceil(rw / 2));
3237
+ const ny = Math.max(0, ry - Math.ceil(rh / 2));
3238
+ roi = { x: nx, y: ny, width: Math.min(w, rx + rw + Math.ceil(rw / 2)) - nx, height: Math.min(h, ry + rh + Math.ceil(rh / 2)) - ny };
3239
+ if (roi.width * roi.height * 2.5 > w * h) roi = { x: 0, y: 0, width: w, height: h };
3240
+ }
3241
+ }
3242
+ function boxBlur(src, w, h, passes) {
3243
+ let a = src;
3244
+ for (let k = 0; k < passes; k++) {
3245
+ const b = new Float32Array(a.length);
3246
+ for (let y = 0; y < h; y++) {
3247
+ for (let x = 0; x < w; x++) {
3248
+ let s = 0;
3249
+ let c = 0;
3250
+ for (let j = Math.max(0, y - 1); j <= Math.min(h - 1, y + 1); j++) {
3251
+ for (let i = Math.max(0, x - 1); i <= Math.min(w - 1, x + 1); i++) {
3252
+ s += a[j * w + i];
3253
+ c++;
3254
+ }
3255
+ }
3256
+ b[y * w + x] = s / c;
3257
+ }
3258
+ }
3259
+ a = b;
3260
+ }
3261
+ return a;
3262
+ }
3263
+ function upscaleRegion(work, pixels, roi, label, enhance) {
3264
+ const { sourceWidth: W, sourceHeight: H, scale, rgb, width: ww } = work;
3265
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3266
+ let soft = Float32Array.from(label);
3267
+ if (enhance) {
3268
+ soft = boxBlur(soft, rw, rh, 2);
3269
+ for (let i = 0; i < soft.length; i++) soft[i] = Math.min(1, Math.max(0, (soft[i] - 0.5) * 3 + 0.5));
3270
+ }
3271
+ const X0 = Math.max(0, Math.floor(rx / scale));
3272
+ const Y0 = Math.max(0, Math.floor(ry / scale));
3273
+ const X1 = Math.min(W, Math.ceil((rx + rw) / scale));
3274
+ const Y1 = Math.min(H, Math.ceil((ry + rh) / scale));
3275
+ const RW = X1 - X0;
3276
+ const RH = Y1 - Y0;
3277
+ const out = new Uint8Array(RW * RH);
3278
+ const at = (x, y) => soft[Math.min(rh - 1, Math.max(0, y)) * rw + Math.min(rw - 1, Math.max(0, x))];
3279
+ const data = pixels.data;
3280
+ const colOf = new Int32Array(RW);
3281
+ const fxOf = new Float32Array(RW);
3282
+ for (let X = 0; X < RW; X++) {
3283
+ const u = (X0 + X + 0.5) * scale - 0.5 - rx;
3284
+ colOf[X] = Math.floor(u);
3285
+ fxOf[X] = u - colOf[X];
3286
+ }
3287
+ const cellState = (i, j) => {
3288
+ const a = at(i, j), b = at(i + 1, j), c = at(i, j + 1), d = at(i + 1, j + 1);
3289
+ if (a === 1 && b === 1 && c === 1 && d === 1) return 1;
3290
+ if (a === 0 && b === 0 && c === 0 && d === 0) return 0;
3291
+ return 2;
3292
+ };
3293
+ for (let Y = 0; Y < RH; Y++) {
3294
+ const v = (Y0 + Y + 0.5) * scale - 0.5 - ry;
3295
+ const j = Math.floor(v);
3296
+ const fy = v - j;
3297
+ for (let X = 0; X < RW; X++) {
3298
+ const i = colOf[X];
3299
+ const state = cellState(i, j);
3300
+ if (state !== 2) {
3301
+ let end = X + 1;
3302
+ while (end < RW && colOf[end] === i) end++;
3303
+ if (state === 1) out.fill(255, Y * RW + X, Y * RW + end);
3304
+ X = end - 1;
3305
+ continue;
3306
+ }
3307
+ const fx = fxOf[X];
3308
+ const m = (at(i, j) * (1 - fx) + at(i + 1, j) * fx) * (1 - fy) + (at(i, j + 1) * (1 - fx) + at(i + 1, j + 1) * fx) * fy;
3309
+ if (m <= 1e-3 || m >= 0.999) {
3310
+ out[Y * RW + X] = m >= 0.999 ? 255 : 0;
3311
+ continue;
3312
+ }
3313
+ let fr = 0, fg = 0, fb = 0, fn = 0, br = 0, bg = 0, bb = 0, bn = 0;
3314
+ for (let jj = j - 1; jj <= j + 2; jj++) {
3315
+ for (let ii = i - 1; ii <= i + 2; ii++) {
3316
+ if (ii < 0 || jj < 0 || ii >= rw || jj >= rh) continue;
3317
+ const k = ((ry + jj) * ww + rx + ii) * 3;
3318
+ if (soft[jj * rw + ii] >= 0.5) {
3319
+ fr += rgb[k];
3320
+ fg += rgb[k + 1];
3321
+ fb += rgb[k + 2];
3322
+ fn++;
3323
+ } else {
3324
+ br += rgb[k];
3325
+ bg += rgb[k + 1];
3326
+ bb += rgb[k + 2];
3327
+ bn++;
3328
+ }
3329
+ }
3330
+ }
3331
+ let alpha = m;
3332
+ if (fn && bn) {
3333
+ fr /= fn;
3334
+ fg /= fn;
3335
+ fb /= fn;
3336
+ br /= bn;
3337
+ bg /= bn;
3338
+ bb /= bn;
3339
+ const dr = fr - br, dg = fg - bg, db = fb - bb;
3340
+ const len2 = dr * dr + dg * dg + db * db;
3341
+ if (len2 > 400) {
3342
+ const p = ((Y0 + Y) * W + X0 + X) * 4;
3343
+ const a = data[p + 3] / 255;
3344
+ const cr = data[p] * a + 255 * (1 - a) - br;
3345
+ const cg = data[p + 1] * a + 255 * (1 - a) - bg;
3346
+ const cb = data[p + 2] * a + 255 * (1 - a) - bb;
3347
+ const proj = Math.min(1, Math.max(0, (cr * dr + cg * dg + cb * db) / len2));
3348
+ alpha = Math.min(1, Math.max(0, proj * 0.75 + m * 0.25));
3349
+ } else alpha = Math.min(1, Math.max(0, (m - 0.5) * 2 + 0.5));
3350
+ }
3351
+ out[Y * RW + X] = Math.round(alpha * 255);
3352
+ }
3353
+ }
3354
+ return selectionFromRegion(W, H, { x: X0, y: Y0, width: RW, height: RH }, out);
3355
+ }
3356
+ function quickSelectStroke(pixels, stroke, options = {}) {
3357
+ if (!stroke.length) throw new Error("A stroke needs at least one point.");
3358
+ const work = workImage(pixels);
3359
+ const { width: w, height: h, scale } = work;
3360
+ const r = Math.max(0.5, (options.size ?? 30) / 2 * scale);
3361
+ const pts = stroke.map((p) => ({ x: p.x * scale, y: p.y * scale }));
3362
+ const seeds = strokePixels(pts, r, w, h);
3363
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
3364
+ for (const p of pts) {
3365
+ x0 = Math.min(x0, p.x);
3366
+ y0 = Math.min(y0, p.y);
3367
+ x1 = Math.max(x1, p.x);
3368
+ y1 = Math.max(y1, p.y);
3369
+ }
3370
+ const margin = Math.max(24, 4 * r);
3371
+ const rx = Math.max(0, Math.floor(x0 - margin));
3372
+ const ry = Math.max(0, Math.floor(y0 - margin));
3373
+ const roi = { x: rx, y: ry, width: Math.min(w, Math.ceil(x1 + margin)) - rx, height: Math.min(h, Math.ceil(y1 + margin)) - ry };
3374
+ let grown = null;
3375
+ const sel = options.grow ? options.selection : null;
3376
+ if (sel?.bounds) {
3377
+ grown = new Uint8Array(w * h);
3378
+ const b = sel.bounds;
3379
+ for (let y = Math.max(0, Math.floor(b.y * scale)); y < Math.min(h, Math.ceil((b.y + b.height) * scale)); y++) {
3380
+ for (let x = Math.max(0, Math.floor(b.x * scale)); x < Math.min(w, Math.ceil((b.x + b.width) * scale)); x++) {
3381
+ if (selectionValueAt(sel, Math.min(sel.width - 1, Math.floor((x + 0.5) / scale)), Math.min(sel.height - 1, Math.floor((y + 0.5) / scale))) >= 0.5) grown[y * w + x] = 1;
3382
+ }
3383
+ }
3384
+ }
3385
+ const { roi: done, label } = segmentStroke(work, seeds, grown, roi);
3386
+ return upscaleRegion(work, pixels, done, label, options.enhanceEdge ?? false);
3387
+ }
3388
+
2806
3389
  // src/image-editor/layers/shape-type-ui.ts
2807
3390
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
2808
3391
  kind: "rect",
@@ -8747,6 +9330,31 @@ var ImageEditorController = class {
8747
9330
  const pixels = await this.requirePixels(options);
8748
9331
  return snapPolygonToEdges(pixels, points, options);
8749
9332
  }
9333
+ /**
9334
+ * Quick Selection: each stroke (the brush's path in natural pixels) grows into the region it is
9335
+ * painted on, out to that region's edges, and is combined with the selection by `mode`; with
9336
+ * `replace`, the first stroke replaces it and the rest add, as the brush does. All strokes are
9337
+ * one undo step. Resolves to the resulting selection.
9338
+ */
9339
+ async quickSelect(strokes, options = {}) {
9340
+ if (!strokes.length || strokes.some((s) => !s.length)) throw new Error("Pass at least one stroke, each with at least one point.");
9341
+ const { width, height } = this.naturalSize;
9342
+ for (const s of strokes) {
9343
+ if (!s.some((p) => p.x >= 0 && p.y >= 0 && p.x < width && p.y < height)) {
9344
+ throw new Error(`A stroke lies entirely outside the image (0..${width - 1}, 0..${height - 1}); paint over the area to select.`);
9345
+ }
9346
+ }
9347
+ const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
9348
+ const mode = options.mode ?? "replace";
9349
+ let sel = this.state.selection ?? null;
9350
+ strokes.forEach((stroke, i) => {
9351
+ const m = mode === "replace" && i > 0 ? "add" : mode;
9352
+ const region = quickSelectStroke(pixels, stroke, { size: options.size, selection: sel, grow: m === "add", enhanceEdge: options.enhanceEdge });
9353
+ sel = combineSelections(sel, region, m);
9354
+ });
9355
+ this.setSelection(sel, "replace");
9356
+ return this.state.selection ?? null;
9357
+ }
8750
9358
  /**
8751
9359
  * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
8752
9360
  * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)
@@ -11634,7 +12242,7 @@ var DEPTH_MASK_CONTROLS = [
11634
12242
  // src/image-editor/playback.ts
11635
12243
  var IMAGE_PLAYBACK_KIND = "layer";
11636
12244
  var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
11637
- var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
12245
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
11638
12246
  var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|set_text_orientation|convert_text_to_shape)$/;
11639
12247
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
11640
12248
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -11756,6 +12364,7 @@ var SELECTION_TOOLS = [
11756
12364
  { id: "lasso", label: "Lasso" },
11757
12365
  { id: "polygon", label: "Polygonal Lasso" },
11758
12366
  { id: "magnetic", label: "Magnetic Lasso" },
12367
+ { id: "quick", label: "Quick Selection" },
11759
12368
  { id: "range", label: "Color Range" },
11760
12369
  { id: "type", label: "Type Mask" },
11761
12370
  { id: "type-vertical", label: "Vertical Type Mask" }
@@ -11793,6 +12402,9 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
11793
12402
  magneticWidth: 10,
11794
12403
  magneticContrast: 10,
11795
12404
  magneticFrequency: 57,
12405
+ quickSize: 30,
12406
+ quickSampleAll: true,
12407
+ quickEnhance: false,
11796
12408
  typeText: "Type",
11797
12409
  typeSize: 72,
11798
12410
  typeFamily: "sans-serif"
@@ -12192,6 +12804,42 @@ function createImageSelectionTools(controller) {
12192
12804
  return { ...summary(controller), outlinePoints: outline.length };
12193
12805
  }
12194
12806
  };
12807
+ const quickSelectTool = {
12808
+ name: "quick_select",
12809
+ title: "Quick Selection",
12810
+ description: "Quick Selection: paints brush strokes (paths in natural image pixels) that each grow into the region they are painted on, out to its edges (a graph cut on colour and edges), combined with the selection by mode. With replace (default) the first stroke replaces the selection and later ones add, as the brush does; use mode subtract to take an area back out. Best for a subject or area that differs from its surroundings: paint inside it, away from the edge, then check with get_selection and capture_view and add or subtract strokes where it leaked or fell short. All strokes are one undoable step. Returns the selection.",
12811
+ inputSchema: {
12812
+ type: "object",
12813
+ properties: {
12814
+ strokes: {
12815
+ type: "array",
12816
+ minItems: 1,
12817
+ items: { type: "array", minItems: 1, items: pointSchema, description: "One stroke: the brush path, in order (one point is a dab)." },
12818
+ description: "Brush strokes in natural image pixels, applied in order."
12819
+ },
12820
+ size: { type: "number", minimum: 1, maximum: 2500, description: "Brush diameter in natural image pixels (default 30). Smaller for a narrow area, larger for a broad one." },
12821
+ mode: modeSchema,
12822
+ sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
12823
+ enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false). Use it before masking a smooth-edged subject." }
12824
+ },
12825
+ required: ["strokes"],
12826
+ additionalProperties: false
12827
+ },
12828
+ annotations: writeAnnotations("Quick Selection"),
12829
+ handler: async (args) => {
12830
+ const strokes = Array.isArray(args.strokes) ? args.strokes.map((s) => Array.isArray(s) ? s.map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) : []) : [];
12831
+ if (!strokes.length || strokes.some((s) => !s.length || !s.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)))) {
12832
+ throw new Error("strokes needs at least one stroke of { x, y } points in natural image pixels.");
12833
+ }
12834
+ await controller.quickSelect(strokes, {
12835
+ size: args.size === void 0 ? void 0 : num2(args.size, 30),
12836
+ mode: parseSelectionMode(args.mode),
12837
+ sampleAllLayers: args.sampleAllLayers,
12838
+ enhanceEdge: args.enhanceEdge
12839
+ });
12840
+ return summary(controller);
12841
+ }
12842
+ };
12195
12843
  const magicWandTool = {
12196
12844
  name: "magic_wand",
12197
12845
  title: "Magic wand",
@@ -12744,6 +13392,7 @@ function createImageSelectionTools(controller) {
12744
13392
  selectRect,
12745
13393
  selectEllipse,
12746
13394
  selectPolygonTool,
13395
+ quickSelectTool,
12747
13396
  lineTool,
12748
13397
  magicWandTool,
12749
13398
  colorRangeTool,
@@ -14131,9 +14780,14 @@ var SelectionGesture = class {
14131
14780
  closeRadius = 8;
14132
14781
  /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
14133
14782
  error = null;
14783
+ pending = Promise.resolve();
14134
14784
  get active() {
14135
14785
  return this.start !== null || this.polygon !== null || this.magnetic !== null;
14136
14786
  }
14787
+ /** Resolves once a Quick Selection stroke has been applied. */
14788
+ get settled() {
14789
+ return this.pending;
14790
+ }
14137
14791
  /** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
14138
14792
  get magneticAnchors() {
14139
14793
  const m = this.magnetic;
@@ -14145,7 +14799,7 @@ var SelectionGesture = class {
14145
14799
  if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
14146
14800
  if (!this.start || !this.current) return null;
14147
14801
  const { tool } = this.getOptions();
14148
- if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
14802
+ if (tool === "lasso" || tool === "quick") return this.points.length > 1 ? this.points.slice() : null;
14149
14803
  const rect = normalizeRect({
14150
14804
  x: this.start.x,
14151
14805
  y: this.start.y,
@@ -14279,7 +14933,8 @@ var SelectionGesture = class {
14279
14933
  }
14280
14934
  if (!this.start) return false;
14281
14935
  this.current = point;
14282
- if (this.getOptions().tool === "lasso") {
14936
+ const drawn = this.getOptions().tool;
14937
+ if (drawn === "lasso" || drawn === "quick") {
14283
14938
  const last = this.points[this.points.length - 1];
14284
14939
  if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
14285
14940
  this.points.push(point);
@@ -14299,6 +14954,25 @@ var SelectionGesture = class {
14299
14954
  if (point) this.move(point);
14300
14955
  const options = this.getOptions();
14301
14956
  const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
14957
+ if (options.tool === "quick") {
14958
+ const stroke = this.points;
14959
+ this.start = null;
14960
+ this.current = null;
14961
+ this.points = [];
14962
+ const strokeMode = mode === "replace" && this.controller.getSelection() ? "add" : mode;
14963
+ this.pending = this.controller.quickSelect([stroke], {
14964
+ size: options.quickSize ?? 30,
14965
+ mode: strokeMode,
14966
+ sampleAllLayers: options.quickSampleAll ?? true,
14967
+ enhanceEdge: options.quickEnhance ?? false
14968
+ }).then(
14969
+ () => void 0,
14970
+ (err) => {
14971
+ this.error = err instanceof Error ? err.message : String(err);
14972
+ }
14973
+ );
14974
+ return false;
14975
+ }
14302
14976
  const path = this.draftPath;
14303
14977
  const rect = normalizeRect({
14304
14978
  x: this.start.x,
@@ -15154,7 +15828,7 @@ function checkSource(controller, source) {
15154
15828
  }
15155
15829
  return p;
15156
15830
  }
15157
- var SOURCE = {
15831
+ var SOURCE2 = {
15158
15832
  type: "object",
15159
15833
  description: "Source point in document pixels (an Alt-click). Given, it replaces the clone source and the offset starts afresh at this stroke; missing, the current clone source is used (set_clone_source).",
15160
15834
  properties: { x: { type: "number", description: "Source x in px." }, y: { type: "number", description: "Source y in px." } },
@@ -15178,7 +15852,7 @@ function createImageRetouchTools(controller) {
15178
15852
  type: "object",
15179
15853
  properties: {
15180
15854
  points: POINTS,
15181
- source: SOURCE,
15855
+ source: SOURCE2,
15182
15856
  aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
15183
15857
  sample: SAMPLE,
15184
15858
  brush: BRUSH2,
@@ -15198,7 +15872,7 @@ function createImageRetouchTools(controller) {
15198
15872
  type: "object",
15199
15873
  properties: {
15200
15874
  points: POINTS,
15201
- source: SOURCE,
15875
+ source: SOURCE2,
15202
15876
  aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
15203
15877
  sample: SAMPLE,
15204
15878
  brush: BRUSH2,
@@ -18981,6 +19655,7 @@ export {
18981
19655
  GRAIN_PATTERNS,
18982
19656
  GRAIN_TILE,
18983
19657
  GUIDED_FILTER_EPS,
19658
+ GridGraph,
18984
19659
  HIDE,
18985
19660
  HISTOGRAM_BINS,
18986
19661
  HUE_RANGE_LABELS,
@@ -19149,6 +19824,7 @@ export {
19149
19824
  WGSL_SHADE,
19150
19825
  WGSL_STEP,
19151
19826
  WORKING_SPACE_OPTIONS,
19827
+ WORK_SIZE,
19152
19828
  WetGpuEngine,
19153
19829
  WetMedium,
19154
19830
  WetWorkerPool,
@@ -19699,6 +20375,7 @@ export {
19699
20375
  put255,
19700
20376
  quadFrameOverlay,
19701
20377
  quickMaskPixels,
20378
+ quickSelectStroke,
19702
20379
  rand01,
19703
20380
  randSigned,
19704
20381
  rangeLightness,
@@ -19941,6 +20618,7 @@ export {
19941
20618
  withFilterMaskSurface,
19942
20619
  withImagePlayback,
19943
20620
  withNewIds,
20621
+ workImage,
19944
20622
  writeAdjustmentBlock,
19945
20623
  writeDeepRegion,
19946
20624
  writeLfx2,