@pixerate/editor 0.196.0 → 0.198.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,875 @@ 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/work.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 GAMMA = 50;
3086
+ var UNITS = 64;
3087
+ function smoothnessWeights(work, roi) {
3088
+ const { width: w, rgb } = work;
3089
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3090
+ const right = new Float64Array(rw * rh);
3091
+ const down = new Float64Array(rw * rh);
3092
+ let sumSq = 0;
3093
+ let pairs = 0;
3094
+ for (let y = 0; y < rh; y++) {
3095
+ for (let x = 0; x < rw; x++) {
3096
+ const p = y * rw + x;
3097
+ const c = ((ry + y) * w + rx + x) * 3;
3098
+ if (x + 1 < rw) {
3099
+ right[p] = (rgb[c] - rgb[c + 3]) ** 2 + (rgb[c + 1] - rgb[c + 4]) ** 2 + (rgb[c + 2] - rgb[c + 5]) ** 2;
3100
+ sumSq += right[p];
3101
+ pairs++;
3102
+ }
3103
+ if (y + 1 < rh) {
3104
+ const k = c + w * 3;
3105
+ down[p] = (rgb[c] - rgb[k]) ** 2 + (rgb[c + 1] - rgb[k + 1]) ** 2 + (rgb[c + 2] - rgb[k + 2]) ** 2;
3106
+ sumSq += down[p];
3107
+ pairs++;
3108
+ }
3109
+ }
3110
+ }
3111
+ const beta = 1 / (2 * Math.max(1e-6, sumSq / Math.max(1, pairs)));
3112
+ for (let p = 0; p < right.length; p++) {
3113
+ right[p] = Math.round(GAMMA * UNITS * Math.exp(-beta * right[p]));
3114
+ down[p] = Math.round(GAMMA * UNITS * Math.exp(-beta * down[p]));
3115
+ }
3116
+ return { right, down };
3117
+ }
3118
+ function boxBlur(src, w, h, passes) {
3119
+ let a = src;
3120
+ for (let k = 0; k < passes; k++) {
3121
+ const b = new Float32Array(a.length);
3122
+ for (let y = 0; y < h; y++) {
3123
+ for (let x = 0; x < w; x++) {
3124
+ let s = 0;
3125
+ let c = 0;
3126
+ for (let j = Math.max(0, y - 1); j <= Math.min(h - 1, y + 1); j++) {
3127
+ for (let i = Math.max(0, x - 1); i <= Math.min(w - 1, x + 1); i++) {
3128
+ s += a[j * w + i];
3129
+ c++;
3130
+ }
3131
+ }
3132
+ b[y * w + x] = s / c;
3133
+ }
3134
+ }
3135
+ a = b;
3136
+ }
3137
+ return a;
3138
+ }
3139
+ function upscaleRegion(work, pixels, roi, label, enhance) {
3140
+ const { sourceWidth: W, sourceHeight: H, scale, rgb, width: ww } = work;
3141
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3142
+ let soft = Float32Array.from(label);
3143
+ if (enhance) {
3144
+ soft = boxBlur(soft, rw, rh, 2);
3145
+ for (let i = 0; i < soft.length; i++) soft[i] = Math.min(1, Math.max(0, (soft[i] - 0.5) * 3 + 0.5));
3146
+ }
3147
+ const X0 = Math.max(0, Math.floor(rx / scale));
3148
+ const Y0 = Math.max(0, Math.floor(ry / scale));
3149
+ const X1 = Math.min(W, Math.ceil((rx + rw) / scale));
3150
+ const Y1 = Math.min(H, Math.ceil((ry + rh) / scale));
3151
+ const RW = X1 - X0;
3152
+ const RH = Y1 - Y0;
3153
+ const out = new Uint8Array(RW * RH);
3154
+ const at = (x, y) => soft[Math.min(rh - 1, Math.max(0, y)) * rw + Math.min(rw - 1, Math.max(0, x))];
3155
+ const data = pixels.data;
3156
+ const colOf = new Int32Array(RW);
3157
+ const fxOf = new Float32Array(RW);
3158
+ for (let X = 0; X < RW; X++) {
3159
+ const u = (X0 + X + 0.5) * scale - 0.5 - rx;
3160
+ colOf[X] = Math.floor(u);
3161
+ fxOf[X] = u - colOf[X];
3162
+ }
3163
+ const cellState = (i, j) => {
3164
+ const a = at(i, j), b = at(i + 1, j), c = at(i, j + 1), d = at(i + 1, j + 1);
3165
+ if (a === 1 && b === 1 && c === 1 && d === 1) return 1;
3166
+ if (a === 0 && b === 0 && c === 0 && d === 0) return 0;
3167
+ return 2;
3168
+ };
3169
+ for (let Y = 0; Y < RH; Y++) {
3170
+ const v = (Y0 + Y + 0.5) * scale - 0.5 - ry;
3171
+ const j = Math.floor(v);
3172
+ const fy = v - j;
3173
+ for (let X = 0; X < RW; X++) {
3174
+ const i = colOf[X];
3175
+ const state = cellState(i, j);
3176
+ if (state !== 2) {
3177
+ let end = X + 1;
3178
+ while (end < RW && colOf[end] === i) end++;
3179
+ if (state === 1) out.fill(255, Y * RW + X, Y * RW + end);
3180
+ X = end - 1;
3181
+ continue;
3182
+ }
3183
+ const fx = fxOf[X];
3184
+ 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;
3185
+ if (m <= 1e-3 || m >= 0.999) {
3186
+ out[Y * RW + X] = m >= 0.999 ? 255 : 0;
3187
+ continue;
3188
+ }
3189
+ let fr = 0, fg = 0, fb = 0, fn = 0, br = 0, bg = 0, bb = 0, bn = 0;
3190
+ for (let jj = j - 1; jj <= j + 2; jj++) {
3191
+ for (let ii = i - 1; ii <= i + 2; ii++) {
3192
+ if (ii < 0 || jj < 0 || ii >= rw || jj >= rh) continue;
3193
+ const k = ((ry + jj) * ww + rx + ii) * 3;
3194
+ if (soft[jj * rw + ii] >= 0.5) {
3195
+ fr += rgb[k];
3196
+ fg += rgb[k + 1];
3197
+ fb += rgb[k + 2];
3198
+ fn++;
3199
+ } else {
3200
+ br += rgb[k];
3201
+ bg += rgb[k + 1];
3202
+ bb += rgb[k + 2];
3203
+ bn++;
3204
+ }
3205
+ }
3206
+ }
3207
+ let alpha = m;
3208
+ if (fn && bn) {
3209
+ fr /= fn;
3210
+ fg /= fn;
3211
+ fb /= fn;
3212
+ br /= bn;
3213
+ bg /= bn;
3214
+ bb /= bn;
3215
+ const dr = fr - br, dg = fg - bg, db = fb - bb;
3216
+ const len2 = dr * dr + dg * dg + db * db;
3217
+ if (len2 > 400) {
3218
+ const p = ((Y0 + Y) * W + X0 + X) * 4;
3219
+ const a = data[p + 3] / 255;
3220
+ const cr = data[p] * a + 255 * (1 - a) - br;
3221
+ const cg = data[p + 1] * a + 255 * (1 - a) - bg;
3222
+ const cb = data[p + 2] * a + 255 * (1 - a) - bb;
3223
+ const proj = Math.min(1, Math.max(0, (cr * dr + cg * dg + cb * db) / len2));
3224
+ alpha = Math.min(1, Math.max(0, proj * 0.75 + m * 0.25));
3225
+ } else alpha = Math.min(1, Math.max(0, (m - 0.5) * 2 + 0.5));
3226
+ }
3227
+ out[Y * RW + X] = Math.round(alpha * 255);
3228
+ }
3229
+ }
3230
+ return selectionFromRegion(W, H, { x: X0, y: Y0, width: RW, height: RH }, out);
3231
+ }
3232
+
3233
+ // src/image-editor/selection/segment/quick.ts
3234
+ var BINS = 16;
3235
+ 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);
3236
+ function costTable(hist, total) {
3237
+ let a = hist;
3238
+ for (const stride of [1, BINS, BINS * BINS]) {
3239
+ const b = new Float64Array(a.length);
3240
+ for (let i = 0; i < a.length; i++) {
3241
+ const pos = Math.floor(i / stride) % BINS;
3242
+ const lo = pos > 0 ? a[i - stride] : a[i];
3243
+ const hi = pos < BINS - 1 ? a[i + stride] : a[i];
3244
+ b[i] = (lo + 2 * a[i] + hi) / 4;
3245
+ }
3246
+ a = b;
3247
+ }
3248
+ const out = new Float64Array(a.length);
3249
+ const floor = 1e-4;
3250
+ const denom = Math.max(1, total) * (1 + floor * a.length);
3251
+ for (let i = 0; i < a.length; i++) out[i] = -Math.log((a[i] + floor * Math.max(1, total)) / denom);
3252
+ return out;
3253
+ }
3254
+ function strokePixels(points, r, w, h) {
3255
+ const mask = new Uint8Array(w * h);
3256
+ const list = [];
3257
+ const add2 = (g) => {
3258
+ if (!mask[g]) {
3259
+ mask[g] = 1;
3260
+ list.push(g);
3261
+ }
3262
+ };
3263
+ const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
3264
+ for (const [a, b] of segs) {
3265
+ const x0 = Math.max(0, Math.floor(Math.min(a.x, b.x) - r));
3266
+ const x1 = Math.min(w - 1, Math.ceil(Math.max(a.x, b.x) + r));
3267
+ const y0 = Math.max(0, Math.floor(Math.min(a.y, b.y) - r));
3268
+ const y1 = Math.min(h - 1, Math.ceil(Math.max(a.y, b.y) + r));
3269
+ const dx = b.x - a.x;
3270
+ const dy = b.y - a.y;
3271
+ const len2 = dx * dx + dy * dy;
3272
+ for (let y = y0; y <= y1; y++) {
3273
+ for (let x = x0; x <= x1; x++) {
3274
+ const px2 = x + 0.5 - a.x;
3275
+ const py = y + 0.5 - a.y;
3276
+ const t = len2 > 0 ? Math.min(1, Math.max(0, (px2 * dx + py * dy) / len2)) : 0;
3277
+ if ((px2 - t * dx) ** 2 + (py - t * dy) ** 2 <= r * r) add2(y * w + x);
3278
+ }
3279
+ }
3280
+ }
3281
+ if (!list.length) {
3282
+ 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))));
3283
+ }
3284
+ return { mask, list };
3285
+ }
3286
+ function segmentStroke(work, stroke, grown, start) {
3287
+ const { width: w, height: h, rgb } = work;
3288
+ let roi = start;
3289
+ for (; ; ) {
3290
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3291
+ const n = rw * rh;
3292
+ const fgHist = new Float64Array(BINS ** 3);
3293
+ const bgHist = new Float64Array(BINS ** 3);
3294
+ let fgN = 0;
3295
+ let bgN = 0;
3296
+ const atImageEdge = { l: rx === 0, t: ry === 0, r: rx + rw === w, b: ry + rh === h };
3297
+ for (let y = 0; y < rh; y++) {
3298
+ for (let x = 0; x < rw; x++) {
3299
+ const g = (ry + y) * w + (rx + x);
3300
+ const bin = binOf(rgb, g);
3301
+ bgHist[bin]++;
3302
+ bgN++;
3303
+ if (stroke.mask[g] || grown?.[g]) {
3304
+ fgHist[bin]++;
3305
+ fgN++;
3306
+ }
3307
+ }
3308
+ }
3309
+ const fgCost = costTable(fgHist, fgN);
3310
+ const bgCost = costTable(bgHist, bgN);
3311
+ const { right, down } = smoothnessWeights(work, roi);
3312
+ const graph = new GridGraph(rw, rh);
3313
+ const HARD = 1e9;
3314
+ for (let y = 0; y < rh; y++) {
3315
+ for (let x = 0; x < rw; x++) {
3316
+ const p = y * rw + x;
3317
+ const g = (ry + y) * w + rx + x;
3318
+ if (stroke.mask[g]) graph.addTerminal(p, HARD, 0);
3319
+ else {
3320
+ const bin = binOf(rgb, g);
3321
+ graph.addTerminal(p, Math.round(bgCost[bin] * UNITS), Math.round(fgCost[bin] * UNITS));
3322
+ }
3323
+ if (x + 1 < rw) graph.setEdge(p, 0, right[p]);
3324
+ if (y + 1 < rh) graph.setEdge(p, 1, down[p]);
3325
+ }
3326
+ }
3327
+ graph.maxflow();
3328
+ const label = new Uint8Array(n);
3329
+ const stack = [];
3330
+ for (const g of stroke.list) {
3331
+ const x = g % w - rx;
3332
+ const y = Math.floor(g / w) - ry;
3333
+ if (x < 0 || y < 0 || x >= rw || y >= rh) continue;
3334
+ const p = y * rw + x;
3335
+ if (!label[p]) {
3336
+ label[p] = 1;
3337
+ stack.push(p);
3338
+ }
3339
+ }
3340
+ while (stack.length) {
3341
+ const p = stack.pop();
3342
+ const x = p % rw;
3343
+ for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
3344
+ if (q >= 0 && !label[q] && graph.inSource(q)) {
3345
+ label[q] = 1;
3346
+ stack.push(q);
3347
+ }
3348
+ }
3349
+ }
3350
+ let touches = false;
3351
+ for (let y = 0; y < rh; y++) {
3352
+ for (let x = 0; x < rw; x++) {
3353
+ const p = y * rw + x;
3354
+ if (!label[p]) continue;
3355
+ if (!atImageEdge.l && x === 0 || !atImageEdge.t && y === 0 || !atImageEdge.r && x === rw - 1 || !atImageEdge.b && y === rh - 1) touches = true;
3356
+ }
3357
+ }
3358
+ if (!touches || atImageEdge.l && atImageEdge.t && atImageEdge.r && atImageEdge.b) return { roi, label };
3359
+ const nx = Math.max(0, rx - Math.ceil(rw / 2));
3360
+ const ny = Math.max(0, ry - Math.ceil(rh / 2));
3361
+ 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 };
3362
+ if (roi.width * roi.height * 2.5 > w * h) roi = { x: 0, y: 0, width: w, height: h };
3363
+ }
3364
+ }
3365
+ function quickSelectStroke(pixels, stroke, options = {}) {
3366
+ if (!stroke.length) throw new Error("A stroke needs at least one point.");
3367
+ const work = workImage(pixels);
3368
+ const { width: w, height: h, scale } = work;
3369
+ const r = Math.max(0.5, (options.size ?? 30) / 2 * scale);
3370
+ const pts = stroke.map((p) => ({ x: p.x * scale, y: p.y * scale }));
3371
+ const seeds = strokePixels(pts, r, w, h);
3372
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
3373
+ for (const p of pts) {
3374
+ x0 = Math.min(x0, p.x);
3375
+ y0 = Math.min(y0, p.y);
3376
+ x1 = Math.max(x1, p.x);
3377
+ y1 = Math.max(y1, p.y);
3378
+ }
3379
+ const margin = Math.max(24, 4 * r);
3380
+ const rx = Math.max(0, Math.floor(x0 - margin));
3381
+ const ry = Math.max(0, Math.floor(y0 - margin));
3382
+ const roi = { x: rx, y: ry, width: Math.min(w, Math.ceil(x1 + margin)) - rx, height: Math.min(h, Math.ceil(y1 + margin)) - ry };
3383
+ let grown = null;
3384
+ const sel = options.grow ? options.selection : null;
3385
+ if (sel?.bounds) {
3386
+ grown = new Uint8Array(w * h);
3387
+ const b = sel.bounds;
3388
+ for (let y = Math.max(0, Math.floor(b.y * scale)); y < Math.min(h, Math.ceil((b.y + b.height) * scale)); y++) {
3389
+ for (let x = Math.max(0, Math.floor(b.x * scale)); x < Math.min(w, Math.ceil((b.x + b.width) * scale)); x++) {
3390
+ 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;
3391
+ }
3392
+ }
3393
+ }
3394
+ const { roi: done, label } = segmentStroke(work, seeds, grown, roi);
3395
+ return upscaleRegion(work, pixels, done, label, options.enhanceEdge ?? false);
3396
+ }
3397
+
3398
+ // src/image-editor/selection/segment/grabcut.ts
3399
+ var K = 5;
3400
+ var ColorGmm = class {
3401
+ logWeight = new Float64Array(K);
3402
+ mean = new Float64Array(K * 3);
3403
+ /** Inverse covariances, row-major 3 × 3. */
3404
+ inv = new Float64Array(K * 9);
3405
+ /** ln(weight) − ½ ln((2π)³ |Σ|) per component; −∞ for an empty one. */
3406
+ logNorm = new Float64Array(K).fill(-Infinity);
3407
+ /** Fits the components to the pixels `ids` of `rgb`, each in component `comp[id]`. */
3408
+ fit(rgb, ids, comp) {
3409
+ const n = new Float64Array(K);
3410
+ const sum = new Float64Array(K * 3);
3411
+ const prod = new Float64Array(K * 9);
3412
+ for (let t = 0; t < ids.length; t++) {
3413
+ const i = ids[t];
3414
+ const k = comp[i];
3415
+ const c = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
3416
+ n[k]++;
3417
+ for (let a = 0; a < 3; a++) {
3418
+ sum[k * 3 + a] += c[a];
3419
+ for (let b = 0; b < 3; b++) prod[k * 9 + a * 3 + b] += c[a] * c[b];
3420
+ }
3421
+ }
3422
+ const total = ids.length || 1;
3423
+ for (let k = 0; k < K; k++) {
3424
+ if (!n[k]) {
3425
+ this.logNorm[k] = -Infinity;
3426
+ continue;
3427
+ }
3428
+ const m = [sum[k * 3] / n[k], sum[k * 3 + 1] / n[k], sum[k * 3 + 2] / n[k]];
3429
+ const cov = new Float64Array(9);
3430
+ for (let a = 0; a < 3; a++) {
3431
+ for (let b = 0; b < 3; b++) cov[a * 3 + b] = prod[k * 9 + a * 3 + b] / n[k] - m[a] * m[b];
3432
+ cov[a * 4] += 4;
3433
+ }
3434
+ const det = cov[0] * (cov[4] * cov[8] - cov[5] * cov[7]) - cov[1] * (cov[3] * cov[8] - cov[5] * cov[6]) + cov[2] * (cov[3] * cov[7] - cov[4] * cov[6]);
3435
+ const d = Math.max(det, 1e-6);
3436
+ const inv = [
3437
+ (cov[4] * cov[8] - cov[5] * cov[7]) / d,
3438
+ (cov[2] * cov[7] - cov[1] * cov[8]) / d,
3439
+ (cov[1] * cov[5] - cov[2] * cov[4]) / d,
3440
+ (cov[5] * cov[6] - cov[3] * cov[8]) / d,
3441
+ (cov[0] * cov[8] - cov[2] * cov[6]) / d,
3442
+ (cov[2] * cov[3] - cov[0] * cov[5]) / d,
3443
+ (cov[3] * cov[7] - cov[4] * cov[6]) / d,
3444
+ (cov[1] * cov[6] - cov[0] * cov[7]) / d,
3445
+ (cov[0] * cov[4] - cov[1] * cov[3]) / d
3446
+ ];
3447
+ this.mean.set(m, k * 3);
3448
+ this.inv.set(inv, k * 9);
3449
+ this.logWeight[k] = Math.log(n[k] / total);
3450
+ this.logNorm[k] = this.logWeight[k] - 0.5 * Math.log(Math.pow(2 * Math.PI, 3) * d);
3451
+ }
3452
+ }
3453
+ /** ln p(colour | component k), up to the shared constant. */
3454
+ componentLog(k, r, g, b) {
3455
+ const ln = this.logNorm[k];
3456
+ if (ln === -Infinity) return -Infinity;
3457
+ const dr = r - this.mean[k * 3];
3458
+ const dg = g - this.mean[k * 3 + 1];
3459
+ const db = b - this.mean[k * 3 + 2];
3460
+ const v = this.inv;
3461
+ const o = k * 9;
3462
+ const q = dr * (v[o] * dr + v[o + 1] * dg + v[o + 2] * db) + dg * (v[o + 3] * dr + v[o + 4] * dg + v[o + 5] * db) + db * (v[o + 6] * dr + v[o + 7] * dg + v[o + 8] * db);
3463
+ return ln - 0.5 * q;
3464
+ }
3465
+ /** −ln p(colour) under the mixture (large for colours the model hasn't seen). */
3466
+ cost(r, g, b) {
3467
+ let best = -Infinity;
3468
+ const logs = new Array(K);
3469
+ for (let k = 0; k < K; k++) {
3470
+ logs[k] = this.componentLog(k, r, g, b);
3471
+ if (logs[k] > best) best = logs[k];
3472
+ }
3473
+ if (best === -Infinity) return 50;
3474
+ let s = 0;
3475
+ for (let k = 0; k < K; k++) if (logs[k] > -Infinity) s += Math.exp(logs[k] - best);
3476
+ return Math.min(50, -(best + Math.log(s)));
3477
+ }
3478
+ /** The component most likely to have produced the colour. */
3479
+ nearest(r, g, b) {
3480
+ let best = 0;
3481
+ let bestLog = -Infinity;
3482
+ for (let k = 0; k < K; k++) {
3483
+ const l = this.componentLog(k, r, g, b);
3484
+ if (l > bestLog) {
3485
+ bestLog = l;
3486
+ best = k;
3487
+ }
3488
+ }
3489
+ return best;
3490
+ }
3491
+ };
3492
+ function initialComponents(rgb, ids, comp) {
3493
+ if (!ids.length) return;
3494
+ const step = Math.max(1, Math.floor(ids.length / 6e3));
3495
+ const sample = [];
3496
+ for (let t = 0; t < ids.length; t += step) sample.push(ids[t]);
3497
+ const centres = [];
3498
+ const dist2 = (i, c) => (rgb[i * 3] - c[0]) ** 2 + (rgb[i * 3 + 1] - c[1]) ** 2 + (rgb[i * 3 + 2] - c[2]) ** 2;
3499
+ centres.push([rgb[sample[0] * 3], rgb[sample[0] * 3 + 1], rgb[sample[0] * 3 + 2]]);
3500
+ const near = sample.map((i) => dist2(i, centres[0]));
3501
+ while (centres.length < K) {
3502
+ let far = 0;
3503
+ for (let t = 1; t < sample.length; t++) if (near[t] > near[far]) far = t;
3504
+ const i = sample[far];
3505
+ const c = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
3506
+ centres.push(c);
3507
+ for (let t = 0; t < sample.length; t++) near[t] = Math.min(near[t], dist2(sample[t], c));
3508
+ }
3509
+ const closest = (i) => {
3510
+ let best = 0;
3511
+ for (let k = 1; k < K; k++) if (dist2(i, centres[k]) < dist2(i, centres[best])) best = k;
3512
+ return best;
3513
+ };
3514
+ for (let round6 = 0; round6 < 6; round6++) {
3515
+ const sum = Array.from({ length: K }, () => [0, 0, 0, 0]);
3516
+ for (const i of sample) {
3517
+ const s = sum[closest(i)];
3518
+ s[0] += rgb[i * 3];
3519
+ s[1] += rgb[i * 3 + 1];
3520
+ s[2] += rgb[i * 3 + 2];
3521
+ s[3]++;
3522
+ }
3523
+ for (let k = 0; k < K; k++) if (sum[k][3]) centres[k] = [sum[k][0] / sum[k][3], sum[k][1] / sum[k][3], sum[k][2] / sum[k][3]];
3524
+ }
3525
+ for (const i of ids) comp[i] = closest(i);
3526
+ }
3527
+ function grabCutLabels(work, setup, iterations) {
3528
+ const { width: w, rgb } = work;
3529
+ const { roi, hardBg, hardFg } = setup;
3530
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3531
+ const n = rw * rh;
3532
+ const label = setup.initial.slice();
3533
+ const global = new Int32Array(n);
3534
+ for (let y = 0; y < rh; y++) for (let x = 0; x < rw; x++) global[y * rw + x] = (ry + y) * w + rx + x;
3535
+ const comp = new Uint8Array(w * work.height);
3536
+ const fg = new ColorGmm();
3537
+ const bg = new ColorGmm();
3538
+ const { right, down } = smoothnessWeights(work, roi);
3539
+ const HARD = 1e9;
3540
+ for (let iter = 0; iter < iterations; iter++) {
3541
+ const fgIds = [];
3542
+ const bgIds = [];
3543
+ for (let p = 0; p < n; p++) (label[p] ? fgIds : bgIds).push(global[p]);
3544
+ if (!fgIds.length) return label;
3545
+ if (iter === 0) {
3546
+ initialComponents(rgb, fgIds, comp);
3547
+ initialComponents(rgb, bgIds, comp);
3548
+ } else {
3549
+ for (const i of fgIds) comp[i] = fg.nearest(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
3550
+ for (const i of bgIds) comp[i] = bg.nearest(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
3551
+ }
3552
+ fg.fit(rgb, fgIds, comp);
3553
+ bg.fit(rgb, bgIds, comp);
3554
+ const graph = new GridGraph(rw, rh);
3555
+ for (let p = 0; p < n; p++) {
3556
+ if (hardBg[p]) graph.addTerminal(p, 0, HARD);
3557
+ else if (hardFg?.[p]) graph.addTerminal(p, HARD, 0);
3558
+ else {
3559
+ const i = global[p] * 3;
3560
+ graph.addTerminal(p, Math.round(bg.cost(rgb[i], rgb[i + 1], rgb[i + 2]) * UNITS), Math.round(fg.cost(rgb[i], rgb[i + 1], rgb[i + 2]) * UNITS));
3561
+ }
3562
+ if (p % rw + 1 < rw) graph.setEdge(p, 0, right[p]);
3563
+ if (p + rw < n) graph.setEdge(p, 1, down[p]);
3564
+ }
3565
+ graph.maxflow();
3566
+ let changed = 0;
3567
+ for (let p = 0; p < n; p++) {
3568
+ const v = graph.inSource(p) ? 1 : 0;
3569
+ if (v !== label[p]) changed++;
3570
+ label[p] = v;
3571
+ }
3572
+ if (changed <= n * 1e-3) break;
3573
+ }
3574
+ return label;
3575
+ }
3576
+ function keepMainPieces(label, rw, rh, seed) {
3577
+ const n = rw * rh;
3578
+ const piece = new Int32Array(n).fill(-1);
3579
+ const sizes = [];
3580
+ const stack = [];
3581
+ for (let s = 0; s < n; s++) {
3582
+ if (!label[s] || piece[s] >= 0) continue;
3583
+ const id = sizes.length;
3584
+ let size = 0;
3585
+ piece[s] = id;
3586
+ stack.push(s);
3587
+ while (stack.length) {
3588
+ const p = stack.pop();
3589
+ size++;
3590
+ const x = p % rw;
3591
+ for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
3592
+ if (q >= 0 && label[q] && piece[q] < 0) {
3593
+ piece[q] = id;
3594
+ stack.push(q);
3595
+ }
3596
+ }
3597
+ }
3598
+ sizes.push(size);
3599
+ }
3600
+ if (!sizes.length) return;
3601
+ const largest = Math.max(...sizes);
3602
+ const keep = (id) => seed !== null && piece[seed] >= 0 ? id === piece[seed] : sizes[id] * 5 >= largest;
3603
+ for (let p = 0; p < n; p++) if (label[p] && !keep(piece[p])) label[p] = 0;
3604
+ }
3605
+ function selectObjectIn(pixels, target, options = {}) {
3606
+ const work = workImage(pixels);
3607
+ const { width: w, height: h, scale } = work;
3608
+ const iterations = Math.max(1, Math.round(options.iterations ?? 5));
3609
+ const enhance = options.enhanceEdge ?? false;
3610
+ if ("rect" in target) {
3611
+ const r = target.rect;
3612
+ const bx0 = Math.max(0, Math.floor(r.x * scale));
3613
+ const by0 = Math.max(0, Math.floor(r.y * scale));
3614
+ const bx1 = Math.min(w, Math.ceil((r.x + r.width) * scale));
3615
+ const by1 = Math.min(h, Math.ceil((r.y + r.height) * scale));
3616
+ if (bx1 - bx0 < 2 || by1 - by0 < 2) return createSelectionMask(pixels.width, pixels.height);
3617
+ const m = Math.max(8, Math.round(0.25 * Math.max(bx1 - bx0, by1 - by0)));
3618
+ const rx = Math.max(0, bx0 - m);
3619
+ const ry = Math.max(0, by0 - m);
3620
+ const roi = { x: rx, y: ry, width: Math.min(w, bx1 + m) - rx, height: Math.min(h, by1 + m) - ry };
3621
+ const n = roi.width * roi.height;
3622
+ const initial = new Uint8Array(n);
3623
+ const hardBg = new Uint8Array(n);
3624
+ for (let y = 0; y < roi.height; y++) {
3625
+ for (let x = 0; x < roi.width; x++) {
3626
+ const gx = rx + x;
3627
+ const gy = ry + y;
3628
+ const inside = gx >= bx0 && gx < bx1 && gy >= by0 && gy < by1;
3629
+ initial[y * roi.width + x] = inside ? 1 : 0;
3630
+ hardBg[y * roi.width + x] = inside ? 0 : 1;
3631
+ }
3632
+ }
3633
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg: null }, iterations);
3634
+ keepMainPieces(label, roi.width, roi.height, null);
3635
+ return upscaleRegion(work, pixels, roi, label, enhance);
3636
+ }
3637
+ const px2 = Math.min(w - 1, Math.max(0, Math.floor(target.point.x * scale)));
3638
+ const py = Math.min(h - 1, Math.max(0, Math.floor(target.point.y * scale)));
3639
+ let half = Math.max(24, Math.round(0.3 * Math.max(w, h)));
3640
+ for (; ; ) {
3641
+ const rx = Math.max(0, px2 - half);
3642
+ const ry = Math.max(0, py - half);
3643
+ const roi = { x: rx, y: ry, width: Math.min(w, px2 + half + 1) - rx, height: Math.min(h, py + half + 1) - ry };
3644
+ const whole = roi.width === w && roi.height === h;
3645
+ const band = Math.max(4, Math.round(0.15 * half));
3646
+ const n = roi.width * roi.height;
3647
+ const initial = new Uint8Array(n);
3648
+ const hardBg = new Uint8Array(n);
3649
+ const hardFg = new Uint8Array(n);
3650
+ const inBand = (x, y) => rx > 0 && x < band || ry > 0 && y < band || rx + roi.width < w && x >= roi.width - band || ry + roi.height < h && y >= roi.height - band;
3651
+ for (let y = 0; y < roi.height; y++) {
3652
+ for (let x = 0; x < roi.width; x++) {
3653
+ const p = y * roi.width + x;
3654
+ if (inBand(x, y)) hardBg[p] = 1;
3655
+ else initial[p] = 1;
3656
+ if (Math.hypot(rx + x - px2, ry + y - py) <= 1.5) hardFg[p] = 1;
3657
+ }
3658
+ }
3659
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, iterations);
3660
+ keepMainPieces(label, roi.width, roi.height, (py - ry) * roi.width + (px2 - rx));
3661
+ let touches = false;
3662
+ for (let y = 0; y < roi.height && !touches; y++) {
3663
+ for (let x = 0; x < roi.width; x++) {
3664
+ if (label[y * roi.width + x] && (inBand(x - 1, y) || inBand(x + 1, y) || inBand(x, y - 1) || inBand(x, y + 1))) {
3665
+ touches = true;
3666
+ break;
3667
+ }
3668
+ }
3669
+ }
3670
+ if (!touches || whole) return upscaleRegion(work, pixels, roi, label, enhance);
3671
+ half = Math.round(half * 1.6);
3672
+ }
3673
+ }
3674
+
2806
3675
  // src/image-editor/layers/shape-type-ui.ts
