@pixerate/editor 0.219.2 → 0.221.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.
Files changed (30) hide show
  1. package/dist/{chunk-S55JFWDA.cjs → chunk-R35YOUP2.cjs} +407 -161
  2. package/dist/{chunk-F7Z2MKY2.js → chunk-S6KKLFZO.js} +261 -15
  3. package/dist/image-editor/controller.d.cts +52 -5
  4. package/dist/image-editor/controller.d.ts +52 -5
  5. package/dist/image-editor/index.cjs +3065 -2536
  6. package/dist/image-editor/index.d.cts +1 -0
  7. package/dist/image-editor/index.d.ts +1 -0
  8. package/dist/image-editor/index.js +1528 -999
  9. package/dist/image-editor/layers/hybrid-mask.d.cts +31 -0
  10. package/dist/image-editor/layers/hybrid-mask.d.ts +31 -0
  11. package/dist/image-editor/layers/model.d.cts +17 -0
  12. package/dist/image-editor/layers/model.d.ts +17 -0
  13. package/dist/image-editor/layers/serialize.d.cts +4 -0
  14. package/dist/image-editor/layers/serialize.d.ts +4 -0
  15. package/dist/image-editor/layers/vector-mask.d.cts +10 -5
  16. package/dist/image-editor/layers/vector-mask.d.ts +10 -5
  17. package/dist/image-editor/path-edit.d.cts +4 -0
  18. package/dist/image-editor/path-edit.d.ts +4 -0
  19. package/dist/image-editor/selection/index.d.cts +1 -0
  20. package/dist/image-editor/selection/index.d.ts +1 -0
  21. package/dist/image-editor/selection/recipe.d.cts +89 -0
  22. package/dist/image-editor/selection/recipe.d.ts +89 -0
  23. package/dist/image-editor/types.d.cts +9 -0
  24. package/dist/image-editor/types.d.ts +9 -0
  25. package/dist/paint-editor/index.cjs +6 -6
  26. package/dist/paint-editor/index.js +1 -1
  27. package/package.json +1 -1
  28. package/skills/pixerate-image-editor/SKILL.md +7 -11
  29. package/skills/pixerate-image-editor/references/painting.md +17 -1
  30. package/skills/pixerate-image-selection-masking/SKILL.md +3 -3
@@ -7,6 +7,7 @@ import {
7
7
  decodeSelectionRle,
8
8
  decontaminateColors,
9
9
  encodeSelectionRle,
10
+ guidedFilterColor,
10
11
  hashMaskContent,
11
12
  resizeSelection
12
13
  } from "./chunk-ZNA3IQZF.js";
