@pixerate/editor 0.190.0 → 0.192.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.192.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -307,6 +307,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
307
307
  | `set_paragraph_style` | Set `align`, `autoLeading`, indents, `spaceBefore` / `spaceAfter` on every paragraph `start`..`end` touches (default all). Returns the text layer. |
308
308
  | `set_text_frame` | `kind: point` (one line per paragraph) or `kind: box` with `width` and `height` (wrapping paragraph text), or resize a box; the first line stays put. Returns the text layer and its lines. |
309
309
  | `convert_text_to_shape` | Type › Convert to Shape: glyph outlines become a shape layer with the same id (text no longer editable; fill from the first run's colour; `warnings` lists losses). Needs an outline font shaper. Destructive. |
310
+ | `select_text_shape` | Horizontal / Vertical Type Mask: select the shape of `text` at `x`, `y` (style, box, rotation as `add_text_layer`; `orientation` `vertical` stacks the letters), by `mode`, with `feather`. No layer is added. Returns the selection. |
310
311
  | `add_shape_layer` | Add a vector shape layer above the active layer (or placed with `aboveId` / `belowId` / `parentId` / `index`) and make it active: `shape` (live rect, ellipse, polygon, star or line, document px), `subpaths` (SVG path data or anchors) or several `components` with path ops; `fill` (colour, gradient or pattern, default the foreground colour; `null` none), `stroke` (`width`, `paint`, `align`, `cap`, `join`, `miterLimit`, `dashes` in widths), `fillRule`, `name`, `opacity`, `blend`. Returns the `layer` and the `shape`. One undo step. |
311
312
  | `update_shape` | Restyle a shape: `fill` (replace, `null` removes), `stroke` (only the settings given change; `null` removes; a new stroke needs `width` and `paint`), `fillRule`. Geometry is unchanged. Returns the shape. |
312
313
  | `set_shape_component` | Change component `componentIndex` (from `get_shape_layer`): `shape` fields (without `kind`, or with the same kind, unset fields keep their values), new `subpaths` (ends the live shape), or `op`. Returns the shape. |
@@ -314,11 +315,13 @@ in the source frame at the document's bit depth, colour mode and profile. Read
314
315
  | `remove_shape_component` | Delete one component (not the last: delete the layer instead). Returns the shape. |
315
316
  | `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
317
  | `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. |
318
+ | `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
319
  | `set_vector_mask_path` | Replace the vector mask's path (properties kept). Returns the vector mask. |
319
320
  | `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
321
  | `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
322
  | `remove_vector_mask` | Delete the vector mask (the raster mask stays). |
323
+ | `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`. |
324
+ | `path_to_selection` | Make Selection: fill a path (`shape`, `subpaths`, `components`) into the selection by `mode`, with `feather` and `antiAlias`. Returns the selection. |
322
325
  | `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
326
  | `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
327
  | `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`. |
@@ -49,6 +49,8 @@ Detail for the `pixerate-image-editor` skill: how to draw and edit vector shapes
49
49
  refused: rasterize first. Masks, clipping, blend modes and effects work as on any layer.
50
50
  5. **Outlines and pixels**: `convert_text_to_shape` makes glyph outlines a shape layer (needs an
51
51
  outline font shaper; the browser canvas cannot); `rasterize_layer` makes it pixels.
52
+ For a selection in the shape of text (photo-filled letters, text cut out of the image) use
53
+ `select_text_shape`, the Type Mask: it lays the text out the same way but adds no layer.
52
54
 
53
55
  ## Photoshop files
54
56
 
@@ -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,8 @@ 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 |
109
+ | Letters (photo-filled text, text cut out of the image) | `select_text_shape` (check fonts with `list_fonts`), then `add_layer_mask` or `layer_via_copy` |
106
110
 
107
111
  ## Recipes
108
112
 
@@ -199,6 +203,8 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
199
203
  - `set_mask_view`: a layer mask alone in grey or as a red overlay, to check it
200
204
  - `add_vector_mask`, `set_vector_mask_path`, `set_vector_mask_properties`, `get_vector_mask`: sharp, editable path masks
201
205
  - `rasterize_vector_mask`, `remove_vector_mask`: turn a vector mask into pixels, or drop it
206
+ - `selection_to_path`, `path_to_selection`: a selection's outline as Bézier paths, and a path back into a selection
207
+ - `select_text_shape`: the Type Mask, a selection in the shape of text
202
208
  - `modify_selection`: expand, contract, feather, border, smooth
203
209
  - `refine_selection`: snap the edge to the image
204
210
  - `transform_selection`: move or scale the outline only