2807
3676
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
2808
3677
  kind: "rect",
@@ -8747,6 +9616,53 @@ var ImageEditorController = class {
8747
9616
  const pixels = await this.requirePixels(options);
8748
9617
  return snapPolygonToEdges(pixels, points, options);
8749
9618
  }
9619
+ /**
9620
+ * Quick Selection: each stroke (the brush's path in natural pixels) grows into the region it is
9621
+ * painted on, out to that region's edges, and is combined with the selection by `mode`; with
9622
+ * `replace`, the first stroke replaces it and the rest add, as the brush does. All strokes are
9623
+ * one undo step. Resolves to the resulting selection.
9624
+ */
9625
+ async quickSelect(strokes, options = {}) {
9626
+ if (!strokes.length || strokes.some((s) => !s.length)) throw new Error("Pass at least one stroke, each with at least one point.");
9627
+ const { width, height } = this.naturalSize;
9628
+ for (const s of strokes) {
9629
+ if (!s.some((p) => p.x >= 0 && p.y >= 0 && p.x < width && p.y < height)) {
9630
+ throw new Error(`A stroke lies entirely outside the image (0..${width - 1}, 0..${height - 1}); paint over the area to select.`);
9631
+ }
9632
+ }
9633
+ const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
9634
+ const mode = options.mode ?? "replace";
9635
+ let sel = this.state.selection ?? null;
9636
+ strokes.forEach((stroke, i) => {
9637
+ const m = mode === "replace" && i > 0 ? "add" : mode;
9638
+ const region = quickSelectStroke(pixels, stroke, { size: options.size, selection: sel, grow: m === "add", enhanceEdge: options.enhanceEdge });
9639
+ sel = combineSelections(sel, region, m);
9640
+ });
9641
+ this.setSelection(sel, "replace");
9642
+ return this.state.selection ?? null;
9643
+ }
9644
+ /**
9645
+ * Object Selection: the object inside `target.rect` (natural pixels; what is outside the box is
9646
+ * background) or under `target.point`, found by GrabCut (colour models of object and background
9647
+ * plus a graph cut along edges), combined with the selection by `mode` as one undo step.
9648
+ * Resolves to the resulting selection.
9649
+ */
9650
+ async selectObject(target, options = {}) {
9651
+ const { width, height } = this.naturalSize;
9652
+ if ("rect" in target) {
9653
+ const r = target.rect;
9654
+ if (!(r.width > 0 && r.height > 0)) throw new Error("The box needs a positive width and height.");
9655
+ if (r.x >= width || r.y >= height || r.x + r.width <= 0 || r.y + r.height <= 0) {
9656
+ throw new Error(`The box lies outside the image (0..${width}, 0..${height}); draw it around the object.`);
9657
+ }
9658
+ } else if (!(target.point.x >= 0 && target.point.y >= 0 && target.point.x < width && target.point.y < height)) {
9659
+ throw new Error(`(${target.point.x}, ${target.point.y}) is outside the image (0..${width - 1}, 0..${height - 1}); click on the object.`);
9660
+ }
9661
+ const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
9662
+ const region = selectObjectIn(pixels, target, { enhanceEdge: options.enhanceEdge });
9663
+ this.setSelection(region, options.mode ?? "replace");
9664
+ return this.state.selection ?? null;
9665
+ }
8750
9666
  /**
8751
9667
  * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
8752
9668
  * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)
@@ -11634,7 +12550,7 @@ var DEPTH_MASK_CONTROLS = [
11634
12550
  // src/image-editor/playback.ts
11635
12551
  var IMAGE_PLAYBACK_KIND = "layer";
11636
12552
  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)$/;
12553
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|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
12554
  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
12555
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
11640
12556
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -11756,6 +12672,8 @@ var SELECTION_TOOLS = [
11756
12672
  { id: "lasso", label: "Lasso" },
11757
12673
  { id: "polygon", label: "Polygonal Lasso" },
11758
12674
  { id: "magnetic", label: "Magnetic Lasso" },
12675
+ { id: "quick", label: "Quick Selection" },
12676
+ { id: "object", label: "Object Selection" },
11759
12677
  { id: "range", label: "Color Range" },
11760
12678
  { id: "type", label: "Type Mask" },
11761
12679
  { id: "type-vertical", label: "Vertical Type Mask" }
@@ -11793,6 +12711,11 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
11793
12711
  magneticWidth: 10,
11794
12712
  magneticContrast: 10,
11795
12713
  magneticFrequency: 57,
12714
+ quickSize: 30,
12715
+ quickSampleAll: true,
12716
+ quickEnhance: false,
12717
+ objectSampleAll: true,
12718
+ objectEnhance: false,
11796
12719
  typeText: "Type",
11797
12720
  typeSize: 72,
11798
12721
  typeFamily: "sans-serif"
@@ -12192,6 +13115,75 @@ function createImageSelectionTools(controller) {
12192
13115
  return { ...summary(controller), outlinePoints: outline.length };
12193
13116
  }
12194
13117
  };
13118
+ const quickSelectTool = {
13119
+ name: "quick_select",
13120
+ title: "Quick Selection",
13121
+ 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.",
13122
+ inputSchema: {
13123
+ type: "object",
13124
+ properties: {
13125
+ strokes: {
13126
+ type: "array",
13127
+ minItems: 1,
13128
+ items: { type: "array", minItems: 1, items: pointSchema, description: "One stroke: the brush path, in order (one point is a dab)." },
13129
+ description: "Brush strokes in natural image pixels, applied in order."
13130
+ },
13131
+ 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." },
13132
+ mode: modeSchema,
13133
+ sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
13134
+ enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false). Use it before masking a smooth-edged subject." }
13135
+ },
13136
+ required: ["strokes"],
13137
+ additionalProperties: false
13138
+ },
13139
+ annotations: writeAnnotations("Quick Selection"),
13140
+ handler: async (args) => {
13141
+ 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) })) : []) : [];
13142
+ if (!strokes.length || strokes.some((s) => !s.length || !s.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)))) {
13143
+ throw new Error("strokes needs at least one stroke of { x, y } points in natural image pixels.");
13144
+ }
13145
+ await controller.quickSelect(strokes, {
13146
+ size: args.size === void 0 ? void 0 : num2(args.size, 30),
13147
+ mode: parseSelectionMode(args.mode),
13148
+ sampleAllLayers: args.sampleAllLayers,
13149
+ enhanceEdge: args.enhanceEdge
13150
+ });
13151
+ return summary(controller);
13152
+ }
13153
+ };
13154
+ const selectObjectTool = {
13155
+ name: "select_object",
13156
+ title: "Object Selection",
13157
+ description: "Object Selection: selects the object inside rect (a box drawn loosely around it, natural image pixels; what lies outside the box is treated as background) or the object under point, found from the colours of object and background and the edges between them (GrabCut). Combined with the selection by mode. A box is more reliable than a point: make it a little larger than the object on every side. Check with get_selection and capture_view, then fix with quick_select strokes or select_object with mode add / subtract. One undoable step. Returns the selection.",
13158
+ inputSchema: {
13159
+ type: "object",
13160
+ properties: {
13161
+ rect: {
13162
+ type: "object",
13163
+ properties: rectProperties,
13164
+ required: ["x", "y", "width", "height"],
13165
+ additionalProperties: false,
13166
+ description: "A box around the object (x, y, width, height in natural image pixels). Pass this or point."
13167
+ },
13168
+ point: { ...pointSchema, description: "A point on the object (natural image pixels). Pass this or rect." },
13169
+ mode: modeSchema,
13170
+ sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
13171
+ enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false)." }
13172
+ },
13173
+ additionalProperties: false
13174
+ },
13175
+ annotations: writeAnnotations("Object Selection"),
13176
+ handler: async (args) => {
13177
+ const rect = args.rect;
13178
+ const point = args.point;
13179
+ if (!rect && !point) throw new Error("Pass rect (a box around the object) or point (a point on it).");
13180
+ if (rect && point) throw new Error("Pass rect or point, not both.");
13181
+ const options = { mode: parseSelectionMode(args.mode), sampleAllLayers: args.sampleAllLayers, enhanceEdge: args.enhanceEdge };
13182
+ if (rect) await controller.selectObject({ rect: { x: num2(rect.x, NaN), y: num2(rect.y, NaN), width: num2(rect.width, NaN), height: num2(rect.height, NaN) } }, options);
13183
+ else await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
13184
+ return summary(controller);
13185
+ }
13186
+ };
12195
13187
  const magicWandTool = {
12196
13188
  name: "magic_wand",
12197
13189
  title: "Magic wand",
@@ -12744,6 +13736,8 @@ function createImageSelectionTools(controller) {
12744
13736
  selectRect,
12745
13737
  selectEllipse,
12746
13738
  selectPolygonTool,
13739
+ quickSelectTool,
13740
+ selectObjectTool,
12747
13741
  lineTool,
12748
13742
  magicWandTool,
12749
13743
  colorRangeTool,
@@ -14131,9 +15125,14 @@ var SelectionGesture = class {
14131
15125
  closeRadius = 8;
14132
15126
  /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
14133
15127
  error = null;
15128
+ pending = Promise.resolve();
14134
15129
  get active() {
14135
15130
  return this.start !== null || this.polygon !== null || this.magnetic !== null;
14136
15131
  }
15132
+ /** Resolves once a Quick Selection stroke or an Object Selection has been applied. */
15133
+ get settled() {
15134
+ return this.pending;
15135
+ }
14137
15136
  /** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
14138
15137
  get magneticAnchors() {
14139
15138
  const m = this.magnetic;
@@ -14145,7 +15144,7 @@ var SelectionGesture = class {
14145
15144
  if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
14146
15145
  if (!this.start || !this.current) return null;
14147
15146
  const { tool } = this.getOptions();
14148
- if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
15147
+ if (tool === "lasso" || tool === "quick") return this.points.length > 1 ? this.points.slice() : null;
14149
15148
  const rect = normalizeRect({
14150
15149
  x: this.start.x,
14151
15150
  y: this.start.y,
@@ -14279,7 +15278,8 @@ var SelectionGesture = class {
14279
15278
  }
14280
15279
  if (!this.start) return false;
14281
15280
  this.current = point;
14282
- if (this.getOptions().tool === "lasso") {
15281
+ const drawn = this.getOptions().tool;
15282
+ if (drawn === "lasso" || drawn === "quick") {
14283
15283
  const last = this.points[this.points.length - 1];
14284
15284
  if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
14285
15285
  this.points.push(point);
@@ -14299,6 +15299,39 @@ var SelectionGesture = class {
14299
15299
  if (point) this.move(point);
14300
15300
  const options = this.getOptions();
14301
15301
  const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
15302
+ if (options.tool === "object") {
15303
+ const rect2 = normalizeRect({ x: this.start.x, y: this.start.y, width: this.current.x - this.start.x, height: this.current.y - this.start.y });
15304
+ this.start = null;
15305
+ this.current = null;
15306
+ this.points = [];
15307
+ const target = rect2.width >= 4 && rect2.height >= 4 ? { rect: rect2 } : { point: { x: rect2.x + rect2.width / 2, y: rect2.y + rect2.height / 2 } };
15308
+ this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
15309
+ () => void 0,
15310
+ (err) => {
15311
+ this.error = err instanceof Error ? err.message : String(err);
15312
+ }
15313
+ );
15314
+ return false;
15315
+ }
15316
+ if (options.tool === "quick") {
15317
+ const stroke = this.points;
15318
+ this.start = null;
15319
+ this.current = null;
15320
+ this.points = [];
15321
+ const strokeMode = mode === "replace" && this.controller.getSelection() ? "add" : mode;
15322
+ this.pending = this.controller.quickSelect([stroke], {
15323
+ size: options.quickSize ?? 30,
15324
+ mode: strokeMode,
15325
+ sampleAllLayers: options.quickSampleAll ?? true,
15326
+ enhanceEdge: options.quickEnhance ?? false
15327
+ }).then(
15328
+ () => void 0,
15329
+ (err) => {
15330
+ this.error = err instanceof Error ? err.message : String(err);
15331
+ }
15332
+ );
15333
+ return false;
15334
+ }
14302
15335
  const path = this.draftPath;
14303
15336
  const rect = normalizeRect({
14304
15337
  x: this.start.x,
@@ -15154,7 +16187,7 @@ function checkSource(controller, source) {
15154
16187
  }
15155
16188
  return p;
15156
16189
  }
15157
- var SOURCE = {
16190
+ var SOURCE2 = {
15158
16191
  type: "object",
15159
16192
  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
16193
  properties: { x: { type: "number", description: "Source x in px." }, y: { type: "number", description: "Source y in px." } },
@@ -15178,7 +16211,7 @@ function createImageRetouchTools(controller) {
15178
16211
  type: "object",
15179
16212
  properties: {
15180
16213
  points: POINTS,
15181
- source: SOURCE,
16214
+ source: SOURCE2,
15182
16215
  aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
15183
16216
  sample: SAMPLE,
15184
16217
  brush: BRUSH2,
@@ -15198,7 +16231,7 @@ function createImageRetouchTools(controller) {
15198
16231
  type: "object",
15199
16232
  properties: {
15200
16233
  points: POINTS,
15201
- source: SOURCE,
16234
+ source: SOURCE2,
15202
16235
  aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
15203
16236
  sample: SAMPLE,
15204
16237
  brush: BRUSH2,
@@ -18926,6 +19959,7 @@ export {
18926
19959
  COLOR_MODE_OPTIONS,
18927
19960
  COLOR_RANGE_PRESETS,
18928
19961
  COLOR_RANGE_PRESET_LABELS,
19962
+ ColorGmm,
18929
19963
  CoverageMap,
18930
19964
  D50,
18931
19965
  D65,
@@ -18978,9 +20012,11 @@ export {
18978
20012
  FREE_TRANSFORM_MODE_OPTIONS,
18979
20013
  FreeTransformSession,
18980
20014
  FreeTransformTool,
20015
+ GAMMA,
18981
20016
  GRAIN_PATTERNS,
18982
20017
  GRAIN_TILE,
18983
20018
  GUIDED_FILTER_EPS,
20019
+ GridGraph,
18984
20020
  HIDE,
18985
20021
  HISTOGRAM_BINS,
18986
20022
  HUE_RANGE_LABELS,
@@ -19135,6 +20171,7 @@ export {
19135
20171
  TransformTooLargeError,
19136
20172
  TransformedPixelSource,
19137
20173
  TypeToolGesture,
20174
+ UNITS,
19138
20175
  U_MAX,
19139
20176
  VIBRANCE_GREY,
19140
20177
  W9,
@@ -19149,6 +20186,7 @@ export {
19149
20186
  WGSL_SHADE,
19150
20187
  WGSL_STEP,
19151
20188
  WORKING_SPACE_OPTIONS,
20189
+ WORK_SIZE,
19152
20190
  WetGpuEngine,
19153
20191
  WetMedium,
19154
20192
  WetWorkerPool,
@@ -19217,6 +20255,7 @@ export {
19217
20255
  blenderDab,
19218
20256
  blurFilter,
19219
20257
  borderCoverage,
20258
+ boxBlur,
19220
20259
  boxBlurPixels,
19221
20260
  boxFrameHit,
19222
20261
  boxFrameOverlay,
@@ -19699,6 +20738,7 @@ export {
19699
20738
  put255,
19700
20739
  quadFrameOverlay,
19701
20740
  quickMaskPixels,
20741
+ quickSelectStroke,
19702
20742
  rand01,
19703
20743
  randSigned,
19704
20744
  rangeLightness,
@@ -19777,6 +20817,7 @@ export {
19777
20817
  segmentToSelection,
19778
20818
  segmentationOutlines,
19779
20819
  segmentsToSelection,
20820
+ selectObjectIn,
19780
20821
  selectSimilar,
19781
20822
  selectionArea,
19782
20823
  selectionFromCoverage,
@@ -19826,6 +20867,7 @@ export {
19826
20867
  smartObjectLayers,
19827
20868
  smoothCoverage,
19828
20869
  smoothPoints,
20870
+ smoothnessWeights,
19829
20871
  smudgeEffect,
19830
20872
  snapPolygonToEdges,
19831
20873
  snapTo45,
@@ -19912,6 +20954,7 @@ export {
19912
20954
  updateShapeLayer,
19913
20955
  updateSmartFilter,
19914
20956
  updateTypeLayer,
20957
+ upscaleRegion,
19915
20958
  userErrorMessage,
19916
20959
  uvToXy,
19917
20960
  validateEffectGradient,
@@ -19941,6 +20984,7 @@ export {
19941
20984
  withFilterMaskSurface,
19942
20985
  withImagePlayback,
19943
20986
  withNewIds,
20987
+ workImage,
19944
20988
  writeAdjustmentBlock,
19945
20989
  writeDeepRegion,
19946
20990
  writeLfx2,