@pixerate/editor 0.212.0 → 0.214.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.
- package/dist/{chunk-5OC22IEO.js → chunk-DJBYJOHM.js} +419 -1
- package/dist/{chunk-3T5VCDIV.cjs → chunk-LCCMOWR7.cjs} +419 -1
- package/dist/image-editor/controller.d.cts +37 -2
- package/dist/image-editor/controller.d.ts +37 -2
- package/dist/image-editor/index.cjs +1618 -585
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +1333 -300
- package/dist/image-editor/path-edit.d.cts +97 -0
- package/dist/image-editor/path-edit.d.ts +97 -0
- package/dist/image-editor/paths.d.cts +14 -0
- package/dist/image-editor/paths.d.ts +14 -0
- package/dist/image-editor/renderer.d.cts +3 -0
- package/dist/image-editor/renderer.d.ts +3 -0
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/selection/inspect.d.cts +70 -0
- package/dist/image-editor/selection/inspect.d.ts +70 -0
- package/dist/image-editor/tool-groups.d.cts +2 -2
- package/dist/image-editor/tool-groups.d.ts +2 -2
- package/dist/image-editor/types.d.cts +3 -1
- package/dist/image-editor/types.d.ts +3 -1
- package/dist/vector/index.cjs +46 -433
- package/dist/vector/index.js +30 -417
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +3 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +9 -9
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
bendSegment,
|
|
2
3
|
loadOpenTypeFont,
|
|
3
4
|
parseOpenTypeFont,
|
|
5
|
+
removeAnchorsKeepingShape,
|
|
4
6
|
traceField
|
|
5
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-DJBYJOHM.js";
|
|
6
8
|
import {
|
|
7
9
|
ColorGmm,
|
|
8
10
|
DEFAULT_REFINE_EDGE_PARAMS,
|
|
@@ -668,6 +670,8 @@ import {
|
|
|
668
670
|
applyMatrix,
|
|
669
671
|
bezierToSubpaths,
|
|
670
672
|
caretAt,
|
|
673
|
+
cloneAnchor,
|
|
674
|
+
convertAnchor,
|
|
671
675
|
createCanvasTextShaper,
|
|
672
676
|
createMetricTextShaper,
|
|
673
677
|
createOutlineTextShaper,
|
|
@@ -679,23 +683,30 @@ import {
|
|
|
679
683
|
fitRuns,
|
|
680
684
|
fontOf,
|
|
681
685
|
identityWarpMesh,
|
|
686
|
+
inferAnchorType,
|
|
687
|
+
insertAnchor,
|
|
682
688
|
invertMatrix,
|
|
683
689
|
layoutText,
|
|
684
690
|
lineToTextSpace,
|
|
691
|
+
moveAnchorHandle,
|
|
685
692
|
multiplyMatrices,
|
|
686
693
|
paragraphRanges,
|
|
687
694
|
pullWarpMesh,
|
|
695
|
+
removeAnchors,
|
|
688
696
|
resizeWarpMesh,
|
|
689
697
|
restyleRange,
|
|
690
698
|
rotateMatrix,
|
|
691
699
|
roundSubpaths,
|
|
692
700
|
spliceRuns,
|
|
693
701
|
styleAt,
|
|
702
|
+
subpathSegment,
|
|
703
|
+
subpathSegmentCount,
|
|
694
704
|
subpathsToBezier,
|
|
695
705
|
textArrowStep,
|
|
696
706
|
textIndexAt,
|
|
697
707
|
textLineMove,
|
|
698
708
|
textSelectionQuads,
|
|
709
|
+
translateAnchor,
|
|
699
710
|
translateMatrix,
|
|
700
711
|
warpMeshColumns,
|
|
701
712
|
warpMeshEditPoints,
|
|
@@ -707,7 +718,9 @@ import {
|
|
|
707
718
|
import "../chunk-F7YA3MMC.js";
|
|
708
719
|
import "../chunk-M6DNZNVY.js";
|
|
709
720
|
import {
|
|
710
|
-
bezierPathToSvg
|
|
721
|
+
bezierPathToSvg,
|
|
722
|
+
pointInBezierPath,
|
|
723
|
+
segmentNearest
|
|
711
724
|
} from "../chunk-WEIK3UYB.js";
|
|
712
725
|
import "../chunk-PWBZH3FK.js";
|
|
713
726
|
import {
|
|
@@ -1175,10 +1188,10 @@ function emptyHistogram() {
|
|
|
1175
1188
|
max: 0
|
|
1176
1189
|
};
|
|
1177
1190
|
}
|
|
1178
|
-
function addToHistogram(h, data, stride,
|
|
1179
|
-
for (let y =
|
|
1180
|
-
let i = (y * stride +
|
|
1181
|
-
for (let x = 0; x <
|
|
1191
|
+
function addToHistogram(h, data, stride, rect2) {
|
|
1192
|
+
for (let y = rect2.y; y < rect2.y + rect2.height; y++) {
|
|
1193
|
+
let i = (y * stride + rect2.x) * 4;
|
|
1194
|
+
for (let x = 0; x < rect2.width; x++, i += 4) {
|
|
1182
1195
|
if (!(data[i + 3] > 0)) {
|
|
1183
1196
|
h.transparent++;
|
|
1184
1197
|
continue;
|
|
@@ -1639,6 +1652,63 @@ var ImageEditorRenderer = class {
|
|
|
1639
1652
|
}
|
|
1640
1653
|
ctx.restore();
|
|
1641
1654
|
}
|
|
1655
|
+
if (options.pathEdit) {
|
|
1656
|
+
const e = options.pathEdit;
|
|
1657
|
+
ctx.save();
|
|
1658
|
+
try {
|
|
1659
|
+
const scr = (p) => projectImagePoint(p, projection);
|
|
1660
|
+
const trace2 = (c) => {
|
|
1661
|
+
const [a, b, d, f] = c.map(scr);
|
|
1662
|
+
ctx.moveTo(a.x, a.y);
|
|
1663
|
+
if (f) ctx.bezierCurveTo(b.x, b.y, d.x, d.y, f.x, f.y);
|
|
1664
|
+
else ctx.lineTo(b.x, b.y);
|
|
1665
|
+
};
|
|
1666
|
+
ctx.beginPath();
|
|
1667
|
+
e.curves.forEach(trace2);
|
|
1668
|
+
ctx.lineWidth = 1.5 * u;
|
|
1669
|
+
ctx.strokeStyle = OVERLAY_HANDLE_EDGE;
|
|
1670
|
+
ctx.stroke();
|
|
1671
|
+
if (e.preview) {
|
|
1672
|
+
ctx.beginPath();
|
|
1673
|
+
trace2(e.preview);
|
|
1674
|
+
ctx.setLineDash([4 * u, 3 * u]);
|
|
1675
|
+
ctx.stroke();
|
|
1676
|
+
ctx.setLineDash([]);
|
|
1677
|
+
}
|
|
1678
|
+
ctx.beginPath();
|
|
1679
|
+
for (const h of e.handles) {
|
|
1680
|
+
const a = scr(h.anchor);
|
|
1681
|
+
const q = scr(h.point);
|
|
1682
|
+
ctx.moveTo(a.x, a.y);
|
|
1683
|
+
ctx.lineTo(q.x, q.y);
|
|
1684
|
+
}
|
|
1685
|
+
ctx.lineWidth = 1 * u;
|
|
1686
|
+
ctx.stroke();
|
|
1687
|
+
for (const h of e.handles) {
|
|
1688
|
+
const q = scr(h.point);
|
|
1689
|
+
ctx.beginPath();
|
|
1690
|
+
ctx.arc(q.x, q.y, 3 * u, 0, Math.PI * 2);
|
|
1691
|
+
ctx.fillStyle = OVERLAY_HANDLE_EDGE;
|
|
1692
|
+
ctx.fill();
|
|
1693
|
+
}
|
|
1694
|
+
const size = 6 * u;
|
|
1695
|
+
ctx.lineWidth = 1 * u;
|
|
1696
|
+
for (const a of e.anchors) {
|
|
1697
|
+
const q = scr(a.point);
|
|
1698
|
+
ctx.fillStyle = a.selected ? OVERLAY_HANDLE_EDGE : "#ffffff";
|
|
1699
|
+
ctx.fillRect(q.x - size / 2, q.y - size / 2, size, size);
|
|
1700
|
+
ctx.strokeRect(q.x - size / 2, q.y - size / 2, size, size);
|
|
1701
|
+
}
|
|
1702
|
+
if (e.marquee) {
|
|
1703
|
+
const a = scr({ x: e.marquee.x, y: e.marquee.y });
|
|
1704
|
+
const b = scr({ x: e.marquee.x + e.marquee.width, y: e.marquee.y + e.marquee.height });
|
|
1705
|
+
ctx.setLineDash([4 * u, 3 * u]);
|
|
1706
|
+
ctx.strokeRect(Math.min(a.x, b.x), Math.min(a.y, b.y), Math.abs(b.x - a.x), Math.abs(b.y - a.y));
|
|
1707
|
+
}
|
|
1708
|
+
} catch {
|
|
1709
|
+
}
|
|
1710
|
+
ctx.restore();
|
|
1711
|
+
}
|
|
1642
1712
|
if (isActivelyCropping) {
|
|
1643
1713
|
const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
|
|
1644
1714
|
this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop), u);
|
|
@@ -1742,7 +1812,7 @@ var ImageEditorRenderer = class {
|
|
|
1742
1812
|
if (!source.enabled || !source.maskSource) return null;
|
|
1743
1813
|
return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;
|
|
1744
1814
|
},
|
|
1745
|
-
rasterizeAnnotation: (annotation,
|
|
1815
|
+
rasterizeAnnotation: (annotation, rect2) => this.rasterizeAnnotation(annotation, rect2),
|
|
1746
1816
|
version: `${host?.version() ?? ""}:${this.assetVersion}`,
|
|
1747
1817
|
globalLight: host?.globalLight?.(),
|
|
1748
1818
|
pattern: host?.pattern ? (id) => host.pattern(id) : void 0
|
|
@@ -1754,15 +1824,15 @@ var ImageEditorRenderer = class {
|
|
|
1754
1824
|
for (const l of flattenLayers(layers)) {
|
|
1755
1825
|
const source = l.mask?.source;
|
|
1756
1826
|
if (source?.enabled && source.maskSource) {
|
|
1757
|
-
const
|
|
1758
|
-
if (this.depthMaps.has(
|
|
1827
|
+
const key2 = `${source.maskSource}|${width}x${height}`;
|
|
1828
|
+
if (this.depthMaps.has(key2)) continue;
|
|
1759
1829
|
try {
|
|
1760
1830
|
const maskImg = await this.imageCache.get(source.maskSource);
|
|
1761
1831
|
const canvas = this.createCanvas(width, height);
|
|
1762
1832
|
const mctx = canvas.getContext("2d");
|
|
1763
1833
|
const data = mctx ? (mctx.drawImage(maskImg, 0, 0, width, height), mctx.getImageData?.(0, 0, width, height)) : null;
|
|
1764
1834
|
if (data) {
|
|
1765
|
-
this.depthMaps.set(
|
|
1835
|
+
this.depthMaps.set(key2, { width, height, data: data.data });
|
|
1766
1836
|
this.assetVersion++;
|
|
1767
1837
|
}
|
|
1768
1838
|
} catch {
|
|
@@ -1783,34 +1853,34 @@ var ImageEditorRenderer = class {
|
|
|
1783
1853
|
return Boolean(ctx && typeof ctx.getImageData === "function");
|
|
1784
1854
|
}
|
|
1785
1855
|
/** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */
|
|
1786
|
-
rasterizeAnnotation(annotation,
|
|
1787
|
-
if (!this.scratch) this.scratch = this.createCanvas(
|
|
1856
|
+
rasterizeAnnotation(annotation, rect2) {
|
|
1857
|
+
if (!this.scratch) this.scratch = this.createCanvas(rect2.width, rect2.height);
|
|
1788
1858
|
const canvas = this.scratch;
|
|
1789
|
-
if (canvas.width !==
|
|
1790
|
-
canvas.width =
|
|
1791
|
-
canvas.height =
|
|
1859
|
+
if (canvas.width !== rect2.width || canvas.height !== rect2.height) {
|
|
1860
|
+
canvas.width = rect2.width;
|
|
1861
|
+
canvas.height = rect2.height;
|
|
1792
1862
|
}
|
|
1793
1863
|
const ctx = canvas.getContext("2d");
|
|
1794
1864
|
if (!ctx || typeof ctx.getImageData !== "function") return null;
|
|
1795
1865
|
try {
|
|
1796
1866
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1797
|
-
ctx.clearRect(0, 0,
|
|
1798
|
-
ctx.setTransform(1, 0, 0, 1, -
|
|
1867
|
+
ctx.clearRect(0, 0, rect2.width, rect2.height);
|
|
1868
|
+
ctx.setTransform(1, 0, 0, 1, -rect2.x, -rect2.y);
|
|
1799
1869
|
const img = annotation.type === "image" ? this.imageCache.peek(annotation.src) ?? null : null;
|
|
1800
1870
|
this.drawAnnotationShape(ctx, annotation, img);
|
|
1801
1871
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1802
|
-
return new Uint8ClampedArray(ctx.getImageData(0, 0,
|
|
1872
|
+
return new Uint8ClampedArray(ctx.getImageData(0, 0, rect2.width, rect2.height).data);
|
|
1803
1873
|
} catch {
|
|
1804
1874
|
return null;
|
|
1805
1875
|
}
|
|
1806
1876
|
}
|
|
1807
|
-
putTile(ctx, data, tx, ty,
|
|
1808
|
-
const ox = tx * TILE -
|
|
1809
|
-
const oy = ty * TILE -
|
|
1877
|
+
putTile(ctx, data, tx, ty, rect2) {
|
|
1878
|
+
const ox = tx * TILE - rect2.x;
|
|
1879
|
+
const oy = ty * TILE - rect2.y;
|
|
1810
1880
|
const x0 = Math.max(0, -ox);
|
|
1811
1881
|
const y0 = Math.max(0, -oy);
|
|
1812
|
-
const x1 = Math.min(TILE,
|
|
1813
|
-
const y1 = Math.min(TILE,
|
|
1882
|
+
const x1 = Math.min(TILE, rect2.width - ox);
|
|
1883
|
+
const y1 = Math.min(TILE, rect2.height - oy);
|
|
1814
1884
|
if (x1 <= x0 || y1 <= y0) return;
|
|
1815
1885
|
const img = ctx.createImageData(TILE, TILE);
|
|
1816
1886
|
img.data.set(data);
|
|
@@ -1822,23 +1892,23 @@ var ImageEditorRenderer = class {
|
|
|
1822
1892
|
* uploading only the tiles that changed since the last render.
|
|
1823
1893
|
*/
|
|
1824
1894
|
updateComposite(layers, region, width, height, options = {}) {
|
|
1825
|
-
const
|
|
1895
|
+
const rect2 = coveringRect(region);
|
|
1826
1896
|
let comp = this.comp;
|
|
1827
1897
|
let fresh = false;
|
|
1828
|
-
if (!comp || comp.rect.x !==
|
|
1829
|
-
const canvas = this.createCanvas(
|
|
1830
|
-
comp = this.comp = new CompositeCanvas(canvas,
|
|
1898
|
+
if (!comp || comp.rect.x !== rect2.x || comp.rect.y !== rect2.y || comp.rect.width !== rect2.width || comp.rect.height !== rect2.height) {
|
|
1899
|
+
const canvas = this.createCanvas(rect2.width, rect2.height);
|
|
1900
|
+
comp = this.comp = new CompositeCanvas(canvas, rect2);
|
|
1831
1901
|
fresh = true;
|
|
1832
1902
|
}
|
|
1833
1903
|
const ctx = comp.canvas.getContext("2d");
|
|
1834
1904
|
if (!ctx || typeof ctx.createImageData !== "function") return null;
|
|
1835
1905
|
let changed = false;
|
|
1836
|
-
this.compositor.compositeTiles(layers,
|
|
1837
|
-
const
|
|
1838
|
-
if (comp.tiles.get(
|
|
1839
|
-
comp.tiles.set(
|
|
1840
|
-
comp.histograms.delete(
|
|
1841
|
-
this.putTile(ctx, data, tx, ty,
|
|
1906
|
+
this.compositor.compositeTiles(layers, rect2, this.compositorSources(width, height), (tx, ty, data) => {
|
|
1907
|
+
const key2 = tileKey(tx, ty);
|
|
1908
|
+
if (comp.tiles.get(key2) === data) return;
|
|
1909
|
+
comp.tiles.set(key2, data);
|
|
1910
|
+
comp.histograms.delete(key2);
|
|
1911
|
+
this.putTile(ctx, data, tx, ty, rect2);
|
|
1842
1912
|
changed = true;
|
|
1843
1913
|
}, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? void 0 : options.dirty });
|
|
1844
1914
|
if (changed) comp.version++;
|
|
@@ -1857,8 +1927,8 @@ var ImageEditorRenderer = class {
|
|
|
1857
1927
|
const rotated = getRotatedSize(region.width, region.height, transform);
|
|
1858
1928
|
const baseW = Math.max(1, Math.round(rotated.width));
|
|
1859
1929
|
const baseH = Math.max(1, Math.round(rotated.height));
|
|
1860
|
-
const
|
|
1861
|
-
if (this.cachedPostCanvas && this.cachedPostKey ===
|
|
1930
|
+
const key2 = `${this.displayKey}:${comp.version}|${region.x},${region.y},${region.width},${region.height}|${normalizeAngle(transform.rotate)},${transform.flipH},${transform.flipV}|${opacity}|${blur}`;
|
|
1931
|
+
if (this.cachedPostCanvas && this.cachedPostKey === key2) return { canvas: this.cachedPostCanvas, histogram: this.cachedPostHistogram };
|
|
1862
1932
|
const canvas = this.cachedPostCanvas && this.cachedPostCanvas.width === baseW && this.cachedPostCanvas.height === baseH ? this.cachedPostCanvas : this.createCanvas(baseW, baseH);
|
|
1863
1933
|
const ctx = canvas.getContext("2d");
|
|
1864
1934
|
if (!ctx) return null;
|
|
@@ -1872,7 +1942,7 @@ var ImageEditorRenderer = class {
|
|
|
1872
1942
|
ctx.restore();
|
|
1873
1943
|
const histogram = this.applyPost(ctx, baseW, baseH, state);
|
|
1874
1944
|
this.cachedPostCanvas = canvas;
|
|
1875
|
-
this.cachedPostKey =
|
|
1945
|
+
this.cachedPostKey = key2;
|
|
1876
1946
|
this.cachedPostHistogram = histogram;
|
|
1877
1947
|
return { canvas, histogram };
|
|
1878
1948
|
}
|
|
@@ -1908,11 +1978,11 @@ var ImageEditorRenderer = class {
|
|
|
1908
1978
|
/** A layer's mask as canvas-sized coverage for its mask view, kept while the mask is unchanged. */
|
|
1909
1979
|
maskViewSelection(layer, width, height) {
|
|
1910
1980
|
if (!layer.mask || layer.mask.source || width < 1 || height < 1) return null;
|
|
1911
|
-
const
|
|
1981
|
+
const key2 = `${width}x${height}:${layer.type === "pixel" ? `${layer.offset.x},${layer.offset.y}` : ""}`;
|
|
1912
1982
|
const cached = this.maskViewCache.get(layer.mask.surface);
|
|
1913
|
-
if (cached?.key ===
|
|
1983
|
+
if (cached?.key === key2) return cached.mask;
|
|
1914
1984
|
const mask = layerMaskSelection(layer, width, height);
|
|
1915
|
-
if (mask) this.maskViewCache.set(layer.mask.surface, { key, mask });
|
|
1985
|
+
if (mask) this.maskViewCache.set(layer.mask.surface, { key: key2, mask });
|
|
1916
1986
|
return mask;
|
|
1917
1987
|
}
|
|
1918
1988
|
/** A mask alone in grey (white = revealed), opaque, drawn in natural image space. */
|
|
@@ -1931,16 +2001,16 @@ var ImageEditorRenderer = class {
|
|
|
1931
2001
|
}
|
|
1932
2002
|
/** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
|
|
1933
2003
|
drawMaskOverlay(ctx, mask, style) {
|
|
1934
|
-
const
|
|
2004
|
+
const key2 = `${style.color?.join(",")}|${style.opacity}|${style.indicates}`;
|
|
1935
2005
|
let cached = this.overlayCache.get(mask);
|
|
1936
|
-
if (!cached || cached.key !==
|
|
2006
|
+
if (!cached || cached.key !== key2) {
|
|
1937
2007
|
const canvas = this.createCanvas(mask.width, mask.height);
|
|
1938
2008
|
const mctx = canvas.getContext("2d");
|
|
1939
2009
|
if (!mctx || typeof mctx.createImageData !== "function") return;
|
|
1940
2010
|
const img = mctx.createImageData(mask.width, mask.height);
|
|
1941
2011
|
img.data.set(quickMaskPixels(mask, style));
|
|
1942
2012
|
mctx.putImageData(img, 0, 0);
|
|
1943
|
-
cached = { key, canvas };
|
|
2013
|
+
cached = { key: key2, canvas };
|
|
1944
2014
|
this.overlayCache.set(mask, cached);
|
|
1945
2015
|
}
|
|
1946
2016
|
ctx.drawImage(cached.canvas, 0, 0, mask.width, mask.height);
|
|
@@ -2001,11 +2071,11 @@ var ImageEditorRenderer = class {
|
|
|
2001
2071
|
if (typeof ctx.setLineDash === "function") ctx.setLineDash([]);
|
|
2002
2072
|
}
|
|
2003
2073
|
getSourceId(img) {
|
|
2004
|
-
const
|
|
2005
|
-
let id = this.sourceIds.get(
|
|
2074
|
+
const key2 = img;
|
|
2075
|
+
let id = this.sourceIds.get(key2);
|
|
2006
2076
|
if (id === void 0) {
|
|
2007
2077
|
id = ++this.nextSourceId;
|
|
2008
|
-
this.sourceIds.set(
|
|
2078
|
+
this.sourceIds.set(key2, id);
|
|
2009
2079
|
}
|
|
2010
2080
|
return id;
|
|
2011
2081
|
}
|
|
@@ -2104,15 +2174,15 @@ var ImageEditorRenderer = class {
|
|
|
2104
2174
|
break;
|
|
2105
2175
|
}
|
|
2106
2176
|
case "rect": {
|
|
2107
|
-
const
|
|
2108
|
-
ctx.strokeStyle =
|
|
2109
|
-
ctx.lineWidth =
|
|
2110
|
-
if (
|
|
2111
|
-
ctx.fillStyle =
|
|
2112
|
-
ctx.fillRect(
|
|
2177
|
+
const rect2 = ann;
|
|
2178
|
+
ctx.strokeStyle = rect2.strokeColor;
|
|
2179
|
+
ctx.lineWidth = rect2.strokeWidth;
|
|
2180
|
+
if (rect2.fillColor) {
|
|
2181
|
+
ctx.fillStyle = rect2.fillColor;
|
|
2182
|
+
ctx.fillRect(rect2.x, rect2.y, rect2.width, rect2.height);
|
|
2113
2183
|
}
|
|
2114
|
-
if (
|
|
2115
|
-
ctx.strokeRect(
|
|
2184
|
+
if (rect2.strokeWidth > 0) {
|
|
2185
|
+
ctx.strokeRect(rect2.x, rect2.y, rect2.width, rect2.height);
|
|
2116
2186
|
}
|
|
2117
2187
|
break;
|
|
2118
2188
|
}
|
|
@@ -2303,12 +2373,12 @@ var ImageEditorRenderer = class {
|
|
|
2303
2373
|
const split = splitOverlayAnnotations(layers);
|
|
2304
2374
|
overlay = split.overlay.map((l) => l.annotation);
|
|
2305
2375
|
await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);
|
|
2306
|
-
const
|
|
2307
|
-
const comp = this.createCanvas(
|
|
2376
|
+
const rect2 = coveringRect(region);
|
|
2377
|
+
const comp = this.createCanvas(rect2.width, rect2.height);
|
|
2308
2378
|
const cctx = comp.getContext("2d");
|
|
2309
2379
|
if (cctx && typeof cctx.createImageData === "function") {
|
|
2310
|
-
this.exportCompositor.compositeTiles(split.composite,
|
|
2311
|
-
this.putTile(cctx, data, tx, ty,
|
|
2380
|
+
this.exportCompositor.compositeTiles(split.composite, rect2, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {
|
|
2381
|
+
this.putTile(cctx, data, tx, ty, rect2);
|
|
2312
2382
|
});
|
|
2313
2383
|
}
|
|
2314
2384
|
baseCanvas = this.createCanvas(baseW, baseH);
|
|
@@ -2319,7 +2389,7 @@ var ImageEditorRenderer = class {
|
|
|
2319
2389
|
bctx.beginPath();
|
|
2320
2390
|
bctx.rect(region.x, region.y, region.width, region.height);
|
|
2321
2391
|
bctx.clip();
|
|
2322
|
-
bctx.drawImage(comp,
|
|
2392
|
+
bctx.drawImage(comp, rect2.x, rect2.y);
|
|
2323
2393
|
bctx.restore();
|
|
2324
2394
|
this.applyPost(bctx, baseW, baseH, state);
|
|
2325
2395
|
}
|
|
@@ -2447,10 +2517,10 @@ function getBaseProjection(region, transform, baseW, baseH) {
|
|
|
2447
2517
|
};
|
|
2448
2518
|
}
|
|
2449
2519
|
var CompositeCanvas = class {
|
|
2450
|
-
constructor(canvas,
|
|
2520
|
+
constructor(canvas, rect2) {
|
|
2451
2521
|
this.canvas = canvas;
|
|
2452
|
-
this.rect =
|
|
2453
|
-
this.region =
|
|
2522
|
+
this.rect = rect2;
|
|
2523
|
+
this.region = rect2;
|
|
2454
2524
|
}
|
|
2455
2525
|
canvas;
|
|
2456
2526
|
rect;
|
|
@@ -2463,18 +2533,18 @@ var CompositeCanvas = class {
|
|
|
2463
2533
|
if (!this.tiles.size) return null;
|
|
2464
2534
|
const r = coveringRect(this.region);
|
|
2465
2535
|
const parts = [];
|
|
2466
|
-
for (const [
|
|
2467
|
-
let h = this.histograms.get(
|
|
2536
|
+
for (const [key2, data] of this.tiles) {
|
|
2537
|
+
let h = this.histograms.get(key2);
|
|
2468
2538
|
if (!h) {
|
|
2469
|
-
const tx =
|
|
2470
|
-
const ty = Math.floor(
|
|
2539
|
+
const tx = key2 % 65536 - 32768;
|
|
2540
|
+
const ty = Math.floor(key2 / 65536) - 32768;
|
|
2471
2541
|
const x0 = Math.max(r.x, tx * TILE);
|
|
2472
2542
|
const y0 = Math.max(r.y, ty * TILE);
|
|
2473
2543
|
const x1 = Math.min(r.x + r.width, tx * TILE + TILE);
|
|
2474
2544
|
const y1 = Math.min(r.y + r.height, ty * TILE + TILE);
|
|
2475
2545
|
h = emptyHistogram();
|
|
2476
2546
|
if (x1 > x0 && y1 > y0) addToHistogram(h, data, TILE, { x: x0 - tx * TILE, y: y0 - ty * TILE, width: x1 - x0, height: y1 - y0 });
|
|
2477
|
-
this.histograms.set(
|
|
2547
|
+
this.histograms.set(key2, h);
|
|
2478
2548
|
}
|
|
2479
2549
|
parts.push(h);
|
|
2480
2550
|
}
|
|
@@ -2496,6 +2566,423 @@ function hexRgb(hex) {
|
|
|
2496
2566
|
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
2497
2567
|
}
|
|
2498
2568
|
|
|
2569
|
+
// src/image-editor/selection/inspect.ts
|
|
2570
|
+
var MASK_EDGE_THRESHOLDS = { halo: 0.35, alignment: 0.5, jaggedness: 15, soft: 0.9 };
|
|
2571
|
+
var STRONG_EDGE = 0.1;
|
|
2572
|
+
function toLab(r, g, b) {
|
|
2573
|
+
const lin = (v) => {
|
|
2574
|
+
v /= 255;
|
|
2575
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
2576
|
+
};
|
|
2577
|
+
const R = lin(r);
|
|
2578
|
+
const G = lin(g);
|
|
2579
|
+
const B = lin(b);
|
|
2580
|
+
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
2581
|
+
const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
|
|
2582
|
+
const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
|
|
2583
|
+
const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
|
|
2584
|
+
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
|
|
2585
|
+
}
|
|
2586
|
+
var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
2587
|
+
var r3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
2588
|
+
function inspectMaskEdge(mask, image, options = {}) {
|
|
2589
|
+
const b = mask.bounds;
|
|
2590
|
+
if (!b) throw new Error("The mask is empty: there is no edge to inspect.");
|
|
2591
|
+
const minArea = Math.max(1, options.minArea ?? 16);
|
|
2592
|
+
const W = mask.width;
|
|
2593
|
+
const H = mask.height;
|
|
2594
|
+
const a = mask.data;
|
|
2595
|
+
const alpha = (x, y) => x < 0 || y < 0 || x >= W || y >= H ? 0 : a[y * W + x];
|
|
2596
|
+
const img = image.data;
|
|
2597
|
+
const rgb = (x, y) => {
|
|
2598
|
+
const ix = x - image.x;
|
|
2599
|
+
const iy = y - image.y;
|
|
2600
|
+
if (ix < 0 || iy < 0 || ix >= image.width || iy >= image.height) return null;
|
|
2601
|
+
const i = (iy * image.width + ix) * 4;
|
|
2602
|
+
if (img[i + 3] < 128) return null;
|
|
2603
|
+
return [img[i], img[i + 1], img[i + 2]];
|
|
2604
|
+
};
|
|
2605
|
+
const premul = (x, y, c) => {
|
|
2606
|
+
const ix = Math.min(image.width - 1, Math.max(0, x - image.x));
|
|
2607
|
+
const iy = Math.min(image.height - 1, Math.max(0, y - image.y));
|
|
2608
|
+
const i = (iy * image.width + ix) * 4;
|
|
2609
|
+
return img[i + c] * img[i + 3] / 255;
|
|
2610
|
+
};
|
|
2611
|
+
const gradCache = /* @__PURE__ */ new Map();
|
|
2612
|
+
const gradient = (x, y) => {
|
|
2613
|
+
const k = y * W + x;
|
|
2614
|
+
const hit = gradCache.get(k);
|
|
2615
|
+
if (hit !== void 0) return hit;
|
|
2616
|
+
let best = 0;
|
|
2617
|
+
for (let c = 0; c < 3; c++) {
|
|
2618
|
+
const p = (dx, dy) => premul(x + dx, y + dy, c);
|
|
2619
|
+
const gx = p(1, -1) + 2 * p(1, 0) + p(1, 1) - p(-1, -1) - 2 * p(-1, 0) - p(-1, 1);
|
|
2620
|
+
const gy = p(-1, 1) + 2 * p(0, 1) + p(1, 1) - p(-1, -1) - 2 * p(0, -1) - p(1, -1);
|
|
2621
|
+
best = Math.max(best, Math.hypot(gx, gy) / 1020);
|
|
2622
|
+
}
|
|
2623
|
+
gradCache.set(k, best);
|
|
2624
|
+
return best;
|
|
2625
|
+
};
|
|
2626
|
+
const B = Math.min(256, Math.max(24, Math.round(Math.max(W, H) / 12)));
|
|
2627
|
+
const BW = Math.ceil(W / B);
|
|
2628
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
2629
|
+
const block = (x, y) => {
|
|
2630
|
+
const k = Math.floor(y / B) * BW + Math.floor(x / B);
|
|
2631
|
+
let blk = blocks.get(k);
|
|
2632
|
+
if (!blk) blocks.set(k, blk = { edge: 0, aligned: 0, halo: 0, haloN: 0, turn: 0, len: 0, x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity });
|
|
2633
|
+
return blk;
|
|
2634
|
+
};
|
|
2635
|
+
const grow = (blk, x, y) => {
|
|
2636
|
+
blk.x0 = Math.min(blk.x0, x);
|
|
2637
|
+
blk.y0 = Math.min(blk.y0, y);
|
|
2638
|
+
blk.x1 = Math.max(blk.x1, x);
|
|
2639
|
+
blk.y1 = Math.max(blk.y1, y);
|
|
2640
|
+
};
|
|
2641
|
+
const x0 = Math.max(0, b.x - 1);
|
|
2642
|
+
const y0 = Math.max(0, b.y - 1);
|
|
2643
|
+
const x1 = Math.min(W, b.x + b.width + 1);
|
|
2644
|
+
const y1 = Math.min(H, b.y + b.height + 1);
|
|
2645
|
+
let area = 0;
|
|
2646
|
+
let edgeLength = 0;
|
|
2647
|
+
let aligned = 0;
|
|
2648
|
+
let haloSum = 0;
|
|
2649
|
+
let haloN = 0;
|
|
2650
|
+
let band = 0;
|
|
2651
|
+
let soft = 0;
|
|
2652
|
+
for (let y = y0; y < y1; y++) {
|
|
2653
|
+
for (let x = x0; x < x1; x++) {
|
|
2654
|
+
const v = a[y * W + x];
|
|
2655
|
+
if (v >= 128) area++;
|
|
2656
|
+
let lo = false;
|
|
2657
|
+
let hi = false;
|
|
2658
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2659
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2660
|
+
const nx = x + dx;
|
|
2661
|
+
const ny = y + dy;
|
|
2662
|
+
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
|
2663
|
+
if (a[ny * W + nx] >= 128) hi = true;
|
|
2664
|
+
else lo = true;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
const partial = v > 0 && v < 255;
|
|
2668
|
+
if (hi && lo || partial) {
|
|
2669
|
+
band++;
|
|
2670
|
+
if (partial) soft++;
|
|
2671
|
+
}
|
|
2672
|
+
if (v < 128) continue;
|
|
2673
|
+
const inCanvasLow = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && a[ny * W + nx] < 128;
|
|
2674
|
+
if (!(inCanvasLow(x - 1, y) || inCanvasLow(x + 1, y) || inCanvasLow(x, y - 1) || inCanvasLow(x, y + 1))) continue;
|
|
2675
|
+
edgeLength++;
|
|
2676
|
+
const blk = block(x, y);
|
|
2677
|
+
blk.edge++;
|
|
2678
|
+
grow(blk, x, y);
|
|
2679
|
+
let strong = false;
|
|
2680
|
+
for (let dy = -2; dy <= 2 && !strong; dy++) {
|
|
2681
|
+
for (let dx = -2; dx <= 2 && !strong; dx++) {
|
|
2682
|
+
const nx = x + dx;
|
|
2683
|
+
const ny = y + dy;
|
|
2684
|
+
if (nx >= 0 && ny >= 0 && nx < W && ny < H && gradient(nx, ny) >= STRONG_EDGE) strong = true;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
if (strong) {
|
|
2688
|
+
aligned++;
|
|
2689
|
+
blk.aligned++;
|
|
2690
|
+
}
|
|
2691
|
+
const s = (dx, dy) => alpha(Math.min(W - 1, Math.max(0, x + dx)), Math.min(H - 1, Math.max(0, y + dy)));
|
|
2692
|
+
const gx = s(1, -1) + 2 * s(1, 0) + s(1, 1) - s(-1, -1) - 2 * s(-1, 0) - s(-1, 1);
|
|
2693
|
+
const gy = s(-1, 1) + 2 * s(0, 1) + s(1, 1) - s(-1, -1) - 2 * s(0, -1) - s(1, -1);
|
|
2694
|
+
const len = Math.hypot(gx, gy);
|
|
2695
|
+
if (len < 1e-6) continue;
|
|
2696
|
+
const ux = gx / len;
|
|
2697
|
+
const uy = gy / len;
|
|
2698
|
+
const at = (d) => rgb(Math.round(x + ux * d), Math.round(y + uy * d));
|
|
2699
|
+
const ring = at(1.5);
|
|
2700
|
+
const deep = at(5);
|
|
2701
|
+
const out = at(-3);
|
|
2702
|
+
if (!ring || !deep || !out) continue;
|
|
2703
|
+
const labOut = toLab(...out);
|
|
2704
|
+
const span = dist(toLab(...deep), labOut);
|
|
2705
|
+
if (span < 10) continue;
|
|
2706
|
+
const h = Math.min(1, Math.max(0, 1 - dist(toLab(...ring), labOut) / span));
|
|
2707
|
+
haloSum += h;
|
|
2708
|
+
haloN++;
|
|
2709
|
+
blk.halo += h;
|
|
2710
|
+
blk.haloN++;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
let turnExcess = 0;
|
|
2714
|
+
let outline = 0;
|
|
2715
|
+
for (const loop of traceLoops(a, W, H, x0 - 1, y0 - 1, x1 + 1, y1 + 1)) {
|
|
2716
|
+
const pts = resample(loop, 2);
|
|
2717
|
+
const n = pts.length;
|
|
2718
|
+
if (n < 6) continue;
|
|
2719
|
+
const L = n * 2;
|
|
2720
|
+
let total = 0;
|
|
2721
|
+
let net = 0;
|
|
2722
|
+
const turns = [];
|
|
2723
|
+
for (let i = 0; i < n; i++) {
|
|
2724
|
+
const p0 = pts[(i + n - 1) % n];
|
|
2725
|
+
const p1 = pts[i];
|
|
2726
|
+
const p2 = pts[(i + 1) % n];
|
|
2727
|
+
let d = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) - Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
|
|
2728
|
+
while (d > Math.PI) d -= 2 * Math.PI;
|
|
2729
|
+
while (d <= -Math.PI) d += 2 * Math.PI;
|
|
2730
|
+
turns.push(d);
|
|
2731
|
+
total += Math.abs(d);
|
|
2732
|
+
net += d;
|
|
2733
|
+
}
|
|
2734
|
+
const closure = Math.abs(net);
|
|
2735
|
+
turnExcess += Math.max(0, total - closure);
|
|
2736
|
+
outline += L;
|
|
2737
|
+
for (let i = 0; i < n; i++) {
|
|
2738
|
+
const [px2, py] = pts[i];
|
|
2739
|
+
if (px2 < 0 || py < 0 || px2 >= W || py >= H) continue;
|
|
2740
|
+
const blk = block(Math.floor(px2), Math.floor(py));
|
|
2741
|
+
blk.turn += Math.abs(turns[i]) - closure / n;
|
|
2742
|
+
blk.len += 2;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
const jaggedness = outline ? 100 * turnExcess / outline : 0;
|
|
2746
|
+
const { holes, islands } = regions(a, W, H, b, minArea);
|
|
2747
|
+
const haloScore = haloN ? haloSum / haloN : 0;
|
|
2748
|
+
const edgeAlignment = edgeLength ? aligned / edgeLength : 0;
|
|
2749
|
+
const softFraction = band ? soft / band : 0;
|
|
2750
|
+
const T2 = MASK_EDGE_THRESHOLDS;
|
|
2751
|
+
const candidates = [];
|
|
2752
|
+
const pad = (r) => {
|
|
2753
|
+
const px0 = Math.max(0, r.x - 8);
|
|
2754
|
+
const py0 = Math.max(0, r.y - 8);
|
|
2755
|
+
return { x: px0, y: py0, width: Math.min(W, r.x + r.width + 8) - px0, height: Math.min(H, r.y + r.height + 8) - py0 };
|
|
2756
|
+
};
|
|
2757
|
+
for (const blk of blocks.values()) {
|
|
2758
|
+
if (blk.edge < 8) continue;
|
|
2759
|
+
const bounds = pad({ x: blk.x0, y: blk.y0, width: blk.x1 - blk.x0 + 1, height: blk.y1 - blk.y0 + 1 });
|
|
2760
|
+
const opts = [];
|
|
2761
|
+
if (blk.haloN >= 8) {
|
|
2762
|
+
const h = blk.halo / blk.haloN;
|
|
2763
|
+
if (h >= T2.halo) opts.push({ severity: h, reason: `halo ${r3(h)}: the colour just inside the edge matches the background` });
|
|
2764
|
+
}
|
|
2765
|
+
const al = blk.aligned / blk.edge;
|
|
2766
|
+
if (al < T2.alignment) opts.push({ severity: (1 - al) * 0.9, reason: `alignment ${r3(al)}: the edge is off the image's edges` });
|
|
2767
|
+
if (blk.len >= 16) {
|
|
2768
|
+
const j = 100 * Math.max(0, blk.turn) / blk.len;
|
|
2769
|
+
if (j >= T2.jaggedness) opts.push({ severity: Math.min(1, j / (4 * T2.jaggedness)), reason: `jaggedness ${r3(j)}: the outline zigzags` });
|
|
2770
|
+
}
|
|
2771
|
+
const top = opts.sort((p, q) => q.severity - p.severity)[0];
|
|
2772
|
+
if (top) candidates.push({ ...top, bounds });
|
|
2773
|
+
}
|
|
2774
|
+
const imageArea = W * H;
|
|
2775
|
+
for (const r of holes.largest) candidates.push({ severity: Math.min(0.95, 0.5 + r.area / (0.01 * imageArea)), bounds: pad(r.bounds), reason: `hole of ${r.area} px inside the selection` });
|
|
2776
|
+
for (const r of islands.largest) candidates.push({ severity: Math.min(0.95, 0.5 + r.area / (0.01 * imageArea)), bounds: pad(r.bounds), reason: `island of ${r.area} px apart from the main selection` });
|
|
2777
|
+
const worst = candidates.sort((p, q) => q.severity - p.severity).slice(0, 5).map(({ bounds, reason }) => ({ bounds, reason }));
|
|
2778
|
+
const issues = [];
|
|
2779
|
+
if (haloScore >= T2.halo)
|
|
2780
|
+
issues.push(`Halo (haloScore ${r3(haloScore)} \u2265 ${T2.halo}): background colour is left inside the edge. Contract the edge (modify_selection contract 1\u20132 px, or refine_selection shiftEdge -20 to -40), or decontaminate on a new-layer output.`);
|
|
2781
|
+
if (edgeLength >= 16 && edgeAlignment < T2.alignment)
|
|
2782
|
+
issues.push(`Off the image's edges (edgeAlignment ${r3(edgeAlignment)} < ${T2.alignment}): much of the edge does not follow edges in the image. Rebuild it with select_subject, select_object or quick_select, or refine_selection with a larger radius.`);
|
|
2783
|
+
if (jaggedness >= T2.jaggedness) issues.push(`Jagged (jaggedness ${r3(jaggedness)} \u2265 ${T2.jaggedness}): the outline zigzags. Smooth it (refine_selection smooth 20\u201340, or modify_selection smooth).`);
|
|
2784
|
+
if (softFraction >= T2.soft && edgeAlignment >= T2.alignment)
|
|
2785
|
+
issues.push(`Very soft (softFraction ${r3(softFraction)} \u2265 ${T2.soft}): almost the whole edge is partly selected. If the subject has hard edges, raise refine_selection contrast or lower feather.`);
|
|
2786
|
+
if (holes.count) issues.push(`${holes.count} hole(s) (${holes.totalArea} px) inside the selection. If they belong to the subject, fill them (quick_select or select_polygon with mode add).`);
|
|
2787
|
+
if (islands.count) issues.push(`${islands.count} island(s) (${islands.totalArea} px) apart from the main selection. If they are stray, remove them (select_polygon or quick_select with mode subtract).`);
|
|
2788
|
+
return {
|
|
2789
|
+
area,
|
|
2790
|
+
bounds: { ...b },
|
|
2791
|
+
edgeLength,
|
|
2792
|
+
haloScore: r3(haloScore),
|
|
2793
|
+
edgeAlignment: r3(edgeAlignment),
|
|
2794
|
+
jaggedness: r3(jaggedness),
|
|
2795
|
+
softFraction: r3(softFraction),
|
|
2796
|
+
holes,
|
|
2797
|
+
islands,
|
|
2798
|
+
worst,
|
|
2799
|
+
issues
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
|
|
2803
|
+
const w = rx1 - rx0;
|
|
2804
|
+
const h = ry1 - ry0;
|
|
2805
|
+
const f = (i, j) => {
|
|
2806
|
+
const x = rx0 + i;
|
|
2807
|
+
const y = ry0 + j;
|
|
2808
|
+
return x < 0 || y < 0 || x >= W || y >= H ? 0 : a[y * W + x];
|
|
2809
|
+
};
|
|
2810
|
+
const on = (i, j) => f(i, j) >= 128;
|
|
2811
|
+
const V = w * h;
|
|
2812
|
+
const point = (id) => {
|
|
2813
|
+
const vertical = id >= V;
|
|
2814
|
+
const k = vertical ? id - V : id;
|
|
2815
|
+
const i = k % w;
|
|
2816
|
+
const j = (k - i) / w;
|
|
2817
|
+
const p = f(i, j);
|
|
2818
|
+
const q = vertical ? f(i, j + 1) : f(i + 1, j);
|
|
2819
|
+
const t = q === p ? 0.5 : Math.min(1, Math.max(0, (127.5 - p) / (q - p)));
|
|
2820
|
+
return vertical ? [rx0 + i, ry0 + j + t] : [rx0 + i + t, ry0 + j];
|
|
2821
|
+
};
|
|
2822
|
+
const segs = [];
|
|
2823
|
+
const touch = /* @__PURE__ */ new Map();
|
|
2824
|
+
const add2 = (e1, e2) => {
|
|
2825
|
+
const s = segs.length / 2;
|
|
2826
|
+
segs.push(e1, e2);
|
|
2827
|
+
for (const e of [e1, e2]) {
|
|
2828
|
+
const list = touch.get(e);
|
|
2829
|
+
if (list) list.push(s);
|
|
2830
|
+
else touch.set(e, [s]);
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
for (let j = 0; j < h - 1; j++) {
|
|
2834
|
+
for (let i = 0; i < w - 1; i++) {
|
|
2835
|
+
const c = (on(i, j) ? 1 : 0) | (on(i + 1, j) ? 2 : 0) | (on(i + 1, j + 1) ? 4 : 0) | (on(i, j + 1) ? 8 : 0);
|
|
2836
|
+
if (c === 0 || c === 15) continue;
|
|
2837
|
+
const top = j * w + i;
|
|
2838
|
+
const bottom = (j + 1) * w + i;
|
|
2839
|
+
const left = V + j * w + i;
|
|
2840
|
+
const right = V + j * w + i + 1;
|
|
2841
|
+
const centre = (f(i, j) + f(i + 1, j) + f(i + 1, j + 1) + f(i, j + 1)) / 4 >= 128;
|
|
2842
|
+
switch (c) {
|
|
2843
|
+
case 1:
|
|
2844
|
+
case 14:
|
|
2845
|
+
add2(left, top);
|
|
2846
|
+
break;
|
|
2847
|
+
case 2:
|
|
2848
|
+
case 13:
|
|
2849
|
+
add2(top, right);
|
|
2850
|
+
break;
|
|
2851
|
+
case 3:
|
|
2852
|
+
case 12:
|
|
2853
|
+
add2(left, right);
|
|
2854
|
+
break;
|
|
2855
|
+
case 4:
|
|
2856
|
+
case 11:
|
|
2857
|
+
add2(right, bottom);
|
|
2858
|
+
break;
|
|
2859
|
+
case 6:
|
|
2860
|
+
case 9:
|
|
2861
|
+
add2(top, bottom);
|
|
2862
|
+
break;
|
|
2863
|
+
case 7:
|
|
2864
|
+
case 8:
|
|
2865
|
+
add2(left, bottom);
|
|
2866
|
+
break;
|
|
2867
|
+
case 5:
|
|
2868
|
+
if (centre) {
|
|
2869
|
+
add2(top, right);
|
|
2870
|
+
add2(left, bottom);
|
|
2871
|
+
} else {
|
|
2872
|
+
add2(left, top);
|
|
2873
|
+
add2(right, bottom);
|
|
2874
|
+
}
|
|
2875
|
+
break;
|
|
2876
|
+
case 10:
|
|
2877
|
+
if (centre) {
|
|
2878
|
+
add2(left, top);
|
|
2879
|
+
add2(right, bottom);
|
|
2880
|
+
} else {
|
|
2881
|
+
add2(top, right);
|
|
2882
|
+
add2(left, bottom);
|
|
2883
|
+
}
|
|
2884
|
+
break;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
const used = new Uint8Array(segs.length / 2);
|
|
2889
|
+
const loops = [];
|
|
2890
|
+
for (let s0 = 0; s0 < used.length; s0++) {
|
|
2891
|
+
if (used[s0]) continue;
|
|
2892
|
+
const loop = [];
|
|
2893
|
+
let s = s0;
|
|
2894
|
+
let from = segs[s * 2];
|
|
2895
|
+
while (!used[s]) {
|
|
2896
|
+
used[s] = 1;
|
|
2897
|
+
loop.push(point(from));
|
|
2898
|
+
const to = segs[s * 2] === from ? segs[s * 2 + 1] : segs[s * 2];
|
|
2899
|
+
const next = touch.get(to).find((t) => !used[t]);
|
|
2900
|
+
from = to;
|
|
2901
|
+
if (next === void 0) break;
|
|
2902
|
+
s = next;
|
|
2903
|
+
}
|
|
2904
|
+
loops.push(loop);
|
|
2905
|
+
}
|
|
2906
|
+
return loops;
|
|
2907
|
+
}
|
|
2908
|
+
function resample(loop, step) {
|
|
2909
|
+
const n = loop.length;
|
|
2910
|
+
if (n < 2) return [];
|
|
2911
|
+
const out = [loop[0]];
|
|
2912
|
+
let carry = 0;
|
|
2913
|
+
for (let i = 0; i < n; i++) {
|
|
2914
|
+
const [ax, ay] = loop[i];
|
|
2915
|
+
const [bx, by] = loop[(i + 1) % n];
|
|
2916
|
+
const seg = Math.hypot(bx - ax, by - ay);
|
|
2917
|
+
let t = step - carry;
|
|
2918
|
+
while (t <= seg) {
|
|
2919
|
+
out.push([ax + (bx - ax) * t / seg, ay + (by - ay) * t / seg]);
|
|
2920
|
+
t += step;
|
|
2921
|
+
}
|
|
2922
|
+
carry = seg - (t - step);
|
|
2923
|
+
}
|
|
2924
|
+
if (out.length > 1 && Math.hypot(out[out.length - 1][0] - out[0][0], out[out.length - 1][1] - out[0][1]) < step / 2) out.pop();
|
|
2925
|
+
return out;
|
|
2926
|
+
}
|
|
2927
|
+
function regions(a, W, H, b, minArea) {
|
|
2928
|
+
const x0 = b.x - 1;
|
|
2929
|
+
const y0 = b.y - 1;
|
|
2930
|
+
const w = b.width + 2;
|
|
2931
|
+
const h = b.height + 2;
|
|
2932
|
+
const on = (i, j) => {
|
|
2933
|
+
const x = x0 + i;
|
|
2934
|
+
const y = y0 + j;
|
|
2935
|
+
return x >= 0 && y >= 0 && x < W && y < H && a[y * W + x] >= 128;
|
|
2936
|
+
};
|
|
2937
|
+
const label = new Int32Array(w * h);
|
|
2938
|
+
const stack = new Int32Array(w * h);
|
|
2939
|
+
const found = [];
|
|
2940
|
+
const N4 = [[1, 0], [-1, 0], [0, 1], [0, -1]];
|
|
2941
|
+
const N8 = [...N4, [1, 1], [1, -1], [-1, 1], [-1, -1]];
|
|
2942
|
+
for (let start = 0; start < w * h; start++) {
|
|
2943
|
+
if (label[start]) continue;
|
|
2944
|
+
const si = start % w;
|
|
2945
|
+
const sj = (start - si) / w;
|
|
2946
|
+
const fg = on(si, sj);
|
|
2947
|
+
const id = found.length + 1;
|
|
2948
|
+
const r = { fg, area: 0, bx0: si, by0: sj, bx1: si, by1: sj, outer: false };
|
|
2949
|
+
let top = 0;
|
|
2950
|
+
stack[top++] = start;
|
|
2951
|
+
label[start] = id;
|
|
2952
|
+
while (top) {
|
|
2953
|
+
const k = stack[--top];
|
|
2954
|
+
const i = k % w;
|
|
2955
|
+
const j = (k - i) / w;
|
|
2956
|
+
r.area++;
|
|
2957
|
+
if (i < r.bx0) r.bx0 = i;
|
|
2958
|
+
if (i > r.bx1) r.bx1 = i;
|
|
2959
|
+
if (j < r.by0) r.by0 = j;
|
|
2960
|
+
if (j > r.by1) r.by1 = j;
|
|
2961
|
+
if (i === 0 || j === 0 || i === w - 1 || j === h - 1) r.outer = true;
|
|
2962
|
+
for (const [dx, dy] of fg ? N8 : N4) {
|
|
2963
|
+
const ni = i + dx;
|
|
2964
|
+
const nj = j + dy;
|
|
2965
|
+
if (ni < 0 || nj < 0 || ni >= w || nj >= h) continue;
|
|
2966
|
+
const nk = nj * w + ni;
|
|
2967
|
+
if (label[nk] || on(ni, nj) !== fg) continue;
|
|
2968
|
+
label[nk] = id;
|
|
2969
|
+
stack[top++] = nk;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
found.push(r);
|
|
2973
|
+
}
|
|
2974
|
+
const box = (r) => ({ x: x0 + r.bx0, y: y0 + r.by0, width: r.bx1 - r.bx0 + 1, height: r.by1 - r.by0 + 1 });
|
|
2975
|
+
const summarize = (list) => {
|
|
2976
|
+
const kept = list.filter((r) => r.area >= minArea).sort((p, q) => q.area - p.area);
|
|
2977
|
+
return { count: kept.length, totalArea: kept.reduce((s, r) => s + r.area, 0), largest: kept.slice(0, 10).map((r) => ({ area: r.area, bounds: box(r) })) };
|
|
2978
|
+
};
|
|
2979
|
+
const pieces = found.filter((r) => r.fg).sort((p, q) => q.area - p.area);
|
|
2980
|
+
return {
|
|
2981
|
+
holes: summarize(found.filter((r) => !r.fg && !r.outer)),
|
|
2982
|
+
islands: summarize(pieces.slice(1))
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2499
2986
|
// src/image-editor/selection/path.ts
|
|
2500
2987
|
function selectionToSubpaths(mask, options = {}) {
|
|
2501
2988
|
const b = mask.bounds;
|
|
@@ -2547,12 +3034,12 @@ function pathToSelection(components, width, height, options = {}) {
|
|
|
2547
3034
|
const rx1 = Math.min(width, Math.ceil(x1));
|
|
2548
3035
|
const ry1 = Math.min(height, Math.ceil(y1));
|
|
2549
3036
|
if (!(rx1 > rx0 && ry1 > ry0)) return createSelectionMask(width, height);
|
|
2550
|
-
const
|
|
2551
|
-
const cov = componentsCoverage(components,
|
|
3037
|
+
const rect2 = { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 };
|
|
3038
|
+
const cov = componentsCoverage(components, rect2, options.fillRule ?? "nonzero");
|
|
2552
3039
|
const hard = options.antiAlias === false;
|
|
2553
3040
|
const bytes = new Uint8Array(cov.length);
|
|
2554
3041
|
for (let i = 0; i < cov.length; i++) bytes[i] = hard ? cov[i] >= 0.5 ? 255 : 0 : coverageToByte(cov[i]);
|
|
2555
|
-
const mask = selectionFromRegion(width, height,
|
|
3042
|
+
const mask = selectionFromRegion(width, height, rect2, bytes);
|
|
2556
3043
|
const feather = options.feather ?? 0;
|
|
2557
3044
|
return feather > 0 && mask.bounds ? modifySelection(mask, "feather", feather) : mask;
|
|
2558
3045
|
}
|
|
@@ -2591,11 +3078,11 @@ var EdgeMap = class {
|
|
|
2591
3078
|
block(x, y) {
|
|
2592
3079
|
const bx = Math.floor(x / BLOCK);
|
|
2593
3080
|
const by = Math.floor(y / BLOCK);
|
|
2594
|
-
const
|
|
2595
|
-
let b = this.blocks.get(
|
|
3081
|
+
const key2 = by * 65536 + bx;
|
|
3082
|
+
let b = this.blocks.get(key2);
|
|
2596
3083
|
if (!b) {
|
|
2597
3084
|
b = this.computeBlock(bx * BLOCK, by * BLOCK);
|
|
2598
|
-
this.blocks.set(
|
|
3085
|
+
this.blocks.set(key2, b);
|
|
2599
3086
|
}
|
|
2600
3087
|
return b;
|
|
2601
3088
|
}
|
|
@@ -2689,7 +3176,7 @@ var Heap = class {
|
|
|
2689
3176
|
ids = new Int32Array(256);
|
|
2690
3177
|
keys = new Float64Array(256);
|
|
2691
3178
|
size = 0;
|
|
2692
|
-
push(id,
|
|
3179
|
+
push(id, key2) {
|
|
2693
3180
|
if (this.size === this.ids.length) {
|
|
2694
3181
|
const ids = new Int32Array(this.size * 2);
|
|
2695
3182
|
ids.set(this.ids);
|
|
@@ -2701,31 +3188,31 @@ var Heap = class {
|
|
|
2701
3188
|
let i = this.size++;
|
|
2702
3189
|
while (i > 0) {
|
|
2703
3190
|
const p = i - 1 >> 1;
|
|
2704
|
-
if (this.keys[p] <=
|
|
3191
|
+
if (this.keys[p] <= key2) break;
|
|
2705
3192
|
this.ids[i] = this.ids[p];
|
|
2706
3193
|
this.keys[i] = this.keys[p];
|
|
2707
3194
|
i = p;
|
|
2708
3195
|
}
|
|
2709
3196
|
this.ids[i] = id;
|
|
2710
|
-
this.keys[i] =
|
|
3197
|
+
this.keys[i] = key2;
|
|
2711
3198
|
}
|
|
2712
3199
|
/** Removes the cheapest entry; read it from `topId` / `topKey` first. */
|
|
2713
3200
|
pop() {
|
|
2714
3201
|
const n = --this.size;
|
|
2715
3202
|
const id = this.ids[n];
|
|
2716
|
-
const
|
|
3203
|
+
const key2 = this.keys[n];
|
|
2717
3204
|
let i = 0;
|
|
2718
3205
|
for (; ; ) {
|
|
2719
3206
|
let c = 2 * i + 1;
|
|
2720
3207
|
if (c >= n) break;
|
|
2721
3208
|
if (c + 1 < n && this.keys[c + 1] < this.keys[c]) c++;
|
|
2722
|
-
if (this.keys[c] >=
|
|
3209
|
+
if (this.keys[c] >= key2) break;
|
|
2723
3210
|
this.ids[i] = this.ids[c];
|
|
2724
3211
|
this.keys[i] = this.keys[c];
|
|
2725
3212
|
i = c;
|
|
2726
3213
|
}
|
|
2727
3214
|
this.ids[i] = id;
|
|
2728
|
-
this.keys[i] =
|
|
3215
|
+
this.keys[i] = key2;
|
|
2729
3216
|
}
|
|
2730
3217
|
get topId() {
|
|
2731
3218
|
return this.ids[0];
|
|
@@ -2784,19 +3271,19 @@ function liveWire(map, from, to, band) {
|
|
|
2784
3271
|
}
|
|
2785
3272
|
}
|
|
2786
3273
|
const scale = maxStrength > 0 ? 1 / maxStrength : 0;
|
|
2787
|
-
const
|
|
3274
|
+
const dist2 = new Float64Array(n).fill(Infinity);
|
|
2788
3275
|
const prev = new Int32Array(n).fill(-1);
|
|
2789
3276
|
const done = new Uint8Array(n);
|
|
2790
3277
|
const start = (ay - ry0) * w + (ax - rx0);
|
|
2791
3278
|
const goal = (by - ry0) * w + (bx - rx0);
|
|
2792
|
-
|
|
3279
|
+
dist2[start] = 0;
|
|
2793
3280
|
const heap = new Heap();
|
|
2794
3281
|
heap.push(start, 0);
|
|
2795
3282
|
while (heap.size) {
|
|
2796
3283
|
const p = heap.topId;
|
|
2797
3284
|
const d = heap.topKey;
|
|
2798
3285
|
heap.pop();
|
|
2799
|
-
if (done[p] || d >
|
|
3286
|
+
if (done[p] || d > dist2[p]) continue;
|
|
2800
3287
|
done[p] = 1;
|
|
2801
3288
|
if (p === goal) break;
|
|
2802
3289
|
const x = p % w;
|
|
@@ -2817,8 +3304,8 @@ function liveWire(map, from, to, band) {
|
|
|
2817
3304
|
const fZ = 1 - zero[q];
|
|
2818
3305
|
const cost = (W_ZERO * fZ + W_GRADIENT * fG + W_DIRECTION * fD + 0.01) * step;
|
|
2819
3306
|
const nd = d + cost;
|
|
2820
|
-
if (nd <
|
|
2821
|
-
|
|
3307
|
+
if (nd < dist2[q]) {
|
|
3308
|
+
dist2[q] = nd;
|
|
2822
3309
|
prev[q] = p;
|
|
2823
3310
|
heap.push(q, nd);
|
|
2824
3311
|
}
|
|
@@ -3534,8 +4021,8 @@ function colorRange(pixels, samples, options = {}) {
|
|
|
3534
4021
|
const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));
|
|
3535
4022
|
let k = Math.min(1, Math.max(0, 1 - d / f));
|
|
3536
4023
|
if (k > 0 && radius !== void 0 && s.at) {
|
|
3537
|
-
const
|
|
3538
|
-
k *= Math.min(1, Math.max(0, 1 -
|
|
4024
|
+
const dist2 = Math.hypot(x - s.at.x, y - s.at.y);
|
|
4025
|
+
k *= Math.min(1, Math.max(0, 1 - dist2 / radius));
|
|
3539
4026
|
}
|
|
3540
4027
|
if (k > best) best = k;
|
|
3541
4028
|
}
|
|
@@ -3885,29 +4372,29 @@ var ImageEffectsEngine = class {
|
|
|
3885
4372
|
return this.pending.size > 0;
|
|
3886
4373
|
}
|
|
3887
4374
|
/** See `SmartEffectResolver`. */
|
|
3888
|
-
resolve(filter, input, width, height,
|
|
3889
|
-
const hit = this.cache.get(
|
|
4375
|
+
resolve(filter, input, width, height, key2) {
|
|
4376
|
+
const hit = this.cache.get(key2);
|
|
3890
4377
|
if (hit) {
|
|
3891
|
-
this.cache.delete(
|
|
3892
|
-
this.cache.set(
|
|
4378
|
+
this.cache.delete(key2);
|
|
4379
|
+
this.cache.set(key2, hit);
|
|
3893
4380
|
return { result: hit, pending: false };
|
|
3894
4381
|
}
|
|
3895
|
-
if (this.pending.has(
|
|
4382
|
+
if (this.pending.has(key2)) return { result: null, pending: true };
|
|
3896
4383
|
const provider = this.slot.provider;
|
|
3897
|
-
if (!provider || this.failures.has(
|
|
4384
|
+
if (!provider || this.failures.has(key2)) return { result: null, pending: false };
|
|
3898
4385
|
const generation = this.generation;
|
|
3899
4386
|
const source = { width, height, data: new Uint8ClampedArray(input) };
|
|
3900
4387
|
const run = this.filter(provider, filter.effect, filter.values, source).then((out) => {
|
|
3901
4388
|
if (generation !== this.generation) return;
|
|
3902
|
-
this.cache.set(
|
|
4389
|
+
this.cache.set(key2, out);
|
|
3903
4390
|
while (this.cache.size > this.maxEntries) this.cache.delete(this.cache.keys().next().value);
|
|
3904
4391
|
}).catch((error) => {
|
|
3905
|
-
if (generation === this.generation) this.failures.set(
|
|
4392
|
+
if (generation === this.generation) this.failures.set(key2, error instanceof Error ? error : new Error(String(error)));
|
|
3906
4393
|
}).finally(() => {
|
|
3907
|
-
if (this.pending.get(
|
|
4394
|
+
if (this.pending.get(key2) === run) this.pending.delete(key2);
|
|
3908
4395
|
if (generation === this.generation) this.notify();
|
|
3909
4396
|
});
|
|
3910
|
-
this.pending.set(
|
|
4397
|
+
this.pending.set(key2, run);
|
|
3911
4398
|
return { result: null, pending: true };
|
|
3912
4399
|
}
|
|
3913
4400
|
/** A filter effect over `source`: its result, or (for an overlay) the effect drawn over it. */
|
|
@@ -3928,12 +4415,12 @@ var ImageEffectsEngine = class {
|
|
|
3928
4415
|
return out.width === size.width && out.height === size.height ? out : createEffectPixels(size.width, size.height);
|
|
3929
4416
|
}
|
|
3930
4417
|
/** Why the result for `key` failed, if it did. */
|
|
3931
|
-
failure(
|
|
3932
|
-
return this.failures.get(
|
|
4418
|
+
failure(key2) {
|
|
4419
|
+
return this.failures.get(key2);
|
|
3933
4420
|
}
|
|
3934
4421
|
/** Every failure so far. */
|
|
3935
4422
|
failuresList() {
|
|
3936
|
-
return [...this.failures].map(([
|
|
4423
|
+
return [...this.failures].map(([key2, error]) => ({ key: key2, error }));
|
|
3937
4424
|
}
|
|
3938
4425
|
/** Resolves when nothing is rendering. */
|
|
3939
4426
|
async idle() {
|
|
@@ -4556,10 +5043,10 @@ function resolveProfile(doc) {
|
|
|
4556
5043
|
let p;
|
|
4557
5044
|
if (doc.builtin) p = builtinProfile(doc.builtin);
|
|
4558
5045
|
else if (doc.icc) {
|
|
4559
|
-
const
|
|
4560
|
-
p = fromBytes.get(
|
|
5046
|
+
const key2 = bytesToBase64(doc.icc);
|
|
5047
|
+
p = fromBytes.get(key2) ?? parseProfile(doc.icc);
|
|
4561
5048
|
if (fromBytes.size > 32) fromBytes.clear();
|
|
4562
|
-
fromBytes.set(
|
|
5049
|
+
fromBytes.set(key2, p);
|
|
4563
5050
|
} else p = srgbProfile();
|
|
4564
5051
|
resolved.set(doc, p);
|
|
4565
5052
|
return p;
|
|
@@ -4649,12 +5136,12 @@ function convertHex(hex, from, to, intent = "relative", bpc = true) {
|
|
|
4649
5136
|
return `#${c(out[0])}${c(out[1])}${c(out[2])}`;
|
|
4650
5137
|
}
|
|
4651
5138
|
function convertColorFields(value, convert) {
|
|
4652
|
-
const walk = (v,
|
|
4653
|
-
if (typeof v === "string") return /color$/i.test(
|
|
5139
|
+
const walk = (v, key2) => {
|
|
5140
|
+
if (typeof v === "string") return /color$/i.test(key2) && /^#[0-9a-f]{6}$/i.test(v) ? convert(v) : v;
|
|
4654
5141
|
if (Array.isArray(v)) {
|
|
4655
5142
|
let changed = false;
|
|
4656
5143
|
const out = v.map((x) => {
|
|
4657
|
-
const y = walk(x,
|
|
5144
|
+
const y = walk(x, key2);
|
|
4658
5145
|
if (y !== x) changed = true;
|
|
4659
5146
|
return y;
|
|
4660
5147
|
});
|
|
@@ -4676,7 +5163,7 @@ function convertSurfaceColors(surface, t) {
|
|
|
4676
5163
|
if (surface.channels !== 4 || t.inputs !== 3 || t.outputs !== 3) return surface;
|
|
4677
5164
|
const tiles = /* @__PURE__ */ new Map();
|
|
4678
5165
|
const memo = /* @__PURE__ */ new Map();
|
|
4679
|
-
for (const [
|
|
5166
|
+
for (const [key2, tile] of surface.tiles) {
|
|
4680
5167
|
let next = memo.get(tile);
|
|
4681
5168
|
if (next === void 0) {
|
|
4682
5169
|
if (tile.deep instanceof Uint16Array) {
|
|
@@ -4699,7 +5186,7 @@ function convertSurfaceColors(surface, t) {
|
|
|
4699
5186
|
}
|
|
4700
5187
|
memo.set(tile, next);
|
|
4701
5188
|
}
|
|
4702
|
-
if (next) tiles.set(
|
|
5189
|
+
if (next) tiles.set(key2, next);
|
|
4703
5190
|
}
|
|
4704
5191
|
return new TiledSurface(4, tiles, surface.defaultValue);
|
|
4705
5192
|
}
|
|
@@ -4716,22 +5203,22 @@ function paintCoverageInto(base, coverage, apply, rects, depth = null) {
|
|
|
4716
5203
|
const done = /* @__PURE__ */ new Set();
|
|
4717
5204
|
for (const r of rects) {
|
|
4718
5205
|
for (const t of tilesInRect({ x: r.x - apply.offset.x, y: r.y - apply.offset.y, width: r.width, height: r.height })) {
|
|
4719
|
-
const
|
|
4720
|
-
if (done.has(
|
|
4721
|
-
done.add(
|
|
5206
|
+
const key2 = tileKey(t.tx, t.ty);
|
|
5207
|
+
if (done.has(key2)) continue;
|
|
5208
|
+
done.add(key2);
|
|
4722
5209
|
if (depth) {
|
|
4723
5210
|
const ch = base.channels;
|
|
4724
|
-
const d = applyStrokeToDeepTile(coverage, tileSamples(base.tiles.get(
|
|
5211
|
+
const d = applyStrokeToDeepTile(coverage, tileSamples(base.tiles.get(key2), ch, depth, base.defaultValue), t.tx, t.ty, apply, { channels: ch });
|
|
4725
5212
|
const tile2 = deepTile(d, ch, base.defaultValue);
|
|
4726
|
-
if (tile2) tiles.set(
|
|
4727
|
-
else tiles.delete(
|
|
5213
|
+
if (tile2) tiles.set(key2, tile2);
|
|
5214
|
+
else tiles.delete(key2);
|
|
4728
5215
|
continue;
|
|
4729
5216
|
}
|
|
4730
|
-
const src = base.tiles.get(
|
|
5217
|
+
const src = base.tiles.get(key2)?.data ?? defaultTile(base);
|
|
4731
5218
|
const data = applyStrokeToTile(coverage, src, base.channels, t.tx, t.ty, apply);
|
|
4732
5219
|
const tile = finishTile(data, base.channels, base.defaultValue);
|
|
4733
|
-
if (tile) tiles.set(
|
|
4734
|
-
else tiles.delete(
|
|
5220
|
+
if (tile) tiles.set(key2, tile);
|
|
5221
|
+
else tiles.delete(key2);
|
|
4735
5222
|
}
|
|
4736
5223
|
}
|
|
4737
5224
|
return new TiledSurface(base.channels, tiles, base.defaultValue);
|
|
@@ -4802,19 +5289,19 @@ var PaintStrokeSession = class {
|
|
|
4802
5289
|
work = /* @__PURE__ */ new Map();
|
|
4803
5290
|
workRead = /* @__PURE__ */ new Map();
|
|
4804
5291
|
/** The pre-stroke tile as a float working tile (cached; `null`: transparent). */
|
|
4805
|
-
readWork(
|
|
4806
|
-
if (!this.workRead.has(
|
|
4807
|
-
return this.workRead.get(
|
|
5292
|
+
readWork(key2) {
|
|
5293
|
+
if (!this.workRead.has(key2)) this.workRead.set(key2, workTileOf(this.base.tiles.get(key2)));
|
|
5294
|
+
return this.workRead.get(key2);
|
|
4808
5295
|
}
|
|
4809
5296
|
/** Clone, healing and the spot-heal trail at depth: `paint` through the coverage on the float working tile. */
|
|
4810
|
-
retouchDeepTile(
|
|
5297
|
+
retouchDeepTile(key2, tx, ty, paint, options) {
|
|
4811
5298
|
const r = this.retouch;
|
|
4812
|
-
const read = this.readWork(
|
|
5299
|
+
const read = this.readWork(key2);
|
|
4813
5300
|
const out = applyRetouchToTile(this.stroke.coverage, read ?? new Float32Array(TILE * TILE * 4), 4, tx, ty, paint, r.apply, options);
|
|
4814
|
-
this.work.set(
|
|
4815
|
-
const d = deepOfWorkTile(out, this.deep, read, this.base.tiles.get(
|
|
4816
|
-
this.liveDeep.set(
|
|
4817
|
-
this.live.set(
|
|
5301
|
+
this.work.set(key2, out);
|
|
5302
|
+
const d = deepOfWorkTile(out, this.deep, read, this.base.tiles.get(key2));
|
|
5303
|
+
this.liveDeep.set(key2, d);
|
|
5304
|
+
this.live.set(key2, { data: proxyOf(d, 4), uniform: null, deep: d });
|
|
4818
5305
|
return d;
|
|
4819
5306
|
}
|
|
4820
5307
|
/** The new tile of a coverage stroke over the pre-stroke tile `src`. */
|
|
@@ -4828,23 +5315,23 @@ var PaintStrokeSession = class {
|
|
|
4828
5315
|
const a = this.apply;
|
|
4829
5316
|
const depth = this.dabDeep;
|
|
4830
5317
|
if (depth) {
|
|
4831
|
-
const read = (
|
|
5318
|
+
const read = (key2) => this.readWork(key2);
|
|
4832
5319
|
return {
|
|
4833
5320
|
offset: a.offset,
|
|
4834
5321
|
lockTransparency: a.lockTransparency,
|
|
4835
5322
|
selection: a.selection,
|
|
4836
5323
|
readTile: (tx, ty) => {
|
|
4837
|
-
const
|
|
4838
|
-
return this.work.get(
|
|
5324
|
+
const key2 = tileKey(tx, ty);
|
|
5325
|
+
return this.work.get(key2) ?? read(key2);
|
|
4839
5326
|
},
|
|
4840
5327
|
writeTile: (tx, ty) => {
|
|
4841
|
-
const
|
|
4842
|
-
if (!this.touched.has(
|
|
4843
|
-
this.work.set(
|
|
4844
|
-
this.touched.add(
|
|
5328
|
+
const key2 = tileKey(tx, ty);
|
|
5329
|
+
if (!this.touched.has(key2)) {
|
|
5330
|
+
this.work.set(key2, read(key2)?.slice() ?? new Float32Array(TILE * TILE * 4));
|
|
5331
|
+
this.touched.add(key2);
|
|
4845
5332
|
}
|
|
4846
|
-
this.fresh.add(
|
|
4847
|
-
return this.work.get(
|
|
5333
|
+
this.fresh.add(key2);
|
|
5334
|
+
return this.work.get(key2);
|
|
4848
5335
|
}
|
|
4849
5336
|
};
|
|
4850
5337
|
}
|
|
@@ -4854,14 +5341,14 @@ var PaintStrokeSession = class {
|
|
|
4854
5341
|
selection: a.selection,
|
|
4855
5342
|
readTile: (tx, ty) => this.live.get(tileKey(tx, ty))?.data ?? (this.base.defaultValue ? defaultTile(this.base) : null),
|
|
4856
5343
|
writeTile: (tx, ty) => {
|
|
4857
|
-
const
|
|
4858
|
-
if (!this.touched.has(
|
|
4859
|
-
const src = this.base.tiles.get(
|
|
4860
|
-
this.live.set(
|
|
4861
|
-
this.touched.add(
|
|
5344
|
+
const key2 = tileKey(tx, ty);
|
|
5345
|
+
if (!this.touched.has(key2)) {
|
|
5346
|
+
const src = this.base.tiles.get(key2)?.data ?? defaultTile(this.base);
|
|
5347
|
+
this.live.set(key2, { data: new Uint8ClampedArray(src), uniform: null });
|
|
5348
|
+
this.touched.add(key2);
|
|
4862
5349
|
}
|
|
4863
|
-
this.fresh.add(
|
|
4864
|
-
return this.live.get(
|
|
5350
|
+
this.fresh.add(key2);
|
|
5351
|
+
return this.live.get(key2).data;
|
|
4865
5352
|
}
|
|
4866
5353
|
};
|
|
4867
5354
|
}
|
|
@@ -4885,49 +5372,49 @@ var PaintStrokeSession = class {
|
|
|
4885
5372
|
const ch = this.base.channels;
|
|
4886
5373
|
if (this.mixer) {
|
|
4887
5374
|
if (this.dabDeep) {
|
|
4888
|
-
for (const
|
|
4889
|
-
const d = deepOfWorkTile(this.work.get(
|
|
4890
|
-
this.liveDeep.set(
|
|
4891
|
-
this.live.set(
|
|
5375
|
+
for (const key2 of this.fresh) {
|
|
5376
|
+
const d = deepOfWorkTile(this.work.get(key2), this.dabDeep, this.workRead.get(key2) ?? null, this.base.tiles.get(key2));
|
|
5377
|
+
this.liveDeep.set(key2, d);
|
|
5378
|
+
this.live.set(key2, { data: proxyOf(d, 4), uniform: null, deep: d });
|
|
4892
5379
|
}
|
|
4893
|
-
} else for (const
|
|
5380
|
+
} else for (const key2 of this.fresh) this.live.set(key2, { data: this.live.get(key2).data, uniform: null });
|
|
4894
5381
|
this.fresh.clear();
|
|
4895
5382
|
this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);
|
|
4896
5383
|
return;
|
|
4897
5384
|
}
|
|
4898
5385
|
const stroke = this.stroke;
|
|
4899
|
-
const
|
|
5386
|
+
const regions2 = /* @__PURE__ */ new Map();
|
|
4900
5387
|
for (const r of rects) {
|
|
4901
5388
|
for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {
|
|
4902
|
-
const
|
|
4903
|
-
const cur =
|
|
4904
|
-
|
|
5389
|
+
const key2 = tileKey(t.tx, t.ty);
|
|
5390
|
+
const cur = regions2.get(key2);
|
|
5391
|
+
regions2.set(key2, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r) : r });
|
|
4905
5392
|
}
|
|
4906
5393
|
}
|
|
4907
|
-
for (const [
|
|
5394
|
+
for (const [key2, { tx, ty, r }] of regions2) {
|
|
4908
5395
|
if (this.deep) {
|
|
4909
5396
|
const rt = this.retouch;
|
|
4910
5397
|
const paint = rt?.kind === "coverage" ? rt.paint() : null;
|
|
4911
5398
|
if (paint && paint.kind !== "tone") {
|
|
4912
|
-
const prevWork = this.touched.has(
|
|
4913
|
-
this.touched.add(
|
|
4914
|
-
this.retouchDeepTile(
|
|
5399
|
+
const prevWork = this.touched.has(key2) ? this.work.get(key2) : void 0;
|
|
5400
|
+
this.touched.add(key2);
|
|
5401
|
+
this.retouchDeepTile(key2, tx, ty, paint, prevWork ? { into: prevWork, region: r } : {});
|
|
4915
5402
|
continue;
|
|
4916
5403
|
}
|
|
4917
|
-
const src2 = tileSamples(this.base.tiles.get(
|
|
4918
|
-
const prev2 = this.touched.has(
|
|
4919
|
-
this.touched.add(
|
|
5404
|
+
const src2 = tileSamples(this.base.tiles.get(key2), ch, this.deep, this.base.defaultValue);
|
|
5405
|
+
const prev2 = this.touched.has(key2) ? this.liveDeep.get(key2) : void 0;
|
|
5406
|
+
this.touched.add(key2);
|
|
4920
5407
|
const opts = prev2 ? { into: prev2, region: r, channels: ch } : { channels: ch };
|
|
4921
5408
|
const d = rt?.kind === "coverage" && paint?.kind === "tone" ? applyToneToDeepTile(stroke.coverage, src2, tx, ty, paint.tone, rt.apply, opts) : applyStrokeToDeepTile(stroke.coverage, src2, tx, ty, this.apply, opts);
|
|
4922
|
-
this.liveDeep.set(
|
|
4923
|
-
this.live.set(
|
|
5409
|
+
this.liveDeep.set(key2, d);
|
|
5410
|
+
this.live.set(key2, { data: proxyOf(d, ch), uniform: null, deep: d });
|
|
4924
5411
|
continue;
|
|
4925
5412
|
}
|
|
4926
|
-
const src = this.base.tiles.get(
|
|
4927
|
-
const prev = this.touched.has(
|
|
4928
|
-
this.touched.add(
|
|
5413
|
+
const src = this.base.tiles.get(key2)?.data ?? defaultTile(this.base);
|
|
5414
|
+
const prev = this.touched.has(key2) ? this.live.get(key2)?.data : void 0;
|
|
5415
|
+
this.touched.add(key2);
|
|
4929
5416
|
const data = this.compositeTile(src, tx, ty, prev ? { into: prev, region: r } : {});
|
|
4930
|
-
this.live.set(
|
|
5417
|
+
this.live.set(key2, { data, uniform: null });
|
|
4931
5418
|
}
|
|
4932
5419
|
this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);
|
|
4933
5420
|
}
|
|
@@ -4944,26 +5431,26 @@ var PaintStrokeSession = class {
|
|
|
4944
5431
|
if (r?.kind === "coverage" && r.finalPaint) {
|
|
4945
5432
|
const final = r.finalPaint(this.stroke.coverage);
|
|
4946
5433
|
if (final) {
|
|
4947
|
-
for (const
|
|
4948
|
-
const { tx, ty } = tileCoord(
|
|
5434
|
+
for (const key2 of this.touched) {
|
|
5435
|
+
const { tx, ty } = tileCoord(key2);
|
|
4949
5436
|
if (this.deep) {
|
|
4950
|
-
this.retouchDeepTile(
|
|
5437
|
+
this.retouchDeepTile(key2, tx, ty, final, {});
|
|
4951
5438
|
continue;
|
|
4952
5439
|
}
|
|
4953
|
-
const src = this.base.tiles.get(
|
|
4954
|
-
this.live.set(
|
|
5440
|
+
const src = this.base.tiles.get(key2)?.data ?? defaultTile(this.base);
|
|
5441
|
+
this.live.set(key2, { data: this.compositeTile(src, tx, ty, {}, final), uniform: null });
|
|
4955
5442
|
}
|
|
4956
5443
|
}
|
|
4957
5444
|
}
|
|
4958
5445
|
const tiles = new Map(this.base.tiles);
|
|
4959
5446
|
let changed = false;
|
|
4960
|
-
for (const
|
|
4961
|
-
const live = this.live.get(
|
|
4962
|
-
const deep = this.liveDeep.get(
|
|
5447
|
+
for (const key2 of this.touched) {
|
|
5448
|
+
const live = this.live.get(key2);
|
|
5449
|
+
const deep = this.liveDeep.get(key2);
|
|
4963
5450
|
const tile = deep ? deepTile(deep, ch, this.base.defaultValue) : finishTile(live.data, ch, this.base.defaultValue);
|
|
4964
|
-
const prev = this.base.tiles.get(
|
|
4965
|
-
if (tile) tiles.set(
|
|
4966
|
-
else tiles.delete(
|
|
5451
|
+
const prev = this.base.tiles.get(key2);
|
|
5452
|
+
if (tile) tiles.set(key2, tile);
|
|
5453
|
+
else tiles.delete(key2);
|
|
4967
5454
|
if (!sameTile(prev, tile)) changed = true;
|
|
4968
5455
|
}
|
|
4969
5456
|
const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;
|
|
@@ -4982,9 +5469,9 @@ var PaintStrokeSession = class {
|
|
|
4982
5469
|
this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);
|
|
4983
5470
|
}
|
|
4984
5471
|
};
|
|
4985
|
-
function workRegionOf(surface,
|
|
4986
|
-
if (!depth) return Float32Array.from(readRegion(surface,
|
|
4987
|
-
const d = readDeepRegion(surface,
|
|
5472
|
+
function workRegionOf(surface, rect2, depth) {
|
|
5473
|
+
if (!depth) return Float32Array.from(readRegion(surface, rect2));
|
|
5474
|
+
const d = readDeepRegion(surface, rect2, depth);
|
|
4988
5475
|
const out = new Float32Array(d.length);
|
|
4989
5476
|
if (d instanceof Uint16Array) {
|
|
4990
5477
|
for (let i = 0; i < d.length; i++) out[i] = d[i] / 257;
|
|
@@ -5671,9 +6158,9 @@ var ImageEditorController = class {
|
|
|
5671
6158
|
/** Recomputes the compatibility views (`adjustments`, `depthMask`, `annotations`) when the stack changed. */
|
|
5672
6159
|
syncViews() {
|
|
5673
6160
|
const s = this.state;
|
|
5674
|
-
const
|
|
5675
|
-
if (
|
|
5676
|
-
this.viewKey =
|
|
6161
|
+
const key2 = [s.layers, s.outputOpacity, s.legacyOutputBlur, s.activeLayerId, s.imageDimensions.width, s.imageDimensions.height];
|
|
6162
|
+
if (key2.length === this.viewKey.length && key2.every((v, i) => v === this.viewKey[i])) return;
|
|
6163
|
+
this.viewKey = key2;
|
|
5677
6164
|
const { width, height } = this.naturalSize;
|
|
5678
6165
|
const view = legacyView(this.stack(), width, height, s.activeLayerId);
|
|
5679
6166
|
s.adjustments = view.adjustments;
|
|
@@ -6000,7 +6487,7 @@ var ImageEditorController = class {
|
|
|
6000
6487
|
getLayerFrame(id) {
|
|
6001
6488
|
const layer = this.getLayer(id);
|
|
6002
6489
|
if (!layer) return null;
|
|
6003
|
-
const
|
|
6490
|
+
const rect2 = (b2) => [
|
|
6004
6491
|
{ x: b2.x, y: b2.y },
|
|
6005
6492
|
{ x: b2.x + b2.width, y: b2.y },
|
|
6006
6493
|
{ x: b2.x + b2.width, y: b2.y + b2.height },
|
|
@@ -6015,10 +6502,10 @@ var ImageEditorController = class {
|
|
|
6015
6502
|
}
|
|
6016
6503
|
if (layer.type === "shape") {
|
|
6017
6504
|
const g = shapeGeometryBounds(layer);
|
|
6018
|
-
return g ?
|
|
6505
|
+
return g ? rect2(g) : null;
|
|
6019
6506
|
}
|
|
6020
6507
|
const b = this.getLayerBounds(id);
|
|
6021
|
-
return b ?
|
|
6508
|
+
return b ? rect2(b) : null;
|
|
6022
6509
|
}
|
|
6023
6510
|
/**
|
|
6024
6511
|
* Hit-tests the layer stack at `point` (in document space) and returns the id of the topmost
|
|
@@ -6099,9 +6586,9 @@ var ImageEditorController = class {
|
|
|
6099
6586
|
* and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and
|
|
6100
6587
|
* output opacity are not applied. With `layerIds`, only those layers are composited.
|
|
6101
6588
|
*/
|
|
6102
|
-
compositePixels(
|
|
6589
|
+
compositePixels(rect2, options = {}) {
|
|
6103
6590
|
const { width, height } = this.naturalSize;
|
|
6104
|
-
const r =
|
|
6591
|
+
const r = rect2 ?? { x: 0, y: 0, width, height };
|
|
6105
6592
|
let layers = this.state.layers;
|
|
6106
6593
|
if (options.layerIds) {
|
|
6107
6594
|
const keep = new Set(options.layerIds);
|
|
@@ -6125,12 +6612,12 @@ var ImageEditorController = class {
|
|
|
6125
6612
|
nextSourceId = 1;
|
|
6126
6613
|
/** Changes whenever the Background's source pixels can have changed. */
|
|
6127
6614
|
sourceVersion() {
|
|
6128
|
-
const
|
|
6129
|
-
if (!
|
|
6130
|
-
let id = this.sourceIds.get(
|
|
6615
|
+
const key2 = this.samplePixelsOverride ? this.samplePixels : this.imageElement ?? this.samplePixels;
|
|
6616
|
+
if (!key2) return "0";
|
|
6617
|
+
let id = this.sourceIds.get(key2);
|
|
6131
6618
|
if (!id) {
|
|
6132
6619
|
id = this.nextSourceId++;
|
|
6133
|
-
this.sourceIds.set(
|
|
6620
|
+
this.sourceIds.set(key2, id);
|
|
6134
6621
|
}
|
|
6135
6622
|
return String(id);
|
|
6136
6623
|
}
|
|
@@ -6572,8 +7059,8 @@ var ImageEditorController = class {
|
|
|
6572
7059
|
this.state.activePathId = id;
|
|
6573
7060
|
this.notify();
|
|
6574
7061
|
}
|
|
6575
|
-
commitPaths(next, active) {
|
|
6576
|
-
this.recordHistory();
|
|
7062
|
+
commitPaths(next, active, commitToHistory = true) {
|
|
7063
|
+
if (commitToHistory) this.recordHistory();
|
|
6577
7064
|
if (next.length) this.state.paths = next;
|
|
6578
7065
|
else delete this.state.paths;
|
|
6579
7066
|
if (active !== void 0) this.state.activePathId = active;
|
|
@@ -6609,6 +7096,64 @@ var ImageEditorController = class {
|
|
|
6609
7096
|
this.commitPaths(this.getPaths().map((p) => p.id === id ? path : p));
|
|
6610
7097
|
return path;
|
|
6611
7098
|
}
|
|
7099
|
+
/**
|
|
7100
|
+
* What the path tools (Path Selection, Direct Selection, Pen) edit: the active path in the Paths
|
|
7101
|
+
* panel, else the active layer when it is a shape, else the active layer's vector mask; null
|
|
7102
|
+
* when there is none of these (the Pen then starts a new Work Path).
|
|
7103
|
+
*/
|
|
7104
|
+
pathEditTarget() {
|
|
7105
|
+
const id = this.state.activePathId;
|
|
7106
|
+
if (id && this.getPath(id)) return { kind: "path", id };
|
|
7107
|
+
const layer = this.getActiveLayer();
|
|
7108
|
+
if (!layer) return null;
|
|
7109
|
+
if (layer.type === "shape") return { kind: "shape", layerId: layer.id };
|
|
7110
|
+
if (layer.vectorMask) return { kind: "vector-mask", layerId: layer.id };
|
|
7111
|
+
return null;
|
|
7112
|
+
}
|
|
7113
|
+
/** The components a path tool target holds (document pixels). Throws for a target that is gone. */
|
|
7114
|
+
getPathEditComponents(target) {
|
|
7115
|
+
if (target.kind === "path") return this.requirePath(target.id).components;
|
|
7116
|
+
if (target.kind === "shape") return requireShapeLayer(this.state.layers, target.layerId).components;
|
|
7117
|
+
return this.requireVectorMask(target.layerId).vm.components;
|
|
7118
|
+
}
|
|
7119
|
+
/**
|
|
7120
|
+
* Replaces a path tool target's components: one undo step, or none with `commitToHistory` false
|
|
7121
|
+
* (the later moves of a drag whose first move recorded one). A component that still has its
|
|
7122
|
+
* `live` shape is kept as given (drop `live` from one whose anchors changed); a shape layer keeps
|
|
7123
|
+
* at least one component.
|
|
7124
|
+
*/
|
|
7125
|
+
setPathEditComponents(target, components, commitToHistory = true) {
|
|
7126
|
+
this.getPathEditComponents(target);
|
|
7127
|
+
const next = components.map(
|
|
7128
|
+
(c, i) => c.live ? { ...c } : this.componentFrom({ op: c.op, subpaths: c.subpaths.filter((sp) => sp.anchors.length) }, `components[${i}].`)
|
|
7129
|
+
);
|
|
7130
|
+
if (target.kind === "path") {
|
|
7131
|
+
this.commitPaths(this.getPaths().map((p) => p.id === target.id ? { ...p, components: next } : p), void 0, commitToHistory);
|
|
7132
|
+
} else if (target.kind === "shape") {
|
|
7133
|
+
if (!next.length) throw new LayerOperationError("A shape needs at least one component; delete the layer instead (delete_layer).");
|
|
7134
|
+
const { layers } = updateShapeLayer(this.state.layers, target.layerId, () => ({ components: next }), this.shapeEnv(), "edit its path");
|
|
7135
|
+
this.commitLayers(layers, void 0, commitToHistory);
|
|
7136
|
+
} else {
|
|
7137
|
+
const { vm } = this.requireVectorMask(target.layerId);
|
|
7138
|
+
this.commitLayers(mapLayer(this.state.layers, target.layerId, (l) => ({ ...l, vectorMask: { ...vm, components: next } })), void 0, commitToHistory);
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
/**
|
|
7142
|
+
* The Pen with no path to add to: a new Work Path from `components` (replacing the last one,
|
|
7143
|
+
* unless it was saved), made the active path. One undo step. Returns the path.
|
|
7144
|
+
*/
|
|
7145
|
+
startWorkPath(components) {
|
|
7146
|
+
const paths = this.getPaths();
|
|
7147
|
+
const old = paths.find((p) => p.work);
|
|
7148
|
+
const work = {
|
|
7149
|
+
id: old?.id ?? newPathId(),
|
|
7150
|
+
name: WORK_PATH_NAME,
|
|
7151
|
+
components: components.map((c, i) => this.componentFrom({ op: c.op, subpaths: c.subpaths }, `components[${i}].`)),
|
|
7152
|
+
work: true
|
|
7153
|
+
};
|
|
7154
|
+
this.commitPaths(old ? paths.map((p) => p === old ? work : p) : [...paths, work], work.id);
|
|
7155
|
+
return work;
|
|
7156
|
+
}
|
|
6612
7157
|
/** Renames a path; renaming the Work Path saves it (Photoshop's Save Path). One undo step. */
|
|
6613
7158
|
renamePath(id, name) {
|
|
6614
7159
|
const old = this.requirePath(id);
|
|
@@ -7485,7 +8030,7 @@ var ImageEditorController = class {
|
|
|
7485
8030
|
return this.getGlobalLight();
|
|
7486
8031
|
}
|
|
7487
8032
|
/** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */
|
|
7488
|
-
async compositeToSurface(layers,
|
|
8033
|
+
async compositeToSurface(layers, rect2) {
|
|
7489
8034
|
const { width, height } = this.naturalSize;
|
|
7490
8035
|
if (flattenLayers(layers).some((l) => l.type === "annotation" && l.visible) && !this.renderer.canRasterize()) {
|
|
7491
8036
|
throw new LayerOperationError("Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.");
|
|
@@ -7494,20 +8039,20 @@ var ImageEditorController = class {
|
|
|
7494
8039
|
const sources = this.renderer.compositorSources(width, height);
|
|
7495
8040
|
const depth = this.bitDepth;
|
|
7496
8041
|
if (depth !== 8) {
|
|
7497
|
-
const samples = workingToSamples(compositeDeep(layers,
|
|
7498
|
-
return surfaceFromDeepPixels({ width:
|
|
8042
|
+
const samples = workingToSamples(compositeDeep(layers, rect2, sources, depth), depth);
|
|
8043
|
+
return surfaceFromDeepPixels({ width: rect2.width, height: rect2.height, data: samples }, 4, rect2.x, rect2.y);
|
|
7499
8044
|
}
|
|
7500
|
-
const data = new LayerCompositor().composite(layers,
|
|
7501
|
-
return surfaceFromPixels({ width:
|
|
8045
|
+
const data = new LayerCompositor().composite(layers, rect2, sources);
|
|
8046
|
+
return surfaceFromPixels({ width: rect2.width, height: rect2.height, data }, 4, rect2.x, rect2.y);
|
|
7502
8047
|
}
|
|
7503
8048
|
/**
|
|
7504
8049
|
* The composite of the layer stack at the document's bit depth (as `compositePixels`, no
|
|
7505
8050
|
* canvas): 16-bit sRGB samples, or 32-bit linear float, over `rect` (default the canvas). At 8
|
|
7506
8051
|
* bits, the 8-bit composite widened to 16 bits.
|
|
7507
8052
|
*/
|
|
7508
|
-
compositeDeepPixels(
|
|
8053
|
+
compositeDeepPixels(rect2) {
|
|
7509
8054
|
const { width, height } = this.naturalSize;
|
|
7510
|
-
const r =
|
|
8055
|
+
const r = rect2 ?? { x: 0, y: 0, width, height };
|
|
7511
8056
|
const depth = this.bitDepth;
|
|
7512
8057
|
if (depth === 8) return { width: r.width, height: r.height, depth: 16, data: to16From8(this.compositePixels(r).data) };
|
|
7513
8058
|
const data = workingToSamples(compositeDeep(this.state.layers, r, this.compositorSources(), depth), depth);
|
|
@@ -7545,12 +8090,12 @@ var ImageEditorController = class {
|
|
|
7545
8090
|
if (!upper.visible) throw new LayerOperationError(`Layer "${upper.name || id}" is hidden; show it or delete it instead of merging.`);
|
|
7546
8091
|
requireUnlocked(upper, "all", "merge it");
|
|
7547
8092
|
requireUnlocked(lower, "pixels", "merge into it");
|
|
7548
|
-
let
|
|
8093
|
+
let rect2 = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
7549
8094
|
const reach = Math.max(effectsMargin(lower.effects), upper.type === "adjustment" ? 0 : effectsMargin(upper.effects));
|
|
7550
|
-
if (reach)
|
|
8095
|
+
if (reach) rect2 = { x: rect2.x - reach, y: rect2.y - reach, width: rect2.width + 2 * reach, height: rect2.height + 2 * reach };
|
|
7551
8096
|
const base = { ...lower, blend: "normal", opacity: 1 };
|
|
7552
8097
|
delete base.clipped;
|
|
7553
|
-
const surface = await this.compositeToSurface([base, upper],
|
|
8098
|
+
const surface = await this.compositeToSurface([base, upper], rect2);
|
|
7554
8099
|
const merged = { ...lower, surface, offset: { x: 0, y: 0 } };
|
|
7555
8100
|
delete merged.mask;
|
|
7556
8101
|
delete merged.fillOpacity;
|
|
@@ -7632,9 +8177,9 @@ var ImageEditorController = class {
|
|
|
7632
8177
|
if (!surface) return null;
|
|
7633
8178
|
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
7634
8179
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
7635
|
-
const
|
|
8180
|
+
const key2 = `${tw}x${th}:${ox},${oy}`;
|
|
7636
8181
|
let byKey = this.thumbCache.get(surface);
|
|
7637
|
-
const hit = byKey?.get(
|
|
8182
|
+
const hit = byKey?.get(key2);
|
|
7638
8183
|
if (hit) return hit;
|
|
7639
8184
|
const out = new Uint8ClampedArray(tw * th * 4);
|
|
7640
8185
|
for (let y = 0; y < th; y++) {
|
|
@@ -7654,7 +8199,7 @@ var ImageEditorController = class {
|
|
|
7654
8199
|
byKey = /* @__PURE__ */ new Map();
|
|
7655
8200
|
this.thumbCache.set(surface, byKey);
|
|
7656
8201
|
}
|
|
7657
|
-
byKey.set(
|
|
8202
|
+
byKey.set(key2, thumb);
|
|
7658
8203
|
return thumb;
|
|
7659
8204
|
}
|
|
7660
8205
|
/**
|
|
@@ -8249,27 +8794,27 @@ var ImageEditorController = class {
|
|
|
8249
8794
|
copyPixels(options = {}) {
|
|
8250
8795
|
const { width, height } = this.naturalSize;
|
|
8251
8796
|
const sel = this.state.selection?.bounds ? this.state.selection : null;
|
|
8252
|
-
const
|
|
8797
|
+
const rect2 = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
8253
8798
|
let data;
|
|
8254
8799
|
if (options.merged) {
|
|
8255
|
-
data = new Uint8ClampedArray(this.compositePixels(
|
|
8800
|
+
data = new Uint8ClampedArray(this.compositePixels(rect2).data);
|
|
8256
8801
|
} else {
|
|
8257
8802
|
const id = options.layerId ?? this.state.activeLayerId;
|
|
8258
8803
|
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId, or merged: true to copy the visible image.");
|
|
8259
8804
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
8260
8805
|
if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is not a pixel layer; copy merged, or rasterize it first.`);
|
|
8261
|
-
data = readRegion(this.surfaceOf(layer), { x:
|
|
8806
|
+
data = readRegion(this.surfaceOf(layer), { x: rect2.x - layer.offset.x, y: rect2.y - layer.offset.y, width: rect2.width, height: rect2.height });
|
|
8262
8807
|
}
|
|
8263
8808
|
if (sel) {
|
|
8264
|
-
for (let y = 0; y <
|
|
8265
|
-
for (let x = 0; x <
|
|
8266
|
-
const k = sel.data[(
|
|
8267
|
-
const p = (y *
|
|
8809
|
+
for (let y = 0; y < rect2.height; y++) {
|
|
8810
|
+
for (let x = 0; x < rect2.width; x++) {
|
|
8811
|
+
const k = sel.data[(rect2.y + y) * sel.width + rect2.x + x];
|
|
8812
|
+
const p = (y * rect2.width + x) * 4 + 3;
|
|
8268
8813
|
data[p] = Math.round(data[p] * k / 255);
|
|
8269
8814
|
}
|
|
8270
8815
|
}
|
|
8271
8816
|
}
|
|
8272
|
-
this.clipboard = { width:
|
|
8817
|
+
this.clipboard = { width: rect2.width, height: rect2.height, x: rect2.x, y: rect2.y, data };
|
|
8273
8818
|
return this.clipboard;
|
|
8274
8819
|
}
|
|
8275
8820
|
/** Edit › Cut: Copy, then clear the selected pixels, as one undo step. */
|
|
@@ -8753,8 +9298,8 @@ var ImageEditorController = class {
|
|
|
8753
9298
|
if (!Number.isFinite(position) || at < 0 || at >= max) {
|
|
8754
9299
|
throw new RangeError(`${orientation === "row" ? "y" : "x"} ${position} is outside the image (0..${max - 1}).`);
|
|
8755
9300
|
}
|
|
8756
|
-
const
|
|
8757
|
-
return this.shapeCommand(rectSelection(width, height,
|
|
9301
|
+
const rect2 = orientation === "row" ? { x: 0, y: at, width, height: 1 } : { x: at, y: 0, width: 1, height };
|
|
9302
|
+
return this.shapeCommand(rectSelection(width, height, rect2, { antiAlias: false }), options);
|
|
8758
9303
|
}
|
|
8759
9304
|
/** Select › Grow: adds connected pixels within `tolerance` of a selected colour (one undo step). */
|
|
8760
9305
|
async growSelection(options = {}) {
|
|
@@ -8810,14 +9355,14 @@ var ImageEditorController = class {
|
|
|
8810
9355
|
return this.setSelection(shaped, options.mode ?? "replace");
|
|
8811
9356
|
}
|
|
8812
9357
|
/** Rectangular marquee in natural image pixels. */
|
|
8813
|
-
selectRect(
|
|
9358
|
+
selectRect(rect2, options = {}) {
|
|
8814
9359
|
const { width, height } = this.naturalSize;
|
|
8815
|
-
return this.shapeCommand(rectSelection(width, height,
|
|
9360
|
+
return this.shapeCommand(rectSelection(width, height, rect2, options), options);
|
|
8816
9361
|
}
|
|
8817
9362
|
/** Elliptical marquee inscribed in `rect`. */
|
|
8818
|
-
selectEllipse(
|
|
9363
|
+
selectEllipse(rect2, options = {}) {
|
|
8819
9364
|
const { width, height } = this.naturalSize;
|
|
8820
|
-
return this.shapeCommand(ellipseSelection(width, height,
|
|
9365
|
+
return this.shapeCommand(ellipseSelection(width, height, rect2, options), options);
|
|
8821
9366
|
}
|
|
8822
9367
|
/** Lasso / polygonal lasso. */
|
|
8823
9368
|
selectPolygon(points, options = {}) {
|
|
@@ -9540,6 +10085,44 @@ var ImageEditorController = class {
|
|
|
9540
10085
|
if (!sel) return false;
|
|
9541
10086
|
return this.setSelection(invert2 ? invertSelection(sel) : sel, mode);
|
|
9542
10087
|
}
|
|
10088
|
+
/**
|
|
10089
|
+
* A measured report on a mask's edge (`inspect_mask_edge`): the selection (default), a layer's
|
|
10090
|
+
* painted mask or its vector mask (its path, without density or feather). Halo and alignment are
|
|
10091
|
+
* measured against the layer's own pixels for a pixel layer's masks, else the composite.
|
|
10092
|
+
* Read-only. Throws for an empty selection or a layer without that mask.
|
|
10093
|
+
*/
|
|
10094
|
+
inspectMaskEdge(target = "selection", options = {}) {
|
|
10095
|
+
const { width, height } = this.naturalSize;
|
|
10096
|
+
let mask;
|
|
10097
|
+
let layer;
|
|
10098
|
+
if (target === "selection") {
|
|
10099
|
+
mask = this.getSelection();
|
|
10100
|
+
if (!mask?.bounds) throw new LayerOperationError("Nothing is selected: make a selection first, or inspect a layer-mask or vector-mask with layerId.");
|
|
10101
|
+
} else if (target === "layer-mask" || target === "vector-mask") {
|
|
10102
|
+
if (!options.layerId) throw new LayerOperationError(`A ${target} needs layerId (see list_layers).`);
|
|
10103
|
+
layer = this.getLayer(options.layerId);
|
|
10104
|
+
if (!layer) throw new LayerOperationError(`There is no layer "${options.layerId}" (see list_layers).`);
|
|
10105
|
+
if (target === "layer-mask") {
|
|
10106
|
+
mask = this.layerMaskCoverage(layer.id);
|
|
10107
|
+
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.`);
|
|
10108
|
+
} else {
|
|
10109
|
+
const { vm } = this.requireVectorMask(layer.id);
|
|
10110
|
+
mask = pathToSelection(vm.components, width, height, { fillRule: vm.fillRule });
|
|
10111
|
+
if (vm.inverted) mask = invertSelection(mask);
|
|
10112
|
+
}
|
|
10113
|
+
if (!mask.bounds) throw new LayerOperationError(`The ${target} of "${layer.name || layer.id}" hides everything: there is no edge to inspect.`);
|
|
10114
|
+
} else {
|
|
10115
|
+
throw new LayerOperationError(`Unknown target "${String(target)}". Use selection, layer-mask or vector-mask.`);
|
|
10116
|
+
}
|
|
10117
|
+
const b = mask.bounds;
|
|
10118
|
+
const x = Math.max(0, b.x - 8);
|
|
10119
|
+
const y = Math.max(0, b.y - 8);
|
|
10120
|
+
const region = { x, y, width: Math.min(width, b.x + b.width + 8) - x, height: Math.min(height, b.y + b.height + 8) - y };
|
|
10121
|
+
const own = layer?.type === "pixel" ? this.getLayerPixels(layer.id) : null;
|
|
10122
|
+
const image = own ?? { ...this.compositePixels(region), x: region.x, y: region.y };
|
|
10123
|
+
const report = inspectMaskEdge(mask, image, options);
|
|
10124
|
+
return { target, ...layer ? { layerId: layer.id } : {}, ...report };
|
|
10125
|
+
}
|
|
9543
10126
|
/** A layer's painted mask over the canvas as a selection mask; null without one (or for a depth mask). */
|
|
9544
10127
|
layerMaskCoverage(layerId) {
|
|
9545
10128
|
const layer = this.getLayer(layerId);
|
|
@@ -10036,15 +10619,15 @@ var ImageEditorController = class {
|
|
|
10036
10619
|
let mask;
|
|
10037
10620
|
if (!b || x1 <= x0 || y1 <= y0) mask = createSelectionMask(width, height);
|
|
10038
10621
|
else {
|
|
10039
|
-
const
|
|
10040
|
-
const rgba = readRegion(layer.surface,
|
|
10041
|
-
const bytes = new Uint8Array(
|
|
10622
|
+
const rect2 = { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
10623
|
+
const rgba = readRegion(layer.surface, rect2);
|
|
10624
|
+
const bytes = new Uint8Array(rect2.width * rect2.height);
|
|
10042
10625
|
const hard = init.antiAlias === false;
|
|
10043
10626
|
for (let i = 0; i < bytes.length; i++) {
|
|
10044
10627
|
const a = rgba[i * 4 + 3];
|
|
10045
10628
|
bytes[i] = hard ? a >= 128 ? 255 : 0 : a;
|
|
10046
10629
|
}
|
|
10047
|
-
mask = selectionFromRegion(width, height,
|
|
10630
|
+
mask = selectionFromRegion(width, height, rect2, bytes);
|
|
10048
10631
|
}
|
|
10049
10632
|
if ((init.feather ?? 0) > 0 && mask.bounds) mask = modifySelection(mask, "feather", init.feather);
|
|
10050
10633
|
return this.setSelection(mask.bounds ? mask : null, init.mode ?? "replace");
|
|
@@ -10403,8 +10986,8 @@ var ImageEditorController = class {
|
|
|
10403
10986
|
delete content.mask;
|
|
10404
10987
|
delete content.clipped;
|
|
10405
10988
|
delete content.fillOpacity;
|
|
10406
|
-
const
|
|
10407
|
-
const surface = await this.compositeToSurface([content],
|
|
10989
|
+
const rect2 = spatial ? canvas : this.getLayerBounds(id) ?? canvas;
|
|
10990
|
+
const surface = await this.compositeToSurface([content], rect2);
|
|
10408
10991
|
next = { ...layer, surface, offset: { x: 0, y: 0 } };
|
|
10409
10992
|
delete next.filters;
|
|
10410
10993
|
delete next.source;
|
|
@@ -10589,7 +11172,7 @@ var ImageEditorController = class {
|
|
|
10589
11172
|
}
|
|
10590
11173
|
if (src.colorSpace !== "rgb") throw new LayerOperationError("Colours are given as #rrggbb, so the source profile must be RGB.");
|
|
10591
11174
|
const t = Transform.create(src, dst, { intent: options.intent ?? "relative", bpc: options.bpc ?? true });
|
|
10592
|
-
const
|
|
11175
|
+
const toLab2 = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
|
|
10593
11176
|
return colors.map((input) => {
|
|
10594
11177
|
const m = /^#?([0-9a-f]{6})$/i.exec(input.trim());
|
|
10595
11178
|
if (!m) throw new LayerOperationError(`Invalid colour "${input}"; use #rrggbb.`);
|
|
@@ -10599,7 +11182,7 @@ var ImageEditorController = class {
|
|
|
10599
11182
|
t.eval(rgb, out);
|
|
10600
11183
|
const values = Array.from(out.subarray(0, t.outputs), (v) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4);
|
|
10601
11184
|
const l = new Float32Array(3);
|
|
10602
|
-
|
|
11185
|
+
toLab2.eval(rgb, l);
|
|
10603
11186
|
const lab = [Math.round(l[0] * 1e4) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];
|
|
10604
11187
|
const res = { input, values, lab };
|
|
10605
11188
|
if (dst.colorSpace === "rgb") res.hex = `#${values.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
@@ -12046,7 +12629,7 @@ function layersOf(args) {
|
|
|
12046
12629
|
add2(args.layerId);
|
|
12047
12630
|
add2(args.id);
|
|
12048
12631
|
add2(args.target);
|
|
12049
|
-
for (const
|
|
12632
|
+
for (const key2 of ["ids", "layerIds"]) if (Array.isArray(args[key2])) args[key2].forEach(add2);
|
|
12050
12633
|
return out.slice(0, 6);
|
|
12051
12634
|
}
|
|
12052
12635
|
function planImageToolPlayback(tool, args) {
|
|
@@ -12059,9 +12642,9 @@ function planImageToolPlayback(tool, args) {
|
|
|
12059
12642
|
else if (box) steps.push({ type: "move", to: box.from }, { type: "press" }, { type: "drag", to: box.to }, { type: "release" });
|
|
12060
12643
|
else if (num(args.x) && num(args.y)) steps.push({ type: "move", to: { x: args.x, y: args.y } }, { type: "press" }, { type: "release" });
|
|
12061
12644
|
else if (tool === "set_adjustments" && args.values && typeof args.values === "object") {
|
|
12062
|
-
for (const
|
|
12063
|
-
const m = /^layers\.([^.]+)\.(.+)$/.exec(
|
|
12064
|
-
steps.push({ type: "target", ref: m ? { kind: "control", id: m[2] } : { kind: "control", id:
|
|
12645
|
+
for (const key2 of Object.keys(args.values).slice(0, 4)) {
|
|
12646
|
+
const m = /^layers\.([^.]+)\.(.+)$/.exec(key2);
|
|
12647
|
+
steps.push({ type: "target", ref: m ? { kind: "control", id: m[2] } : { kind: "control", id: key2 } });
|
|
12065
12648
|
}
|
|
12066
12649
|
} else for (const id of layersOf(args)) steps.push({ type: "target", ref: { kind: IMAGE_PLAYBACK_KIND, id } });
|
|
12067
12650
|
if (typeof args.text === "string" && args.text.trim()) {
|
|
@@ -12237,10 +12820,10 @@ function sampleOptions(args) {
|
|
|
12237
12820
|
return { sampleMerged: args.sampleMerged === true, sample: args.sample ?? "composite" };
|
|
12238
12821
|
}
|
|
12239
12822
|
function rectOf(args) {
|
|
12240
|
-
const
|
|
12241
|
-
if (![
|
|
12242
|
-
if (
|
|
12243
|
-
return
|
|
12823
|
+
const rect2 = { x: num2(args.x, NaN), y: num2(args.y, NaN), width: num2(args.width, NaN), height: num2(args.height, NaN) };
|
|
12824
|
+
if (![rect2.x, rect2.y, rect2.width, rect2.height].every(Number.isFinite)) throw new Error("Pass numeric x, y, width and height in natural image pixels.");
|
|
12825
|
+
if (rect2.width <= 0 || rect2.height <= 0) throw new Error("width and height must be greater than 0.");
|
|
12826
|
+
return rect2;
|
|
12244
12827
|
}
|
|
12245
12828
|
function summary(controller) {
|
|
12246
12829
|
const sel = controller.getSelection();
|
|
@@ -12271,6 +12854,26 @@ function createImageSelectionTools(controller) {
|
|
|
12271
12854
|
annotations: { title: "Read the selection", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
12272
12855
|
handler: () => summary(controller)
|
|
12273
12856
|
};
|
|
12857
|
+
const inspectEdge = {
|
|
12858
|
+
name: "inspect_mask_edge",
|
|
12859
|
+
title: "Inspect a mask edge",
|
|
12860
|
+
description: "Measures the edge of the selection (default), a layer's painted mask (layer-mask) or its vector mask (vector-mask), so you can check it without looking. Returns haloScore 0-1 (background colour left just inside the edge; worse above 0.35), edgeAlignment 0-1 (share of the edge on strong image edges; worse below 0.5), jaggedness (outline turning in radians per 100 px beyond what closing it needs; smooth outlines score under 3, worse above 15), softFraction 0-1 (share of edge pixels partly selected), holes and islands (count, total area, the 10 largest with bounds), worst (up to 5 regions with a reason, ready for capture_view rect) and issues (findings with a suggested fix; empty when clean). Measures a pixel layer's masks against its own pixels, otherwise the composite. Read-only: inspect, fix the issues, inspect again.",
|
|
12861
|
+
inputSchema: {
|
|
12862
|
+
type: "object",
|
|
12863
|
+
properties: {
|
|
12864
|
+
target: { type: "string", enum: ["selection", "layer-mask", "vector-mask"], description: "What to inspect (default selection). layer-mask and vector-mask need layerId." },
|
|
12865
|
+
layerId: { type: "string", description: "The layer whose mask to inspect (see list_layers); only with layer-mask or vector-mask." },
|
|
12866
|
+
minArea: { type: "number", minimum: 1, maximum: 1e6, description: "Holes and islands smaller than this many pixels are left out (default 16)." }
|
|
12867
|
+
},
|
|
12868
|
+
additionalProperties: false
|
|
12869
|
+
},
|
|
12870
|
+
annotations: { title: "Inspect a mask edge", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
12871
|
+
handler: ({ target, layerId, minArea }) => {
|
|
12872
|
+
const t = target ?? "selection";
|
|
12873
|
+
if (t === "selection" && layerId !== void 0) throw new Error("layerId is only for target layer-mask or vector-mask; leave it out to inspect the selection.");
|
|
12874
|
+
return controller.inspectMaskEdge(t, { layerId, minArea });
|
|
12875
|
+
}
|
|
12876
|
+
};
|
|
12274
12877
|
const selectRegion = {
|
|
12275
12878
|
name: "select_region",
|
|
12276
12879
|
title: "Select a region",
|
|
@@ -12315,12 +12918,12 @@ function createImageSelectionTools(controller) {
|
|
|
12315
12918
|
break;
|
|
12316
12919
|
case "rect":
|
|
12317
12920
|
case "ellipse": {
|
|
12318
|
-
const
|
|
12319
|
-
if (![
|
|
12921
|
+
const rect2 = { x: num2(x, NaN), y: num2(y, NaN), width: num2(width, NaN), height: num2(height, NaN) };
|
|
12922
|
+
if (![rect2.x, rect2.y, rect2.width, rect2.height].every(Number.isFinite)) {
|
|
12320
12923
|
throw new Error("rect and ellipse need numeric x, y, width and height.");
|
|
12321
12924
|
}
|
|
12322
|
-
if (shape === "rect") controller.selectRect(
|
|
12323
|
-
else controller.selectEllipse(
|
|
12925
|
+
if (shape === "rect") controller.selectRect(rect2, opts);
|
|
12926
|
+
else controller.selectEllipse(rect2, opts);
|
|
12324
12927
|
break;
|
|
12325
12928
|
}
|
|
12326
12929
|
case "polygon": {
|
|
@@ -12691,10 +13294,10 @@ function createImageSelectionTools(controller) {
|
|
|
12691
13294
|
},
|
|
12692
13295
|
annotations: { ...writeAnnotations("Object Selection"), openWorldHint: true },
|
|
12693
13296
|
handler: async (args, context) => {
|
|
12694
|
-
const
|
|
13297
|
+
const rect2 = args.rect;
|
|
12695
13298
|
const point = args.point;
|
|
12696
|
-
if (!
|
|
12697
|
-
if (
|
|
13299
|
+
if (!rect2 && !point) throw new Error("Pass rect (a box around the object) or point (a point on it).");
|
|
13300
|
+
if (rect2 && point) throw new Error("Pass rect or point, not both.");
|
|
12698
13301
|
const options = {
|
|
12699
13302
|
mode: parseSelectionMode(args.mode),
|
|
12700
13303
|
sampleAllLayers: args.sampleAllLayers,
|
|
@@ -12702,7 +13305,7 @@ function createImageSelectionTools(controller) {
|
|
|
12702
13305
|
useModel: args.useModel,
|
|
12703
13306
|
signal: context?.signal
|
|
12704
13307
|
};
|
|
12705
|
-
const { method } =
|
|
13308
|
+
const { method } = rect2 ? await controller.selectObject({ rect: { x: num2(rect2.x, NaN), y: num2(rect2.y, NaN), width: num2(rect2.width, NaN), height: num2(rect2.height, NaN) } }, options) : await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
|
|
12706
13309
|
return { ...summary(controller), method };
|
|
12707
13310
|
}
|
|
12708
13311
|
};
|
|
@@ -13316,6 +13919,7 @@ function createImageSelectionTools(controller) {
|
|
|
13316
13919
|
};
|
|
13317
13920
|
return [
|
|
13318
13921
|
getSelection,
|
|
13922
|
+
inspectEdge,
|
|
13319
13923
|
selectAll,
|
|
13320
13924
|
deselect,
|
|
13321
13925
|
invert2,
|
|
@@ -13454,7 +14058,7 @@ async function renderImageRegion(controller, request) {
|
|
|
13454
14058
|
out.height = request.height;
|
|
13455
14059
|
const context = out.getContext("2d");
|
|
13456
14060
|
if (!context) throw new Error(NO_CANVAS);
|
|
13457
|
-
const { rect } = request;
|
|
14061
|
+
const { rect: rect2 } = request;
|
|
13458
14062
|
context.imageSmoothingEnabled = request.scale < 1;
|
|
13459
14063
|
if (request.format === "jpeg") {
|
|
13460
14064
|
context.fillStyle = "#ffffff";
|
|
@@ -13462,22 +14066,22 @@ async function renderImageRegion(controller, request) {
|
|
|
13462
14066
|
}
|
|
13463
14067
|
const kx = view.width / state.imageDimensions.width;
|
|
13464
14068
|
const ky = view.height / state.imageDimensions.height;
|
|
13465
|
-
context.drawImage(view,
|
|
14069
|
+
context.drawImage(view, rect2.x * kx, rect2.y * ky, rect2.width * kx, rect2.height * ky, 0, 0, out.width, out.height);
|
|
13466
14070
|
if (request.includeOverlays) {
|
|
13467
14071
|
const s = request.scale;
|
|
13468
14072
|
const maskView = maskViewCanvas(controller, document);
|
|
13469
|
-
if (maskView) context.drawImage(maskView,
|
|
14073
|
+
if (maskView) context.drawImage(maskView, rect2.x, rect2.y, rect2.width, rect2.height, 0, 0, out.width, out.height);
|
|
13470
14074
|
const selection = state.selection;
|
|
13471
14075
|
if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {
|
|
13472
14076
|
const shade = selectionShade(selection, document);
|
|
13473
|
-
if (shade) context.drawImage(shade,
|
|
14077
|
+
if (shade) context.drawImage(shade, rect2.x, rect2.y, rect2.width, rect2.height, 0, 0, out.width, out.height);
|
|
13474
14078
|
}
|
|
13475
14079
|
if (state.crop) {
|
|
13476
14080
|
context.save();
|
|
13477
14081
|
context.strokeStyle = "#ffd400";
|
|
13478
14082
|
context.lineWidth = 2;
|
|
13479
14083
|
context.setLineDash([6, 4]);
|
|
13480
|
-
context.strokeRect((state.crop.x -
|
|
14084
|
+
context.strokeRect((state.crop.x - rect2.x) * s, (state.crop.y - rect2.y) * s, state.crop.width * s, state.crop.height * s);
|
|
13481
14085
|
context.restore();
|
|
13482
14086
|
}
|
|
13483
14087
|
}
|
|
@@ -14779,19 +15383,19 @@ var SelectionGesture = class {
|
|
|
14779
15383
|
const { tool } = this.getOptions();
|
|
14780
15384
|
if (tool === "quick") return null;
|
|
14781
15385
|
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
14782
|
-
const
|
|
15386
|
+
const rect2 = normalizeRect({
|
|
14783
15387
|
x: this.start.x,
|
|
14784
15388
|
y: this.start.y,
|
|
14785
15389
|
width: this.current.x - this.start.x,
|
|
14786
15390
|
height: this.current.y - this.start.y
|
|
14787
15391
|
});
|
|
14788
|
-
if (
|
|
14789
|
-
if (tool === "ellipse") return ellipsePoints(
|
|
15392
|
+
if (rect2.width < 1 && rect2.height < 1) return null;
|
|
15393
|
+
if (tool === "ellipse") return ellipsePoints(rect2);
|
|
14790
15394
|
return [
|
|
14791
|
-
{ x:
|
|
14792
|
-
{ x:
|
|
14793
|
-
{ x:
|
|
14794
|
-
{ x:
|
|
15395
|
+
{ x: rect2.x, y: rect2.y },
|
|
15396
|
+
{ x: rect2.x + rect2.width, y: rect2.y },
|
|
15397
|
+
{ x: rect2.x + rect2.width, y: rect2.y + rect2.height },
|
|
15398
|
+
{ x: rect2.x, y: rect2.y + rect2.height }
|
|
14795
15399
|
];
|
|
14796
15400
|
}
|
|
14797
15401
|
/** Pointer down. Resolves once a wand click has been applied. */
|
|
@@ -14947,11 +15551,11 @@ var SelectionGesture = class {
|
|
|
14947
15551
|
const options = this.getOptions();
|
|
14948
15552
|
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
14949
15553
|
if (options.tool === "object") {
|
|
14950
|
-
const
|
|
15554
|
+
const rect3 = normalizeRect({ x: this.start.x, y: this.start.y, width: this.current.x - this.start.x, height: this.current.y - this.start.y });
|
|
14951
15555
|
this.start = null;
|
|
14952
15556
|
this.current = null;
|
|
14953
15557
|
this.points = [];
|
|
14954
|
-
const target =
|
|
15558
|
+
const target = rect3.width >= 4 && rect3.height >= 4 ? { rect: rect3 } : { point: { x: rect3.x + rect3.width / 2, y: rect3.y + rect3.height / 2 } };
|
|
14955
15559
|
this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
|
|
14956
15560
|
() => void 0,
|
|
14957
15561
|
(err) => {
|
|
@@ -14981,7 +15585,7 @@ var SelectionGesture = class {
|
|
|
14981
15585
|
return false;
|
|
14982
15586
|
}
|
|
14983
15587
|
const path = this.draftPath;
|
|
14984
|
-
const
|
|
15588
|
+
const rect2 = normalizeRect({
|
|
14985
15589
|
x: this.start.x,
|
|
14986
15590
|
y: this.start.y,
|
|
14987
15591
|
width: this.current.x - this.start.x,
|
|
@@ -14993,7 +15597,7 @@ var SelectionGesture = class {
|
|
|
14993
15597
|
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
14994
15598
|
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
14995
15599
|
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
14996
|
-
return options.tool === "ellipse" ? this.controller.selectEllipse(
|
|
15600
|
+
return options.tool === "ellipse" ? this.controller.selectEllipse(rect2, opts) : this.controller.selectRect(rect2, opts);
|
|
14997
15601
|
}
|
|
14998
15602
|
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
14999
15603
|
close() {
|
|
@@ -15017,11 +15621,11 @@ var SelectionGesture = class {
|
|
|
15017
15621
|
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
15018
15622
|
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
15019
15623
|
*/
|
|
15020
|
-
key(
|
|
15624
|
+
key(key2) {
|
|
15021
15625
|
if (!this.polygon && !this.magnetic) return false;
|
|
15022
|
-
if (
|
|
15023
|
-
else if (
|
|
15024
|
-
else if (
|
|
15626
|
+
if (key2 === "Enter") this.close();
|
|
15627
|
+
else if (key2 === "Escape") this.cancel();
|
|
15628
|
+
else if (key2 === "Backspace" || key2 === "Delete") {
|
|
15025
15629
|
const m = this.magnetic;
|
|
15026
15630
|
if (m) {
|
|
15027
15631
|
m.anchors.pop();
|
|
@@ -15121,7 +15725,7 @@ var SelectionGesture = class {
|
|
|
15121
15725
|
};
|
|
15122
15726
|
function handleSelectionShortcut(controller, e, onError) {
|
|
15123
15727
|
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
15124
|
-
const
|
|
15728
|
+
const key2 = e.key.toLowerCase();
|
|
15125
15729
|
const run = (fn) => {
|
|
15126
15730
|
try {
|
|
15127
15731
|
fn();
|
|
@@ -15130,11 +15734,11 @@ function handleSelectionShortcut(controller, e, onError) {
|
|
|
15130
15734
|
}
|
|
15131
15735
|
return true;
|
|
15132
15736
|
};
|
|
15133
|
-
if (
|
|
15134
|
-
if (
|
|
15135
|
-
if (
|
|
15136
|
-
if (
|
|
15137
|
-
if (
|
|
15737
|
+
if (key2 === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
15738
|
+
if (key2 === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
15739
|
+
if (key2 === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
15740
|
+
if (key2 === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
15741
|
+
if (key2 === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
15138
15742
|
return false;
|
|
15139
15743
|
}
|
|
15140
15744
|
function handleMaskViewKey(controller, e) {
|
|
@@ -15284,14 +15888,14 @@ function handlePaintShortcut(controller, e) {
|
|
|
15284
15888
|
if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
|
|
15285
15889
|
const brush = controller.getState().paint.brush;
|
|
15286
15890
|
const set = (b) => controller.setPaintOptions({ brush: b });
|
|
15287
|
-
const
|
|
15891
|
+
const key2 = e.key;
|
|
15288
15892
|
if (handleMaskViewKey(controller, e)) return true;
|
|
15289
|
-
switch (
|
|
15893
|
+
switch (key2.toLowerCase()) {
|
|
15290
15894
|
case "q":
|
|
15291
15895
|
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
15292
15896
|
return true;
|
|
15293
15897
|
case "b":
|
|
15294
|
-
controller.setTool(e.shiftKey ||
|
|
15898
|
+
controller.setTool(e.shiftKey || key2 === "B" ? "mixer" : "brush");
|
|
15295
15899
|
return true;
|
|
15296
15900
|
case "e":
|
|
15297
15901
|
controller.setTool("eraser");
|
|
@@ -15303,11 +15907,11 @@ function handlePaintShortcut(controller, e) {
|
|
|
15303
15907
|
controller.setTool("clone");
|
|
15304
15908
|
return true;
|
|
15305
15909
|
case "j":
|
|
15306
|
-
controller.setTool(e.shiftKey ||
|
|
15910
|
+
controller.setTool(e.shiftKey || key2 === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
|
|
15307
15911
|
return true;
|
|
15308
15912
|
case "o": {
|
|
15309
15913
|
const cur = controller.getState().activeTool;
|
|
15310
|
-
const next = e.shiftKey ||
|
|
15914
|
+
const next = e.shiftKey || key2 === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
|
|
15311
15915
|
controller.setTool(next);
|
|
15312
15916
|
return true;
|
|
15313
15917
|
}
|
|
@@ -15321,18 +15925,18 @@ function handlePaintShortcut(controller, e) {
|
|
|
15321
15925
|
controller.swapPaintColors();
|
|
15322
15926
|
return true;
|
|
15323
15927
|
}
|
|
15324
|
-
if (
|
|
15325
|
-
if (e.shiftKey ||
|
|
15928
|
+
if (key2 === "[" || key2 === "{") {
|
|
15929
|
+
if (e.shiftKey || key2 === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
|
|
15326
15930
|
else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
|
|
15327
15931
|
return true;
|
|
15328
15932
|
}
|
|
15329
|
-
if (
|
|
15330
|
-
if (e.shiftKey ||
|
|
15933
|
+
if (key2 === "]" || key2 === "}") {
|
|
15934
|
+
if (e.shiftKey || key2 === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
|
|
15331
15935
|
else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
|
|
15332
15936
|
return true;
|
|
15333
15937
|
}
|
|
15334
|
-
if (/^[0-9]$/.test(
|
|
15335
|
-
set({ opacity:
|
|
15938
|
+
if (/^[0-9]$/.test(key2) && isPaintTool(controller.getState().activeTool)) {
|
|
15939
|
+
set({ opacity: key2 === "0" ? 1 : Number(key2) / 10 });
|
|
15336
15940
|
return true;
|
|
15337
15941
|
}
|
|
15338
15942
|
return false;
|
|
@@ -18136,11 +18740,11 @@ function scalePixels(pixels, w, h) {
|
|
|
18136
18740
|
return { width: w, height: h, data: out };
|
|
18137
18741
|
}
|
|
18138
18742
|
function scaleEdit(edit, s) {
|
|
18139
|
-
const
|
|
18743
|
+
const pt2 = (p) => ({ x: p.x * s, y: p.y * s });
|
|
18140
18744
|
if (edit.kind === "object") {
|
|
18141
|
-
return { ...edit, rect: edit.rect && { x: edit.rect.x * s, y: edit.rect.y * s, width: edit.rect.width * s, height: edit.rect.height * s }, point: edit.point &&
|
|
18745
|
+
return { ...edit, rect: edit.rect && { x: edit.rect.x * s, y: edit.rect.y * s, width: edit.rect.width * s, height: edit.rect.height * s }, point: edit.point && pt2(edit.point) };
|
|
18142
18746
|
}
|
|
18143
|
-
const scaled = { ...edit, points: edit.points.map(
|
|
18747
|
+
const scaled = { ...edit, points: edit.points.map(pt2) };
|
|
18144
18748
|
if ("size" in edit && edit.size !== void 0) scaled.size = Math.max(1, edit.size * s);
|
|
18145
18749
|
return scaled;
|
|
18146
18750
|
}
|
|
@@ -18432,8 +19036,8 @@ var SelectAndMaskSession = class {
|
|
|
18432
19036
|
if (!live || !this.dragStart) return;
|
|
18433
19037
|
if (live.kind === "object") {
|
|
18434
19038
|
const a = this.dragStart;
|
|
18435
|
-
const
|
|
18436
|
-
this.live =
|
|
19039
|
+
const rect2 = { x: Math.min(a.x, point.x), y: Math.min(a.y, point.y), width: Math.abs(point.x - a.x), height: Math.abs(point.y - a.y) };
|
|
19040
|
+
this.live = rect2.width >= 4 && rect2.height >= 4 ? { kind: "object", rect: rect2, mode: live.mode } : { kind: "object", point: a, mode: live.mode };
|
|
18437
19041
|
return;
|
|
18438
19042
|
}
|
|
18439
19043
|
const last = live.points[live.points.length - 1];
|
|
@@ -18470,8 +19074,8 @@ var SelectAndMaskSession = class {
|
|
|
18470
19074
|
* Show Original), J (Show Edge), P (Show Original), a tool letter, [ and ] (brush size).
|
|
18471
19075
|
* Returns true when the key was used.
|
|
18472
19076
|
*/
|
|
18473
|
-
handleKey(
|
|
18474
|
-
const k =
|
|
19077
|
+
handleKey(key2) {
|
|
19078
|
+
const k = key2.toLowerCase();
|
|
18475
19079
|
const view = SELECT_AND_MASK_VIEWS.find((v) => v.key === k);
|
|
18476
19080
|
if (view) {
|
|
18477
19081
|
this.update({ view: view.id });
|
|
@@ -18549,10 +19153,10 @@ function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
|
|
|
18549
19153
|
if (x1 <= x0 || y1 <= y0) return null;
|
|
18550
19154
|
return { x0, y0, x1, y1 };
|
|
18551
19155
|
}
|
|
18552
|
-
function applyRegionMosaic(imageData,
|
|
19156
|
+
function applyRegionMosaic(imageData, regions2, blockSize = 12) {
|
|
18553
19157
|
const { data, width, height } = imageData;
|
|
18554
19158
|
const step = Math.max(2, Math.round(blockSize));
|
|
18555
|
-
for (const region of
|
|
19159
|
+
for (const region of regions2) {
|
|
18556
19160
|
const bounds = resolveMosaicRegionBounds(region, width, height);
|
|
18557
19161
|
if (!bounds) continue;
|
|
18558
19162
|
const { x0, y0, x1, y1 } = bounds;
|
|
@@ -18594,10 +19198,10 @@ function applyRegionMosaic(imageData, regions, blockSize = 12) {
|
|
|
18594
19198
|
}
|
|
18595
19199
|
return imageData;
|
|
18596
19200
|
}
|
|
18597
|
-
function applyRegionBlur(imageData,
|
|
19201
|
+
function applyRegionBlur(imageData, regions2, radius = 10) {
|
|
18598
19202
|
const { data, width, height } = imageData;
|
|
18599
19203
|
const rad = Math.max(1, Math.min(64, Math.round(radius)));
|
|
18600
|
-
for (const region of
|
|
19204
|
+
for (const region of regions2) {
|
|
18601
19205
|
const bounds = resolveMosaicRegionBounds(region, width, height);
|
|
18602
19206
|
if (!bounds) continue;
|
|
18603
19207
|
const { x0, y0, x1, y1 } = bounds;
|
|
@@ -19148,9 +19752,9 @@ function gradientControls(prefix, g) {
|
|
|
19148
19752
|
{ key: `${prefix}.align`, label: "Align with layer", type: "checkbox", value: g.align }
|
|
19149
19753
|
];
|
|
19150
19754
|
}
|
|
19151
|
-
var px = (
|
|
19152
|
-
var pct = (
|
|
19153
|
-
var deg = (
|
|
19755
|
+
var px = (key2, label, value, max = 250) => ({ key: key2, label, type: "range", min: 0, max, step: 1, value, unit: "px" });
|
|
19756
|
+
var pct = (key2, label, value, min = 0, max = 1) => ({ key: key2, label, type: "range", min, max, step: 0.01, value, percent: true, unit: "%" });
|
|
19757
|
+
var deg = (key2, label, value, min = -180, max = 180) => ({ key: key2, label, type: "range", min, max, step: 1, value, unit: "\xB0" });
|
|
19154
19758
|
function layerEffectControls(e, patterns = []) {
|
|
19155
19759
|
const blended = e.kind === "bevel-emboss" ? [] : [
|
|
19156
19760
|
{ key: "blend", label: "Blend mode", type: "select", value: e.blend, options: BLENDS },
|
|
@@ -19235,8 +19839,8 @@ function fillOfType(current, type, patterns) {
|
|
|
19235
19839
|
if (type === "pattern") return { type: "pattern", patternId: patterns[0]?.id ?? "checker", scale: 1 };
|
|
19236
19840
|
return { type: "color", color: current.type === "color" ? current.color : "#000000" };
|
|
19237
19841
|
}
|
|
19238
|
-
function effectControlChange(effect,
|
|
19239
|
-
const [top, ...rest] =
|
|
19842
|
+
function effectControlChange(effect, key2, value, patterns = []) {
|
|
19843
|
+
const [top, ...rest] = key2.split(".");
|
|
19240
19844
|
if (top === "fill" && rest[0] === "type") return { fill: fillOfType(effect.fill, String(value), patterns) };
|
|
19241
19845
|
if (top === "patternId" && !rest.length) {
|
|
19242
19846
|
const name = patternOptions(patterns).find((o) => o.value === value)?.label;
|
|
@@ -20088,7 +20692,9 @@ var IMAGE_TOOL_GROUPS = [
|
|
|
20088
20692
|
{ id: "focus", label: "Blur & Sharpen", tools: ["blur", "sharpen", "smudge"] },
|
|
20089
20693
|
{ id: "tone", label: "Dodge & Burn", tools: ["dodge", "burn", "sponge"] },
|
|
20090
20694
|
{ id: "shape", label: "Shape", tools: ["shape"] },
|
|
20695
|
+
{ id: "pen", label: "Pen", tools: ["path-pen"] },
|
|
20091
20696
|
{ id: "type", label: "Type", tools: ["type"] },
|
|
20697
|
+
{ id: "path-select", label: "Path Selection", tools: ["path-select", "direct-select"] },
|
|
20092
20698
|
{ id: "annotate", label: "Annotate", tools: ["pen", "line", "arrow", "rect", "circle"] }
|
|
20093
20699
|
];
|
|
20094
20700
|
function imageToolGroup(tool) {
|
|
@@ -20099,7 +20705,10 @@ var OTHER_TOOL_LABELS = {
|
|
|
20099
20705
|
crop: "Crop",
|
|
20100
20706
|
shape: "Shape",
|
|
20101
20707
|
type: "Type",
|
|
20102
|
-
pen: "Pen",
|
|
20708
|
+
"path-pen": "Pen",
|
|
20709
|
+
"path-select": "Path Selection",
|
|
20710
|
+
"direct-select": "Direct Selection",
|
|
20711
|
+
pen: "Freehand",
|
|
20103
20712
|
line: "Line",
|
|
20104
20713
|
arrow: "Arrow",
|
|
20105
20714
|
rect: "Rectangle",
|
|
@@ -20115,10 +20724,13 @@ var IMAGE_TOOL_SHORTCUTS = {
|
|
|
20115
20724
|
select: "M",
|
|
20116
20725
|
crop: "C",
|
|
20117
20726
|
shape: "U",
|
|
20118
|
-
type: "T"
|
|
20727
|
+
type: "T",
|
|
20728
|
+
"path-pen": "P",
|
|
20729
|
+
"path-select": "A",
|
|
20730
|
+
"direct-select": "Shift+A"
|
|
20119
20731
|
};
|
|
20120
|
-
function imageToolForShortcut(
|
|
20121
|
-
const shortcut = `${shift ? "Shift+" : ""}${
|
|
20732
|
+
function imageToolForShortcut(key2, shift = false) {
|
|
20733
|
+
const shortcut = `${shift ? "Shift+" : ""}${key2.toUpperCase()}`;
|
|
20122
20734
|
for (const group of IMAGE_TOOL_GROUPS) {
|
|
20123
20735
|
for (const tool of group.tools) if (IMAGE_TOOL_SHORTCUTS[tool] === shortcut) return tool;
|
|
20124
20736
|
}
|
|
@@ -20175,6 +20787,422 @@ function createImageEditorDocument(controller, options = {}) {
|
|
|
20175
20787
|
});
|
|
20176
20788
|
}
|
|
20177
20789
|
|
|
20790
|
+
// src/image-editor/path-edit.ts
|
|
20791
|
+
var PATH_EDIT_TOOLS = ["path-select", "direct-select", "path-pen"];
|
|
20792
|
+
function isPathEditTool(tool) {
|
|
20793
|
+
return PATH_EDIT_TOOLS.includes(tool);
|
|
20794
|
+
}
|
|
20795
|
+
var key = (r) => `${r.component}:${r.subpath}:${r.anchor}`;
|
|
20796
|
+
var parseKey = (k) => {
|
|
20797
|
+
const [component, subpath, anchor] = k.split(":").map(Number);
|
|
20798
|
+
return { component, subpath, anchor };
|
|
20799
|
+
};
|
|
20800
|
+
var pt = (p) => ({ x: p.x, y: p.y });
|
|
20801
|
+
var cloneComponents = (cs) => cs.map((c) => ({ op: c.op, subpaths: c.subpaths.map((sp) => ({ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed })), ...c.live ? { live: c.live } : {} }));
|
|
20802
|
+
var sameTarget = (a, b) => !!a && !!b && a.kind === b.kind && (a.kind === "path" ? a.id === b.id : a.layerId === b.layerId);
|
|
20803
|
+
var PathEditGesture = class {
|
|
20804
|
+
constructor(controller) {
|
|
20805
|
+
this.controller = controller;
|
|
20806
|
+
}
|
|
20807
|
+
controller;
|
|
20808
|
+
/** Selected anchors, as `component:subpath:anchor` keys. */
|
|
20809
|
+
selected = /* @__PURE__ */ new Set();
|
|
20810
|
+
target = null;
|
|
20811
|
+
drag = null;
|
|
20812
|
+
/** The Pen's open subpath being drawn. */
|
|
20813
|
+
drawing = null;
|
|
20814
|
+
pointer = null;
|
|
20815
|
+
scale = 1;
|
|
20816
|
+
error = null;
|
|
20817
|
+
/** Screen pixels per document pixel (the viewport projection's scale). */
|
|
20818
|
+
setView(scale) {
|
|
20819
|
+
if (scale > 0 && Number.isFinite(scale)) this.scale = scale;
|
|
20820
|
+
}
|
|
20821
|
+
get active() {
|
|
20822
|
+
return this.drag !== null;
|
|
20823
|
+
}
|
|
20824
|
+
/** The anchors selected now (on the current target). */
|
|
20825
|
+
get selection() {
|
|
20826
|
+
return [...this.selected].map(parseKey);
|
|
20827
|
+
}
|
|
20828
|
+
/** True while the Pen has an open subpath it adds to. */
|
|
20829
|
+
get isDrawing() {
|
|
20830
|
+
return this.drawing !== null;
|
|
20831
|
+
}
|
|
20832
|
+
get tolerance() {
|
|
20833
|
+
return 6 / this.scale;
|
|
20834
|
+
}
|
|
20835
|
+
/** The target's components, or null (and the selection dropped) when the target changed or is gone. */
|
|
20836
|
+
components() {
|
|
20837
|
+
const target = this.controller.pathEditTarget();
|
|
20838
|
+
if (!sameTarget(target, this.target)) {
|
|
20839
|
+
this.target = target;
|
|
20840
|
+
this.selected.clear();
|
|
20841
|
+
this.drawing = null;
|
|
20842
|
+
}
|
|
20843
|
+
if (!target) return null;
|
|
20844
|
+
let cs;
|
|
20845
|
+
try {
|
|
20846
|
+
cs = this.controller.getPathEditComponents(target);
|
|
20847
|
+
} catch {
|
|
20848
|
+
return null;
|
|
20849
|
+
}
|
|
20850
|
+
for (const k of [...this.selected]) {
|
|
20851
|
+
const r = parseKey(k);
|
|
20852
|
+
if (!cs[r.component]?.subpaths[r.subpath]?.anchors[r.anchor]) this.selected.delete(k);
|
|
20853
|
+
}
|
|
20854
|
+
const d = this.drawing;
|
|
20855
|
+
if (d && (!cs[d.component]?.subpaths[d.subpath] || cs[d.component].subpaths[d.subpath].closed)) this.drawing = null;
|
|
20856
|
+
return cs;
|
|
20857
|
+
}
|
|
20858
|
+
anchorAt(cs, p) {
|
|
20859
|
+
const tol = this.tolerance;
|
|
20860
|
+
let best = null;
|
|
20861
|
+
let bestD = tol;
|
|
20862
|
+
cs.forEach(
|
|
20863
|
+
(c, ci) => c.subpaths.forEach(
|
|
20864
|
+
(sp, si) => sp.anchors.forEach((a, ai) => {
|
|
20865
|
+
const d = Math.hypot(a.x - p.x, a.y - p.y);
|
|
20866
|
+
if (d <= bestD) {
|
|
20867
|
+
bestD = d;
|
|
20868
|
+
best = { component: ci, subpath: si, anchor: ai };
|
|
20869
|
+
}
|
|
20870
|
+
})
|
|
20871
|
+
)
|
|
20872
|
+
);
|
|
20873
|
+
return best;
|
|
20874
|
+
}
|
|
20875
|
+
/** A handle of a selected anchor near `p`. */
|
|
20876
|
+
handleAt(cs, p) {
|
|
20877
|
+
const tol = this.tolerance;
|
|
20878
|
+
for (const r of this.selection) {
|
|
20879
|
+
const a = cs[r.component].subpaths[r.subpath].anchors[r.anchor];
|
|
20880
|
+
for (const side of ["out", "in"]) {
|
|
20881
|
+
const h = a[side];
|
|
20882
|
+
if (h && Math.hypot(h.x - p.x, h.y - p.y) <= tol) return { ref: r, side };
|
|
20883
|
+
}
|
|
20884
|
+
}
|
|
20885
|
+
return null;
|
|
20886
|
+
}
|
|
20887
|
+
/** The segment near `p`: its first anchor (as `anchor`) and the parameter on it. */
|
|
20888
|
+
segmentAt(cs, p) {
|
|
20889
|
+
let best = null;
|
|
20890
|
+
let bestD = this.tolerance;
|
|
20891
|
+
cs.forEach(
|
|
20892
|
+
(c, ci) => c.subpaths.forEach((sp, si) => {
|
|
20893
|
+
for (let i = 0; i < subpathSegmentCount(sp); i++) {
|
|
20894
|
+
const { t, distance } = segmentNearest(subpathSegment(sp, i), [p.x, p.y]);
|
|
20895
|
+
if (distance <= bestD) {
|
|
20896
|
+
bestD = distance;
|
|
20897
|
+
best = { ref: { component: ci, subpath: si, anchor: i }, t };
|
|
20898
|
+
}
|
|
20899
|
+
}
|
|
20900
|
+
})
|
|
20901
|
+
);
|
|
20902
|
+
return best;
|
|
20903
|
+
}
|
|
20904
|
+
/** The component under `p`: on its outline, or inside its fill. */
|
|
20905
|
+
componentAt(cs, p) {
|
|
20906
|
+
const seg = this.segmentAt(cs, p) ?? (this.anchorAt(cs, p) ? { ref: this.anchorAt(cs, p) } : null);
|
|
20907
|
+
if (seg) return seg.ref.component;
|
|
20908
|
+
for (let i = cs.length - 1; i >= 0; i--) {
|
|
20909
|
+
const closed = cs[i].subpaths.filter((sp) => sp.anchors.length > 2);
|
|
20910
|
+
if (closed.length && pointInBezierPath(subpathsToBezier(closed), [p.x, p.y])) return i;
|
|
20911
|
+
}
|
|
20912
|
+
return null;
|
|
20913
|
+
}
|
|
20914
|
+
selectComponent(cs, ci) {
|
|
20915
|
+
cs[ci].subpaths.forEach((sp, si) => sp.anchors.forEach((_, ai) => this.selected.add(key({ component: ci, subpath: si, anchor: ai }))));
|
|
20916
|
+
}
|
|
20917
|
+
commit(cs, record) {
|
|
20918
|
+
if (!this.target) return false;
|
|
20919
|
+
try {
|
|
20920
|
+
this.controller.setPathEditComponents(this.target, cs, record);
|
|
20921
|
+
return true;
|
|
20922
|
+
} catch (e) {
|
|
20923
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
20924
|
+
return false;
|
|
20925
|
+
}
|
|
20926
|
+
}
|
|
20927
|
+
down(p, tool, mods = {}) {
|
|
20928
|
+
this.error = null;
|
|
20929
|
+
this.pointer = pt(p);
|
|
20930
|
+
const cs = this.components();
|
|
20931
|
+
if (tool === "path-pen") return this.penDown(cs, p, mods);
|
|
20932
|
+
if (!cs) {
|
|
20933
|
+
this.error = "There is no path to edit: pick a path in the Paths panel, a shape layer, or a layer with a vector mask (or draw one with the Pen).";
|
|
20934
|
+
return;
|
|
20935
|
+
}
|
|
20936
|
+
if (tool === "path-select") {
|
|
20937
|
+
const ci = this.componentAt(cs, p);
|
|
20938
|
+
if (ci === null) {
|
|
20939
|
+
this.drag = { kind: "marquee", start: pt(p), end: pt(p), keep: mods.shift ? new Set(this.selected) : /* @__PURE__ */ new Set() };
|
|
20940
|
+
if (!mods.shift) this.selected.clear();
|
|
20941
|
+
return;
|
|
20942
|
+
}
|
|
20943
|
+
const ref = { component: ci, subpath: 0, anchor: 0 };
|
|
20944
|
+
if (!mods.shift && !this.selected.has(key(ref))) this.selected.clear();
|
|
20945
|
+
this.selectComponent(cs, ci);
|
|
20946
|
+
this.drag = { kind: "anchors", start: pt(p), base: cloneComponents(cs), moved: false };
|
|
20947
|
+
return;
|
|
20948
|
+
}
|
|
20949
|
+
const handle = this.handleAt(cs, p);
|
|
20950
|
+
if (handle) {
|
|
20951
|
+
this.drag = { kind: "handle", ...handle, base: cloneComponents(cs), breakHandles: !!mods.alt, moved: false };
|
|
20952
|
+
return;
|
|
20953
|
+
}
|
|
20954
|
+
const anchor = this.anchorAt(cs, p);
|
|
20955
|
+
if (anchor) {
|
|
20956
|
+
if (mods.alt) return this.convert(cs, anchor);
|
|
20957
|
+
const k = key(anchor);
|
|
20958
|
+
if (mods.shift) {
|
|
20959
|
+
if (this.selected.has(k)) this.selected.delete(k);
|
|
20960
|
+
else this.selected.add(k);
|
|
20961
|
+
} else if (!this.selected.has(k)) {
|
|
20962
|
+
this.selected.clear();
|
|
20963
|
+
this.selected.add(k);
|
|
20964
|
+
}
|
|
20965
|
+
this.drag = { kind: "anchors", start: pt(p), base: cloneComponents(cs), moved: false };
|
|
20966
|
+
return;
|
|
20967
|
+
}
|
|
20968
|
+
const seg = this.segmentAt(cs, p);
|
|
20969
|
+
if (seg) {
|
|
20970
|
+
if (!mods.shift) this.selected.clear();
|
|
20971
|
+
this.drag = { kind: "segment", ...seg, base: cloneComponents(cs), moved: false };
|
|
20972
|
+
return;
|
|
20973
|
+
}
|
|
20974
|
+
this.drag = { kind: "marquee", start: pt(p), end: pt(p), keep: mods.shift ? new Set(this.selected) : /* @__PURE__ */ new Set() };
|
|
20975
|
+
if (!mods.shift) this.selected.clear();
|
|
20976
|
+
}
|
|
20977
|
+
/** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
|
|
20978
|
+
convert(cs, r) {
|
|
20979
|
+
const next = cloneComponents(cs);
|
|
20980
|
+
const c = next[r.component];
|
|
20981
|
+
const sp = c.subpaths[r.subpath];
|
|
20982
|
+
const a = sp.anchors[r.anchor];
|
|
20983
|
+
const type = inferAnchorType(a) === "corner" && !a.in && !a.out ? "smooth" : "corner";
|
|
20984
|
+
c.subpaths[r.subpath] = convertAnchor(sp, r.anchor, type);
|
|
20985
|
+
delete c.live;
|
|
20986
|
+
this.selected.clear();
|
|
20987
|
+
this.selected.add(key(r));
|
|
20988
|
+
this.commit(next, true);
|
|
20989
|
+
}
|
|
20990
|
+
penDown(cs, p, mods) {
|
|
20991
|
+
const d = this.drawing;
|
|
20992
|
+
if (cs && d) {
|
|
20993
|
+
const next = cloneComponents(cs);
|
|
20994
|
+
const c = next[d.component];
|
|
20995
|
+
const sp = c.subpaths[d.subpath];
|
|
20996
|
+
const first = sp.anchors[0];
|
|
20997
|
+
delete c.live;
|
|
20998
|
+
if (sp.anchors.length > 1 && Math.hypot(first.x - p.x, first.y - p.y) <= this.tolerance) {
|
|
20999
|
+
sp.closed = true;
|
|
21000
|
+
this.drawing = null;
|
|
21001
|
+
const ref3 = { component: d.component, subpath: d.subpath, anchor: 0 };
|
|
21002
|
+
if (this.commit(next, true)) this.drag = { kind: "pen", ref: ref3, base: next };
|
|
21003
|
+
return;
|
|
21004
|
+
}
|
|
21005
|
+
sp.anchors.push({ x: p.x, y: p.y });
|
|
21006
|
+
const ref2 = { component: d.component, subpath: d.subpath, anchor: sp.anchors.length - 1 };
|
|
21007
|
+
this.selected = /* @__PURE__ */ new Set([key(ref2)]);
|
|
21008
|
+
if (this.commit(next, true)) this.drag = { kind: "pen", ref: ref2, base: next };
|
|
21009
|
+
return;
|
|
21010
|
+
}
|
|
21011
|
+
if (cs) {
|
|
21012
|
+
const anchor = this.anchorAt(cs, p);
|
|
21013
|
+
if (anchor && mods.alt) return this.convert(cs, anchor);
|
|
21014
|
+
if (anchor) {
|
|
21015
|
+
const next = cloneComponents(cs);
|
|
21016
|
+
const c = next[anchor.component];
|
|
21017
|
+
const sp = c.subpaths[anchor.subpath];
|
|
21018
|
+
delete c.live;
|
|
21019
|
+
c.subpaths[anchor.subpath] = sp.anchors.length > 2 ? removeAnchorsKeepingShape(sp, [anchor.anchor]) : removeAnchors(sp, [anchor.anchor]);
|
|
21020
|
+
this.selected.clear();
|
|
21021
|
+
this.commit(prune(next), true);
|
|
21022
|
+
return;
|
|
21023
|
+
}
|
|
21024
|
+
const seg = this.segmentAt(cs, p);
|
|
21025
|
+
if (seg) {
|
|
21026
|
+
const next = cloneComponents(cs);
|
|
21027
|
+
const c = next[seg.ref.component];
|
|
21028
|
+
const { subpath, index } = insertAnchor(c.subpaths[seg.ref.subpath], seg.ref.anchor, seg.t);
|
|
21029
|
+
c.subpaths[seg.ref.subpath] = subpath;
|
|
21030
|
+
delete c.live;
|
|
21031
|
+
this.selected = /* @__PURE__ */ new Set([key({ ...seg.ref, anchor: index })]);
|
|
21032
|
+
this.commit(next, true);
|
|
21033
|
+
return;
|
|
21034
|
+
}
|
|
21035
|
+
}
|
|
21036
|
+
const start = { anchors: [{ x: p.x, y: p.y }], closed: false };
|
|
21037
|
+
if (!this.target || !cs) {
|
|
21038
|
+
try {
|
|
21039
|
+
const path = this.controller.startWorkPath([{ op: "combine", subpaths: [start] }]);
|
|
21040
|
+
this.target = { kind: "path", id: path.id };
|
|
21041
|
+
} catch (e) {
|
|
21042
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
21043
|
+
return;
|
|
21044
|
+
}
|
|
21045
|
+
this.drawing = { component: 0, subpath: 0 };
|
|
21046
|
+
} else {
|
|
21047
|
+
const next = cloneComponents(cs);
|
|
21048
|
+
if (!next.length) next.push({ op: "combine", subpaths: [] });
|
|
21049
|
+
const ci = next.length - 1;
|
|
21050
|
+
delete next[ci].live;
|
|
21051
|
+
next[ci].subpaths.push(start);
|
|
21052
|
+
if (!this.commit(next, true)) return;
|
|
21053
|
+
this.drawing = { component: ci, subpath: next[ci].subpaths.length - 1 };
|
|
21054
|
+
}
|
|
21055
|
+
const ref = { component: this.drawing.component, subpath: this.drawing.subpath, anchor: 0 };
|
|
21056
|
+
this.selected = /* @__PURE__ */ new Set([key(ref)]);
|
|
21057
|
+
this.drag = { kind: "pen", ref, base: cloneComponents(this.controller.getPathEditComponents(this.target)) };
|
|
21058
|
+
}
|
|
21059
|
+
move(p, mods = {}) {
|
|
21060
|
+
this.pointer = pt(p);
|
|
21061
|
+
const g = this.drag;
|
|
21062
|
+
if (!g) return;
|
|
21063
|
+
if (g.kind === "marquee") {
|
|
21064
|
+
g.end = pt(p);
|
|
21065
|
+
const box = rect(g.start, g.end);
|
|
21066
|
+
const cs = this.target ? this.controller.getPathEditComponents(this.target) : [];
|
|
21067
|
+
this.selected = new Set(g.keep);
|
|
21068
|
+
cs.forEach(
|
|
21069
|
+
(c, ci) => c.subpaths.forEach(
|
|
21070
|
+
(sp, si) => sp.anchors.forEach((a, ai) => {
|
|
21071
|
+
if (a.x >= box.x && a.x <= box.x + box.width && a.y >= box.y && a.y <= box.y + box.height) this.selected.add(key({ component: ci, subpath: si, anchor: ai }));
|
|
21072
|
+
})
|
|
21073
|
+
)
|
|
21074
|
+
);
|
|
21075
|
+
return;
|
|
21076
|
+
}
|
|
21077
|
+
const next = cloneComponents(g.base);
|
|
21078
|
+
if (g.kind === "anchors") {
|
|
21079
|
+
let dx = p.x - g.start.x;
|
|
21080
|
+
let dy = p.y - g.start.y;
|
|
21081
|
+
if (!g.moved && Math.hypot(dx, dy) < 1 / this.scale) return;
|
|
21082
|
+
if (mods.shift) {
|
|
21083
|
+
if (Math.abs(dx) > Math.abs(dy)) dy = 0;
|
|
21084
|
+
else dx = 0;
|
|
21085
|
+
}
|
|
21086
|
+
for (const r of this.selection) {
|
|
21087
|
+
const c = next[r.component];
|
|
21088
|
+
c.subpaths[r.subpath].anchors[r.anchor] = translateAnchor(c.subpaths[r.subpath].anchors[r.anchor], dx, dy);
|
|
21089
|
+
delete c.live;
|
|
21090
|
+
}
|
|
21091
|
+
} else if (g.kind === "handle") {
|
|
21092
|
+
const c = next[g.ref.component];
|
|
21093
|
+
const a = c.subpaths[g.ref.subpath].anchors[g.ref.anchor];
|
|
21094
|
+
const typed = { ...a, type: a.type ?? inferAnchorType(a) };
|
|
21095
|
+
const moved = moveAnchorHandle(typed, g.side, p, g.breakHandles);
|
|
21096
|
+
delete moved.type;
|
|
21097
|
+
c.subpaths[g.ref.subpath].anchors[g.ref.anchor] = moved;
|
|
21098
|
+
delete c.live;
|
|
21099
|
+
} else if (g.kind === "segment") {
|
|
21100
|
+
const c = next[g.ref.component];
|
|
21101
|
+
c.subpaths[g.ref.subpath] = bendSegment(c.subpaths[g.ref.subpath], g.ref.anchor, g.t, p);
|
|
21102
|
+
delete c.live;
|
|
21103
|
+
} else {
|
|
21104
|
+
const c = next[g.ref.component];
|
|
21105
|
+
const a = c.subpaths[g.ref.subpath].anchors[g.ref.anchor];
|
|
21106
|
+
if (Math.hypot(p.x - a.x, p.y - a.y) < 1 / this.scale) return;
|
|
21107
|
+
a.out = { x: p.x, y: p.y };
|
|
21108
|
+
a.in = { x: 2 * a.x - p.x, y: 2 * a.y - p.y };
|
|
21109
|
+
delete c.live;
|
|
21110
|
+
this.commit(next, false);
|
|
21111
|
+
return;
|
|
21112
|
+
}
|
|
21113
|
+
if (this.commit(next, !g.moved)) g.moved = true;
|
|
21114
|
+
}
|
|
21115
|
+
/** The pointer moving with no button down (the Pen's preview of its next segment). */
|
|
21116
|
+
hover(p) {
|
|
21117
|
+
this.pointer = pt(p);
|
|
21118
|
+
}
|
|
21119
|
+
up() {
|
|
21120
|
+
this.drag = null;
|
|
21121
|
+
}
|
|
21122
|
+
/** Ends the Pen's subpath (left open). */
|
|
21123
|
+
finish() {
|
|
21124
|
+
this.drawing = null;
|
|
21125
|
+
this.drag = null;
|
|
21126
|
+
}
|
|
21127
|
+
cancel() {
|
|
21128
|
+
this.drag = null;
|
|
21129
|
+
this.drawing = null;
|
|
21130
|
+
this.selected.clear();
|
|
21131
|
+
}
|
|
21132
|
+
/**
|
|
21133
|
+
* Keys: Delete / Backspace remove the selected anchors (one undo step), Enter and Escape end the
|
|
21134
|
+
* Pen's subpath, Escape also deselects. Returns true when the key was handled.
|
|
21135
|
+
*/
|
|
21136
|
+
key(k) {
|
|
21137
|
+
if (k === "Enter" && this.drawing) {
|
|
21138
|
+
this.finish();
|
|
21139
|
+
return true;
|
|
21140
|
+
}
|
|
21141
|
+
if (k === "Escape") {
|
|
21142
|
+
if (!this.drawing && !this.selected.size) return false;
|
|
21143
|
+
this.drawing = null;
|
|
21144
|
+
this.selected.clear();
|
|
21145
|
+
return true;
|
|
21146
|
+
}
|
|
21147
|
+
if ((k === "Delete" || k === "Backspace") && this.selected.size && !this.drawing) {
|
|
21148
|
+
const cs = this.components();
|
|
21149
|
+
if (!cs) return false;
|
|
21150
|
+
const next = cloneComponents(cs);
|
|
21151
|
+
const bySub = /* @__PURE__ */ new Map();
|
|
21152
|
+
for (const r of this.selection) {
|
|
21153
|
+
const sk = `${r.component}:${r.subpath}`;
|
|
21154
|
+
bySub.set(sk, [...bySub.get(sk) ?? [], r.anchor]);
|
|
21155
|
+
}
|
|
21156
|
+
for (const [sk, indices] of bySub) {
|
|
21157
|
+
const [ci, si] = sk.split(":").map(Number);
|
|
21158
|
+
next[ci].subpaths[si] = removeAnchors(next[ci].subpaths[si], indices);
|
|
21159
|
+
delete next[ci].live;
|
|
21160
|
+
}
|
|
21161
|
+
this.selected.clear();
|
|
21162
|
+
this.commit(prune(next), true);
|
|
21163
|
+
return true;
|
|
21164
|
+
}
|
|
21165
|
+
return false;
|
|
21166
|
+
}
|
|
21167
|
+
/** What to draw (`RenderOptions.pathEdit`) for `tool`, or null when there is no target. */
|
|
21168
|
+
overlay(tool) {
|
|
21169
|
+
const cs = this.components();
|
|
21170
|
+
if (!cs) return null;
|
|
21171
|
+
const curves = [];
|
|
21172
|
+
const anchors = [];
|
|
21173
|
+
const handles = [];
|
|
21174
|
+
const selectedComponents = new Set(this.selection.map((r) => r.component));
|
|
21175
|
+
cs.forEach(
|
|
21176
|
+
(c, ci) => c.subpaths.forEach((sp, si) => {
|
|
21177
|
+
for (let i = 0; i < subpathSegmentCount(sp); i++) curves.push(subpathSegment(sp, i).map(([x, y]) => ({ x, y })));
|
|
21178
|
+
sp.anchors.forEach((a, ai) => {
|
|
21179
|
+
const selected = this.selected.has(key({ component: ci, subpath: si, anchor: ai }));
|
|
21180
|
+
if (tool === "path-select" && !selectedComponents.has(ci)) return;
|
|
21181
|
+
anchors.push({ point: pt(a), selected });
|
|
21182
|
+
if (!selected || tool === "path-select") return;
|
|
21183
|
+
for (const side of ["in", "out"]) if (a[side]) handles.push({ point: pt(a[side]), anchor: pt(a) });
|
|
21184
|
+
});
|
|
21185
|
+
})
|
|
21186
|
+
);
|
|
21187
|
+
let preview = null;
|
|
21188
|
+
const d = this.drawing;
|
|
21189
|
+
if (tool === "path-pen" && d && this.pointer && !this.drag) {
|
|
21190
|
+
const sp = cs[d.component]?.subpaths[d.subpath];
|
|
21191
|
+
const last = sp?.anchors[sp.anchors.length - 1];
|
|
21192
|
+
if (last) preview = last.out ? [pt(last), pt(last.out), this.pointer, this.pointer] : [pt(last), this.pointer];
|
|
21193
|
+
}
|
|
21194
|
+
const marquee = this.drag?.kind === "marquee" ? rect(this.drag.start, this.drag.end) : null;
|
|
21195
|
+
return { curves, anchors, handles, marquee, preview };
|
|
21196
|
+
}
|
|
21197
|
+
};
|
|
21198
|
+
function rect(a, b) {
|
|
21199
|
+
return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
|
|
21200
|
+
}
|
|
21201
|
+
function prune(cs) {
|
|
21202
|
+
for (const c of cs) c.subpaths = c.subpaths.filter((sp) => sp.anchors.length);
|
|
21203
|
+
return cs.filter((c) => c.subpaths.length);
|
|
21204
|
+
}
|
|
21205
|
+
|
|
20178
21206
|
// src/image-editor/content-aware-fill-session.ts
|
|
20179
21207
|
var CONTENT_AWARE_FILL_TOOLS = [
|
|
20180
21208
|
{ id: "sampling-brush", label: "Sampling Brush", key: "b" },
|
|
@@ -20466,8 +21494,8 @@ var ContentAwareFillSession = class _ContentAwareFillSession {
|
|
|
20466
21494
|
/**
|
|
20467
21495
|
* Keys: B, H, Z for the tools, [ and ] for the brush size. Returns true when the key was used.
|
|
20468
21496
|
*/
|
|
20469
|
-
handleKey(
|
|
20470
|
-
const k =
|
|
21497
|
+
handleKey(key2) {
|
|
21498
|
+
const k = key2.toLowerCase();
|
|
20471
21499
|
const tool = CONTENT_AWARE_FILL_TOOLS.find((t) => t.key === k);
|
|
20472
21500
|
if (tool) {
|
|
20473
21501
|
this.update({ tool: tool.id });
|
|
@@ -20607,6 +21635,7 @@ export {
|
|
|
20607
21635
|
LayerEffectError,
|
|
20608
21636
|
LayerOperationError,
|
|
20609
21637
|
MARCHING_ANTS_STEP_MS,
|
|
21638
|
+
MASK_EDGE_THRESHOLDS,
|
|
20610
21639
|
MASK_FILE_ACCEPT,
|
|
20611
21640
|
MAX_BATCH,
|
|
20612
21641
|
MAX_BRUSH_SIZE,
|
|
@@ -20646,6 +21675,7 @@ export {
|
|
|
20646
21675
|
PARAM,
|
|
20647
21676
|
PARAM_FLOATS,
|
|
20648
21677
|
PASTE_MODES,
|
|
21678
|
+
PATH_EDIT_TOOLS,
|
|
20649
21679
|
PATTERN_BLOCK_KEYS,
|
|
20650
21680
|
PIGMENT_BANDS,
|
|
20651
21681
|
PIGMENT_CHANNELS,
|
|
@@ -20675,6 +21705,7 @@ export {
|
|
|
20675
21705
|
PaintSourceError,
|
|
20676
21706
|
PaintStrokeSession,
|
|
20677
21707
|
PaperGrain,
|
|
21708
|
+
PathEditGesture,
|
|
20678
21709
|
PatternPlacement,
|
|
20679
21710
|
PatternTile,
|
|
20680
21711
|
PsdError,
|
|
@@ -21099,6 +22130,7 @@ export {
|
|
|
21099
22130
|
inflateRaw,
|
|
21100
22131
|
insertCurvePoint,
|
|
21101
22132
|
insertLayer,
|
|
22133
|
+
inspectMaskEdge,
|
|
21102
22134
|
instancesOf,
|
|
21103
22135
|
invertHomography,
|
|
21104
22136
|
invertMatrix3,
|
|
@@ -21122,6 +22154,7 @@ export {
|
|
|
21122
22154
|
isPaintTool,
|
|
21123
22155
|
isPaintableLayer,
|
|
21124
22156
|
isPainterlyBlendMode,
|
|
22157
|
+
isPathEditTool,
|
|
21125
22158
|
isPng,
|
|
21126
22159
|
isQuarterTurn,
|
|
21127
22160
|
isQuarterTurnSwapped,
|