@pixerate/editor 0.205.0 → 0.206.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.
@@ -4015,10 +4015,10 @@ function edgeWidth(guide, w, h, r) {
4015
4015
  for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
4016
4016
  return out;
4017
4017
  }
4018
- function refineCoverage(guide, m, w, h, params = {}) {
4018
+ function refineCoverage(guide, m, w, h, params = {}, band = null) {
4019
4019
  const p = normalizeRefineEdgeParams(params);
4020
4020
  let a = Float32Array.from(m);
4021
- if (p.radius > 0 && guide) {
4021
+ if ((p.radius > 0 || band) && guide) {
4022
4022
  const n = w * h;
4023
4023
  const boundary = new Uint8Array(n);
4024
4024
  let any = false;
@@ -4033,14 +4033,15 @@ function refineCoverage(guide, m, w, h, params = {}) {
4033
4033
  }
4034
4034
  }
4035
4035
  }
4036
- if (any) {
4036
+ if (any || band) {
4037
4037
  const dist = distanceTransform(boundary, w, h);
4038
- const r = gfRadius(p);
4038
+ const r = band ? Math.max(4, gfRadius(p)) : gfRadius(p);
4039
4039
  const q = guidedFilterColor(guide, m, w, h, r);
4040
4040
  const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
4041
4041
  for (let i = 0; i < n; i++) {
4042
4042
  const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
4043
- const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));
4043
+ const edge = p.radius > 0 ? Math.min(1, Math.max(0, reff + 1 - dist[i])) : 0;
4044
+ const wgt = band ? Math.max(edge, Math.min(1, Math.max(0, band[i]))) : edge;
4044
4045
  if (wgt > 0) {
4045
4046
  const qi = Math.min(1, Math.max(0, q[i]));
4046
4047
  a[i] = m[i] + (qi - m[i]) * wgt;
@@ -4081,17 +4082,24 @@ function guidePlanes(pixels, win) {
4081
4082
  }
4082
4083
  return [r, g, b];
4083
4084
  }
4084
- function refineSelection(pixels, mask, params = {}) {
4085
+ function refineSelection(pixels, mask, params = {}, refineBrush = null) {
4085
4086
  const p = normalizeRefineEdgeParams(params);
4086
4087
  if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
4087
4088
  throw new Error("Refine Edge needs pixels the size of the selection");
4088
4089
  }
4089
4090
  if (!mask.bounds) return mask;
4090
- const halo = refineEdgeHalo(p);
4091
- const x0 = Math.max(0, mask.bounds.x - halo);
4092
- const y0 = Math.max(0, mask.bounds.y - halo);
4093
- const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);
4094
- const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);
4091
+ const brush = refineBrush?.bounds ? refineBrush : null;
4092
+ const halo = refineEdgeHalo(p) + (brush ? 2 * Math.max(4, gfRadius(p)) : 0);
4093
+ const b = brush ? {
4094
+ x: Math.min(mask.bounds.x, brush.bounds.x),
4095
+ y: Math.min(mask.bounds.y, brush.bounds.y),
4096
+ r: Math.max(mask.bounds.x + mask.bounds.width, brush.bounds.x + brush.bounds.width),
4097
+ btm: Math.max(mask.bounds.y + mask.bounds.height, brush.bounds.y + brush.bounds.height)
4098
+ } : { x: mask.bounds.x, y: mask.bounds.y, r: mask.bounds.x + mask.bounds.width, btm: mask.bounds.y + mask.bounds.height };
4099
+ const x0 = Math.max(0, b.x - halo);
4100
+ const y0 = Math.max(0, b.y - halo);
4101
+ const x1 = Math.min(mask.width, b.r + halo);
4102
+ const y1 = Math.min(mask.height, b.btm + halo);
4095
4103
  const ww = x1 - x0;
4096
4104
  const wh = y1 - y0;
4097
4105
  const m = new Float32Array(ww * wh);
@@ -4099,8 +4107,17 @@ function refineSelection(pixels, mask, params = {}) {
4099
4107
  const src = (y0 + y) * mask.width + x0;
4100
4108
  for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
4101
4109
  }
4102
- const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
4103
- const refined = refineCoverage(guide, m, ww, wh, p);
4110
+ const guide = pixels && (p.radius > 0 || brush) ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
4111
+ let band = null;
4112
+ if (brush) {
4113
+ band = new Float32Array(ww * wh);
4114
+ const bd = brush.data;
4115
+ for (let y = 0; y < wh; y++) {
4116
+ const src = (y0 + y) * mask.width + x0;
4117
+ for (let x = 0; x < ww; x++) band[y * ww + x] = bd[src + x] / 255;
4118
+ }
4119
+ }
4120
+ const refined = refineCoverage(guide, m, ww, wh, p, band);
4104
4121
  const out = new Uint8Array(mask.width * mask.height);
4105
4122
  for (let y = 0; y < wh; y++) {
4106
4123
  const dst = (y0 + y) * mask.width + x0;
@@ -5302,6 +5319,66 @@ function segmentsToSelection(width, height, results) {
5302
5319
  return out;
5303
5320
  }
5304
5321
 
5322
+ // src/image-editor/selection/refine-edits.ts
5323
+ var REFINE_EDIT_KINDS = ["quick-select", "brush", "refine-brush"];
5324
+ function brushStrokeMask(width, height, points, size, hardness = 1) {
5325
+ const r = Math.max(0.5, size / 2);
5326
+ const solid = r * Math.min(1, Math.max(0, hardness));
5327
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
5328
+ for (const p of points) {
5329
+ x0 = Math.min(x0, p.x);
5330
+ y0 = Math.min(y0, p.y);
5331
+ x1 = Math.max(x1, p.x);
5332
+ y1 = Math.max(y1, p.y);
5333
+ }
5334
+ const bx = Math.max(0, Math.floor(x0 - r - 1));
5335
+ const by = Math.max(0, Math.floor(y0 - r - 1));
5336
+ const bw = Math.min(width, Math.ceil(x1 + r + 1)) - bx;
5337
+ const bh = Math.min(height, Math.ceil(y1 + r + 1)) - by;
5338
+ if (!(bw > 0 && bh > 0)) return createSelectionMask(width, height);
5339
+ const out = new Uint8Array(bw * bh);
5340
+ const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
5341
+ for (let y = 0; y < bh; y++) {
5342
+ for (let x = 0; x < bw; x++) {
5343
+ const px2 = bx + x + 0.5;
5344
+ const py = by + y + 0.5;
5345
+ let d = Infinity;
5346
+ for (const [a, b] of segs) {
5347
+ const dx = b.x - a.x;
5348
+ const dy = b.y - a.y;
5349
+ const len2 = dx * dx + dy * dy;
5350
+ const t = len2 > 0 ? Math.min(1, Math.max(0, ((px2 - a.x) * dx + (py - a.y) * dy) / len2)) : 0;
5351
+ d = Math.min(d, Math.hypot(px2 - a.x - t * dx, py - a.y - t * dy));
5352
+ }
5353
+ let v = d <= solid ? 1 : solid >= r ? 0 : 1 - (d - solid) / (r - solid);
5354
+ v = Math.min(v, Math.min(1, Math.max(0, r + 0.5 - d)));
5355
+ out[y * bw + x] = Math.round(Math.max(0, v) * 255);
5356
+ }
5357
+ }
5358
+ return selectionFromRegion(width, height, { x: bx, y: by, width: bw, height: bh }, out);
5359
+ }
5360
+ function applyRefineEdits(pixels, selection, edits, width, height) {
5361
+ let sel = selection?.bounds ? selection : null;
5362
+ let band = null;
5363
+ for (const e of edits) {
5364
+ if (!e.points.length) throw new Error(`A ${e.kind} stroke needs at least one point.`);
5365
+ if (e.kind === "refine-brush") {
5366
+ band = combineSelections(band, brushStrokeMask(width, height, e.points, e.size ?? 40), "add");
5367
+ continue;
5368
+ }
5369
+ const mode = e.mode ?? "add";
5370
+ let region;
5371
+ if (e.kind === "quick-select") {
5372
+ if (!pixels) throw new Error("Quick Selection strokes need the image pixels.");
5373
+ region = quickSelectStroke(pixels, e.points, { size: e.size, selection: sel, grow: mode === "add" });
5374
+ } else {
5375
+ region = brushStrokeMask(width, height, e.points, e.size ?? 20, e.hardness ?? 1);
5376
+ }
5377
+ sel = combineSelections(sel, region, mode);
5378
+ }
5379
+ return { selection: sel, refineBrush: band };
5380
+ }
5381
+
5305
5382
  // src/image-editor/selection/stroke.ts
5306
5383
  function selectionRing(sel, width, location) {
5307
5384
  const half = location === "center" ? width / 2 : width;
@@ -10332,10 +10409,19 @@ var ImageEditorController = class {
10332
10409
  * - `new-layer-with-mask`: a copy of the layer masked by the selection, the source hidden.
10333
10410
  * `decontaminate` (0..100, new-layer outputs only) replaces the old background's colour in the
10334
10411
  * soft edge with the subject's own (see `decontaminateColors`).
10412
+ *
10413
+ * `edits` are the workspace's strokes, applied first in order (Quick Selection, Brush and
10414
+ * Refine Edge Brush; see `RefineEdit`); with them, an empty selection is a valid start.
10415
+ * `refineMode: 'object'` asks the segmentation provider for the object in the selection's
10416
+ * bounds and refines from its outline, kept to near the selection's edge.
10335
10417
  */
10336
10418
  async refineSelectionTo(params, output = "selection", options = {}) {
10337
- const sel = this.state.selection;
10338
- if (!sel?.bounds) throw new Error("Nothing is selected; select first.");
10419
+ const edits = options.edits ?? [];
10420
+ const refineMode = options.refineMode ?? "color";
10421
+ if (!this.state.selection?.bounds && !edits.length) throw new Error("Nothing is selected; select first, or pass edits that paint a selection.");
10422
+ if (refineMode === "object" && !this.segmentation) {
10423
+ throw new Error("Object aware refinement needs a segmentation model (setSegmentationProvider); use refineMode color.");
10424
+ }
10339
10425
  const amount = Math.min(100, Math.max(0, options.decontaminate ?? 0)) / 100;
10340
10426
  const toLayer = output === "new-layer" || output === "new-layer-with-mask";
10341
10427
  if (amount > 0 && !toLayer) throw new Error("Decontaminate colors writes the cleaned colours to a new layer: use output new-layer or new-layer-with-mask.");
@@ -10346,16 +10432,30 @@ var ImageEditorController = class {
10346
10432
  const layer = requireLayer(this.state.layers, layerId).layer;
10347
10433
  if (toLayer && layer.type !== "pixel") throw new LayerOperationError(`Output ${output} needs a pixel layer; "${layerId}" is a ${layer.type} layer.`);
10348
10434
  }
10349
- const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;
10350
- const refined = refineSelection(pixels, sel, params);
10435
+ const needsPixels = (params.radius ?? 0) > 0 || edits.some((e) => e.kind !== "brush");
10436
+ const pixels = needsPixels ? await this.requirePixels(options) : null;
10437
+ const { width, height } = this.naturalSize;
10438
+ const edited = applyRefineEdits(pixels, this.state.selection ?? null, edits, width, height);
10439
+ let sel = edited.selection;
10440
+ if (!sel?.bounds) throw new Error("The edits leave nothing selected; paint with mode add first.");
10441
+ if (refineMode === "object") {
10442
+ const b = sel.bounds;
10443
+ const found = await this.modelObject({ rect: b }, options.signal);
10444
+ if (!found) throw new Error("The segmentation model found no object in the selection; use refineMode color.");
10445
+ const near = modifySelection(sel, "expand", Math.max(4, params.radius ?? 8));
10446
+ const clipped = combineSelections(found, near, "intersect");
10447
+ if (!clipped?.bounds) throw new Error("The segmentation model found no object in the selection; use refineMode color.");
10448
+ sel = clipped;
10449
+ }
10450
+ const refined = refineSelection(pixels, sel, params, edited.refineBrush);
10351
10451
  if (!refined.bounds) throw new Error("Refining left nothing selected; use a smaller shiftEdge or contrast.");
10352
10452
  return this.asOneStep(() => {
10353
10453
  this.setSelection(refined);
10354
- if (output === "selection") return { output };
10454
+ if (output === "selection") return { output, refineMode };
10355
10455
  if (output === "layer-mask") {
10356
10456
  this.addLayerMask(layerId, "selection");
10357
10457
  this.setSelection(null);
10358
- return { output, layerId };
10458
+ return { output, layerId, refineMode };
10359
10459
  }
10360
10460
  let newLayerId2;
10361
10461
  if (output === "new-layer") newLayerId2 = this.layerViaCopy({ layerId });
@@ -10367,7 +10467,7 @@ var ImageEditorController = class {
10367
10467
  if (amount > 0) this.decontaminateLayer(newLayerId2, layerId, refined, amount, Math.max(4, params.radius ?? 0));
10368
10468
  this.setLayerProperties(layerId, { visible: false });
10369
10469
  this.setSelection(null);
10370
- return { output, layerId, newLayerId: newLayerId2 };
10470
+ return { output, layerId, newLayerId: newLayerId2, refineMode };
10371
10471
  });
10372
10472
  }
10373
10473
  /**
@@ -13586,7 +13686,7 @@ function createImageSelectionTools(controller) {
13586
13686
  const refine = {
13587
13687
  name: "refine_selection",
13588
13688
  title: "Refine the selection edge",
13589
- description: "Select and Mask / Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %), and puts the result where output says: the selection (default), a layer-mask on layerId (default the active layer; then deselects), a new-layer of its selected pixels, or a new-layer-with-mask (a masked copy); the new-layer outputs hide the source layer. decontaminate (0-100, new-layer outputs only) replaces the old background colour in soft edges (hair, fur) with the subject's own. Use it after loading a coarse AI mask, or to cut a subject out cleanly. One undoable step. Returns the selection and, for layer outputs, layerId and newLayerId.",
13689
+ description: "Select and Mask / Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %), and puts the result where output says: the selection (default), a layer-mask on layerId (default the active layer; then deselects), a new-layer of its selected pixels, or a new-layer-with-mask (a masked copy); the new-layer outputs hide the source layer. decontaminate (0-100, new-layer outputs only) replaces the old background colour in soft edges (hair, fur) with the subject's own. Use it after loading a coarse AI mask, or to cut a subject out cleanly. edits are the Select and Mask workspace's strokes, applied first in order: quick-select (grows to the region's edges), brush (paints the selection in or out) and refine-brush (marks hair or fur for the edge to be re-estimated there); with edits, an empty selection is a valid start. refineMode object (needs a host segmentation model) refines from the model's outline of the object. One undoable step. Returns the selection, refineMode and, for layer outputs, layerId and newLayerId.",
13590
13690
  inputSchema: {
13591
13691
  type: "object",
13592
13692
  properties: {
@@ -13602,13 +13702,35 @@ function createImageSelectionTools(controller) {
13602
13702
  description: "Where the result goes (default selection). layer-mask keeps every pixel and is easiest to revise; new-layer-with-mask too, on a copy."
13603
13703
  },
13604
13704
  layerId: { type: "string", description: "The layer for layer outputs (default the active layer; see list_layers). New-layer outputs need a pixel layer." },
13605
- decontaminate: { type: "number", minimum: 0, maximum: 100, description: "Decontaminate Colors amount 0-100 (default 0), only with output new-layer or new-layer-with-mask." }
13705
+ decontaminate: { type: "number", minimum: 0, maximum: 100, description: "Decontaminate Colors amount 0-100 (default 0), only with output new-layer or new-layer-with-mask." },
13706
+ edits: {
13707
+ type: "array",
13708
+ description: "Strokes applied before refining, in order.",
13709
+ items: {
13710
+ type: "object",
13711
+ properties: {
13712
+ kind: { type: "string", enum: ["quick-select", "brush", "refine-brush"], description: "quick-select grows to edges; brush paints directly; refine-brush marks soft detail (hair) for the edge to be re-estimated." },
13713
+ points: { type: "array", minItems: 1, items: pointSchema, description: "The stroke path in natural image pixels (one point is a dab)." },
13714
+ size: { type: "number", minimum: 1, maximum: 2500, description: "Brush diameter in natural image pixels (default 30 quick-select, 20 brush, 40 refine-brush)." },
13715
+ mode: { type: "string", enum: ["add", "subtract"], description: "quick-select and brush: add (default) or subtract." },
13716
+ hardness: { type: "number", minimum: 0, maximum: 1, description: "brush: the solid share of the radius, 0..1 (default 1)." }
13717
+ },
13718
+ required: ["kind", "points"],
13719
+ additionalProperties: false
13720
+ }
13721
+ },
13722
+ refineMode: { type: "string", enum: ["color", "object"], description: "color (default): refine from the image colours; object: from a host segmentation model's outline of the object (needs a model)." }
13606
13723
  },
13607
13724
  additionalProperties: false
13608
13725
  },
13609
13726
  annotations: writeAnnotations("Refine the selection edge"),
13610
13727
  handler: async (params) => {
13611
- if (!controller.getSelection()) throw new Error("Nothing is selected.");
13728
+ const edits = Array.isArray(params.edits) ? params.edits.map((e) => ({ ...e, points: (e.points ?? []).map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) })) : [];
13729
+ for (const e of edits) {
13730
+ if (!e.points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) throw new Error("Each edit needs points of { x, y } in natural image pixels.");
13731
+ if (e.kind === "refine-brush" && e.mode !== void 0) throw new Error("refine-brush strokes have no mode; they mark where the edge is re-estimated.");
13732
+ }
13733
+ if (!controller.getSelection() && !edits.length) throw new Error("Nothing is selected.");
13612
13734
  const result = await controller.refineSelectionTo(
13613
13735
  {
13614
13736
  radius: num2(params.radius, 8),
@@ -13619,7 +13741,12 @@ function createImageSelectionTools(controller) {
13619
13741
  shiftEdge: num2(params.shiftEdge, 0)
13620
13742
  },
13621
13743
  params.output ?? "selection",
13622
- { layerId: typeof params.layerId === "string" ? params.layerId : void 0, decontaminate: num2(params.decontaminate, 0) }
13744
+ {
13745
+ layerId: typeof params.layerId === "string" ? params.layerId : void 0,
13746
+ decontaminate: num2(params.decontaminate, 0),
13747
+ edits,
13748
+ refineMode: params.refineMode === "object" ? "object" : "color"
13749
+ }
13623
13750
  );
13624
13751
  return { ...summary(controller), ...result };
13625
13752
  }
@@ -20840,6 +20967,7 @@ export {
20840
20967
  PsdError,
20841
20968
  QUICK_MASK_TARGET,
20842
20969
  REFINE_EDGE_CONTROLS,
20970
+ REFINE_EDIT_KINDS,
20843
20971
  REFINE_OUTPUTS,
20844
20972
  RETOUCH_CHOICES,
20845
20973
  RETOUCH_OPTION_CONTROLS,
@@ -20952,6 +21080,7 @@ export {
20952
21080
  applyParams,
20953
21081
  applyProcess2Adjustments,
20954
21082
  applyProcess2Float,
21083
+ applyRefineEdits,
20955
21084
  applyRegionBlur,
20956
21085
  applyRegionMosaic,
20957
21086
  applyRetouchToTile,
@@ -20984,6 +21113,7 @@ export {
20984
21113
  boxQuad,
20985
21114
  boxToCurvePoint,
20986
21115
  bradfordAdaptation,
21116
+ brushStrokeMask,
20987
21117
  brushstrokeDataUrl,
20988
21118
  brushstrokeKey,
20989
21119
  brushstrokeSamples,
@@ -14,3 +14,4 @@ export * from './mask-files.cjs';
14
14
  export * from './path.cjs';
15
15
  export * from './magnetic.cjs';
16
16
  export * from './segment/index.cjs';
17
+ export * from './refine-edits.cjs';
@@ -14,3 +14,4 @@ export * from './mask-files.js';
14
14
  export * from './path.js';
15
15
  export * from './magnetic.js';
16
16
  export * from './segment/index.js';
17
+ export * from './refine-edits.js';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The Select and Mask workspace's strokes as plain data, so an agent can send what a person
3
+ * paints: Quick Selection strokes (grow to the region's edges), Brush strokes (paint the
4
+ * selection in or out directly) and Refine Edge Brush strokes (mark where the edge should be
5
+ * re-estimated from the image, e.g. over hair).
6
+ */
7
+ import type { ImagePoint } from '../types.cjs';
8
+ import { type PixelBuffer, type SelectionMask } from './mask.cjs';
9
+ /** One stroke in the workspace, in natural image pixels. */
10
+ export type RefineEdit = {
11
+ kind: 'quick-select';
12
+ points: ImagePoint[];
13
+ size?: number;
14
+ mode?: 'add' | 'subtract';
15
+ } | {
16
+ kind: 'brush';
17
+ points: ImagePoint[];
18
+ size?: number;
19
+ mode?: 'add' | 'subtract';
20
+ hardness?: number;
21
+ } | {
22
+ kind: 'refine-brush';
23
+ points: ImagePoint[];
24
+ size?: number;
25
+ };
26
+ export declare const REFINE_EDIT_KINDS: readonly ["quick-select", "brush", "refine-brush"];
27
+ /**
28
+ * Coverage of a round brush of diameter `size` dragged along `points` (one point is a dab), over
29
+ * a `width × height` image. `hardness` 0..1 (default 1): the solid share of the radius; the rest
30
+ * fades out. The edge is anti-aliased either way.
31
+ */
32
+ export declare function brushStrokeMask(width: number, height: number, points: readonly ImagePoint[], size: number, hardness?: number): SelectionMask;
33
+ /**
34
+ * Applies `edits` in order to `selection` (`null` starts empty) over `pixels` (needed for
35
+ * Quick Selection strokes). Returns the edited selection and the Refine Edge Brush's painted
36
+ * area (`null` when there were no such strokes), for `refineSelection`.
37
+ */
38
+ export declare function applyRefineEdits(pixels: PixelBuffer | null, selection: SelectionMask | null, edits: readonly RefineEdit[], width: number, height: number): {
39
+ selection: SelectionMask | null;
40
+ refineBrush: SelectionMask | null;
41
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The Select and Mask workspace's strokes as plain data, so an agent can send what a person
3
+ * paints: Quick Selection strokes (grow to the region's edges), Brush strokes (paint the
4
+ * selection in or out directly) and Refine Edge Brush strokes (mark where the edge should be
5
+ * re-estimated from the image, e.g. over hair).
6
+ */
7
+ import type { ImagePoint } from '../types.js';
8
+ import { type PixelBuffer, type SelectionMask } from './mask.js';
9
+ /** One stroke in the workspace, in natural image pixels. */
10
+ export type RefineEdit = {
11
+ kind: 'quick-select';
12
+ points: ImagePoint[];
13
+ size?: number;
14
+ mode?: 'add' | 'subtract';
15
+ } | {
16
+ kind: 'brush';
17
+ points: ImagePoint[];
18
+ size?: number;
19
+ mode?: 'add' | 'subtract';
20
+ hardness?: number;
21
+ } | {
22
+ kind: 'refine-brush';
23
+ points: ImagePoint[];
24
+ size?: number;
25
+ };
26
+ export declare const REFINE_EDIT_KINDS: readonly ["quick-select", "brush", "refine-brush"];
27
+ /**
28
+ * Coverage of a round brush of diameter `size` dragged along `points` (one point is a dab), over
29
+ * a `width × height` image. `hardness` 0..1 (default 1): the solid share of the radius; the rest
30
+ * fades out. The edge is anti-aliased either way.
31
+ */
32
+ export declare function brushStrokeMask(width: number, height: number, points: readonly ImagePoint[], size: number, hardness?: number): SelectionMask;
33
+ /**
34
+ * Applies `edits` in order to `selection` (`null` starts empty) over `pixels` (needed for
35
+ * Quick Selection strokes). Returns the edited selection and the Refine Edge Brush's painted
36
+ * area (`null` when there were no such strokes), for `refineSelection`.
37
+ */
38
+ export declare function applyRefineEdits(pixels: PixelBuffer | null, selection: SelectionMask | null, edits: readonly RefineEdit[], width: number, height: number): {
39
+ selection: SelectionMask | null;
40
+ refineBrush: SelectionMask | null;
41
+ };
@@ -42,10 +42,12 @@ export declare function guidedFilterColor(guide: readonly [Float32Array, Float32
42
42
  export declare function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array;
43
43
  /**
44
44
  * Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when
45
- * `radius > 0`): edge-aware re-estimation in a band around the edge, then smooth, feather,
46
- * contrast and shift edge, in that order.
45
+ * `radius > 0` or a `band` is given): edge-aware re-estimation in a band around the edge, then
46
+ * smooth, feather, contrast and shift edge, in that order. `band` (0..1 per pixel, the Refine
47
+ * Edge Brush) adds to the band where it is painted, so hair or fur far from the edge is
48
+ * re-estimated too.
47
49
  */
48
- export declare function refineCoverage(guide: readonly [Float32Array, Float32Array, Float32Array] | null, m: Float32Array, w: number, h: number, params?: RefineEdgeParams): Float32Array;
50
+ export declare function refineCoverage(guide: readonly [Float32Array, Float32Array, Float32Array] | null, m: Float32Array, w: number, h: number, params?: RefineEdgeParams, band?: ArrayLike<number> | null): Float32Array;
49
51
  /** Three 0..1 planes of a window of an RGBA buffer. */
50
52
  export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array];
51
53
  /**
@@ -53,7 +55,7 @@ export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float3
53
55
  * same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small
54
56
  * selection on a large image stays cheap.
55
57
  */
56
- export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams): SelectionMask;
58
+ export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams, refineBrush?: SelectionMask | null): SelectionMask;
57
59
  /**
58
60
  * Decontaminate Colors: along a soft edge, a pixel's colour is part subject and part the old
59
61
  * background (C = αF + (1 − α)B), which shows as a fringe once the subject is on a new background.
@@ -42,10 +42,12 @@ export declare function guidedFilterColor(guide: readonly [Float32Array, Float32
42
42
  export declare function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array;
43
43
  /**
44
44
  * Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when
45
- * `radius > 0`): edge-aware re-estimation in a band around the edge, then smooth, feather,
46
- * contrast and shift edge, in that order.
45
+ * `radius > 0` or a `band` is given): edge-aware re-estimation in a band around the edge, then
46
+ * smooth, feather, contrast and shift edge, in that order. `band` (0..1 per pixel, the Refine
47
+ * Edge Brush) adds to the band where it is painted, so hair or fur far from the edge is
48
+ * re-estimated too.
47
49
  */
48
- export declare function refineCoverage(guide: readonly [Float32Array, Float32Array, Float32Array] | null, m: Float32Array, w: number, h: number, params?: RefineEdgeParams): Float32Array;
50
+ export declare function refineCoverage(guide: readonly [Float32Array, Float32Array, Float32Array] | null, m: Float32Array, w: number, h: number, params?: RefineEdgeParams, band?: ArrayLike<number> | null): Float32Array;
49
51
  /** Three 0..1 planes of a window of an RGBA buffer. */
50
52
  export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array];
51
53
  /**
@@ -53,7 +55,7 @@ export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float3
53
55
  * same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small
54
56
  * selection on a large image stays cheap.
55
57
  */
56
- export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams): SelectionMask;
58
+ export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams, refineBrush?: SelectionMask | null): SelectionMask;
57
59
  /**
58
60
  * Decontaminate Colors: along a soft edge, a pixel's colour is part subject and part the old
59
61
  * background (C = αF + (1 − α)B), which shows as a fringe once the subject is on a new background.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.205.0",
3
+ "version": "0.206.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -269,7 +269,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
269
269
  | `grow_selection` | Select › Grow: add connected pixels within `tolerance` (default 32) of a selected colour; follows colour, not distance. `sample`, `sampleMerged`. Fails when nothing is selected. Returns the summary. |
270
270
  | `select_similar` | Select › Similar: add every pixel in the image within `tolerance` of a selected colour, connected or not. Fails when nothing is selected. Returns the summary. |
271
271
  | `reselect` | Bring back the last selection `deselect` cleared (one undo step). Fails when there is none. Returns the summary. |
272
- | `refine_selection` | Select and Mask: snap the selection edge to image edges within `radius` px (0..64, default 8), with `smartRadius`, `smooth` 0..100, `feather` px, `contrast` 0..100 and `shiftEdge` -100..100 %, then `output` to `selection` (default), `layer-mask`, `new-layer` or `new-layer-with-mask` (on `layerId`, default active; new layers hide the source), with `decontaminate` 0..100 for new layers. One step; returns `layerId` / `newLayerId`. Use on hair, fur, foliage or after a coarse AI mask. |
272
+ | `refine_selection` | Select and Mask: snap the selection edge to image edges within `radius` px (0..64, default 8), with `smartRadius`, `smooth` 0..100, `feather` px, `contrast` 0..100 and `shiftEdge` -100..100 %, then `output` to `selection` (default), `layer-mask`, `new-layer` or `new-layer-with-mask` (on `layerId`, default active; new layers hide the source), with `decontaminate` 0..100 for new layers. `edits` (`quick-select`, `brush` add/subtract, `refine-brush` over hair) apply first; `refineMode: "object"` uses the host model. One step; returns `refineMode`, `layerId` / `newLayerId`. Use on hair, fur, foliage or a coarse AI mask. |
273
273
  | `update_adjustment_layer` | Change a layer's `enabled` (false hides it without deleting), `blend`, `opacity` (0..1) or `name` in one undo step. Returns the layer. Its parameters change with `set_adjustments`. Toggle `enabled` to compare a layer's effect before and after. |
274
274
  | `move_adjustment_layer` | Move a layer to index `to` (0 = bottom, applied first; past the top means top). Order matters: a curves layer above a hue/saturation layer works on its output. Fails when the layer is already there. Returns the ids bottom to top. |
275
275
  | `remove_adjustment_layer` | Delete a layer and its mask. Prefer hiding it with `update_adjustment_layer` when the user may want it back. Returns the remaining ids. |
@@ -57,7 +57,10 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
57
57
  `layerId` (white adds, black removes, a soft brush feathers; `blur_stroke` softens a part
58
58
  of the edge), then `set_quick_mask` `enabled: false`.
59
59
  - `refine_selection` snaps to real edges: radius 2–4 for hard edges, 8–20 for hair and fur;
60
- `smartRadius: true` for mixed edges.
60
+ `smartRadius: true` for mixed edges. For flyaway hair beyond the radius, add `edits` with a
61
+ `refine-brush` stroke along it (size covering the strands); fix misses and leaks in the same
62
+ call with `quick-select` or `brush` strokes (`mode: "subtract"` to remove). With a host
63
+ model, `refineMode: "object"` refines from the model's outline.
61
64
  - `shiftEdge` −10..−30 % tightens a refined edge that picked up background.
62
65
  **Check:** `percent` changed by roughly what you intended (a few percent at most for a
63
66
  cleanup).