@pixerate/editor 0.219.2 → 0.220.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.
@@ -38,6 +38,7 @@ export * from './smart-tools.cjs';
38
38
  export * from './layers/shape.cjs';
39
39
  export * from './layers/vector-mask.cjs';
40
40
  export * from './layers/edge-color.cjs';
41
+ export * from './layers/hybrid-mask.cjs';
41
42
  export * from './shape-tools.cjs';
42
43
  export * from './layers/text.cjs';
43
44
  export * from './layers/text-shapers.cjs';
@@ -38,6 +38,7 @@ export * from './smart-tools.js';
38
38
  export * from './layers/shape.js';
39
39
  export * from './layers/vector-mask.js';
40
40
  export * from './layers/edge-color.js';
41
+ export * from './layers/hybrid-mask.js';
41
42
  export * from './shape-tools.js';
42
43
  export * from './layers/text.js';
43
44
  export * from './layers/text-shapers.js';
@@ -91,6 +91,7 @@ import {
91
91
  TINT_SCALE,
92
92
  TONE_LUT_SIZE,
93
93
  TRANSFORM_ANCHORS,
94
+ VECTOR_MASK_EDGE_DEFAULTS,
94
95
  VIBRANCE_GREY,
95
96
  XYZ_D50_TO_SRGB,
96
97
  activeAdjustmentLayers,
@@ -201,6 +202,7 @@ import {
201
202
  hueRangeTables,
202
203
  hueRangeWeight,
203
204
  hueSaturation,
205
+ hybridVectorMaskCoverage,
204
206
  identityCurve,
205
207
  identityLevels,
206
208
  identityTransform,
@@ -224,6 +226,7 @@ import {
224
226
  isValidWarp,
225
227
  kelvinToTemperature,
226
228
  layerEffectsOf,
229
+ layerGuide,
227
230
  layerMaskCoverage,
228
231
  layerMaskFromSelection,
229
232
  layerMaskKey,
@@ -383,8 +386,10 @@ import {
383
386
  updateTypeLayer,
384
387
  userErrorMessage,
385
388
  uvToXy,
389
+ validVectorMaskEdges,
386
390
  vectorMaskBounds,
387
391
  vectorMaskCoverage,
392
+ vectorMaskSegments,
388
393
  vectorMaskThumbnail,
389
394
  versionedDescriptorBytes,
390
395
  vibranceLinear,
@@ -400,7 +405,7 @@ import {
400
405
  writePsd,
401
406
  xyToTempTint,
402
407
  xyToUv
403
- } from "../chunk-F7Z2MKY2.js";
408
+ } from "../chunk-S6KKLFZO.js";
404
409
  import {
405
410
  parseCssColor
406
411
  } from "../chunk-3CI7FRMK.js";
@@ -1822,10 +1827,22 @@ var ImageEditorRenderer = class {
1822
1827
  else ctx.lineTo(b.x, b.y);
1823
1828
  };
1824
1829
  ctx.beginPath();
1825
- e.curves.forEach(trace2);
1830
+ e.curves.forEach((c, i) => (e.curveModes?.[i] ?? "hard") === "hard" && trace2(c));
1826
1831
  ctx.lineWidth = 1.5 * u;
1827
1832
  ctx.strokeStyle = OVERLAY_HANDLE_EDGE;
1828
1833
  ctx.stroke();
1834
+ for (const [mode, color, width, dash] of [["feather", "#f59e0b", 2, [5, 3]], ["matte", "#d946ef", 3, []]]) {
1835
+ if (!e.curveModes?.includes(mode)) continue;
1836
+ ctx.beginPath();
1837
+ e.curves.forEach((c, i) => e.curveModes[i] === mode && trace2(c));
1838
+ ctx.lineWidth = width * u;
1839
+ ctx.strokeStyle = color;
1840
+ ctx.setLineDash(dash.map((d) => d * u));
1841
+ ctx.stroke();
1842
+ ctx.setLineDash([]);
1843
+ }
1844
+ ctx.lineWidth = 1.5 * u;
1845
+ ctx.strokeStyle = OVERLAY_HANDLE_EDGE;
1829
1846
  if (e.preview) {
1830
1847
  ctx.beginPath();
1831
1848
  trace2(e.preview);
@@ -6931,6 +6948,8 @@ var ImageEditorController = class {
6931
6948
  return String(id);
6932
6949
  }
6933
6950
  /** The pixels a pixel layer, smart object or shape composites (the decoded source for the Background). */
6951
+ /** A layer's real pixels for hybrid vector masks' matte guide (the Background's from the source). */
6952
+ guideSurfaceOf = (layer) => layer.type === "pixel" || layer.type === "smart" || layer.type === "shape" || layer.type === "type" ? this.surfaceOf(layer) : void 0;
6934
6953
  surfaceOf(layer) {
6935
6954
  if (layer.type !== "pixel" || layer.source !== "document") return layer.surface;
6936
6955
  return this.sourceSurface() ?? layer.surface;
@@ -7149,7 +7168,7 @@ var ImageEditorController = class {
7149
7168
  /** Whether the layer's mask (painted and vector, with feather) has a soft band. */
7150
7169
  maskHasSoftBand(layer) {
7151
7170
  const { width, height } = this.naturalSize;
7152
- const mask = effectiveLayerMask(layer, width, height);
7171
+ const mask = effectiveLayerMask(layer, width, height, this.guideSurfaceOf);
7153
7172
  if (!mask || mask.enabled === false || mask.source) return false;
7154
7173
  const ox = layer.type === "pixel" ? layer.offset.x : 0;
7155
7174
  const oy = layer.type === "pixel" ? layer.offset.y : 0;
@@ -7523,7 +7542,11 @@ var ImageEditorController = class {
7523
7542
  this.commitLayers(layers, void 0, commitToHistory);
7524
7543
  } else {
7525
7544
  const { vm } = this.requireVectorMask(target.layerId);
7526
- this.commitLayers(mapLayer(this.state.layers, target.layerId, (l) => ({ ...l, vectorMask: { ...vm, components: next } })), void 0, commitToHistory);
7545
+ const edges = validVectorMaskEdges(vm.edges, next);
7546
+ const nextVm = { ...vm, components: next };
7547
+ if (edges.length) nextVm.edges = edges;
7548
+ else delete nextVm.edges;
7549
+ this.commitLayers(mapLayer(this.state.layers, target.layerId, (l) => ({ ...l, vectorMask: nextVm })), void 0, commitToHistory);
7527
7550
  }
7528
7551
  }
7529
7552
  /**
@@ -7618,6 +7641,9 @@ var ImageEditorController = class {
7618
7641
  setVectorMaskPath(id, path) {
7619
7642
  const { vm } = this.requireVectorMask(id);
7620
7643
  const next = { ...vm, components: this.vectorMaskComponents(path) };
7644
+ const kept = validVectorMaskEdges(vm.edges, next.components);
7645
+ if (kept.length) next.edges = kept;
7646
+ else delete next.edges;
7621
7647
  if (path.fillRule === "evenodd") next.fillRule = "evenodd";
7622
7648
  else if (path.fillRule === "nonzero") delete next.fillRule;
7623
7649
  this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: next })));
@@ -7667,7 +7693,7 @@ var ImageEditorController = class {
7667
7693
  const { width, height } = this.canvasSize;
7668
7694
  const raster = layer.mask;
7669
7695
  const plain3 = { ...layer, vectorMask: { ...layer.vectorMask, enabled: true }, mask: raster ? { surface: raster.surface, defaultValue: raster.defaultValue } : void 0 };
7670
- const baked = effectiveLayerMask(plain3, width, height);
7696
+ const baked = effectiveLayerMask(plain3, width, height, this.guideSurfaceOf);
7671
7697
  const mask = raster ? { ...raster, surface: baked.surface, defaultValue: baked.defaultValue } : baked;
7672
7698
  this.commitLayers(
7673
7699
  mapLayer(this.state.layers, id, (l) => {
@@ -7703,8 +7729,55 @@ var ImageEditorController = class {
7703
7729
  density: vm.density ?? 1,
7704
7730
  feather: vm.feather ?? 0,
7705
7731
  bounds: b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null,
7706
- components: vm.components.map((c, index) => ({ index, op: c.op, live: c.live ? JSON.parse(JSON.stringify(c.live)) : null, subpaths: roundSubpaths(c.subpaths) }))
7732
+ components: vm.components.map((c, index) => ({ index, op: c.op, live: c.live ? JSON.parse(JSON.stringify(c.live)) : null, subpaths: roundSubpaths(c.subpaths) })),
7733
+ edges: validVectorMaskEdges(vm.edges, vm.components).map((e) => ({ ...e })),
7734
+ segmentCount: vectorMaskSegments(vm.components).length
7735
+ };
7736
+ }
7737
+ /**
7738
+ * Hybrid mask: a vector mask segment's edge mode. `hard` (the default) is the path's own sharp
7739
+ * edge; `feather` blurs across it by `radius` px (default 4); `matte` solves a corridor from
7740
+ * `inner` px inside to `outer` px outside (defaults 4 and 8) from the layer's colours, for hair
7741
+ * and fur. Modes blend where they meet. One undo step. Returns the vector mask's info.
7742
+ */
7743
+ setVectorMaskEdge(id, at, mode, options = {}, commitToHistory = true) {
7744
+ const { layer, vm } = this.requireVectorMask(id);
7745
+ const name = layer.name || id;
7746
+ const sp = vm.components[at.component]?.subpaths[at.subpath];
7747
+ const count = sp ? subpathSegmentCount(sp) : 0;
7748
+ if (!sp || !Number.isInteger(at.segment) || at.segment < 0 || at.segment >= count) {
7749
+ const where = vm.components.map((c, ci) => c.subpaths.map((p, si) => `component ${ci} subpath ${si}: segments 0..${subpathSegmentCount(p) - 1}`).join("; ")).join("; ");
7750
+ throw new LayerOperationError(`The vector mask of "${name}" has no segment ${at.component}/${at.subpath}/${at.segment}; it has ${where || "no segments"} (see get_vector_mask).`);
7751
+ }
7752
+ if (mode !== "hard" && mode !== "feather" && mode !== "matte") throw new LayerOperationError(`mode must be hard, feather or matte, got "${String(mode)}".`);
7753
+ const num3 = (v, what, lo, hi) => {
7754
+ if (v === void 0) return void 0;
7755
+ if (!(typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi)) throw new LayerOperationError(`${what} must be ${lo} to ${hi} px, got ${String(v)}.`);
7756
+ return v;
7707
7757
  };
7758
+ const radius = num3(options.radius, "radius", 0.5, 250);
7759
+ const inner = num3(options.inner, "inner", 0.5, 200);
7760
+ const outer = num3(options.outer, "outer", 0.5, 200);
7761
+ if (mode !== "feather" && radius !== void 0) throw new LayerOperationError("radius is for feather edges; matte edges take inner and outer.");
7762
+ if (mode !== "matte" && (inner !== void 0 || outer !== void 0)) throw new LayerOperationError("inner and outer are for matte edges; feather edges take radius.");
7763
+ const same = (e) => e.component === at.component && e.subpath === at.subpath && e.segment === at.segment;
7764
+ const edges = validVectorMaskEdges(vm.edges, vm.components).filter((e) => !same(e));
7765
+ if (mode === "feather") edges.push({ ...at, mode, radius: radius ?? VECTOR_MASK_EDGE_DEFAULTS.radius });
7766
+ else if (mode === "matte") edges.push({ ...at, mode, inner: inner ?? VECTOR_MASK_EDGE_DEFAULTS.inner, outer: outer ?? VECTOR_MASK_EDGE_DEFAULTS.outer });
7767
+ const next = { ...vm };
7768
+ if (edges.length) next.edges = edges;
7769
+ else delete next.edges;
7770
+ this.commitLayers(mapLayer(this.state.layers, layer.id, (l) => ({ ...l, vectorMask: next })), void 0, commitToHistory);
7771
+ return this.getVectorMask(layer.id);
7772
+ }
7773
+ /** A layer's vector mask over the canvas as a selection mask, hybrid edges, feather and density included. */
7774
+ vectorMaskSelection(layer) {
7775
+ const { width, height } = this.naturalSize;
7776
+ const vm = layer.vectorMask;
7777
+ const v = vectorMaskCoverage(vm, width, height, vm.edges?.length ? layerGuide(layer, width, height, this.guideSurfaceOf) : null);
7778
+ const bytes = new Uint8Array(v.length);
7779
+ for (let i = 0; i < v.length; i++) bytes[i] = Math.round(v[i] * 255);
7780
+ return createSelectionMask(width, height, bytes);
7708
7781
  }
7709
7782
  /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
7710
7783
  setLayerMaskProps(id, props, commitToHistory = true) {
@@ -10546,9 +10619,8 @@ var ImageEditorController = class {
10546
10619
  mask = this.layerMaskCoverage(layer.id);
10547
10620
  if (!mask) throw new LayerOperationError(`Layer "${layer.name || layer.id}" has no painted mask; add one with add_layer_mask, or inspect its vector-mask.`);
10548
10621
  } else {
10549
- const { vm } = this.requireVectorMask(layer.id);
10550
- mask = pathToSelection(vm.components, width, height, { fillRule: vm.fillRule });
10551
- if (vm.inverted) mask = invertSelection(mask);
10622
+ this.requireVectorMask(layer.id);
10623
+ mask = this.vectorMaskSelection(layer);
10552
10624
  }
10553
10625
  if (!mask.bounds) throw new LayerOperationError(`The ${target} of "${layer.name || layer.id}" hides everything: there is no edge to inspect.`);
10554
10626
  } else {
@@ -10622,9 +10694,8 @@ var ImageEditorController = class {
10622
10694
  case "vector-mask": {
10623
10695
  const layer = this.getLayer(source.layerId);
10624
10696
  if (!layer) throw new LayerOperationError(`There is no layer "${source.layerId}" (see list_layers).`);
10625
- const { vm } = this.requireVectorMask(layer.id);
10626
- const mask = pathToSelection(vm.components, width, height, { fillRule: vm.fillRule });
10627
- return vm.inverted ? invertSelection(mask) : mask;
10697
+ this.requireVectorMask(layer.id);
10698
+ return this.vectorMaskSelection(layer);
10628
10699
  }
10629
10700
  default:
10630
10701
  throw new LayerOperationError(`Unknown selection source ${JSON.stringify(source)}. Use selection, history, channel, layer-mask or vector-mask.`);
@@ -11905,6 +11976,12 @@ var ImageEditorController = class {
11905
11976
  if (layer.edge?.ambient) add2("The live edge cast (set_edge_ambient) has no PSD equivalent and was left out", layer.name);
11906
11977
  const p = await prepareLayer(layer);
11907
11978
  if (p?.edge) delete p.edge;
11979
+ if (p && isActiveVectorMask(layer.vectorMask) && layer.vectorMask.edges?.length) {
11980
+ add2("Hybrid vector masks (soft edge segments) were baked into pixel masks", layer.name);
11981
+ const baked = effectiveLayerMask(layer, width, height, this.guideSurfaceOf);
11982
+ if (baked) p.mask = baked;
11983
+ delete p.vectorMask;
11984
+ }
11908
11985
  if (p?.blend === "pigment") add2("The Pigment blend mode has no PSD equivalent and was written as Normal", layer.name);
11909
11986
  return p;
11910
11987
  };
@@ -11980,7 +12057,7 @@ var ImageEditorController = class {
11980
12057
  }
11981
12058
  if (layer.filters?.some((f) => !f.enabled)) add2("Disabled layer filters were left out", layer.name);
11982
12059
  delete out.filters;
11983
- const unmixMask = hasEdgeUnmix(layer.edge) ? effectiveLayerMask(layer, width, height) : void 0;
12060
+ const unmixMask = hasEdgeUnmix(layer.edge) ? effectiveLayerMask(layer, width, height, this.guideSurfaceOf) : void 0;
11984
12061
  if (unmixMask && !unmixMask.source && unmixMask.enabled !== false) {
11985
12062
  add2("Edge decontamination and despill were baked into the pixels", layer.name);
11986
12063
  const { x: ox, y: oy } = layer.offset;
@@ -18645,6 +18722,37 @@ function createImageShapeTools(controller) {
18645
18722
  return { vectorMask: controller.getVectorMask(id) };
18646
18723
  }
18647
18724
  };
18725
+ const setVectorMaskEdge = {
18726
+ name: "set_vector_mask_edge",
18727
+ title: "Set a vector mask segment's edge",
18728
+ description: "Hybrid mask: gives one segment of a layer's vector mask its own edge, so one mask holds sharp edges and hair or fur. hard (default) is the path's sharp edge; feather blurs across it by radius px; matte solves a corridor from inner px inside the path to outer px outside it from the layer's colours (the colour-guided filter), so strands keep their detail. Modes blend where they meet. Address the segment with component, subpath and segment (the segment from anchor segment to the next; see get_vector_mask segmentCount and components). Check the result with inspect_mask_edge target vector-mask. One undo step. Returns the vector mask with its edges.",
18729
+ inputSchema: {
18730
+ type: "object",
18731
+ properties: {
18732
+ layerId: LAYER,
18733
+ component: { type: "integer", minimum: 0, description: "Component index of the path (0 for a single shape)." },
18734
+ subpath: { type: "integer", minimum: 0, description: "Subpath index in the component (0 for a single outline)." },
18735
+ segment: { type: "integer", minimum: 0, description: "Segment index in the subpath: from anchor segment to the next (the last closes back to anchor 0)." },
18736
+ mode: { type: "string", enum: ["hard", "feather", "matte"], description: "hard (sharp, the default), feather (a soft falloff) or matte (a solved corridor for hair and fur)." },
18737
+ radius: { type: "number", minimum: 0.5, maximum: 250, description: "feather only: Gaussian falloff in px across the curve (default 4)." },
18738
+ inner: { type: "number", minimum: 0.5, maximum: 200, description: "matte only: how far inside the path the unsure corridor reaches, in px (default 4)." },
18739
+ outer: { type: "number", minimum: 0.5, maximum: 200, description: "matte only: how far outside the path the unsure corridor reaches, in px (default 8; cover the longest strands)." }
18740
+ },
18741
+ required: ["layerId", "component", "subpath", "segment", "mode"],
18742
+ additionalProperties: false
18743
+ },
18744
+ annotations: { title: "Set a vector mask segment's edge", ...WRITE_IDEMPOTENT5 },
18745
+ handler: ({ layerId, component, subpath, segment, mode, radius, inner, outer }) => {
18746
+ const id = layerArg(layerId);
18747
+ const vectorMask = controller.setVectorMaskEdge(
18748
+ id,
18749
+ { component, subpath, segment },
18750
+ mode,
18751
+ { radius, inner, outer }
18752
+ );
18753
+ return { vectorMask };
18754
+ }
18755
+ };
18648
18756
  const rasterizeVectorMask = {
18649
18757
  name: "rasterize_vector_mask",
18650
18758
  title: "Rasterize a vector mask",
@@ -18787,6 +18895,7 @@ function createImageShapeTools(controller) {
18787
18895
  addVectorMask,
18788
18896
  setVectorMaskPath,
18789
18897
  setVectorMaskProps,
18898
+ setVectorMaskEdge,
18790
18899
  rasterizeVectorMask,
18791
18900
  removeVectorMask,
18792
18901
  selectionToPath,
@@ -21649,6 +21758,7 @@ var PathEditGesture = class {
21649
21758
  return;
21650
21759
  }
21651
21760
  const seg = this.segmentAt(cs, p);
21761
+ if (seg && mods.alt && mods.shift && this.target?.kind === "vector-mask") return this.cycleEdge(this.target.layerId, seg.ref);
21652
21762
  if (seg) {
21653
21763
  if (!mods.shift) this.selected.clear();
21654
21764
  this.drag = { kind: "segment", ...seg, base: cloneComponents(cs), moved: false };
@@ -21657,6 +21767,17 @@ var PathEditGesture = class {
21657
21767
  this.drag = { kind: "marquee", start: pt(p), end: pt(p), keep: mods.shift ? new Set(this.selected) : /* @__PURE__ */ new Set() };
21658
21768
  if (!mods.shift) this.selected.clear();
21659
21769
  }
21770
+ /** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
21771
+ cycleEdge(layerId, r) {
21772
+ const at = { component: r.component, subpath: r.subpath, segment: r.anchor };
21773
+ const now = this.controller.getVectorMask(layerId)?.edges.find((e) => e.component === at.component && e.subpath === at.subpath && e.segment === at.segment)?.mode ?? "hard";
21774
+ const next = now === "hard" ? "feather" : now === "feather" ? "matte" : "hard";
21775
+ try {
21776
+ this.controller.setVectorMaskEdge(layerId, at, next);
21777
+ } catch (e) {
21778
+ this.error = e instanceof Error ? e.message : String(e);
21779
+ }
21780
+ }
21660
21781
  /** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
21661
21782
  convert(cs, r) {
21662
21783
  const next = cloneComponents(cs);
@@ -21852,12 +21973,18 @@ var PathEditGesture = class {
21852
21973
  const cs = this.components();
21853
21974
  if (!cs) return null;
21854
21975
  const curves = [];
21976
+ const curveModes = [];
21977
+ const edges = this.target?.kind === "vector-mask" ? this.controller.getVectorMask(this.target.layerId)?.edges ?? [] : [];
21978
+ const modeOf = (ci, si, k) => edges.find((e) => e.component === ci && e.subpath === si && e.segment === k)?.mode ?? "hard";
21855
21979
  const anchors = [];
21856
21980
  const handles = [];
21857
21981
  const selectedComponents = new Set(this.selection.map((r) => r.component));
21858
21982
  cs.forEach(
21859
21983
  (c, ci) => c.subpaths.forEach((sp, si) => {
21860
- for (let i = 0; i < subpathSegmentCount(sp); i++) curves.push(subpathSegment(sp, i).map(([x, y]) => ({ x, y })));
21984
+ for (let i = 0; i < subpathSegmentCount(sp); i++) {
21985
+ curves.push(subpathSegment(sp, i).map(([x, y]) => ({ x, y })));
21986
+ curveModes.push(modeOf(ci, si, i));
21987
+ }
21861
21988
  sp.anchors.forEach((a, ai) => {
21862
21989
  const selected = this.selected.has(key({ component: ci, subpath: si, anchor: ai }));
21863
21990
  if (tool === "path-select" && !selectedComponents.has(ci)) return;
@@ -21875,7 +22002,7 @@ var PathEditGesture = class {
21875
22002
  if (last) preview = last.out ? [pt(last), pt(last.out), this.pointer, this.pointer] : [pt(last), this.pointer];
21876
22003
  }
21877
22004
  const marquee = this.drag?.kind === "marquee" ? rect(this.drag.start, this.drag.end) : null;
21878
- return { curves, anchors, handles, marquee, preview };
22005
+ return { curves, ...edges.length ? { curveModes } : {}, anchors, handles, marquee, preview };
21879
22006
  }
21880
22007
  };
21881
22008
  function rect(a, b) {
@@ -22453,6 +22580,7 @@ export {
22453
22580
  TypeToolGesture,
22454
22581
  UNITS,
22455
22582
  U_MAX,
22583
+ VECTOR_MASK_EDGE_DEFAULTS,
22456
22584
  VIBRANCE_GREY,
22457
22585
  W9,
22458
22586
  WARP_GRID_OPTIONS,
@@ -22801,6 +22929,7 @@ export {
22801
22929
  hueRangeTables,
22802
22930
  hueRangeWeight,
22803
22931
  hueSaturation,
22932
+ hybridVectorMaskCoverage,
22804
22933
  identityCurve,
22805
22934
  identityLevels,
22806
22935
  identityTransform,
@@ -22871,6 +23000,7 @@ export {
22871
23000
  layerEffectsBadge,
22872
23001
  layerEffectsOf,
22873
23002
  layerEffectsPanel,
23003
+ layerGuide,
22874
23004
  layerHasThumbnail,
22875
23005
  layerLabel,
22876
23006
  layerMaskCoverage,
@@ -23276,11 +23406,13 @@ export {
23276
23406
  upscaleRegion,
23277
23407
  userErrorMessage,
23278
23408
  uvToXy,
23409
+ validVectorMaskEdges,
23279
23410
  validateEffectGradient,
23280
23411
  validateLayerEffect,
23281
23412
  validateLayerEffects,
23282
23413
  vectorMaskBounds,
23283
23414
  vectorMaskCoverage,
23415
+ vectorMaskSegments,
23284
23416
  vectorMaskThumbnail,
23285
23417
  vectorMaskThumbnailClick,
23286
23418
  versionedDescriptorBytes,
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Hybrid vector masks: a vector mask whose segments each have an edge mode. Hard segments are
3
+ * the path's own anti-aliased edge; feather segments blur across the curve; matte segments solve
4
+ * a corridor between two rails (inside by `inner`, outside by `outer`) from the layer's pixels
5
+ * with the colour-guided filter, so hair and fur keep their detail while the rest stays sharp.
6
+ * Where modes meet, they blend over a few pixels so there is no seam. Rendered on demand from
7
+ * the path and the pixels, and cached with the layer like any vector mask.
8
+ */
9
+ import type { ShapeComponent, VectorMask, VectorMaskEdge } from './model.cjs';
10
+ export type VectorMaskEdgeMode = 'hard' | 'feather' | 'matte';
11
+ /** Defaults for a segment switched to a soft mode. */
12
+ export declare const VECTOR_MASK_EDGE_DEFAULTS: {
13
+ readonly radius: 4;
14
+ readonly inner: 4;
15
+ readonly outer: 8;
16
+ };
17
+ /** The segments of a vector mask's path as `{ component, subpath, segment }` triples. */
18
+ export declare function vectorMaskSegments(components: readonly ShapeComponent[]): Array<{
19
+ component: number;
20
+ subpath: number;
21
+ segment: number;
22
+ }>;
23
+ /** Edges whose segment still exists in `components` (after the path was edited). */
24
+ export declare function validVectorMaskEdges(edges: readonly VectorMaskEdge[] | undefined, components: readonly ShapeComponent[]): VectorMaskEdge[];
25
+ /**
26
+ * The hybrid coverage (0..1, `width × height`) from the path's hard coverage `hard` (as
27
+ * `componentsCoverage` gives it, before the mask's own feather and density) and the soft
28
+ * `edges`. `guide` is the layer's RGBA over the canvas, for matte corridors; without one a matte
29
+ * segment is feathered instead. Returns `hard` itself when no edge is soft.
30
+ */
31
+ export declare function hybridVectorMaskCoverage(vm: Pick<VectorMask, 'components' | 'edges'>, hard: Float32Array, width: number, height: number, guide: ArrayLike<number> | null): Float32Array;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Hybrid vector masks: a vector mask whose segments each have an edge mode. Hard segments are
3
+ * the path's own anti-aliased edge; feather segments blur across the curve; matte segments solve
4
+ * a corridor between two rails (inside by `inner`, outside by `outer`) from the layer's pixels
5
+ * with the colour-guided filter, so hair and fur keep their detail while the rest stays sharp.
6
+ * Where modes meet, they blend over a few pixels so there is no seam. Rendered on demand from
7
+ * the path and the pixels, and cached with the layer like any vector mask.
8
+ */
9
+ import type { ShapeComponent, VectorMask, VectorMaskEdge } from './model.js';
10
+ export type VectorMaskEdgeMode = 'hard' | 'feather' | 'matte';
11
+ /** Defaults for a segment switched to a soft mode. */
12
+ export declare const VECTOR_MASK_EDGE_DEFAULTS: {
13
+ readonly radius: 4;
14
+ readonly inner: 4;
15
+ readonly outer: 8;
16
+ };
17
+ /** The segments of a vector mask's path as `{ component, subpath, segment }` triples. */
18
+ export declare function vectorMaskSegments(components: readonly ShapeComponent[]): Array<{
19
+ component: number;
20
+ subpath: number;
21
+ segment: number;
22
+ }>;
23
+ /** Edges whose segment still exists in `components` (after the path was edited). */
24
+ export declare function validVectorMaskEdges(edges: readonly VectorMaskEdge[] | undefined, components: readonly ShapeComponent[]): VectorMaskEdge[];
25
+ /**
26
+ * The hybrid coverage (0..1, `width × height`) from the path's hard coverage `hard` (as
27
+ * `componentsCoverage` gives it, before the mask's own feather and density) and the soft
28
+ * `edges`. `guide` is the layer's RGBA over the canvas, for matte corridors; without one a matte
29
+ * segment is feathered instead. Returns `hard` itself when no edge is soft.
30
+ */
31
+ export declare function hybridVectorMaskCoverage(vm: Pick<VectorMask, 'components' | 'edges'>, hard: Float32Array, width: number, height: number, guide: ArrayLike<number> | null): Float32Array;
@@ -51,6 +51,23 @@ export interface VectorMask {
51
51
  density?: number;
52
52
  /** Gaussian feather σ in px (default 0). */
53
53
  feather?: number;
54
+ /**
55
+ * Hybrid mask: soft edge modes per path segment (see `hybridVectorMaskCoverage`); segments not
56
+ * listed are hard. Indices follow the path's components, subpaths and segments.
57
+ */
58
+ edges?: VectorMaskEdge[];
59
+ }
60
+ /** One segment's edge mode in a hybrid vector mask. */
61
+ export interface VectorMaskEdge {
62
+ component: number;
63
+ subpath: number;
64
+ /** Segment index in the subpath (from anchor `segment` to the next). */
65
+ segment: number;
66
+ /** `feather`: a Gaussian falloff of `radius` px across the curve; `matte`: a corridor from `inner` px inside to `outer` px outside, solved from the layer's colours. */
67
+ mode: 'hard' | 'feather' | 'matte';
68
+ radius?: number;
69
+ inner?: number;
70
+ outer?: number;
54
71
  }
55
72
  /** The base sliders of the old document-level adjustments, as a filter on one layer. */
56
73
  export interface DevelopSettings {
@@ -51,6 +51,23 @@ export interface VectorMask {
51
51
  density?: number;
52
52
  /** Gaussian feather σ in px (default 0). */
53
53
  feather?: number;
54
+ /**
55
+ * Hybrid mask: soft edge modes per path segment (see `hybridVectorMaskCoverage`); segments not
56
+ * listed are hard. Indices follow the path's components, subpaths and segments.
57
+ */
58
+ edges?: VectorMaskEdge[];
59
+ }
60
+ /** One segment's edge mode in a hybrid vector mask. */
61
+ export interface VectorMaskEdge {
62
+ component: number;
63
+ subpath: number;
64
+ /** Segment index in the subpath (from anchor `segment` to the next). */
65
+ segment: number;
66
+ /** `feather`: a Gaussian falloff of `radius` px across the curve; `matte`: a corridor from `inner` px inside to `outer` px outside, solved from the layer's colours. */
67
+ mode: 'hard' | 'feather' | 'matte';
68
+ radius?: number;
69
+ inner?: number;
70
+ outer?: number;
54
71
  }
55
72
  /** The base sliders of the old document-level adjustments, as a filter on one layer. */
56
73
  export interface DevelopSettings {
@@ -57,6 +57,8 @@ export interface SerializedVectorMask {
57
57
  linked?: false;
58
58
  density?: number;
59
59
  feather?: number;
60
+ /** Hybrid mask: soft edge modes per segment. */
61
+ edges?: unknown[];
60
62
  }
61
63
  /** A layer's effects: the effect list as JSON plus the PSD block they came from (base64). */
62
64
  export interface SerializedLayerEffects {
@@ -57,6 +57,8 @@ export interface SerializedVectorMask {
57
57
  linked?: false;
58
58
  density?: number;
59
59
  feather?: number;
60
+ /** Hybrid mask: soft edge modes per segment. */
61
+ edges?: unknown[];
60
62
  }
61
63
  /** A layer's effects: the effect list as JSON plus the PSD block they came from (base64). */
62
64
  export interface SerializedLayerEffects {
@@ -6,26 +6,31 @@
6
6
  */
7
7
  import type { CropBox } from '../types.cjs';
8
8
  import type { AffineMatrix } from '../../geom/types.cjs';
9
+ import { readRegion } from '../../raster/surface.cjs';
9
10
  import type { ImageLayer, RasterMask, VectorMask } from './model.cjs';
10
11
  /** Whether a vector mask takes part in compositing. */
11
12
  export declare function isActiveVectorMask(vm: VectorMask | undefined): vm is VectorMask;
12
13
  /**
13
- * The vector mask's coverage (0..1) over the `width × height` canvas, with its feather and
14
- * density applied.
14
+ * The vector mask's coverage (0..1) over the `width × height` canvas, with its hybrid edges,
15
+ * feather and density applied.
15
16
  */
16
- export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number): Float32Array;
17
+ export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number, guide?: ArrayLike<number> | null): Float32Array;
18
+ /** Where a layer's pixels really are (the Background's come from the source image). */
19
+ export type LayerSurfaceOf = (layer: ImageLayer) => Parameters<typeof readRegion>[0] | undefined;
20
+ /** A layer's own RGBA over the canvas (the guide for hybrid matte edges), or null without pixels. */
21
+ export declare function layerGuide(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): Uint8ClampedArray | null;
17
22
  /**
18
23
  * The raster mask a layer composites with: its raster mask times its vector mask (layer space,
19
24
  * feather and density baked in), or the raster mask alone when there is no active vector mask.
20
25
  * A depth (parametric) mask cannot be combined, so it wins.
21
26
  */
22
- export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number): RasterMask | undefined;
27
+ export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): RasterMask | undefined;
23
28
  /**
24
29
  * The stack as the compositor should see it: each layer with an active vector mask replaced by a
25
30
  * copy whose `mask` is `effectiveLayerMask` (cached per layer object and canvas size, so the
26
31
  * engine's tile caches keep working). Returns `layers` itself when there are none.
27
32
  */
28
- export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number): readonly ImageLayer[];
33
+ export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number, surfaceOf?: LayerSurfaceOf): readonly ImageLayer[];
29
34
  /** A vector mask moved by (dx, dy) document pixels. */
30
35
  export declare function offsetVectorMask(vm: VectorMask, dx: number, dy: number): VectorMask;
31
36
  /** A vector mask with its paths mapped by `f` (document space); `affine` keeps live shapes live. */
@@ -6,26 +6,31 @@
6
6
  */
7
7
  import type { CropBox } from '../types.js';
8
8
  import type { AffineMatrix } from '../../geom/types.js';
9
+ import { readRegion } from '../../raster/surface.js';
9
10
  import type { ImageLayer, RasterMask, VectorMask } from './model.js';
10
11
  /** Whether a vector mask takes part in compositing. */
11
12
  export declare function isActiveVectorMask(vm: VectorMask | undefined): vm is VectorMask;
12
13
  /**
13
- * The vector mask's coverage (0..1) over the `width × height` canvas, with its feather and
14
- * density applied.
14
+ * The vector mask's coverage (0..1) over the `width × height` canvas, with its hybrid edges,
15
+ * feather and density applied.
15
16
  */
16
- export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number): Float32Array;
17
+ export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number, guide?: ArrayLike<number> | null): Float32Array;
18
+ /** Where a layer's pixels really are (the Background's come from the source image). */
19
+ export type LayerSurfaceOf = (layer: ImageLayer) => Parameters<typeof readRegion>[0] | undefined;
20
+ /** A layer's own RGBA over the canvas (the guide for hybrid matte edges), or null without pixels. */
21
+ export declare function layerGuide(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): Uint8ClampedArray | null;
17
22
  /**
18
23
  * The raster mask a layer composites with: its raster mask times its vector mask (layer space,
19
24
  * feather and density baked in), or the raster mask alone when there is no active vector mask.
20
25
  * A depth (parametric) mask cannot be combined, so it wins.
21
26
  */
22
- export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number): RasterMask | undefined;
27
+ export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): RasterMask | undefined;
23
28
  /**
24
29
  * The stack as the compositor should see it: each layer with an active vector mask replaced by a
25
30
  * copy whose `mask` is `effectiveLayerMask` (cached per layer object and canvas size, so the
26
31
  * engine's tile caches keep working). Returns `layers` itself when there are none.
27
32
  */
28
- export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number): readonly ImageLayer[];
33
+ export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number, surfaceOf?: LayerSurfaceOf): readonly ImageLayer[];
29
34
  /** A vector mask moved by (dx, dy) document pixels. */
30
35
  export declare function offsetVectorMask(vm: VectorMask, dx: number, dy: number): VectorMask;
31
36
  /** A vector mask with its paths mapped by `f` (document space); `affine` keeps live shapes live. */
@@ -22,6 +22,8 @@ export interface PathAnchorRef {
22
22
  export interface PathEditOverlay {
23
23
  /** The target's segments: 2 points for a line, 4 for a cubic. */
24
24
  curves: ImagePoint[][];
25
+ /** A hybrid vector mask's edge mode per curve (same order); missing: all hard. */
26
+ curveModes?: Array<'hard' | 'feather' | 'matte'>;
25
27
  /** Anchors shown; selected ones are drawn filled. */
26
28
  anchors: Array<{
27
29
  point: ImagePoint;
@@ -77,6 +79,8 @@ export declare class PathEditGesture {
77
79
  private selectComponent;
78
80
  private commit;
79
81
  down(p: ImagePoint, tool: PathEditTool, mods?: PathEditModifiers): void;
82
+ /** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
83
+ private cycleEdge;
80
84
  /** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
81
85
  private convert;
82
86
  private penDown;
@@ -22,6 +22,8 @@ export interface PathAnchorRef {
22
22
  export interface PathEditOverlay {
23
23
  /** The target's segments: 2 points for a line, 4 for a cubic. */
24
24
  curves: ImagePoint[][];
25
+ /** A hybrid vector mask's edge mode per curve (same order); missing: all hard. */
26
+ curveModes?: Array<'hard' | 'feather' | 'matte'>;
25
27
  /** Anchors shown; selected ones are drawn filled. */
26
28
  anchors: Array<{
27
29
  point: ImagePoint;
@@ -77,6 +79,8 @@ export declare class PathEditGesture {
77
79
  private selectComponent;
78
80
  private commit;
79
81
  down(p: ImagePoint, tool: PathEditTool, mods?: PathEditModifiers): void;
82
+ /** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
83
+ private cycleEdge;
80
84
  /** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
81
85
  private convert;
82
86
  private penDown;