@@ -94,6 +95,8 @@ import {
94
95
  spliceRuns,
95
96
  starSubpath,
96
97
  styleAt,
98
+ subpathSegment,
99
+ subpathSegmentCount,
97
100
  subpathsToBezier,
98
101
  textDecorations,
99
102
  textFragmentOutline,
@@ -106,6 +109,7 @@ import {
106
109
  bezierPathBounds,
107
110
  outlineStroke,
108
111
  pathBoolean,
112
+ segmentPoint,
109
113
  strokeOutlineRings,
110
114
  transformBezierPath
111
115
  } from "./chunk-WEIK3UYB.js";
@@ -1669,9 +1673,9 @@ function getProcess2Plan(adjustments) {
1669
1673
  const luts = [];
1670
1674
  const scale = [0, 0, 0];
1671
1675
  for (let c = 0; c < 3; c++) {
1672
- let reach = 0;
1673
- for (let k = 0; k < 3; k++) reach += Math.max(0, m[c * 3 + k]);
1674
- const headroom = Math.max(1, reach);
1676
+ let reach2 = 0;
1677
+ for (let k = 0; k < 3; k++) reach2 += Math.max(0, m[c * 3 + k]);
1678
+ const headroom = Math.max(1, reach2);
1675
1679
  luts[c] = buildToneLut(contrast, brightness, headroom);
1676
1680
  scale[c] = TONE_LUT_SIZE / headroom;
1677
1681
  }
@@ -2946,12 +2950,224 @@ function annotationOfShape(layer) {
2946
2950
  return out;
2947
2951
  }
2948
2952
 
2953
+ // src/image-editor/layers/hybrid-mask.ts
2954
+ var VECTOR_MASK_EDGE_DEFAULTS = { radius: 4, inner: 4, outer: 8 };
2955
+ var JOIN_SIGMA = 2;
2956
+ function vectorMaskSegments(components) {
2957
+ const out = [];
2958
+ components.forEach((c, ci) => c.subpaths.forEach((sp, si) => {
2959
+ for (let k = 0; k < subpathSegmentCount(sp); k++) out.push({ component: ci, subpath: si, segment: k });
2960
+ }));
2961
+ return out;
2962
+ }
2963
+ function validVectorMaskEdges(edges, components) {
2964
+ if (!edges?.length) return [];
2965
+ return edges.filter((e) => {
2966
+ const sp = components[e.component]?.subpaths[e.subpath];
2967
+ return Boolean(sp) && e.segment >= 0 && e.segment < subpathSegmentCount(sp);
2968
+ });
2969
+ }
2970
+ function reach(e) {
2971
+ return e.mode === "feather" ? 3 * (e.radius ?? VECTOR_MASK_EDGE_DEFAULTS.radius) : Math.max(e.inner ?? VECTOR_MASK_EDGE_DEFAULTS.inner, e.outer ?? VECTOR_MASK_EDGE_DEFAULTS.outer);
2972
+ }
2973
+ function hybridVectorMaskCoverage(vm, hard, width, height, guide) {
2974
+ const edges = validVectorMaskEdges(vm.edges, vm.components).filter((e) => e.mode !== "hard");
2975
+ if (!edges.length) return hard;
2976
+ const classKey = (e) => e.mode === "feather" ? `f:${e.radius ?? VECTOR_MASK_EDGE_DEFAULTS.radius}` : `m:${e.inner ?? VECTOR_MASK_EDGE_DEFAULTS.inner}:${e.outer ?? VECTOR_MASK_EDGE_DEFAULTS.outer}`;
2977
+ const classes = [{ key: "hard", edge: null }];
2978
+ const classOfEdge = /* @__PURE__ */ new Map();
2979
+ for (const e of edges) {
2980
+ const k = classKey(e);
2981
+ if (!classes.some((c) => c.key === k)) classes.push({ key: k, edge: e });
2982
+ classOfEdge.set(`${e.component}:${e.subpath}:${e.segment}`, classes.findIndex((c) => c.key === k));
2983
+ }
2984
+ const maxReach = Math.max(...edges.map(reach)) + 3 * JOIN_SIGMA + 2;
2985
+ const seedX = [];
2986
+ const seedY = [];
2987
+ const seedClass = [];
2988
+ vm.components.forEach(
2989
+ (c, ci) => c.subpaths.forEach((sp, si) => {
2990
+ for (let k = 0; k < subpathSegmentCount(sp); k++) {
2991
+ const seg = subpathSegment(sp, k);
2992
+ let len = 0;
2993
+ for (let i = 1; i < seg.length; i++) len += Math.hypot(seg[i][0] - seg[i - 1][0], seg[i][1] - seg[i - 1][1]);
2994
+ const steps = Math.max(2, Math.ceil(len * 2));
2995
+ const cls2 = classOfEdge.get(`${ci}:${si}:${k}`) ?? 0;
2996
+ for (let s = 0; s <= steps; s++) {
2997
+ const [x, y] = segmentPoint(seg, s / steps);
2998
+ seedX.push(x);
2999
+ seedY.push(y);
3000
+ seedClass.push(cls2);
3001
+ }
3002
+ }
3003
+ })
3004
+ );
3005
+ if (!seedX.length) return hard;
3006
+ let minX = Infinity;
3007
+ let minY = Infinity;
3008
+ let maxX = -Infinity;
3009
+ let maxY = -Infinity;
3010
+ for (let i = 0; i < seedX.length; i++) {
3011
+ if (seedX[i] < minX) minX = seedX[i];
3012
+ if (seedX[i] > maxX) maxX = seedX[i];
3013
+ if (seedY[i] < minY) minY = seedY[i];
3014
+ if (seedY[i] > maxY) maxY = seedY[i];
3015
+ }
3016
+ const x0 = Math.max(0, Math.floor(minX - maxReach));
3017
+ const y0 = Math.max(0, Math.floor(minY - maxReach));
3018
+ const x1 = Math.min(width, Math.ceil(maxX + maxReach) + 1);
3019
+ const y1 = Math.min(height, Math.ceil(maxY + maxReach) + 1);
3020
+ const w = x1 - x0;
3021
+ const h = y1 - y0;
3022
+ if (w < 1 || h < 1) return hard;
3023
+ const n = w * h;
3024
+ const dist = new Float32Array(n).fill(Infinity);
3025
+ const cls = new Int16Array(n);
3026
+ for (let i = 0; i < seedX.length; i++) {
3027
+ const x = Math.round(seedX[i]) - x0;
3028
+ const y = Math.round(seedY[i]) - y0;
3029
+ if (x < 0 || y < 0 || x >= w || y >= h) continue;
3030
+ const k = y * w + x;
3031
+ const d = Math.hypot(seedX[i] - (x + x0), seedY[i] - (y + y0));
3032
+ if (d < dist[k]) {
3033
+ dist[k] = d;
3034
+ cls[k] = seedClass[i];
3035
+ }
3036
+ }
3037
+ const D = Math.SQRT2;
3038
+ const relax = (k, j, step) => {
3039
+ const d = dist[j] + step;
3040
+ if (d < dist[k]) {
3041
+ dist[k] = d;
3042
+ cls[k] = cls[j];
3043
+ }
3044
+ };
3045
+ for (let y = 0; y < h; y++) {
3046
+ for (let x = 0; x < w; x++) {
3047
+ const k = y * w + x;
3048
+ if (x > 0) relax(k, k - 1, 1);
3049
+ if (y > 0) {
3050
+ relax(k, k - w, 1);
3051
+ if (x > 0) relax(k, k - w - 1, D);
3052
+ if (x < w - 1) relax(k, k - w + 1, D);
3053
+ }
3054
+ }
3055
+ }
3056
+ for (let y = h - 1; y >= 0; y--) {
3057
+ for (let x = w - 1; x >= 0; x--) {
3058
+ const k = y * w + x;
3059
+ if (x < w - 1) relax(k, k + 1, 1);
3060
+ if (y < h - 1) {
3061
+ relax(k, k + w, 1);
3062
+ if (x < w - 1) relax(k, k + w + 1, D);
3063
+ if (x > 0) relax(k, k + w - 1, D);
3064
+ }
3065
+ }
3066
+ }
3067
+ const out = Float32Array.from(hard);
3068
+ const pad = maxReach + 3 * JOIN_SIGMA + 2;
3069
+ const T = 128;
3070
+ for (let ty = 0; ty < h; ty += T) {
3071
+ for (let tx = 0; tx < w; tx += T) {
3072
+ const tw = Math.min(T, w - tx);
3073
+ const th = Math.min(T, h - ty);
3074
+ let active = false;
3075
+ for (let y = ty; y < ty + th && !active; y++) for (let x = tx; x < tx + tw; x++) if (dist[y * w + x] <= maxReach) {
3076
+ active = true;
3077
+ break;
3078
+ }
3079
+ if (!active) continue;
3080
+ const px0 = Math.max(0, tx - pad);
3081
+ const py0 = Math.max(0, ty - pad);
3082
+ const px1 = Math.min(w, tx + tw + pad);
3083
+ const py1 = Math.min(h, ty + th + pad);
3084
+ const result = solveTile(px0, py0, px1 - px0, py1 - py0);
3085
+ for (let y = ty; y < ty + th; y++) {
3086
+ for (let x = tx; x < tx + tw; x++) {
3087
+ const k = y * w + x;
3088
+ if (dist[k] > maxReach + 3 * JOIN_SIGMA) continue;
3089
+ const v = result[(y - py0) * (px1 - px0) + (x - px0)];
3090
+ if (!Number.isNaN(v)) out[(y + y0) * width + x + x0] = v;
3091
+ }
3092
+ }
3093
+ }
3094
+ }
3095
+ return out;
3096
+ function solveTile(bx, by, bw, bh) {
3097
+ const m = bw * bh;
3098
+ const at = (x, y) => (by + y) * w + bx + x;
3099
+ const Ht = new Float32Array(m);
3100
+ for (let y = 0; y < bh; y++) for (let x = 0; x < bw; x++) Ht[y * bw + x] = hard[(by + y + y0) * width + bx + x + x0];
3101
+ const total = new Float32Array(m);
3102
+ const blended = new Float32Array(m);
3103
+ let guidePlanes = null;
3104
+ const planes = () => {
3105
+ if (guidePlanes) return guidePlanes;
3106
+ const p = [new Float32Array(m), new Float32Array(m), new Float32Array(m)];
3107
+ for (let y = 0; y < bh; y++) {
3108
+ for (let x = 0; x < bw; x++) {
3109
+ const s = ((by + y + y0) * width + bx + x + x0) * 4;
3110
+ const a = guide[s + 3] / 255;
3111
+ for (let c = 0; c < 3; c++) p[c][y * bw + x] = guide[s + c] / 255 * a;
3112
+ }
3113
+ }
3114
+ return guidePlanes = p;
3115
+ };
3116
+ for (let c = 0; c < classes.length; c++) {
3117
+ let weight = new Float32Array(m);
3118
+ let any = false;
3119
+ for (let y = 0; y < bh; y++) {
3120
+ for (let x = 0; x < bw; x++) {
3121
+ const k = at(x, y);
3122
+ const mine = dist[k] <= maxReach ? cls[k] === c : c === 0;
3123
+ if (mine) {
3124
+ weight[y * bw + x] = 1;
3125
+ any = true;
3126
+ }
3127
+ }
3128
+ }
3129
+ if (!any) continue;
3130
+ weight = gaussianBlur(weight, bw, bh, JOIN_SIGMA, "clamp");
3131
+ const e = classes[c].edge;
3132
+ let cov;
3133
+ if (!e) cov = Ht;
3134
+ else if (e.mode === "feather" || !guide) {
3135
+ const sigma = e.mode === "feather" ? e.radius ?? VECTOR_MASK_EDGE_DEFAULTS.radius : ((e.inner ?? VECTOR_MASK_EDGE_DEFAULTS.inner) + (e.outer ?? VECTOR_MASK_EDGE_DEFAULTS.outer)) / 3;
3136
+ cov = gaussianBlur(Ht, bw, bh, sigma, "clamp");
3137
+ } else {
3138
+ const inner = e.inner ?? VECTOR_MASK_EDGE_DEFAULTS.inner;
3139
+ const outer = e.outer ?? VECTOR_MASK_EDGE_DEFAULTS.outer;
3140
+ const r = Math.max(2, Math.round((inner + outer) / 2));
3141
+ const q2 = guidedFilterColor(planes(), Ht, bw, bh, r);
3142
+ cov = new Float32Array(m);
3143
+ for (let y = 0; y < bh; y++) {
3144
+ for (let x = 0; x < bw; x++) {
3145
+ const i = y * bw + x;
3146
+ const inside = Ht[i] >= 0.5;
3147
+ const d = dist[at(x, y)];
3148
+ const unknown = inside ? d <= inner : d <= outer;
3149
+ cov[i] = unknown ? Math.min(1, Math.max(0, q2[i])) : inside ? 1 : 0;
3150
+ }
3151
+ }
3152
+ }
3153
+ for (let i = 0; i < m; i++) {
3154
+ total[i] += weight[i];
3155
+ blended[i] += weight[i] * cov[i];
3156
+ }
3157
+ }
3158
+ const res = new Float32Array(m);
3159
+ for (let i = 0; i < m; i++) res[i] = total[i] > 0 ? blended[i] / total[i] : NaN;
3160
+ return res;
3161
+ }
3162
+ }
3163
+
2949
3164
  // src/image-editor/layers/vector-mask.ts
2950
3165
  function isActiveVectorMask(vm) {
2951
3166
  return Boolean(vm && vm.enabled !== false);
2952
3167
  }
2953
- function vectorMaskCoverage(vm, width, height) {
3168
+ function vectorMaskCoverage(vm, width, height, guide = null) {
2954
3169
  let v = componentsCoverage(vm.components, { x: 0, y: 0, width, height }, vm.fillRule ?? "nonzero", vm.inverted === true);
3170
+ if (vm.edges?.length) v = hybridVectorMaskCoverage(vm, v, width, height, guide);
2955
3171
  if ((vm.feather ?? 0) > 0) v = gaussianBlur(v, width, height, vm.feather, "clamp");
2956
3172
  const density = vm.density ?? 1;
2957
3173
  if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);
@@ -2970,12 +3186,18 @@ function rasterCoverage(mask, ox, oy, width, height) {
2970
3186
  return v;
2971
3187
  }
2972
3188
  var layerOffset = (layer) => layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
2973
- function effectiveLayerMask(layer, width, height) {
3189
+ function layerGuide(layer, width, height, surfaceOf) {
3190
+ const surface = surfaceOf?.(layer) ?? layer.surface;
3191
+ if (!surface || surface.channels !== 4) return null;
3192
+ const { x: ox, y: oy } = layerOffset(layer);
3193
+ return readRegion(surface, { x: -ox, y: -oy, width, height });
3194
+ }
3195
+ function effectiveLayerMask(layer, width, height, surfaceOf) {
2974
3196
  const vm = layer.vectorMask;
2975
3197
  const raster = layer.mask;
2976
3198
  if (!isActiveVectorMask(vm) || raster?.source || width < 1 || height < 1) return raster;
2977
3199
  const { x: ox, y: oy } = layerOffset(layer);
2978
- const v = vectorMaskCoverage(vm, width, height);
3200
+ const v = vectorMaskCoverage(vm, width, height, vm.edges?.length ? layerGuide(layer, width, height, surfaceOf) : null);
2979
3201
  let outside = outsideValue(vm);
2980
3202
  if (raster && raster.enabled !== false) {
2981
3203
  const r = rasterCoverage(raster, ox, oy, width, height);
@@ -2995,17 +3217,17 @@ function hasVectorMask(layers) {
2995
3217
  }
2996
3218
  return false;
2997
3219
  }
2998
- function resolveVectorMasks(layers, width, height) {
3220
+ function resolveVectorMasks(layers, width, height, surfaceOf) {
2999
3221
  if (!hasVectorMask(layers)) return layers;
3000
- return layers.map((l) => resolveLayer(l, width, height));
3222
+ return layers.map((l) => resolveLayer(l, width, height, surfaceOf));
3001
3223
  }
3002
- function resolveLayer(layer, width, height) {
3224
+ function resolveLayer(layer, width, height, surfaceOf) {
3003
3225
  let next = layer;
3004
3226
  if (layer.type === "group" && hasVectorMask(layer.children)) {
3005
3227
  const key2 = `${width}x${height}`;
3006
3228
  const hit2 = resolved.get(layer);
3007
3229
  if (hit2?.key === key2) return hit2.layer;
3008
- next = { ...layer, children: layer.children.map((c) => resolveLayer(c, width, height)) };
3230
+ next = { ...layer, children: layer.children.map((c) => resolveLayer(c, width, height, surfaceOf)) };
3009
3231
  if (!isActiveVectorMask(layer.vectorMask)) {
3010
3232
  resolved.set(layer, { key: key2, layer: next });
3011
3233
  return next;
@@ -3015,7 +3237,7 @@ function resolveLayer(layer, width, height) {
3015
3237
  const key = `${width}x${height}:v`;
3016
3238
  const hit = resolved.get(layer);
3017
3239
  if (hit?.key === key) return hit.layer;
3018
- const mask = effectiveLayerMask(layer, width, height);
3240
+ const mask = effectiveLayerMask(layer, width, height, surfaceOf);
3019
3241
  const out = { ...next, mask };
3020
3242
  delete out.vectorMask;
3021
3243
  if (!mask) delete out.mask;
@@ -3560,6 +3782,11 @@ function filterReach(filters) {
3560
3782
  return r;
3561
3783
  }
3562
3784
  var filterSpecCache = /* @__PURE__ */ new WeakMap();
3785
+ function guideSurfaces(sources) {
3786
+ const of = sources.surfaceOf;
3787
+ if (!of) return void 0;
3788
+ return (l) => l.type === "pixel" || l.type === "smart" ? of(l) : void 0;
3789
+ }
3563
3790
  var maskIds = /* @__PURE__ */ new WeakMap();
3564
3791
  var nextMaskId = 1;
3565
3792
  var edgeSpecCache = /* @__PURE__ */ new WeakMap();
@@ -3633,13 +3860,13 @@ var LayerCompositor = class extends RasterCompositor {
3633
3860
  }
3634
3861
  // Vector masks reach the engine as part of each layer's raster mask (see `resolveVectorMasks`).
3635
3862
  composite(layers, rect, sources, options) {
3636
- return super.composite(resolveVectorMasks(layers, sources.width, sources.height), rect, sources, options);
3863
+ return super.composite(resolveVectorMasks(layers, sources.width, sources.height, guideSurfaces(sources)), rect, sources, options);
3637
3864
  }
3638
3865
  compositeTiles(layers, rect, sources, onTile, options) {
3639
- super.compositeTiles(resolveVectorMasks(layers, sources.width, sources.height), rect, sources, onTile, options);
3866
+ super.compositeTiles(resolveVectorMasks(layers, sources.width, sources.height, guideSurfaces(sources)), rect, sources, onTile, options);
3640
3867
  }
3641
3868
  tile(layers, tx, ty, sources) {
3642
- return super.tile(resolveVectorMasks(layers, sources.width, sources.height), tx, ty, sources);
3869
+ return super.tile(resolveVectorMasks(layers, sources.width, sources.height, guideSurfaces(sources)), tx, ty, sources);
3643
3870
  }
3644
3871
  };
3645
3872
  function compositeDeep(layers, rect, sources, depth) {
@@ -3652,7 +3879,7 @@ function compositeDeep(layers, rect, sources, depth) {
3652
3879
  return { reach: filterReach(filters), apply: (data, width, height, linear) => runLayerFiltersFloat(data, width, height, filters, linear) };
3653
3880
  }
3654
3881
  }, depth);
3655
- return deep.composite(resolveVectorMasks(layers, sources.width, sources.height), rect, sources);
3882
+ return deep.composite(resolveVectorMasks(layers, sources.width, sources.height, guideSurfaces(sources)), rect, sources);
3656
3883
  }
3657
3884
 
3658
3885
  // src/image-editor/layers/text.ts
@@ -5268,6 +5495,7 @@ function serializeVectorMask(vm) {
5268
5495
  if (vm.linked === false) out.linked = false;
5269
5496
  if (vm.density !== void 0 && vm.density !== 1) out.density = vm.density;
5270
5497
  if (vm.feather) out.feather = vm.feather;
5498
+ if (vm.edges?.length) out.edges = vm.edges.map((e) => ({ ...e }));
5271
5499
  return out;
5272
5500
  }
5273
5501
  function readShapeComponents(stored) {
@@ -5290,6 +5518,19 @@ function readVectorMask(r) {
5290
5518
  if (r.linked === false) vm.linked = false;
5291
5519
  if (typeof r.density === "number" && r.density !== 1) vm.density = num2(r.density, 0, 1, 1);
5292
5520
  if (typeof r.feather === "number" && r.feather > 0) vm.feather = num2(r.feather, 0, 1e3, 0);
5521
+ if (Array.isArray(r.edges)) {
5522
+ const edges = [];
5523
+ for (const raw of r.edges) {
5524
+ const e = raw;
5525
+ const int = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : -1;
5526
+ const at = { component: int(e?.component), subpath: int(e?.subpath), segment: int(e?.segment) };
5527
+ if (at.component < 0 || at.subpath < 0 || at.segment < 0) continue;
5528
+ if (e.mode === "feather") edges.push({ ...at, mode: "feather", radius: num2(e.radius, 0.5, 250, 4) });
5529
+ else if (e.mode === "matte") edges.push({ ...at, mode: "matte", inner: num2(e.inner, 0.5, 200, 4), outer: num2(e.outer, 0.5, 200, 8) });
5530
+ }
5531
+ const valid = validVectorMaskEdges(edges, vm.components);
5532
+ if (valid.length) vm.edges = valid;
5533
+ }
5293
5534
  return vm;
5294
5535
  }
5295
5536
  function serializeMask(mask, hint, ctx) {
@@ -11646,8 +11887,13 @@ export {
11646
11887
  shapeFromAnnotation,
11647
11888
  annotationBlend,
11648
11889
  annotationOfShape,
11890
+ VECTOR_MASK_EDGE_DEFAULTS,
11891
+ vectorMaskSegments,
11892
+ validVectorMaskEdges,
11893
+ hybridVectorMaskCoverage,
11649
11894
  isActiveVectorMask,
11650
11895
  vectorMaskCoverage,
11896
+ layerGuide,
11651
11897
  effectiveLayerMask,
11652
11898
  resolveVectorMasks,
11653
11899
  offsetVectorMask,
@@ -1,5 +1,6 @@
1
1
  import { AdjustmentKind, AdjustmentLayer, AdjustmentParamsMap, Annotation, CreateAnnotationPayload, AspectRatio, CropBox, DepthMaskOptions, ExportOptions, ImageAdjustments, ImageDimensions, ImageEditorFit, ImageEditorOptions, ImageEditorState, ImageEditorTool, ImageTransform, ImagePoint, SerializedImageEditorState, DocumentOpener } from './types.cjs';
2
2
  import { type InspectMaskEdgeOptions, type MaskEdgeReport } from './selection/inspect.cjs';
3
+ import { type SelectionRecipeStep } from './selection/recipe.cjs';
3
4
  import { type CompareSelectionsOptions, type SelectionDiff } from './selection/diff.cjs';
4
5
  import { type SelectionUncertainty, type SelectionUncertaintyOptions } from './selection/uncertainty.cjs';
5
6
  import { type PathToSelectionOptions, type SelectionToPathOptions } from './selection/path.cjs';
@@ -10,6 +11,7 @@ import { type SelectionJob, type SelectionJobResult, type SelectionWorkerFactory
10
11
  import type { SelectionProgress } from './selection/segment/work.cjs';
11
12
  import type { FocusAreaOptions } from './selection/segment/focus.cjs';
12
13
  import type { SkyOptions } from './selection/segment/sky.cjs';
14
+ import { type VectorMaskEdgeMode } from './layers/hybrid-mask.cjs';
13
15
  import { RenderOptions } from './renderer.cjs';
14
16
  import { type BlendMode } from './blend.cjs';
15
17
  import type { ImageHistogram } from './histogram.cjs';
@@ -23,7 +25,7 @@ import { type RefineEdit } from './selection/refine-edits.cjs';
23
25
  import { type LayerMaskProps } from './selection/layer-mask.cjs';
24
26
  import { type AlphaChannel } from './selection/channels.cjs';
25
27
  import type { AdjustmentLayerMask } from './types.cjs';
26
- import { type LayerBlendMode, type ImageLayer, type LayerFilter, type LayerTarget, type PixelLayer, type RasterMask, type SmartContents, type SmartFilter, type SmartPlacement, type LiveShape, type ShapeComponent, type ShapeFillRule, type VectorMask, type ShapeOperation, type ShapePaint, type ShapeStroke, type ParagraphStyle, type TextStyle, type TextOrientation, type TypeLayer, type ImageEffectAnimation } from './layers/model.cjs';
28
+ import { type LayerBlendMode, type ImageLayer, type LayerFilter, type LayerTarget, type PixelLayer, type RasterMask, type SmartContents, type SmartFilter, type SmartPlacement, type LiveShape, type ShapeComponent, type ShapeFillRule, type VectorMask, type VectorMaskEdge, type ShapeOperation, type ShapePaint, type ShapeStroke, type ParagraphStyle, type TextStyle, type TextOrientation, type TypeLayer, type ImageEffectAnimation } from './layers/model.cjs';
27
29
  import { type TextLayout, type TextShaper } from './layers/text.cjs';
28
30
  import type { VectorSubpath } from '../vector/types.cjs';
29
31
  import { EffectsSlot } from '../effects/helpers.cjs';
@@ -345,6 +347,10 @@ export interface VectorMaskInfo {
345
347
  live: LiveShape | null;
346
348
  subpaths: VectorSubpath[];
347
349
  }>;
350
+ /** Hybrid mask: the segments with a soft edge mode (the rest are hard). */
351
+ edges: VectorMaskEdge[];
352
+ /** How many segments the path has (`{ component, subpath, segment }` address each). */
353
+ segmentCount: number;
348
354
  }
349
355
  export interface ShapeGeometryInput {
350
356
  shape?: LiveShape | Record<string, unknown>;
@@ -740,10 +746,6 @@ export declare function migrateToLayeredState(data: Partial<SerializedImageEdito
740
746
  }, size: ImageDimensions): SerializedLayeredImageEditorState;
741
747
  export declare const DEFAULT_TRANSFORM: ImageTransform;
742
748
  export declare const DEFAULT_DEPTH_MASK: DepthMaskOptions;
743
- /**
744
- * ImageEditorController manages the complete lifecycle, canvas rendering pipeline,
745
- * tool interaction states, undo/redo history, serialization, and image export.
746
- */
747
749
  export declare class ImageEditorController {
748
750
  private state;
749
751
  private renderer;
@@ -1028,6 +1030,8 @@ export declare class ImageEditorController {
1028
1030
  /** Changes whenever the Background's source pixels can have changed. */
1029
1031
  private sourceVersion;
1030
1032
  /** The pixels a pixel layer, smart object or shape composites (the decoded source for the Background). */
1033
+ /** A layer's real pixels for hybrid vector masks' matte guide (the Background's from the source). */
1034
+ private readonly guideSurfaceOf;
1031
1035
  private surfaceOf;
1032
1036
  /**
1033
1037
  * The source image as a surface (cached): host-supplied sample pixels first, then the decoded
@@ -1237,6 +1241,23 @@ export declare class ImageEditorController {
1237
1241
  selectionFromVectorMask(id: string, mode?: SelectionMode, invert?: boolean): boolean;
1238
1242
  /** A layer's vector mask with its paths (rounded), or `null` without one. */
1239
1243
  getVectorMask(id: string): VectorMaskInfo | null;
1244
+ /**
1245
+ * Hybrid mask: a vector mask segment's edge mode. `hard` (the default) is the path's own sharp
1246
+ * edge; `feather` blurs across it by `radius` px (default 4); `matte` solves a corridor from
1247
+ * `inner` px inside to `outer` px outside (defaults 4 and 8) from the layer's colours, for hair
1248
+ * and fur. Modes blend where they meet. One undo step. Returns the vector mask's info.
1249
+ */
1250
+ setVectorMaskEdge(id: string, at: {
1251
+ component: number;
1252
+ subpath: number;
1253
+ segment: number;
1254
+ }, mode: VectorMaskEdgeMode, options?: {
1255
+ radius?: number;
1256
+ inner?: number;
1257
+ outer?: number;
1258
+ }, commitToHistory?: boolean): VectorMaskInfo;
1259
+ /** A layer's vector mask over the canvas as a selection mask, hybrid edges, feather and density included. */
1260
+ private vectorMaskSelection;
1240
1261
  /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
1241
1262
  setLayerMaskProps(id: string, props: LayerMaskProps, commitToHistory?: boolean): RasterMask;
1242
1263
  /**
@@ -1778,6 +1799,32 @@ export declare class ImageEditorController {
1778
1799
  * Returns false when nothing changed.
1779
1800
  */
1780
1801
  setSelection(mask: SelectionMask | null, mode?: SelectionMode): boolean;
1802
+ /** The recipe step of the selection command now running, taken by `setSelection`. */
1803
+ private pendingStep;
1804
+ /** Runs a selection command whose change `step` describes, so the selection's recipe records it. */
1805
+ private withRecipeStep;
1806
+ /** The selection's recipe as agents and panels read it (empty and not live when frozen or empty). */
1807
+ getSelectionRecipe(): {
1808
+ live: boolean;
1809
+ steps: Array<{
1810
+ index: number;
1811
+ } & SelectionRecipeStep>;
1812
+ };
1813
+ private requireRecipe;
1814
+ /**
1815
+ * Replaces the selection's recipe with `steps` (checked first) and the selection with what they
1816
+ * make, from nothing. One undo step; nothing changes on an error. Returns the recipe.
1817
+ */
1818
+ setSelectionRecipe(steps: readonly unknown[]): Promise<ReturnType<ImageEditorController['getSelectionRecipe']>>;
1819
+ /** Changes step `index` of the selection's recipe (fields given replace its own; `op` replaces it whole) and replays. One undo step. */
1820
+ editSelectionStep(index: number, changes: Record<string, unknown>): Promise<ReturnType<ImageEditorController['getSelectionRecipe']>>;
1821
+ /** Inserts a step at `index` (0..length; length appends) and replays. One undo step. */
1822
+ insertSelectionStep(index: number, step: unknown): Promise<ReturnType<ImageEditorController['getSelectionRecipe']>>;
1823
+ /** Removes step `index` and replays (the last step cannot go: deselect instead). One undo step. */
1824
+ removeSelectionStep(index: number): Promise<ReturnType<ImageEditorController['getSelectionRecipe']>>;
1825
+ /** Drops the selection's recipe, keeping the selection as it is (a plain bitmap). One undo step. */
1826
+ freezeSelection(): boolean;
1827
+ private recipeIndex;
1781
1828
  /**
1782
1829
  * Shows `mask` in place of the selection while a gesture runs (Quick Selection as the brush
1783
1830
  * moves; `null` shows nothing selected), or with `undefined` stops. View state only: no undo
@@ -1,5 +1,6 @@
1
1
  import { AdjustmentKind, AdjustmentLayer, AdjustmentParamsMap, Annotation, CreateAnnotationPayload, AspectRatio, CropBox, DepthMaskOptions, ExportOptions, ImageAdjustments, ImageDimensions, ImageEditorFit, ImageEditorOptions, ImageEditorState, ImageEditorTool, ImageTransform, ImagePoint, SerializedImageEditorState, DocumentOpener } from './types.js';
2
2
  import { type InspectMaskEdgeOptions, type MaskEdgeReport } from './selection/inspect.js';
3
+ import { type SelectionRecipeStep } from './selection/recipe.js';
3
4
  import { type CompareSelectionsOptions, type SelectionDiff } from './selection/diff.js';
4
5
  import { type SelectionUncertainty, type SelectionUncertaintyOptions } from './selection/uncertainty.js';
5
6
  import { type PathToSelectionOptions, type SelectionToPathOptions } from './selection/path.js';
@@ -10,6 +11,7 @@ import { type SelectionJob, type SelectionJobResult, type SelectionWorkerFactory
10
11
  import type { SelectionProgress } from './selection/segment/work.js';
11
12
  import type { FocusAreaOptions } from './selection/segment/focus.js';
12
13
  import type { SkyOptions } from './selection/segment/sky.js';
14
+ import { type VectorMaskEdgeMode } from './layers/hybrid-mask.js';
13
15
  import { RenderOptions } from './renderer.js';
14
16
  import { type BlendMode } from './blend.js';
15
17
  import type { ImageHistogram } from './histogram.js';
@@ -23,7 +25,7 @@ import { type RefineEdit } from './selection/refine-edits.js';
23
25
  import { type LayerMaskProps } from './selection/layer-mask.js';
24
26
  import { type AlphaChannel } from './selection/channels.js';
25
27
  import type { AdjustmentLayerMask } from './types.js';
26
- import { type LayerBlendMode, type ImageLayer, type LayerFilter, type LayerTarget, type PixelLayer, type RasterMask, type SmartContents, type SmartFilter, type SmartPlacement, type LiveShape, type ShapeComponent, type ShapeFillRule, type VectorMask, type ShapeOperation, type ShapePaint, type ShapeStroke, type ParagraphStyle, type TextStyle, type TextOrientation, type TypeLayer, type ImageEffectAnimation } from './layers/model.js';
28
+ import { type LayerBlendMode, type ImageLayer, type LayerFilter, type LayerTarget, type PixelLayer, type RasterMask, type SmartContents, type SmartFilter, type SmartPlacement, type LiveShape, type ShapeComponent, type ShapeFillRule, type VectorMask, type VectorMaskEdge, type ShapeOperation, type ShapePaint, type ShapeStroke, type ParagraphStyle, type TextStyle, type TextOrientation, type TypeLayer, type ImageEffectAnimation } from './layers/model.js';
27
29
  import { type TextLayout, type TextShaper } from './layers/text.js';
28
30
  import type { VectorSubpath } from '../vector/types.js';
29
31
  import { EffectsSlot } from '../effects/helpers.js';
@@ -345,6 +347,10 @@ export interface VectorMaskInfo {
345
347
  live: LiveShape | null;
346
348
  subpaths: VectorSubpath[];
347
349
  }>;
350
+ /** Hybrid mask: the segments with a soft edge mode (the rest are hard). */
351
+ edges: VectorMaskEdge[];
352
+ /** How many segments the path has (`{ component, subpath, segment }` address each). */
353
+ segmentCount: number;
348
354
  }
349
355
  export interface ShapeGeometryInput {
350
356
  shape?: LiveShape | Record<string, unknown>;
@@ -740,10 +746,6 @@ export declare function migrateToLayeredState(data: Partial<SerializedImageEdito
740
746
  }, size: ImageDimensions): SerializedLayeredImageEditorState;
741
747
  export declare const DEFAULT_TRANSFORM: ImageTransform;
742
748
  export declare const DEFAULT_DEPTH_MASK: DepthMaskOptions;
743
- /**
744
- * ImageEditorController manages the complete lifecycle, canvas rendering pipeline,
745
- * tool interaction states, undo/redo history, serialization, and image export.
746
- */
747
749
  export declare class ImageEditorController {
748
750
  private state;
749
751
  private renderer;
@@ -1028,6 +1030,8 @@ export declare class ImageEditorController {
1028
1030
  /** Changes whenever the Background's source pixels can have changed. */
1029
1031
  private sourceVersion;
1030
1032
  /** The pixels a pixel layer, smart object or shape composites (the decoded source for the Background). */
1033
+ /** A layer's real pixels for hybrid vector masks' matte guide (the Background's from the source). */
1034
+ private readonly guideSurfaceOf;
1031
1035
  private surfaceOf;
1032
1036
  /**
1033
1037
  * The source image as a surface (cached): host-supplied sample pixels first, then the decoded
@@ -1237,6 +1241,23 @@ export declare class ImageEditorController {
1237
1241
  selectionFromVectorMask(id: string, mode?: SelectionMode, invert?: boolean): boolean;
1238
1242
  /** A layer's vector mask with its paths (rounded), or `null` without one. */
1239
1243
  getVectorMask(id: string): VectorMaskInfo | null;
1244
+ /**
1245
+ * Hybrid mask: a vector mask segment's edge mode. `hard` (the default) is the path's own sharp
1246
+ * edge; `feather` blurs across it by `radius` px (default 4); `matte` solves a corridor from
1247
+ * `inner` px inside to `outer` px outside (defaults 4 and 8) from the layer's colours, for hair
1248
+ * and fur. Modes blend where they meet. One undo step. Returns the vector mask's info.
1249
+ */
1250
+ setVectorMaskEdge(id: string, at: {
1251
+ component: number;
1252
+ subpath: number;
1253
+ segment: number;
1254
+ }, mode: VectorMaskEdgeMode, options?: {
1255
+ radius?: number;
1256
+ inner?: number;
1257
+ outer?: number;
1258
+ }, commitToHistory?: boolean): VectorMaskInfo;
1259
+ /** A layer's vector mask over the canvas as a selection mask, hybrid edges, feather and density included. */
1260
+ private vectorMaskSelection;
1240
1261
  /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
1241
1262
  setLayerMaskProps(id: string, props: LayerMaskProps, commitToHistory?: boolean): RasterMask;
1242
1263
  /**
@@ -1778,6 +1799,32 @@ export declare class ImageEditorController {
1778
1799
  * Returns false when nothing changed.
1779
1800
  */
1780
1801
  setSelection(mask: SelectionMask | null, mode?: SelectionMode): boolean;
1802
+ /** The recipe step of the selection command now running, taken by `setSelection`. */
1803
+ private pendingStep;
1804
+ /** Runs a selection command whose change `step` describes, so the selection's recipe records it. */
1805
+ private withRecipeStep;
1806
+ /** The selection's recipe as agents and panels read it (empty and not live when frozen or empty). */
1807
+ getSelectionRecipe(): {
1808
+ live: boolean;
1809
+ steps: Array<{
1810
+ index: number;
1811
+ } & SelectionRecipeStep>;
1812
+ };
1813
+ private requireRecipe;
1814
+ /**
1815
+ * Replaces the selection's recipe with `steps` (checked first) and the selection with what they
1816
+ * make, from nothing. One undo step; nothing changes on an error. Returns the recipe.
1817
+ */
1818
+ setSelectionRecipe(steps: readonly unknown[]): Promise<ReturnType<ImageEditorController['getSelectionRecipe']>>;
1819
+ /** Changes step `index` of the selection's recipe (fields given replace its own; `op` replaces it whole) and replays. One undo step. */
1820
+ editSelectionStep(index: number, changes: Record<string, unknown>): Promise<ReturnType<ImageEditorController['getSelectionRecipe']>>;
1821
+ /** Inserts a step at `index` (0..length; length appends) and replays. One undo step. */
1822
+ insertSelectionStep(index: number, step: unknown): Promise<ReturnType<ImageEditorController['getSelectionRecipe']>>;
1823
+ /** Removes step `index` and replays (the last step cannot go: deselect instead). One undo step. */
1824
+ removeSelectionStep(index: number): Promise<ReturnType<ImageEditorController['getSelectionRecipe']>>;
1825
+ /** Drops the selection's recipe, keeping the selection as it is (a plain bitmap). One undo step. */
1826
+ freezeSelection(): boolean;
1827
+ private recipeIndex;
1781
1828
  /**
1782
1829
  * Shows `mask` in place of the selection while a gesture runs (Quick Selection as the brush
1783
1830
  * moves; `null` shows nothing selected), or with `undefined` stops. View state only: no undo