@pixerate/editor 0.190.0 → 0.191.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.
@@ -9,9 +9,14 @@ import {
9
9
  pathfinder
10
10
  } from "../chunk-65MDXWA7.js";
11
11
  import {
12
+ VECTOR_TRACE_MAX_PIXELS,
12
13
  loadOpenTypeFont,
13
- parseOpenTypeFont
14
- } from "../chunk-M7XIC2FC.js";
14
+ parseOpenTypeFont,
15
+ quantizeVectorTraceColors,
16
+ traceField,
17
+ traceVectorImage,
18
+ vectorTraceContours
19
+ } from "../chunk-RQ2EM5TY.js";
15
20
  import {
16
21
  applyHomography,
17
22
  homographyFromPoints,
@@ -533,8 +538,7 @@ import {
533
538
  import {
534
539
  fitCubicWithTangents,
535
540
  fitCubics,
536
- parseSvgPath,
537
- simplifyRing
541
+ parseSvgPath
538
542
  } from "../chunk-PWBZH3FK.js";
539
543
  import {
540
544
  EffectUnsupportedError,
@@ -575,293 +579,6 @@ import "../chunk-YBAPT7NL.js";
575
579
  import "../chunk-MHLU6GRJ.js";
576
580
  import "../chunk-6ONMQ6WF.js";
577
581
 
578
- // src/vector/trace.ts
579
- var VECTOR_TRACE_MAX_PIXELS = 4e6;
580
- function signedArea(p) {
581
- let a = 0;
582
- for (let i = 0; i < p.length; i++) {
583
- const [x0, y0] = p[i];
584
- const [x1, y1] = p[(i + 1) % p.length];
585
- a += x0 * y1 - x1 * y0;
586
- }
587
- return a / 2;
588
- }
589
- function pointInPoly(p, poly) {
590
- let inside = false;
591
- for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
592
- const [xi, yi] = poly[i];
593
- const [xj, yj] = poly[j];
594
- if (yi > p[1] !== yj > p[1] && p[0] < (xj - xi) * (p[1] - yi) / (yj - yi) + xi) inside = !inside;
595
- }
596
- return inside;
597
- }
598
- function vectorTraceContours(field, w, h, threshold) {
599
- const gw = w + 2;
600
- const gh = h + 2;
601
- const val = (i, j) => i === 0 || j === 0 || i > w || j > h ? -Infinity : field[(j - 1) * w + (i - 1)];
602
- const inside = (i, j) => val(i, j) >= threshold;
603
- const pos = (i, j) => [i - 0.5, j - 0.5];
604
- const cross2 = (a, b) => {
605
- const va = val(a[0], a[1]);
606
- const vb = val(b[0], b[1]);
607
- let t;
608
- if (Number.isFinite(va) && Number.isFinite(vb) && Math.abs(vb - va) > 1e-12) t = Math.min(1, Math.max(0, (threshold - va) / (vb - va)));
609
- else if (Number.isFinite(va)) t = va >= threshold ? 0.5 : 0;
610
- else t = vb >= threshold ? 0.5 : 1;
611
- const pa = pos(a[0], a[1]);
612
- const pb = pos(b[0], b[1]);
613
- return [pa[0] + (pb[0] - pa[0]) * t, pa[1] + (pb[1] - pa[1]) * t];
614
- };
615
- const hid = (i, j) => 2 * (j * gw + i);
616
- const vid = (i, j) => 2 * (j * gw + i) + 1;
617
- const next = /* @__PURE__ */ new Map();
618
- const point = /* @__PURE__ */ new Map();
619
- for (let j = 0; j < gh - 1; j++) {
620
- for (let i = 0; i < gw - 1; i++) {
621
- const tl = inside(i, j);
622
- const tr = inside(i + 1, j);
623
- const br = inside(i + 1, j + 1);
624
- const bl = inside(i, j + 1);
625
- const code = +tl << 3 | +tr << 2 | +br << 1 | +bl;
626
- if (code === 0 || code === 15) continue;
627
- const top = hid(i, j);
628
- const bottom = hid(i, j + 1);
629
- const left = vid(i, j);
630
- const right = vid(i + 1, j);
631
- const seg = (a, b) => next.set(a, b);
632
- const mean = () => (val(i, j) + val(i + 1, j) + val(i + 1, j + 1) + val(i, j + 1)) / 4;
633
- switch (code) {
634
- case 1:
635
- seg(left, bottom);
636
- break;
637
- case 2:
638
- seg(bottom, right);
639
- break;
640
- case 3:
641
- seg(left, right);
642
- break;
643
- case 4:
644
- seg(right, top);
645
- break;
646
- case 5:
647
- if (mean() >= threshold) {
648
- seg(left, top);
649
- seg(right, bottom);
650
- } else {
651
- seg(left, bottom);
652
- seg(right, top);
653
- }
654
- break;
655
- case 6:
656
- seg(bottom, top);
657
- break;
658
- case 7:
659
- seg(left, top);
660
- break;
661
- case 8:
662
- seg(top, left);
663
- break;
664
- case 9:
665
- seg(top, bottom);
666
- break;
667
- case 10:
668
- if (mean() >= threshold) {
669
- seg(top, right);
670
- seg(bottom, left);
671
- } else {
672
- seg(top, left);
673
- seg(bottom, right);
674
- }
675
- break;
676
- case 11:
677
- seg(top, right);
678
- break;
679
- case 12:
680
- seg(right, left);
681
- break;
682
- case 13:
683
- seg(right, bottom);
684
- break;
685
- case 14:
686
- seg(bottom, left);
687
- break;
688
- }
689
- const edges = [
690
- [top, [i, j], [i + 1, j]],
691
- [bottom, [i, j + 1], [i + 1, j + 1]],
692
- [left, [i, j], [i, j + 1]],
693
- [right, [i + 1, j], [i + 1, j + 1]]
694
- ];
695
- for (const [e, a, b] of edges) if (inside(a[0], a[1]) !== inside(b[0], b[1]) && !point.has(e)) point.set(e, cross2(a, b));
696
- }
697
- }
698
- const out = [];
699
- for (const start of [...next.keys()].sort((a, b) => a - b)) {
700
- if (!next.has(start)) continue;
701
- const poly = [];
702
- let e = start;
703
- for (; ; ) {
704
- const n2 = next.get(e);
705
- if (n2 === void 0) break;
706
- next.delete(e);
707
- const p = point.get(e);
708
- if (p) poly.push(p);
709
- e = n2;
710
- if (e === start) break;
711
- }
712
- if (poly.length >= 3) out.push(poly);
713
- }
714
- return out;
715
- }
716
- function loopsOf(field, w, h, threshold, tolerance, minArea) {
717
- const loops = [];
718
- for (const raw of vectorTraceContours(field, w, h, threshold)) {
719
- const area = signedArea(raw);
720
- if (Math.abs(area) < Math.max(0, minArea)) continue;
721
- const simple = simplifyRing(raw, Math.max(0.01, tolerance));
722
- if (simple.length < 3) continue;
723
- loops.push({ raw, simple, area, depth: 0 });
724
- }
725
- for (const l of loops) l.depth = loops.filter((o) => o !== l && Math.abs(o.area) > Math.abs(l.area) && pointInPoly(l.simple[0], o.simple)).length;
726
- return loops.sort((a, b) => a.depth - b.depth || Math.abs(b.area) - Math.abs(a.area));
727
- }
728
- function loopSubpath(l, tolerance, roundness) {
729
- if (roundness <= 0) return { closed: true, anchors: l.simple.map(([x, y]) => ({ x, y })) };
730
- const cornerAngleDeg = 25 + roundness * 125;
731
- const cubics = fitCubics(l.raw, Math.max(0.05, tolerance), {
732
- closed: true,
733
- cornerAngleDeg
734
- });
735
- if (!cubics.length) return { closed: true, anchors: l.simple.map(([x, y]) => ({ x, y })) };
736
- const anchors = cubics.map((c, k) => {
737
- const prev = cubics[(k - 1 + cubics.length) % cubics.length];
738
- return {
739
- x: c.p0[0],
740
- y: c.p0[1],
741
- in: { x: prev.p2[0], y: prev.p2[1] },
742
- out: { x: c.p1[0], y: c.p1[1] }
743
- };
744
- });
745
- return { closed: true, anchors };
746
- }
747
- function traceField(field, w, h, threshold, o) {
748
- const tolerance = o.tolerance ?? 1;
749
- const roundness = Math.min(1, Math.max(0, o.cornerRoundness ?? 0.5));
750
- return loopsOf(field, w, h, threshold, tolerance, o.minArea ?? 10).map((l) => loopSubpath(l, tolerance, roundness));
751
- }
752
- function quantizeVectorTraceColors(pixels, count) {
753
- const n2 = pixels.width * pixels.height;
754
- const d = pixels.data;
755
- const opaque = [];
756
- for (let i = 0; i < n2; i++) if (d[i * 4 + 3] >= 128) opaque.push(i);
757
- const boxes = opaque.length ? [opaque] : [];
758
- const range = (box, c) => {
759
- let lo = 255;
760
- let hi = 0;
761
- for (const i of box) {
762
- const v = d[i * 4 + c];
763
- if (v < lo) lo = v;
764
- if (v > hi) hi = v;
765
- }
766
- return hi - lo;
767
- };
768
- while (boxes.length < count) {
769
- let best = -1;
770
- let bestRange = 0;
771
- let bestC = 0;
772
- boxes.forEach((b, k) => {
773
- if (b.length < 2) return;
774
- for (let c = 0; c < 3; c++) {
775
- const r = range(b, c);
776
- if (r > bestRange) {
777
- bestRange = r;
778
- best = k;
779
- bestC = c;
780
- }
781
- }
782
- });
783
- if (best < 0) break;
784
- const box = boxes[best].slice().sort((a, b) => d[a * 4 + bestC] - d[b * 4 + bestC]);
785
- const key = (k) => d[box[k] * 4 + bestC];
786
- const v = key(box.length >> 1);
787
- let mid = box.findIndex((_, k) => key(k) >= v);
788
- if (mid <= 0) mid = box.findIndex((_, k) => key(k) > v);
789
- boxes.splice(best, 1, box.slice(0, mid), box.slice(mid));
790
- }
791
- const palette = boxes.map((b) => {
792
- const s = [0, 0, 0];
793
- for (const i of b) for (let c = 0; c < 3; c++) s[c] += d[i * 4 + c];
794
- return [Math.round(s[0] / b.length), Math.round(s[1] / b.length), Math.round(s[2] / b.length)];
795
- });
796
- const index = new Int16Array(n2).fill(-1);
797
- const counts = palette.map(() => 0);
798
- for (const i of opaque) {
799
- let best = 0;
800
- let bestD = Infinity;
801
- palette.forEach((p, k) => {
802
- const dd = (d[i * 4] - p[0]) ** 2 + (d[i * 4 + 1] - p[1]) ** 2 + (d[i * 4 + 2] - p[2]) ** 2;
803
- if (dd < bestD) {
804
- bestD = dd;
805
- best = k;
806
- }
807
- });
808
- index[i] = best;
809
- counts[best]++;
810
- }
811
- return { palette, index, counts };
812
- }
813
- var hex = (c) => `#${c.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("")}`;
814
- function traceVectorImage(pixels, options = {}) {
815
- const { width: w, height: h, data } = pixels;
816
- if (!(w > 0 && h > 0) || data.length < w * h * 4) throw new Error("The image has no pixels (or fewer than width \xD7 height \xD7 4 values).");
817
- if (w * h > VECTOR_TRACE_MAX_PIXELS) throw new Error(`The image has ${w * h} pixels; scale it down to at most ${VECTOR_TRACE_MAX_PIXELS} to trace it.`);
818
- const n2 = w * h;
819
- if ((options.mode ?? "threshold") === "threshold") {
820
- const field = new Float32Array(n2);
821
- const byAlpha = options.channel === "alpha";
822
- for (let i = 0; i < n2; i++) {
823
- const a = data[i * 4 + 3] / 255;
824
- field[i] = byAlpha ? a : a * (1 - (0.2126 * data[i * 4] + 0.7152 * data[i * 4 + 1] + 0.0722 * data[i * 4 + 2]) / 255);
825
- }
826
- const subpaths = traceField(field, w, h, Math.min(1, Math.max(0, options.threshold ?? 0.5)), options);
827
- return subpaths.length ? [
828
- {
829
- id: "trace-1",
830
- type: "path",
831
- name: "Trace",
832
- fill: options.color ?? "#000000",
833
- stroke: null,
834
- fillRule: "evenodd",
835
- subpaths
836
- }
837
- ] : [];
838
- }
839
- const count = Math.min(32, Math.max(2, Math.round(options.colors ?? 6)));
840
- const q = quantizeVectorTraceColors(pixels, count);
841
- const order = q.palette.map((_, k) => k).filter((k) => q.counts[k] > 0).sort((a, b) => q.counts[b] - q.counts[a]);
842
- const rank = new Map(order.map((k, r) => [k, r]));
843
- const out = [];
844
- order.forEach((k, r) => {
845
- const field = new Uint8Array(n2);
846
- for (let i = 0; i < n2; i++) {
847
- const ix = q.index[i];
848
- field[i] = ix >= 0 && rank.get(ix) >= r ? 1 : 0;
849
- }
850
- const subpaths = traceField(field, w, h, 0.5, options);
851
- if (subpaths.length)
852
- out.push({
853
- id: `trace-${out.length + 1}`,
854
- type: "path",
855
- name: hex(q.palette[k]),
856
- fill: hex(q.palette[k]),
857
- stroke: null,
858
- fillRule: "evenodd",
859
- subpaths
860
- });
861
- });
862
- return out;
863
- }
864
-
865
582
  // src/vector/stylize.ts
866
583
  var VECTOR_STYLIZE_STYLES = ["mosaic", "halftone", "lowPoly", "hatch"];
867
584
  var VECTOR_STYLIZE_LABELS = { mosaic: "Mosaic", halftone: "Halftone", lowPoly: "Low Poly", hatch: "Engraving" };
@@ -889,7 +606,7 @@ function sampleBox(px, cx, cy, size) {
889
606
  return [s[0] / s[3], s[1] / s[3], s[2] / s[3], s[3] / count];
890
607
  }
891
608
  var darkness = (c) => (1 - (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255) * (c[3] / 255);
892
- var hex2 = (c) => `#${c.slice(0, 3).map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0")).join("")}`;
609
+ var hex = (c) => `#${c.slice(0, 3).map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0")).join("")}`;
893
610
  function paletteOf(colors, count) {
894
611
  const data = new Uint8ClampedArray(colors.length * 4);
895
612
  colors.forEach((c, i) => {
@@ -899,7 +616,7 @@ function paletteOf(colors, count) {
899
616
  data[i * 4 + 3] = c[3];
900
617
  });
901
618
  const q = quantizeVectorTraceColors({ width: colors.length, height: 1, data }, Math.min(32, Math.max(2, Math.round(count))));
902
- return { palette: q.palette.map(hex2), index: q.index };
619
+ return { palette: q.palette.map(hex), index: q.index };
903
620
  }
904
621
  function byColour(shapes, palette, index, name, extra = {}) {
905
622
  const groups = palette.map(() => []);
@@ -3835,8 +3552,8 @@ function vectorPaintToHex(paint) {
3835
3552
  if (typeof paint !== "string") return null;
3836
3553
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());
3837
3554
  if (!m) return null;
3838
- const hex3 = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
3839
- return `#${hex3.toLowerCase()}`;
3555
+ const hex2 = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
3556
+ return `#${hex2.toLowerCase()}`;
3840
3557
  }
3841
3558
  var VECTOR_GRADIENT_TYPE_LABELS = { linear: "Linear", radial: "Radial", conic: "Conic", reflected: "Reflected", diamond: "Diamond" };
3842
3559
  var VECTOR_GRADIENT_SPREAD_LABELS = { pad: "Pad", reflect: "Reflect", repeat: "Repeat" };
@@ -4728,8 +4445,8 @@ function subpathOf(prop, frame) {
4728
4445
  }
4729
4446
  function cssColor(c) {
4730
4447
  const k = c.slice(0, 3).some((v) => v > 1) ? 255 : 1;
4731
- const hex3 = (v) => Math.round(Math.min(1, Math.max(0, v / k)) * 255).toString(16).padStart(2, "0");
4732
- return `#${hex3(c[0] ?? 0)}${hex3(c[1] ?? 0)}${hex3(c[2] ?? 0)}`;
4448
+ const hex2 = (v) => Math.round(Math.min(1, Math.max(0, v / k)) * 255).toString(16).padStart(2, "0");
4449
+ return `#${hex2(c[0] ?? 0)}${hex2(c[1] ?? 0)}${hex2(c[2] ?? 0)}`;
4733
4450
  }
4734
4451
  function geometryOf(it, frame, notes) {
4735
4452
  switch (it.ty) {
@@ -19228,6 +18945,7 @@ export {
19228
18945
  toVectorKeyInput,
19229
18946
  toVectorModifiers,
19230
18947
  toVectorPointerInput,
18948
+ traceField,
19231
18949
  traceVectorImage,
19232
18950
  transformVectorEffects,
19233
18951
  transformVectorGradient,
@@ -11,7 +11,7 @@
11
11
  * stacked: a colour's shape covers the colours traced above it too, so no gaps open between them.
12
12
  */
13
13
  import type { Point } from '../canvas/types.cjs';
14
- import type { VectorPathNode } from './types.cjs';
14
+ import type { VectorPathNode, VectorSubpath } from './types.cjs';
15
15
  /** RGBA pixels, row by row (8 bits a channel). */
16
16
  export interface VectorTracePixels {
17
17
  width: number;
@@ -36,6 +36,11 @@ export interface VectorTraceOptions {
36
36
  minArea?: number;
37
37
  /** 0: straight sides and sharp corners; 1: as smooth as the outline allows (default 0.5). */
38
38
  cornerRoundness?: number;
39
+ /**
40
+ * Give hard pixel edges their square corners back: marching squares cuts each corner where an
41
+ * axis-aligned edge turns with a half-pixel diagonal, which skews a traced rectangle (default false).
42
+ */
43
+ squareCorners?: boolean;
39
44
  }
40
45
  /** The most pixels traced (larger images are refused; scale them down first). */
41
46
  export declare const VECTOR_TRACE_MAX_PIXELS = 4000000;
@@ -44,6 +49,8 @@ export declare const VECTOR_TRACE_MAX_PIXELS = 4000000;
44
49
  * y + 0.5)): the closed contours where the field reaches `threshold`, in pixel coordinates.
45
50
  */
46
51
  export declare function vectorTraceContours(field: ArrayLike<number>, w: number, h: number, threshold: number): Point[][];
52
+ /** The subpaths of a field's outline (holes included; fill even-odd), in its pixel coordinates (pixel centres at +0.5). */
53
+ export declare function traceField(field: ArrayLike<number>, w: number, h: number, threshold: number, o: VectorTraceOptions): VectorSubpath[];
47
54
  /**
48
55
  * The palette of the opaque pixels quantised to `count` colours (median cut on RGB) and each
49
56
  * pixel's palette index (−1 for transparent pixels), with how many pixels each colour got.
@@ -11,7 +11,7 @@
11
11
  * stacked: a colour's shape covers the colours traced above it too, so no gaps open between them.
12
12
  */
13
13
  import type { Point } from '../canvas/types.js';
14
- import type { VectorPathNode } from './types.js';
14
+ import type { VectorPathNode, VectorSubpath } from './types.js';
15
15
  /** RGBA pixels, row by row (8 bits a channel). */
16
16
  export interface VectorTracePixels {
17
17
  width: number;
@@ -36,6 +36,11 @@ export interface VectorTraceOptions {
36
36
  minArea?: number;
37
37
  /** 0: straight sides and sharp corners; 1: as smooth as the outline allows (default 0.5). */
38
38
  cornerRoundness?: number;
39
+ /**
40
+ * Give hard pixel edges their square corners back: marching squares cuts each corner where an
41
+ * axis-aligned edge turns with a half-pixel diagonal, which skews a traced rectangle (default false).
42
+ */
43
+ squareCorners?: boolean;
39
44
  }
40
45
  /** The most pixels traced (larger images are refused; scale them down first). */
41
46
  export declare const VECTOR_TRACE_MAX_PIXELS = 4000000;
@@ -44,6 +49,8 @@ export declare const VECTOR_TRACE_MAX_PIXELS = 4000000;
44
49
  * y + 0.5)): the closed contours where the field reaches `threshold`, in pixel coordinates.
45
50
  */
46
51
  export declare function vectorTraceContours(field: ArrayLike<number>, w: number, h: number, threshold: number): Point[][];
52
+ /** The subpaths of a field's outline (holes included; fill even-odd), in its pixel coordinates (pixel centres at +0.5). */
53
+ export declare function traceField(field: ArrayLike<number>, w: number, h: number, threshold: number, o: VectorTraceOptions): VectorSubpath[];
47
54
  /**
48
55
  * The palette of the opaque pixels quantised to `count` colours (median cut on RGB) and each
49
56
  * pixel's palette index (−1 for transparent pixels), with how many pixels each colour got.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.190.0",
3
+ "version": "0.191.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -314,11 +314,13 @@ in the source frame at the document's bit depth, colour mode and profile. Read
314
314
  | `remove_shape_component` | Delete one component (not the last: delete the layer instead). Returns the shape. |
315
315
  | `merge_shape_components` | Merge Shape Components: the combined area becomes one plain component (live shapes become paths); the look does not change. Returns the shape. |
316
316
  | `get_vector_mask` | Read-only. A layer's vector mask: enabled, linked, inverted, fill rule, density, feather, bounds and components (subpaths in document px), or null. Read it before editing the path. |
317
- | `add_vector_mask` | Layer › Vector Mask on any layer but a shape: `from` `path` (a `shape`, `subpaths` or `components`, like `add_shape_layer`), `reveal-all` or `hide-all` (empty path). Reveals inside the path, sharp at any size; it combines with the raster mask. Returns the vector mask. |
317
+ | `add_vector_mask` | Layer › Vector Mask on any layer but a shape: `from` `path` (a `shape`, `subpaths` or `components`, like `add_shape_layer`), `selection` (traced, `tolerance` px), `reveal-all` or `hide-all` (empty path). Reveals inside the path, sharp at any size; it combines with the raster mask. Returns the vector mask. |
318
318
  | `set_vector_mask_path` | Replace the vector mask's path (properties kept). Returns the vector mask. |
319
319
  | `set_vector_mask_properties` | `enabled`, `linked` (false: stays put when the layer moves), `inverted`, `density`, `feather` px; the path is kept. Returns the vector mask. |
320
320
  | `rasterize_vector_mask` | Multiply the path into the raster mask (adding one) and drop the vector mask, so it can be painted. Returns the mask summary. |
321
321
  | `remove_vector_mask` | Delete the vector mask (the raster mask stays). |
322
+ | `selection_to_path` | Read-only. Make Work Path: the selection's outline as closed Bézier subpaths (`tolerance` px, `cornerRoundness`, `minArea`); returns `svg` and `fillRule: "evenodd"` to pass to `add_vector_mask`, `set_vector_mask_path` or `add_shape_layer`. |
323
+ | `path_to_selection` | Make Selection: fill a path (`shape`, `subpaths`, `components`) into the selection by `mode`, with `feather` and `antiAlias`. Returns the selection. |
322
324
  | `convert_to_smart_object` | Convert sibling `layerIds` (default the active layer) to one smart object at the top one's position, optional `name`. One pixel layer keeps its id, look and properties (filters become smart filters); several layers, groups or adjustments nest as a layer document. Returns the `layer` and the `smartObject` summary. |
323
325
  | `rasterize_smart_object` | Turn a smart object into a pixel layer holding what it shows now (smart filters baked; same id, mask, effects, opacity, blend). Destructive: the contents and filters are gone. Returns the layer. |
324
326
  | `set_smart_object_transform` | Set a smart object's transform absolutely: `corners`, `matrix` (contents → document), or `scaleX` / `scaleY` (percent of the contents), `rotate`, `skewX`, `skewY`, `x`, `y` (where the contents' centre goes; unset numbers keep their values); `warp` preset or mesh in contents px, `null` removes it; `interpolation`. Re-renders from the contents. Returns `placement`, `masksMoved` and `bounds`. |
@@ -67,8 +67,10 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
67
67
  `add_smart_filter`) are also limited to an active selection.
68
68
  - A cutout: `layer_via_copy` (the original stays), or `add_layer_mask` `from: "selection"` on
69
69
  the layer itself. For a hard geometric or traced edge that must stay sharp and editable
70
- (a product, a screen, a window), `add_vector_mask` with the path instead; it moves and
71
- transforms with the layer, and `rasterize_vector_mask` turns it into paintable pixels.
70
+ (a product, a screen, a window), `add_vector_mask` with the path instead (`from:
71
+ "selection"` traces the current selection); it moves and transforms with the layer, and
72
+ `rasterize_vector_mask` turns it into paintable pixels. Trace with `tolerance` 0.5–1 px for
73
+ a tight edge; check the result with `set_mask_view` before you go on.
72
74
  - Painting or fills inside it: keep the selection active and paint; `stroke_selection` for
73
75
  an outline that follows it.
74
76
  - Removing what it covers: `content_aware_fill` `output: "new-layer"`.
@@ -103,6 +105,7 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
103
105
  | An existing layer mask | `load_selection_from_mask` (`source: "vector"` for a vector mask) |
104
106
  | A cutout, shape or text already on its own layer | `select_layer_transparency` (exact, soft where the layer is) |
105
107
  | A dead pixel row or column | `select_line`, then fill or heal it |
108
+ | Smooth curves you can describe (a logo, a road, a lens) | `path_to_selection` with Bézier `subpaths` or SVG path data |
106
109
 
107
110
  ## Recipes
108
111
 
@@ -199,6 +202,7 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
199
202
  - `set_mask_view`: a layer mask alone in grey or as a red overlay, to check it
200
203
  - `add_vector_mask`, `set_vector_mask_path`, `set_vector_mask_properties`, `get_vector_mask`: sharp, editable path masks
201
204
  - `rasterize_vector_mask`, `remove_vector_mask`: turn a vector mask into pixels, or drop it
205
+ - `selection_to_path`, `path_to_selection`: a selection's outline as Bézier paths, and a path back into a selection
202
206
  - `modify_selection`: expand, contract, feather, border, smooth
203
207
  - `refine_selection`: snap the edge to the image
204
208
  - `transform_selection`: move or scale the outline only