@pixerate/editor 0.107.0 → 0.108.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/THIRD_PARTY_NOTICES.md +4 -0
- package/dist/animation-editor/index.cjs +39 -39
- package/dist/animation-editor/index.d.cts +1 -1
- package/dist/animation-editor/index.d.ts +1 -1
- package/dist/animation-editor/index.js +1 -1
- package/dist/{chunk-63EFQCT5.js → chunk-HUQ7EHVI.js} +1140 -371
- package/dist/chunk-HUQ7EHVI.js.map +1 -0
- package/dist/{chunk-KANRAHTL.cjs → chunk-UTEVPQTS.cjs} +1353 -584
- package/dist/chunk-UTEVPQTS.cjs.map +1 -0
- package/dist/paint-editor/index.cjs +196 -11
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +96 -2
- package/dist/paint-editor/index.d.ts +96 -2
- package/dist/paint-editor/index.js +190 -5
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/{svg-Bj1MQFh6.d.cts → svg-ClVIG4T7.d.cts} +26 -2
- package/dist/{svg-vme0PLNo.d.ts → svg-SseJTIrA.d.ts} +26 -2
- package/dist/vector/index.cjs +977 -948
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +266 -7
- package/dist/vector/index.d.ts +266 -7
- package/dist/vector/index.js +427 -398
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +50 -7
- package/skills/pixerate-vector/SKILL.md +35 -5
- package/dist/chunk-63EFQCT5.js.map +0 -1
- package/dist/chunk-KANRAHTL.cjs.map +0 -1
package/dist/vector/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
VECTOR_GRADIENT_MIN_STOPS,
|
|
33
33
|
VECTOR_GRADIENT_SPREADS,
|
|
34
34
|
VECTOR_GRADIENT_TYPES,
|
|
35
|
+
VECTOR_MASK_TYPES,
|
|
35
36
|
VECTOR_MERGE_MODES,
|
|
36
37
|
VECTOR_PATHFINDER_OPERATIONS,
|
|
37
38
|
VECTOR_PRESSURE_NOMINAL,
|
|
@@ -77,6 +78,9 @@ import {
|
|
|
77
78
|
effectPathVertexCount,
|
|
78
79
|
effectiveStrokeAlign,
|
|
79
80
|
ellipseSubpath,
|
|
81
|
+
eraseFromFill,
|
|
82
|
+
eraseFromOutline,
|
|
83
|
+
eraserRegion,
|
|
80
84
|
evaluateVectorPathKeyframes,
|
|
81
85
|
expandVectorAppearance,
|
|
82
86
|
expandVectorAppearances,
|
|
@@ -122,6 +126,10 @@ import {
|
|
|
122
126
|
isVectorFilterType,
|
|
123
127
|
isVectorGradient,
|
|
124
128
|
isVectorLiveBoolean,
|
|
129
|
+
isVectorMaskActive,
|
|
130
|
+
isVectorMaskGroup,
|
|
131
|
+
isVectorMaskType,
|
|
132
|
+
knifePath,
|
|
125
133
|
lineSubpath,
|
|
126
134
|
locateVectorNode,
|
|
127
135
|
mapBezierPath,
|
|
@@ -154,6 +162,7 @@ import {
|
|
|
154
162
|
periodicVectorGradientStops,
|
|
155
163
|
pinVectorGradient,
|
|
156
164
|
polygonSubpath,
|
|
165
|
+
polylineBounds,
|
|
157
166
|
pressureWidthProfile,
|
|
158
167
|
puckerBloatPaths,
|
|
159
168
|
rectSubpath,
|
|
@@ -177,6 +186,7 @@ import {
|
|
|
177
186
|
sanitizeVectorFills,
|
|
178
187
|
sanitizeVectorFilters,
|
|
179
188
|
sanitizeVectorGradient,
|
|
189
|
+
sanitizeVectorMask,
|
|
180
190
|
sanitizeVectorStrokeSettings,
|
|
181
191
|
sanitizeVectorStrokes,
|
|
182
192
|
sanitizeWidthProfile,
|
|
@@ -184,8 +194,10 @@ import {
|
|
|
184
194
|
setSubpathCorners,
|
|
185
195
|
setVectorGradientVector,
|
|
186
196
|
setWidthPoint,
|
|
197
|
+
simplifyPolyline,
|
|
187
198
|
sortedVectorGradientStops,
|
|
188
199
|
spanVectorGradientStops,
|
|
200
|
+
splitIntoFaces,
|
|
189
201
|
starSubpath,
|
|
190
202
|
strokePositionPoint,
|
|
191
203
|
strokeWidthProfileOf,
|
|
@@ -213,6 +225,7 @@ import {
|
|
|
213
225
|
vectorBlendModeCss,
|
|
214
226
|
vectorBlendModeFromCss,
|
|
215
227
|
vectorBoxMatrix,
|
|
228
|
+
vectorChildAppearanceShapes,
|
|
216
229
|
vectorCornerAt,
|
|
217
230
|
vectorCorners,
|
|
218
231
|
vectorCornersAttribute,
|
|
@@ -243,6 +256,19 @@ import {
|
|
|
243
256
|
vectorGradientTAt,
|
|
244
257
|
vectorGradientUnitsMatrix,
|
|
245
258
|
vectorLiveBooleanSteps,
|
|
259
|
+
vectorMaskContains,
|
|
260
|
+
vectorMaskContentBounds,
|
|
261
|
+
vectorMaskContents,
|
|
262
|
+
vectorMaskOwner,
|
|
263
|
+
vectorMaskRegion,
|
|
264
|
+
vectorMaskRegionPath,
|
|
265
|
+
vectorMaskRegionShapes,
|
|
266
|
+
vectorMaskRenderData,
|
|
267
|
+
vectorMaskSource,
|
|
268
|
+
vectorMaskSummary,
|
|
269
|
+
vectorMaskedGeometry,
|
|
270
|
+
vectorMaskedLocalBounds,
|
|
271
|
+
vectorMaskedShapes,
|
|
246
272
|
vectorMeshFromOutline,
|
|
247
273
|
vectorMeshOutline,
|
|
248
274
|
vectorNodeBounds,
|
|
@@ -284,7 +310,7 @@ import {
|
|
|
284
310
|
withVectorStrokes,
|
|
285
311
|
withVectorTargetPaint,
|
|
286
312
|
zigzagPaths
|
|
287
|
-
} from "../chunk-
|
|
313
|
+
} from "../chunk-HUQ7EHVI.js";
|
|
288
314
|
import {
|
|
289
315
|
bezierPathBounds,
|
|
290
316
|
bezierPathToSvg,
|
|
@@ -294,20 +320,14 @@ import {
|
|
|
294
320
|
distanceToBezierPath,
|
|
295
321
|
fitCubicWithTangents,
|
|
296
322
|
fitCubics,
|
|
297
|
-
normalizeBezierPath,
|
|
298
|
-
outlineStroke,
|
|
299
323
|
pathBoolean,
|
|
300
324
|
pathToContours,
|
|
301
|
-
pointInBezierPath,
|
|
302
|
-
segmentBounds,
|
|
303
325
|
segmentDerivative,
|
|
304
326
|
segmentIntersections,
|
|
305
327
|
segmentNearest,
|
|
306
328
|
segmentPoint,
|
|
307
329
|
segmentTangent,
|
|
308
|
-
|
|
309
|
-
transformBezierPath,
|
|
310
|
-
windingNumber
|
|
330
|
+
transformBezierPath
|
|
311
331
|
} from "../chunk-YCSFS3GK.js";
|
|
312
332
|
import {
|
|
313
333
|
IDENTITY_MATRIX,
|
|
@@ -408,14 +428,14 @@ function vectorGradientBarPosition(a, p) {
|
|
|
408
428
|
return { offset: (d.x * v.x + d.y * v.y) / l2, across: d.x * a.normal.x + d.y * a.normal.y };
|
|
409
429
|
}
|
|
410
430
|
function hitVectorGradientAnnotator(a, p) {
|
|
411
|
-
const
|
|
412
|
-
for (let i = a.stops.length - 1; i >= 0; i--) if (
|
|
413
|
-
for (const m of a.midpoints) if (
|
|
431
|
+
const near = (q, r) => !!q && Math.hypot(q.x - p.x, q.y - p.y) <= r;
|
|
432
|
+
for (let i = a.stops.length - 1; i >= 0; i--) if (near(a.stops[i], STOP_HIT)) return { kind: "stop", index: a.stops[i].index };
|
|
433
|
+
for (const m of a.midpoints) if (near(m, MIDPOINT_HIT)) return { kind: "midpoint", index: m.index };
|
|
414
434
|
const centred = isCentredVectorGradient(a.gradient.type);
|
|
415
|
-
if (
|
|
416
|
-
if (a.focal &&
|
|
417
|
-
if (
|
|
418
|
-
if (
|
|
435
|
+
if (near(a.end, HANDLE_HIT)) return { kind: "handle", handle: centred ? "radius" : "end" };
|
|
436
|
+
if (a.focal && near(a.focal, HANDLE_HIT) && !near(a.start, 1)) return { kind: "handle", handle: "focal" };
|
|
437
|
+
if (near(a.start, HANDLE_HIT)) return { kind: "handle", handle: "start" };
|
|
438
|
+
if (near(a.aspect, HANDLE_HIT)) return { kind: "handle", handle: "aspect" };
|
|
419
439
|
const { offset, across } = vectorGradientBarPosition(a, p);
|
|
420
440
|
if (offset >= 0 && offset <= 1 && across >= -BAR_HIT && across <= VECTOR_GRADIENT_STOP_GAP + STOP_HIT) return { kind: "bar", offset };
|
|
421
441
|
return null;
|
|
@@ -955,6 +975,10 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
955
975
|
const visit = (list, parent) => {
|
|
956
976
|
for (const stored of list) {
|
|
957
977
|
if (exclude.has(stored.id) || stored.visible === false) continue;
|
|
978
|
+
if (isVectorMaskActive(stored) && !contains(stored, exclude)) {
|
|
979
|
+
visit(vectorAppearanceNodes(stored), multiplyMatrices(nodeMatrix(stored), parent));
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
958
982
|
const n = contains(stored, exclude) ? stored : expandVectorAppearance(stored);
|
|
959
983
|
const m = multiplyMatrices(nodeMatrix(n), parent);
|
|
960
984
|
if (n.type === "group") {
|
|
@@ -1025,14 +1049,14 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1025
1049
|
}
|
|
1026
1050
|
const point = this.nearestPoint(p, tol);
|
|
1027
1051
|
if (point) return at(point.p, point.kind);
|
|
1028
|
-
const
|
|
1029
|
-
const crossing = this.crossing(
|
|
1052
|
+
const near = this.segmentsNear(p, tol);
|
|
1053
|
+
const crossing = this.crossing(near, p, tol);
|
|
1030
1054
|
if (crossing) return at(crossing, "intersect");
|
|
1031
1055
|
if (leave && leave.curves !== false) {
|
|
1032
|
-
const foot = this.curvePoint(
|
|
1056
|
+
const foot = this.curvePoint(near, leave.at, p, tol);
|
|
1033
1057
|
if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
|
|
1034
1058
|
}
|
|
1035
|
-
const on = this.onSegment(
|
|
1059
|
+
const on = this.onSegment(near);
|
|
1036
1060
|
if (on) return at(on, "path");
|
|
1037
1061
|
if (leave?.angles?.length) {
|
|
1038
1062
|
const c = construction(leave, p, tol);
|
|
@@ -1115,12 +1139,12 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1115
1139
|
}
|
|
1116
1140
|
return out.sort((a, b) => a.distance - b.distance);
|
|
1117
1141
|
}
|
|
1118
|
-
onSegment(
|
|
1119
|
-
return
|
|
1142
|
+
onSegment(near) {
|
|
1143
|
+
return near.length ? toVec(segmentPoint(near[0].seg, near[0].t)) : null;
|
|
1120
1144
|
}
|
|
1121
1145
|
/** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
|
|
1122
|
-
crossing(
|
|
1123
|
-
const list =
|
|
1146
|
+
crossing(near, p, tol) {
|
|
1147
|
+
const list = near.slice(0, CROSSING_PAIRS);
|
|
1124
1148
|
let best = null;
|
|
1125
1149
|
let bestD = Infinity;
|
|
1126
1150
|
for (let i = 0; i < list.length; i++) {
|
|
@@ -1142,9 +1166,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1142
1166
|
* (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`
|
|
1143
1167
|
* bracketed by sign changes of samples and narrowed by bisection.
|
|
1144
1168
|
*/
|
|
1145
|
-
curvePoint(
|
|
1169
|
+
curvePoint(near, base, p, tol) {
|
|
1146
1170
|
let best = null;
|
|
1147
|
-
for (const { seg } of
|
|
1171
|
+
for (const { seg } of near) {
|
|
1148
1172
|
for (const kind of ["perpendicular", "tangent"]) {
|
|
1149
1173
|
if (kind === "tangent" && seg.length === 2) continue;
|
|
1150
1174
|
for (const q of solveOnSegment(seg, base, kind === "perpendicular" ? dotCondition : crossCondition)) {
|
|
@@ -1886,346 +1910,6 @@ function removeRedundantPoints(sp, tolerance) {
|
|
|
1886
1910
|
return segsToSubpath(segs, sp.closed, types) ?? { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
1887
1911
|
}
|
|
1888
1912
|
|
|
1889
|
-
// src/vector/pathCut.ts
|
|
1890
|
-
var loopArea = (l) => contourSignedArea(l.segs);
|
|
1891
|
-
function loopSample(l) {
|
|
1892
|
-
const k = l.chord.findIndex((c) => !c);
|
|
1893
|
-
return segmentPoint(l.segs[k >= 0 ? k : 0], 0.5);
|
|
1894
|
-
}
|
|
1895
|
-
var contains2 = (l, p) => windingNumber([l.segs], p) !== 0;
|
|
1896
|
-
function groupFaces(loops) {
|
|
1897
|
-
const usable2 = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
|
|
1898
|
-
if (!usable2.length) return [];
|
|
1899
|
-
const largest = usable2.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
|
|
1900
|
-
const sign = Math.sign(loopArea(largest));
|
|
1901
|
-
const outers = usable2.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
|
|
1902
|
-
const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
|
|
1903
|
-
for (const hole of usable2.filter((l) => Math.sign(loopArea(l)) !== sign)) {
|
|
1904
|
-
const p = loopSample(hole);
|
|
1905
|
-
const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
|
|
1906
|
-
if (parent) faces.get(parent).loops.push(hole);
|
|
1907
|
-
}
|
|
1908
|
-
return outers.reverse().map((o) => faces.get(o));
|
|
1909
|
-
}
|
|
1910
|
-
function faceToPath(face) {
|
|
1911
|
-
return contoursToPath(face.loops.map((l) => ({ segments: l.segs, closed: true })));
|
|
1912
|
-
}
|
|
1913
|
-
function facesOf(path, fillRule) {
|
|
1914
|
-
const normalized = normalizeBezierPath(path, fillRule);
|
|
1915
|
-
const loops = pathToContours(normalized).map((c) => {
|
|
1916
|
-
const segs = closeContour(c, true).map((s) => s.slice());
|
|
1917
|
-
return { segs, chord: segs.map(() => false) };
|
|
1918
|
-
});
|
|
1919
|
-
return groupFaces(loops);
|
|
1920
|
-
}
|
|
1921
|
-
function splitIntoFaces(path, fillRule = "nonzero") {
|
|
1922
|
-
return facesOf(path, fillRule).map(faceToPath);
|
|
1923
|
-
}
|
|
1924
|
-
function rootsInUnit(a, b, c, d, scale) {
|
|
1925
|
-
const f = (t) => ((a * t + b) * t + c) * t + d;
|
|
1926
|
-
const tol = 1e-12 * Math.max(1, scale);
|
|
1927
|
-
const cuts = [0];
|
|
1928
|
-
const qa = 3 * a;
|
|
1929
|
-
const qb = 2 * b;
|
|
1930
|
-
if (Math.abs(qa) > 1e-18) {
|
|
1931
|
-
const disc = qb * qb - 4 * qa * c;
|
|
1932
|
-
if (disc >= 0) {
|
|
1933
|
-
const r = Math.sqrt(disc);
|
|
1934
|
-
for (const t of [(-qb - r) / (2 * qa), (-qb + r) / (2 * qa)].sort((x, y) => x - y)) if (t > 0 && t < 1) cuts.push(t);
|
|
1935
|
-
}
|
|
1936
|
-
} else if (Math.abs(qb) > 1e-18) {
|
|
1937
|
-
const t = -c / qb;
|
|
1938
|
-
if (t > 0 && t < 1) cuts.push(t);
|
|
1939
|
-
}
|
|
1940
|
-
cuts.push(1);
|
|
1941
|
-
const roots = [];
|
|
1942
|
-
const push = (t) => {
|
|
1943
|
-
if (!roots.some((r) => Math.abs(r - t) < 1e-9)) roots.push(t);
|
|
1944
|
-
};
|
|
1945
|
-
for (let i = 0; i < cuts.length - 1; i++) {
|
|
1946
|
-
let lo = cuts[i];
|
|
1947
|
-
let hi = cuts[i + 1];
|
|
1948
|
-
let flo = f(lo);
|
|
1949
|
-
const fhi = f(hi);
|
|
1950
|
-
if (Math.abs(flo) <= tol) push(lo);
|
|
1951
|
-
if (Math.abs(fhi) <= tol) push(hi);
|
|
1952
|
-
if (flo * fhi >= 0) continue;
|
|
1953
|
-
for (let k = 0; k < 80; k++) {
|
|
1954
|
-
const m = (lo + hi) / 2;
|
|
1955
|
-
const fm = f(m);
|
|
1956
|
-
if (fm === 0) {
|
|
1957
|
-
lo = hi = m;
|
|
1958
|
-
break;
|
|
1959
|
-
}
|
|
1960
|
-
if (fm * flo < 0) hi = m;
|
|
1961
|
-
else {
|
|
1962
|
-
lo = m;
|
|
1963
|
-
flo = fm;
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
push((lo + hi) / 2);
|
|
1967
|
-
}
|
|
1968
|
-
return roots.sort((x, y) => x - y);
|
|
1969
|
-
}
|
|
1970
|
-
function lineHits(p, q, seg) {
|
|
1971
|
-
const dx = q[0] - p[0];
|
|
1972
|
-
const dy = q[1] - p[1];
|
|
1973
|
-
const len2 = dx * dx + dy * dy;
|
|
1974
|
-
if (len2 < 1e-24) return [];
|
|
1975
|
-
const h = (pt) => (pt[0] - p[0]) * -dy + (pt[1] - p[1]) * dx;
|
|
1976
|
-
let coeffs;
|
|
1977
|
-
if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
|
|
1978
|
-
else {
|
|
1979
|
-
const [h0, h1, h2, h3] = seg.map(h);
|
|
1980
|
-
coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
|
|
1981
|
-
}
|
|
1982
|
-
if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
|
|
1983
|
-
const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt) => Math.abs(pt[0]) + Math.abs(pt[1])));
|
|
1984
|
-
const out = [];
|
|
1985
|
-
for (const t of rootsInUnit(...coeffs, scale)) {
|
|
1986
|
-
const point = segmentPoint(seg, t);
|
|
1987
|
-
const s = ((point[0] - p[0]) * dx + (point[1] - p[1]) * dy) / len2;
|
|
1988
|
-
if (s >= -1e-9 && s <= 1 + 1e-9) out.push({ s: Math.min(1, Math.max(0, s)), t, point });
|
|
1989
|
-
}
|
|
1990
|
-
return out;
|
|
1991
|
-
}
|
|
1992
|
-
var near = (a, b, eps) => Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps;
|
|
1993
|
-
var degenerate = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
|
|
1994
|
-
function arc(l, a, b, eps) {
|
|
1995
|
-
const n = l.segs.length;
|
|
1996
|
-
const segs = [];
|
|
1997
|
-
const chord = [];
|
|
1998
|
-
const push = (k, t0, t1) => {
|
|
1999
|
-
const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : subSegment(l.segs[k], t0, t1);
|
|
2000
|
-
if (!degenerate(piece, eps)) {
|
|
2001
|
-
segs.push(piece);
|
|
2002
|
-
chord.push(l.chord[k]);
|
|
2003
|
-
}
|
|
2004
|
-
};
|
|
2005
|
-
if (a.seg === b.seg && b.t >= a.t + 1e-12) {
|
|
2006
|
-
push(a.seg, a.t, b.t);
|
|
2007
|
-
return { segs, chord };
|
|
2008
|
-
}
|
|
2009
|
-
push(a.seg, a.t, 1);
|
|
2010
|
-
for (let k = (a.seg + 1) % n; k !== b.seg; k = (k + 1) % n) push(k, 0, 1);
|
|
2011
|
-
push(b.seg, 0, b.t);
|
|
2012
|
-
return { segs, chord };
|
|
2013
|
-
}
|
|
2014
|
-
function polySegs(points, eps) {
|
|
2015
|
-
const out = [];
|
|
2016
|
-
for (let i = 1; i < points.length; i++) if (!near(points[i - 1], points[i], eps)) out.push([points[i - 1], points[i]]);
|
|
2017
|
-
return out;
|
|
2018
|
-
}
|
|
2019
|
-
function knifePath(path, fillRule, knife) {
|
|
2020
|
-
const pts = knife.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
2021
|
-
if (pts.length < 2) return null;
|
|
2022
|
-
let faces = facesOf(path, fillRule);
|
|
2023
|
-
if (!faces.length) return null;
|
|
2024
|
-
const b = bezierPathBounds(path);
|
|
2025
|
-
const size = b ? Math.max(b.maxX - b.minX, b.maxY - b.minY, 1) : 1;
|
|
2026
|
-
const eps = size * 1e-9;
|
|
2027
|
-
let cursor = -Infinity;
|
|
2028
|
-
let cuts = 0;
|
|
2029
|
-
for (let guard = 0; guard < 512; guard++) {
|
|
2030
|
-
let best = null;
|
|
2031
|
-
faces.forEach((face2, fi2) => {
|
|
2032
|
-
const hits = [];
|
|
2033
|
-
for (let m = 0; m < pts.length - 1; m++) {
|
|
2034
|
-
if (m + 1 < cursor - 1e-9) continue;
|
|
2035
|
-
const kb = { minX: Math.min(pts[m][0], pts[m + 1][0]), maxX: Math.max(pts[m][0], pts[m + 1][0]), minY: Math.min(pts[m][1], pts[m + 1][1]), maxY: Math.max(pts[m][1], pts[m + 1][1]) };
|
|
2036
|
-
face2.loops.forEach(
|
|
2037
|
-
(l, li) => l.segs.forEach((seg, si) => {
|
|
2038
|
-
const sb = segmentBounds(seg);
|
|
2039
|
-
if (sb.maxX < kb.minX - eps || sb.minX > kb.maxX + eps || sb.maxY < kb.minY - eps || sb.minY > kb.maxY + eps) return;
|
|
2040
|
-
for (const h of lineHits(pts[m], pts[m + 1], seg)) {
|
|
2041
|
-
const s = m + h.s;
|
|
2042
|
-
if (s >= cursor - 1e-9) hits.push({ s, loop: li, seg: si, t: h.t, point: h.point });
|
|
2043
|
-
}
|
|
2044
|
-
})
|
|
2045
|
-
);
|
|
2046
|
-
}
|
|
2047
|
-
hits.sort((x, y) => x.s - y.s);
|
|
2048
|
-
const unique = hits.filter((h, i) => i === 0 || h.s - hits[i - 1].s > 1e-9 || !near(h.point, hits[i - 1].point, eps * 10));
|
|
2049
|
-
const inside = (s) => {
|
|
2050
|
-
const m = Math.min(pts.length - 2, Math.floor(s));
|
|
2051
|
-
const u = s - m;
|
|
2052
|
-
const q = [pts[m][0] + (pts[m + 1][0] - pts[m][0]) * u, pts[m][1] + (pts[m + 1][1] - pts[m][1]) * u];
|
|
2053
|
-
return windingNumber(face2.loops.map((l) => l.segs), q) !== 0;
|
|
2054
|
-
};
|
|
2055
|
-
for (let i = 0; i < unique.length - 1; i++) {
|
|
2056
|
-
const from2 = unique[i];
|
|
2057
|
-
const to2 = unique[i + 1];
|
|
2058
|
-
if (near(from2.point, to2.point, eps * 10) || !inside((from2.s + to2.s) / 2)) continue;
|
|
2059
|
-
if (!best || from2.s < best.from.s) best = { face: fi2, from: from2, to: to2 };
|
|
2060
|
-
break;
|
|
2061
|
-
}
|
|
2062
|
-
});
|
|
2063
|
-
if (!best) break;
|
|
2064
|
-
const { face: fi, from, to } = best;
|
|
2065
|
-
const face = faces[fi];
|
|
2066
|
-
const corners2 = [from.point];
|
|
2067
|
-
for (let m = Math.floor(from.s) + 1; m <= to.s - 1e-12 && m < pts.length; m++) if (m > from.s) corners2.push(pts[m]);
|
|
2068
|
-
corners2.push(to.point);
|
|
2069
|
-
const forward = polySegs(corners2, eps);
|
|
2070
|
-
const back = polySegs(corners2.slice().reverse(), eps);
|
|
2071
|
-
const la = face.loops[from.loop];
|
|
2072
|
-
const loops = face.loops.filter((_, i) => i !== from.loop && i !== to.loop);
|
|
2073
|
-
if (from.loop === to.loop) {
|
|
2074
|
-
const ab = arc(la, from, to, eps);
|
|
2075
|
-
const ba = arc(la, to, from, eps);
|
|
2076
|
-
loops.push(
|
|
2077
|
-
{ segs: [...ab.segs, ...back], chord: [...ab.chord, ...back.map(() => true)] },
|
|
2078
|
-
{ segs: [...ba.segs, ...forward], chord: [...ba.chord, ...forward.map(() => true)] }
|
|
2079
|
-
);
|
|
2080
|
-
} else {
|
|
2081
|
-
const lb = face.loops[to.loop];
|
|
2082
|
-
const roundA = arc(la, from, from, eps);
|
|
2083
|
-
const roundB = arc(lb, to, to, eps);
|
|
2084
|
-
loops.push({
|
|
2085
|
-
segs: [...roundA.segs, ...forward, ...roundB.segs, ...back],
|
|
2086
|
-
chord: [...roundA.chord, ...forward.map(() => true), ...roundB.chord, ...back.map(() => true)]
|
|
2087
|
-
});
|
|
2088
|
-
}
|
|
2089
|
-
faces = [...faces.slice(0, fi), ...groupFaces(loops), ...faces.slice(fi + 1)];
|
|
2090
|
-
cursor = to.s;
|
|
2091
|
-
cuts++;
|
|
2092
|
-
}
|
|
2093
|
-
return cuts ? faces.map(faceToPath) : null;
|
|
2094
|
-
}
|
|
2095
|
-
function eraserRegion(points, width) {
|
|
2096
|
-
const pts = points.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
2097
|
-
const r = width / 2;
|
|
2098
|
-
if (!pts.length || !(r > 0)) return [];
|
|
2099
|
-
const moved = pts.some((p) => !near(p, pts[0], 1e-9));
|
|
2100
|
-
if (!moved) {
|
|
2101
|
-
const [cx, cy] = pts[0];
|
|
2102
|
-
const k = r * 0.5522847498307936;
|
|
2103
|
-
return [
|
|
2104
|
-
{ type: "M", x: cx, y: cy - r },
|
|
2105
|
-
{ type: "C", x1: cx + k, y1: cy - r, x2: cx + r, y2: cy - k, x: cx + r, y: cy },
|
|
2106
|
-
{ type: "C", x1: cx + r, y1: cy + k, x2: cx + k, y2: cy + r, x: cx, y: cy + r },
|
|
2107
|
-
{ type: "C", x1: cx - k, y1: cy + r, x2: cx - r, y2: cy + k, x: cx - r, y: cy },
|
|
2108
|
-
{ type: "C", x1: cx - r, y1: cy - k, x2: cx - k, y2: cy - r, x: cx, y: cy - r },
|
|
2109
|
-
{ type: "Z" }
|
|
2110
|
-
];
|
|
2111
|
-
}
|
|
2112
|
-
const line2 = [{ type: "M", x: pts[0][0], y: pts[0][1] }, ...pts.slice(1).map((p) => ({ type: "L", x: p[0], y: p[1] }))];
|
|
2113
|
-
return outlineStroke(line2, { width, cap: "round", join: "round" });
|
|
2114
|
-
}
|
|
2115
|
-
function eraseFromFill(path, fillRule, region) {
|
|
2116
|
-
const overlap = pathBoolean("intersection", [{ path, fillRule }, { path: region }]).path;
|
|
2117
|
-
if (!pathToContours(overlap).some((c) => Math.abs(contourSignedArea(closeContour(c, true))) > 1e-9)) return null;
|
|
2118
|
-
const rest = pathBoolean("difference", [{ path, fillRule }, { path: region }]).path;
|
|
2119
|
-
return splitIntoFaces(rest);
|
|
2120
|
-
}
|
|
2121
|
-
function eraseFromOutline(sp, region) {
|
|
2122
|
-
const regionSegs = pathToContours(region).flatMap((c) => closeContour(c, true));
|
|
2123
|
-
if (!regionSegs.length) return null;
|
|
2124
|
-
const rb = bezierPathBounds(region);
|
|
2125
|
-
const count = subpathSegmentCount(sp);
|
|
2126
|
-
if (!rb || count === 0) return null;
|
|
2127
|
-
const removed = (p) => pointInBezierPath(region, p, "nonzero");
|
|
2128
|
-
const runs = [];
|
|
2129
|
-
let cur = [];
|
|
2130
|
-
let any = false;
|
|
2131
|
-
for (let i = 0; i < count; i++) {
|
|
2132
|
-
const seg = subpathSegment(sp, i);
|
|
2133
|
-
const sb = segmentBounds(seg);
|
|
2134
|
-
const ts = [0, 1];
|
|
2135
|
-
if (!(sb.maxX < rb.minX || sb.minX > rb.maxX || sb.maxY < rb.minY || sb.minY > rb.maxY)) {
|
|
2136
|
-
for (const r of regionSegs) {
|
|
2137
|
-
const b = segmentBounds(r);
|
|
2138
|
-
if (b.maxX < sb.minX || b.minX > sb.maxX || b.maxY < sb.minY || b.minY > sb.maxY) continue;
|
|
2139
|
-
for (const hit of segmentIntersections(seg, r)) if (hit.ta > 1e-9 && hit.ta < 1 - 1e-9) ts.push(hit.ta);
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
ts.sort((a, b) => a - b);
|
|
2143
|
-
for (let k = 0; k < ts.length - 1; k++) {
|
|
2144
|
-
const t0 = ts[k];
|
|
2145
|
-
const t1 = ts[k + 1];
|
|
2146
|
-
if (t1 - t0 < 1e-9) continue;
|
|
2147
|
-
if (removed(segmentPoint(seg, (t0 + t1) / 2))) {
|
|
2148
|
-
any = true;
|
|
2149
|
-
if (cur.length) runs.push(cur);
|
|
2150
|
-
cur = [];
|
|
2151
|
-
} else cur.push({ seg: i, t0, t1 });
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
if (cur.length) runs.push(cur);
|
|
2155
|
-
if (!any) return null;
|
|
2156
|
-
if (sp.closed && runs.length >= 2) {
|
|
2157
|
-
const first = runs[0][0];
|
|
2158
|
-
const lastRun = runs[runs.length - 1];
|
|
2159
|
-
const last = lastRun[lastRun.length - 1];
|
|
2160
|
-
if (first.seg === 0 && first.t0 === 0 && last.seg === count - 1 && last.t1 === 1) lastRun.push(...runs.shift());
|
|
2161
|
-
}
|
|
2162
|
-
const out = [];
|
|
2163
|
-
for (const run of runs) {
|
|
2164
|
-
const anchors = [];
|
|
2165
|
-
for (const { seg, t0, t1 } of run) {
|
|
2166
|
-
const c = subSegment(subpathSegment(sp, seg), t0, t1);
|
|
2167
|
-
const line2 = c.length === 2;
|
|
2168
|
-
const start = c[0];
|
|
2169
|
-
const end = c[c.length - 1];
|
|
2170
|
-
if (!anchors.length) anchors.push({ x: start[0], y: start[1] });
|
|
2171
|
-
if (!line2) anchors[anchors.length - 1].out = { x: c[1][0], y: c[1][1] };
|
|
2172
|
-
const next = { x: end[0], y: end[1] };
|
|
2173
|
-
if (!line2) next.in = { x: c[2][0], y: c[2][1] };
|
|
2174
|
-
anchors.push(next);
|
|
2175
|
-
}
|
|
2176
|
-
if (anchors.length < 2) continue;
|
|
2177
|
-
for (const a of anchors) {
|
|
2178
|
-
const type = inferAnchorType(a);
|
|
2179
|
-
if (type !== "corner") a.type = type;
|
|
2180
|
-
}
|
|
2181
|
-
out.push({ anchors, closed: false });
|
|
2182
|
-
}
|
|
2183
|
-
return out;
|
|
2184
|
-
}
|
|
2185
|
-
function polylineBounds(points, pad = 0) {
|
|
2186
|
-
if (!points.length) return null;
|
|
2187
|
-
let minX = Infinity;
|
|
2188
|
-
let minY = Infinity;
|
|
2189
|
-
let maxX = -Infinity;
|
|
2190
|
-
let maxY = -Infinity;
|
|
2191
|
-
for (const [x, y] of points) {
|
|
2192
|
-
minX = Math.min(minX, x);
|
|
2193
|
-
minY = Math.min(minY, y);
|
|
2194
|
-
maxX = Math.max(maxX, x);
|
|
2195
|
-
maxY = Math.max(maxY, y);
|
|
2196
|
-
}
|
|
2197
|
-
return { x: minX - pad, y: minY - pad, width: maxX - minX + 2 * pad, height: maxY - minY + 2 * pad };
|
|
2198
|
-
}
|
|
2199
|
-
function simplifyPolyline(points, tolerance) {
|
|
2200
|
-
const n = points.length;
|
|
2201
|
-
if (n < 3) return points.slice();
|
|
2202
|
-
const keep = new Array(n).fill(false);
|
|
2203
|
-
keep[0] = keep[n - 1] = true;
|
|
2204
|
-
const stack = [[0, n - 1]];
|
|
2205
|
-
while (stack.length) {
|
|
2206
|
-
const [a, b] = stack.pop();
|
|
2207
|
-
const [ax, ay] = points[a];
|
|
2208
|
-
const dx = points[b][0] - ax;
|
|
2209
|
-
const dy = points[b][1] - ay;
|
|
2210
|
-
const len = Math.hypot(dx, dy);
|
|
2211
|
-
let far = -1;
|
|
2212
|
-
let farD = 0;
|
|
2213
|
-
for (let i = a + 1; i < b; i++) {
|
|
2214
|
-
const [px, py] = points[i];
|
|
2215
|
-
const d = len < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len;
|
|
2216
|
-
if (d > farD) {
|
|
2217
|
-
farD = d;
|
|
2218
|
-
far = i;
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
if (far >= 0 && farD > tolerance) {
|
|
2222
|
-
keep[far] = true;
|
|
2223
|
-
stack.push([a, far], [far, b]);
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
return points.filter((_, i) => keep[i]);
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
1913
|
// src/vector/ink.ts
|
|
2230
1914
|
var LazyMouse = class {
|
|
2231
1915
|
/** `length` is the string length in the units of the points (0 turns smoothing off). */
|
|
@@ -2457,7 +2141,7 @@ function vectorPathEntries(doc, roots, options = {}) {
|
|
|
2457
2141
|
const out = [];
|
|
2458
2142
|
const visit = (stored, parent, inside) => {
|
|
2459
2143
|
if (stored.visible === false || stored.locked && !options.locked) return;
|
|
2460
|
-
const node = options.expand === false ? stored : expandVectorAppearance(stored);
|
|
2144
|
+
const node = options.expand === false ? stored : expandVectorAppearance(vectorMaskedGeometry(stored));
|
|
2461
2145
|
const picked = inside || roots === null || roots.includes(node.id);
|
|
2462
2146
|
if (node.type === "group") {
|
|
2463
2147
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
@@ -4538,7 +4222,7 @@ var VectorEditorController = class {
|
|
|
4538
4222
|
const drop = new Set(ids.filter((id) => this.getNode(id)));
|
|
4539
4223
|
if (!drop.size) return { ok: false, message: "Nothing to delete." };
|
|
4540
4224
|
this.change(options.label ?? "Delete", () => {
|
|
4541
|
-
this.setDoc(mapTree(this.doc.nodes, (n) => drop.has(n.id) ? null : n));
|
|
4225
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => drop.has(n.id) ? null : isVectorMaskGroup(n) && drop.has(n.children[0].id) ? { ...n, mask: null } : n));
|
|
4542
4226
|
this.selection = this.selection.filter((id) => !drop.has(id) && this.getNode(id));
|
|
4543
4227
|
this.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));
|
|
4544
4228
|
if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
|
|
@@ -5197,7 +4881,7 @@ var VectorEditorController = class {
|
|
|
5197
4881
|
/** Whether stored node `rootId` holds `id` as drawn (`id` may be an expanded path inside it). */
|
|
5198
4882
|
holds(rootId, id) {
|
|
5199
4883
|
const n = this.getNode(rootId);
|
|
5200
|
-
return !!n && (!!findVectorNode([n], id) || !!findVectorNode([expandVectorAppearance(n)], id));
|
|
4884
|
+
return !!n && (!!findVectorNode([n], id) || !!findVectorNode([expandVectorAppearance(n)], id) || !!findVectorNode([vectorMaskedGeometry(n)], id));
|
|
5201
4885
|
}
|
|
5202
4886
|
liveBooleanOp(op, roots) {
|
|
5203
4887
|
const nodes = roots.map((id2) => this.getNode(id2));
|
|
@@ -5394,6 +5078,153 @@ var VectorEditorController = class {
|
|
|
5394
5078
|
});
|
|
5395
5079
|
return id;
|
|
5396
5080
|
}
|
|
5081
|
+
// -------------------------------------------------------------------------
|
|
5082
|
+
// Masks
|
|
5083
|
+
// -------------------------------------------------------------------------
|
|
5084
|
+
/**
|
|
5085
|
+
* Make Clipping Mask (`clip`) / Make Opacity Mask (`alpha`, `luminance`): groups the selected (or
|
|
5086
|
+
* given) objects into a mask group whose mask is the topmost of them (or `maskId`), moved to the
|
|
5087
|
+
* bottom of the group. They must be two or more, unlocked, with one parent. One undo step.
|
|
5088
|
+
* Returns the group's id, or why it could not.
|
|
5089
|
+
*/
|
|
5090
|
+
makeMask(type, options = {}) {
|
|
5091
|
+
if (!isVectorMaskType(type)) return { ok: false, message: `Unknown mask type "${type}": use clip, alpha or luminance.` };
|
|
5092
|
+
const ids = [...new Set(options.ids ?? this.selectedRoots())];
|
|
5093
|
+
if (ids.length < 2) return { ok: false, message: "Select at least two objects: the topmost becomes the mask, the ones below show through it." };
|
|
5094
|
+
for (const id2 of ids) {
|
|
5095
|
+
const n = this.getNode(id2);
|
|
5096
|
+
if (!n) return { ok: false, message: `There is no node "${id2}".` };
|
|
5097
|
+
if (n.locked) return { ok: false, message: `"${id2}" is locked; unlock it first.` };
|
|
5098
|
+
if (n.blendStep) return { ok: false, message: `"${id2}" is a blend step; select its blend instead.` };
|
|
5099
|
+
}
|
|
5100
|
+
const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
|
|
5101
|
+
const parent = locs[0].parent?.id ?? null;
|
|
5102
|
+
if (locs.some((l) => (l.parent?.id ?? null) !== parent)) return { ok: false, message: "The objects must share a parent (group them, or pick objects at one level)." };
|
|
5103
|
+
const order = ids.slice().sort((a, b) => locs[ids.indexOf(a)].index - locs[ids.indexOf(b)].index);
|
|
5104
|
+
const maskId = options.maskId ?? order[order.length - 1];
|
|
5105
|
+
if (!order.includes(maskId)) return { ok: false, message: `The mask "${maskId}" must be one of the objects (${order.map((id2) => `"${id2}"`).join(", ")}).` };
|
|
5106
|
+
const id = this.newId();
|
|
5107
|
+
const mask = options.invert ? { type, invert: true } : { type };
|
|
5108
|
+
this.change(type === "clip" ? "Make Clipping Mask" : "Make Opacity Mask", () => {
|
|
5109
|
+
const set = new Set(order);
|
|
5110
|
+
const regroup = (list) => {
|
|
5111
|
+
const members = order.map((m) => list.find((n) => n.id === m));
|
|
5112
|
+
const top = Math.max(...members.map((m) => list.indexOf(m)));
|
|
5113
|
+
const children = [members.find((m) => m.id === maskId), ...members.filter((m) => m.id !== maskId)];
|
|
5114
|
+
const out = [];
|
|
5115
|
+
list.forEach((n, i) => {
|
|
5116
|
+
if (i === top) out.push({ id, type: "group", name: options.name ?? (type === "clip" ? "Clip Group" : "Opacity Mask"), mask, children });
|
|
5117
|
+
if (!set.has(n.id)) out.push(n);
|
|
5118
|
+
});
|
|
5119
|
+
return out;
|
|
5120
|
+
};
|
|
5121
|
+
if (parent === null) this.setDoc(regroup(this.doc.nodes));
|
|
5122
|
+
else this.setDoc(mapTree(this.doc.nodes, (n) => n.id === parent && n.type === "group" ? { ...n, children: regroup(n.children) } : n));
|
|
5123
|
+
this.selection = [id];
|
|
5124
|
+
this.anchors = [];
|
|
5125
|
+
this.editingId = null;
|
|
5126
|
+
});
|
|
5127
|
+
return { ok: true, id };
|
|
5128
|
+
}
|
|
5129
|
+
/** Mask groups among `ids` (default: the selection), or holding one of them directly. */
|
|
5130
|
+
maskTargets(ids) {
|
|
5131
|
+
const out = /* @__PURE__ */ new Map();
|
|
5132
|
+
for (const id of ids ?? this.selection) {
|
|
5133
|
+
const n = this.getNode(id);
|
|
5134
|
+
if (isVectorMaskGroup(n)) out.set(n.id, n);
|
|
5135
|
+
else {
|
|
5136
|
+
const parent = locateVectorNode(this.doc.nodes, id)?.parent;
|
|
5137
|
+
if (isVectorMaskGroup(parent)) out.set(parent.id, parent);
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
return [...out.values()];
|
|
5141
|
+
}
|
|
5142
|
+
/**
|
|
5143
|
+
* Release: the selected (or given) mask groups stop masking. The mask goes back on top of the
|
|
5144
|
+
* contents, drawn as itself again. A group with appearance of its own (filters, a blend mode,
|
|
5145
|
+
* path effects) stays a plain group; otherwise it is replaced by its children. One undo step.
|
|
5146
|
+
* Returns the released nodes' ids (the mask last).
|
|
5147
|
+
*/
|
|
5148
|
+
releaseMask(options = {}) {
|
|
5149
|
+
const groups = this.maskTargets(options.ids);
|
|
5150
|
+
if (!groups.length) return [];
|
|
5151
|
+
const released = [];
|
|
5152
|
+
this.change("Release Mask", () => {
|
|
5153
|
+
for (const g of groups) {
|
|
5154
|
+
const [mask, ...contents] = g.children;
|
|
5155
|
+
const children = [...contents, mask];
|
|
5156
|
+
released.push(...children.map((c) => c.id));
|
|
5157
|
+
const { mask: _m, ...rest } = g;
|
|
5158
|
+
const keep = !!(g.filters?.length || g.blendMode && g.blendMode !== "normal" || g.effects?.length);
|
|
5159
|
+
if (keep) {
|
|
5160
|
+
this.replaceNode(g.id, { ...rest, children });
|
|
5161
|
+
continue;
|
|
5162
|
+
}
|
|
5163
|
+
const m = nodeMatrix(g);
|
|
5164
|
+
const opacity = g.opacity;
|
|
5165
|
+
const flat = children.map((c) => g.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c);
|
|
5166
|
+
this.setDoc(spliceTree(this.doc.nodes, g.id, flat));
|
|
5167
|
+
}
|
|
5168
|
+
this.selection = released;
|
|
5169
|
+
this.anchors = [];
|
|
5170
|
+
this.editingId = null;
|
|
5171
|
+
});
|
|
5172
|
+
return released;
|
|
5173
|
+
}
|
|
5174
|
+
/**
|
|
5175
|
+
* Changes the type or inversion of the selected (or given) mask groups, or turns their masks off
|
|
5176
|
+
* (`enabled: false` hides the mask, so the contents draw whole) and on. One undo step.
|
|
5177
|
+
*/
|
|
5178
|
+
setMaskOptions(patch, options = {}) {
|
|
5179
|
+
if (patch.type !== void 0 && !isVectorMaskType(patch.type)) return { ok: false, message: `Unknown mask type "${patch.type}": use clip, alpha or luminance.` };
|
|
5180
|
+
if (patch.invert !== void 0 && typeof patch.invert !== "boolean") return { ok: false, message: "invert must be true or false." };
|
|
5181
|
+
if (patch.enabled !== void 0 && typeof patch.enabled !== "boolean") return { ok: false, message: "enabled must be true or false." };
|
|
5182
|
+
const groups = this.maskTargets(options.ids);
|
|
5183
|
+
if (!groups.length) return { ok: false, message: "Select a mask group (made with Make Clipping Mask or Make Opacity Mask)." };
|
|
5184
|
+
const set = new Set(groups.map((g) => g.id));
|
|
5185
|
+
this.change("Mask Options", () => {
|
|
5186
|
+
this.setDoc(
|
|
5187
|
+
mapTree(this.doc.nodes, (n) => {
|
|
5188
|
+
if (!set.has(n.id) || !isVectorMaskGroup(n)) return n;
|
|
5189
|
+
const type = patch.type ?? n.mask.type;
|
|
5190
|
+
const invert = patch.invert ?? !!n.mask.invert;
|
|
5191
|
+
const [source, ...rest] = n.children;
|
|
5192
|
+
const shown = patch.enabled === void 0 || patch.enabled === (source.visible !== false) ? source : patch.enabled ? (({ visible: _v, ...r }) => r)(source) : { ...source, visible: false };
|
|
5193
|
+
return { ...n, mask: invert ? { type, invert } : { type }, ...shown !== source ? { children: [shown, ...rest] } : {} };
|
|
5194
|
+
})
|
|
5195
|
+
);
|
|
5196
|
+
});
|
|
5197
|
+
return { ok: true };
|
|
5198
|
+
}
|
|
5199
|
+
/**
|
|
5200
|
+
* Edit Mask: selects the mask of the selected (or given) mask group, or of the group holding the
|
|
5201
|
+
* selection. A path mask opens in the direct tool for anchor editing (`editPath: false` keeps the
|
|
5202
|
+
* tool). Returns the mask's id, or null.
|
|
5203
|
+
*/
|
|
5204
|
+
editMask(options = {}) {
|
|
5205
|
+
const group = this.maskTargets(options.id ? [options.id] : void 0)[0];
|
|
5206
|
+
if (!group) return null;
|
|
5207
|
+
const mask = group.children[0];
|
|
5208
|
+
this.selection = [mask.id];
|
|
5209
|
+
this.anchors = [];
|
|
5210
|
+
this.editingId = null;
|
|
5211
|
+
if (mask.type === "path" && options.editPath !== false && !mask.locked) {
|
|
5212
|
+
this.tool = "direct";
|
|
5213
|
+
this.editingId = mask.id;
|
|
5214
|
+
}
|
|
5215
|
+
this.notify();
|
|
5216
|
+
return mask.id;
|
|
5217
|
+
}
|
|
5218
|
+
/** Edit Contents: selects what the selected (or given) mask group masks. Returns their ids. */
|
|
5219
|
+
editMaskContents(options = {}) {
|
|
5220
|
+
const group = this.maskTargets(options.id ? [options.id] : void 0)[0];
|
|
5221
|
+
if (!group) return [];
|
|
5222
|
+
this.selection = group.children.slice(1).map((c) => c.id);
|
|
5223
|
+
this.anchors = [];
|
|
5224
|
+
this.editingId = null;
|
|
5225
|
+
this.notify();
|
|
5226
|
+
return [...this.selection];
|
|
5227
|
+
}
|
|
5397
5228
|
/** Sets the options new blends get (spacing, steps, distance, orientation, morph). */
|
|
5398
5229
|
setBlendDefaults(patch) {
|
|
5399
5230
|
this.blendDefaults = { ...this.blendDefaults, ...blendOptionsOf(patch) };
|
|
@@ -6018,11 +5849,11 @@ var VectorEditorController = class {
|
|
|
6018
5849
|
if (!hit) return null;
|
|
6019
5850
|
const sp = node.subpaths[hit.subpath];
|
|
6020
5851
|
const n = sp.anchors.length;
|
|
6021
|
-
const
|
|
6022
|
-
if (!sp.closed && n > 1 && (
|
|
5852
|
+
const near = [hit.segment, (hit.segment + 1) % n].find((i) => Math.hypot(sp.anchors[i].x - hit.point.x, sp.anchors[i].y - hit.point.y) <= this.tolerance());
|
|
5853
|
+
if (!sp.closed && n > 1 && (near === 0 || near === n - 1) && !(hit.t > 1e-6 && hit.t < 1 - 1e-6)) return null;
|
|
6023
5854
|
let ids = null;
|
|
6024
5855
|
this.change("Cut Path", () => {
|
|
6025
|
-
ids = this.applyCuts(node, (s, i) => i !== hit.subpath ? null :
|
|
5856
|
+
ids = this.applyCuts(node, (s, i) => i !== hit.subpath ? null : near !== void 0 ? cutSubpathAt(s, [near]) : splitSubpathAt(s, hit.segment, hit.t));
|
|
6026
5857
|
if (ids) {
|
|
6027
5858
|
this.selection = ids;
|
|
6028
5859
|
this.anchors = [];
|
|
@@ -6306,7 +6137,7 @@ var VectorEditorController = class {
|
|
|
6306
6137
|
const n = list[i];
|
|
6307
6138
|
if (n.visible === false || n.locked) continue;
|
|
6308
6139
|
if (n.type === "group") {
|
|
6309
|
-
if (hasOwnVectorAppearance(n) && !entered.has(n.id) && !hitTestVectorNode(n, p, tol, parent, { entered })) continue;
|
|
6140
|
+
if ((hasOwnVectorAppearance(n) || isVectorMaskActive(n)) && !entered.has(n.id) && !hitTestVectorNode(n, p, tol, parent, { entered })) continue;
|
|
6310
6141
|
const found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));
|
|
6311
6142
|
if (found) return found;
|
|
6312
6143
|
} else if (hitTestVectorNode(n, p, tol, parent, { entered })) return n;
|
|
@@ -6428,24 +6259,24 @@ var VectorEditorController = class {
|
|
|
6428
6259
|
if (!g) return;
|
|
6429
6260
|
const m = this.worldOf(n.id);
|
|
6430
6261
|
const local = applyMatrix(invertMatrix(m), p);
|
|
6431
|
-
const
|
|
6432
|
-
if (!
|
|
6262
|
+
const near = nearestStrokePosition(g.line, [local.x, local.y]);
|
|
6263
|
+
if (!near) return;
|
|
6433
6264
|
const profile = strokeWidthProfileOf(n);
|
|
6434
|
-
const w = widthProfileAt(profile,
|
|
6435
|
-
const reach = Math.max(
|
|
6436
|
-
if (
|
|
6437
|
-
if (best &&
|
|
6438
|
-
const nx =
|
|
6439
|
-
const ny = -
|
|
6440
|
-
const [cx, cy] =
|
|
6265
|
+
const w = widthProfileAt(profile, near.t);
|
|
6266
|
+
const reach = Math.max(near.side === "left" ? w.left : w.right, 0) * g.unit + tol;
|
|
6267
|
+
if (near.distance > reach || near.distance > widthProfileMaxScale(profile) * g.unit + tol) return;
|
|
6268
|
+
if (best && near.distance >= best.distance) return;
|
|
6269
|
+
const nx = near.tangent[1];
|
|
6270
|
+
const ny = -near.tangent[0];
|
|
6271
|
+
const [cx, cy] = near.point;
|
|
6441
6272
|
best = {
|
|
6442
6273
|
nodeId: n.id,
|
|
6443
|
-
subpath:
|
|
6444
|
-
t:
|
|
6274
|
+
subpath: near.contour,
|
|
6275
|
+
t: near.t,
|
|
6445
6276
|
point: applyMatrix(m, { x: cx, y: cy }),
|
|
6446
6277
|
left: applyMatrix(m, { x: cx + nx * w.left * g.unit, y: cy + ny * w.left * g.unit }),
|
|
6447
6278
|
right: applyMatrix(m, { x: cx - nx * w.right * g.unit, y: cy - ny * w.right * g.unit }),
|
|
6448
|
-
distance:
|
|
6279
|
+
distance: near.distance
|
|
6449
6280
|
};
|
|
6450
6281
|
});
|
|
6451
6282
|
if (!best) return null;
|
|
@@ -6494,13 +6325,13 @@ var VectorEditorController = class {
|
|
|
6494
6325
|
} else if (mode === "copy" || !selected) this.widthPoints = [ref];
|
|
6495
6326
|
if (mode !== "copy") group = this.widthPoints.filter((r) => r.index !== ref.index).map((r) => r.index);
|
|
6496
6327
|
} else {
|
|
6497
|
-
const
|
|
6498
|
-
const inserted = insertWidthPoint(strokeWidthProfileOf(node),
|
|
6328
|
+
const near = nearestStrokePosition(g.line, [local.x, local.y]);
|
|
6329
|
+
const inserted = insertWidthPoint(strokeWidthProfileOf(node), near.t);
|
|
6499
6330
|
profile = inserted.points;
|
|
6500
|
-
subpath =
|
|
6501
|
-
t =
|
|
6331
|
+
subpath = near.contour;
|
|
6332
|
+
t = near.t;
|
|
6502
6333
|
ref = { nodeId: node.id, index: inserted.index, subpath };
|
|
6503
|
-
if (input.alt) side =
|
|
6334
|
+
if (input.alt) side = near.side;
|
|
6504
6335
|
this.widthPoints = [ref];
|
|
6505
6336
|
}
|
|
6506
6337
|
const at = strokePositionPoint(g.line, subpath, t);
|
|
@@ -7095,9 +6926,9 @@ var VectorEditorController = class {
|
|
|
7095
6926
|
let index = drag.source;
|
|
7096
6927
|
if (drag.mode !== "width") {
|
|
7097
6928
|
const g = vectorStrokeWidthGeometry(node);
|
|
7098
|
-
const
|
|
7099
|
-
if (!g || !
|
|
7100
|
-
const t = drag.group.length ?
|
|
6929
|
+
const near = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
|
|
6930
|
+
if (!g || !near) return;
|
|
6931
|
+
const t = drag.group.length ? near.t : this.snapWidthT(drag, node, g.line, near.t, drag.source);
|
|
7101
6932
|
if (drag.mode === "copy") {
|
|
7102
6933
|
({ points: profile, index } = insertWidthPoint(drag.profile, t, { left: src.left, right: src.right }, { step: true }));
|
|
7103
6934
|
} else if (drag.group.length) {
|
|
@@ -7456,6 +7287,10 @@ var VectorEditorController = class {
|
|
|
7456
7287
|
return true;
|
|
7457
7288
|
case "j":
|
|
7458
7289
|
return input.alt ? this.averageAnchors("both").ok : this.joinPaths().ok;
|
|
7290
|
+
// Make Clipping Mask; with Alt, Release (Option-7 types ¶ on a Mac).
|
|
7291
|
+
case "7":
|
|
7292
|
+
case "\xB6":
|
|
7293
|
+
return input.alt || key === "\xB6" ? this.releaseMask().length > 0 : this.makeMask("clip").ok;
|
|
7459
7294
|
case "]":
|
|
7460
7295
|
this.arrange(input.alt ? "front" : "forward");
|
|
7461
7296
|
return true;
|
|
@@ -7663,13 +7498,17 @@ function renderNode(stored, defs, filters, idPrefix) {
|
|
|
7663
7498
|
if (stored.visible === false) return null;
|
|
7664
7499
|
const node = expandVectorAppearance(stored);
|
|
7665
7500
|
if (node.type === "group") {
|
|
7666
|
-
const
|
|
7501
|
+
const finish2 = nodeFinish(node, filters, idPrefix);
|
|
7502
|
+
const masked = isVectorMaskGroup(node) ? renderMask(node, defs, filters, idPrefix) : null;
|
|
7503
|
+
const list = isVectorMaskGroup(node) ? node.children.slice(1) : node.children;
|
|
7504
|
+
const children = list.map((c) => renderNode(c, defs, filters, idPrefix)).filter((c) => !!c);
|
|
7667
7505
|
return {
|
|
7668
7506
|
kind: "group",
|
|
7669
7507
|
id: node.id,
|
|
7670
7508
|
...node.transform ? { transform: fmtMatrix(node.transform) } : {},
|
|
7671
7509
|
opacity: node.opacity ?? 1,
|
|
7672
|
-
...
|
|
7510
|
+
...finish2,
|
|
7511
|
+
...masked ? { mask: masked } : {},
|
|
7673
7512
|
children
|
|
7674
7513
|
};
|
|
7675
7514
|
}
|
|
@@ -7711,6 +7550,16 @@ function renderNode(stored, defs, filters, idPrefix) {
|
|
|
7711
7550
|
...lineStyle(node)
|
|
7712
7551
|
};
|
|
7713
7552
|
}
|
|
7553
|
+
function renderMask(group, defs, filters, idPrefix) {
|
|
7554
|
+
if (!isVectorMaskActive(group)) return null;
|
|
7555
|
+
const data = vectorMaskRenderData(group, `${idPrefix}mask-${group.id}`, `${idPrefix}mask-filter-${group.id}`);
|
|
7556
|
+
if (!data) return null;
|
|
7557
|
+
const { invertFilter, ...mask } = data;
|
|
7558
|
+
if (invertFilter) filters.push(invertFilter);
|
|
7559
|
+
if (mask.type === "clip") return mask;
|
|
7560
|
+
const source = renderNode(group.children[0], defs, filters, idPrefix);
|
|
7561
|
+
return { ...mask, nodes: source ? [source] : [] };
|
|
7562
|
+
}
|
|
7714
7563
|
function getVectorSceneRenderData(state, options = {}) {
|
|
7715
7564
|
const defs = [];
|
|
7716
7565
|
const filters = [];
|
|
@@ -7737,7 +7586,7 @@ function screenOutline(state, node, entered) {
|
|
|
7737
7586
|
function outlinePaths(node, parent, entered) {
|
|
7738
7587
|
if (node.type === "path") return [vectorPathWorldBezier(node, parent, { effects: false })];
|
|
7739
7588
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
7740
|
-
if (hasOwnVectorAppearance(node) && !entered.has(node.id)) return [transformBezierPath(vectorAppearanceOutline(node), m)];
|
|
7589
|
+
if ((hasOwnVectorAppearance(node) || isVectorMaskActive(node)) && !entered.has(node.id)) return [transformBezierPath(vectorAppearanceOutline(node), m)];
|
|
7741
7590
|
return node.children.filter((c) => c.visible !== false).flatMap((c) => outlinePaths(c, m, entered));
|
|
7742
7591
|
}
|
|
7743
7592
|
function getVectorEnteredGroups(state) {
|
|
@@ -7901,7 +7750,35 @@ function getVectorOverlayRenderData(state) {
|
|
|
7901
7750
|
}
|
|
7902
7751
|
const envelope = state.tool === "envelope" ? envelopeOverlay(state) : null;
|
|
7903
7752
|
if (state.tool === "envelope" && !envelope && !it) status = "Select an object with an envelope to edit its mesh";
|
|
7904
|
-
return { outlines, selectionBox, distort, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend, gradient, envelope };
|
|
7753
|
+
return { outlines, selectionBox, distort, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend, gradient, envelope, masks: maskOverlay(state) };
|
|
7754
|
+
}
|
|
7755
|
+
function maskOverlay(state) {
|
|
7756
|
+
const ids = state.editingId ? [...state.selection, state.editingId] : state.selection;
|
|
7757
|
+
if (!ids.length) return [];
|
|
7758
|
+
const want = new Set(ids);
|
|
7759
|
+
const v = state.viewport;
|
|
7760
|
+
const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
|
|
7761
|
+
const out = [];
|
|
7762
|
+
const visit = (list, parent) => {
|
|
7763
|
+
let any = false;
|
|
7764
|
+
for (const n of list) {
|
|
7765
|
+
if (n.type !== "group") {
|
|
7766
|
+
if (want.has(n.id)) any = true;
|
|
7767
|
+
continue;
|
|
7768
|
+
}
|
|
7769
|
+
const m = multiplyMatrices(nodeMatrix(n), parent);
|
|
7770
|
+
const inside = visit(n.children, m);
|
|
7771
|
+
const hit = want.has(n.id) || inside;
|
|
7772
|
+
if (hit) any = true;
|
|
7773
|
+
if (!hit || !isVectorMaskGroup(n)) continue;
|
|
7774
|
+
const summary = vectorMaskSummary(n);
|
|
7775
|
+
const d = outlinePaths(n.children[0], m, /* @__PURE__ */ new Set()).map((path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 })).join(" ");
|
|
7776
|
+
out.push({ id: n.id, maskId: summary.maskId, d, type: summary.type, invert: summary.invert, enabled: summary.enabled, editing: want.has(summary.maskId) });
|
|
7777
|
+
}
|
|
7778
|
+
return any;
|
|
7779
|
+
};
|
|
7780
|
+
visit(state.document.nodes, IDENTITY_MATRIX);
|
|
7781
|
+
return out;
|
|
7905
7782
|
}
|
|
7906
7783
|
function envelopeOverlay(state) {
|
|
7907
7784
|
const target = getVectorEnvelopeTarget(state.document, state.selection);
|
|
@@ -8069,7 +7946,10 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
8069
7946
|
builderErase: "#ef4444",
|
|
8070
7947
|
builderEraseFill: "rgba(239, 68, 68, 0.22)",
|
|
8071
7948
|
/** Opacity of the hatch lines over Shape Builder faces (drawn in the face's outline colour). */
|
|
8072
|
-
builderHatchOpacity: 0.6
|
|
7949
|
+
builderHatchOpacity: 0.6,
|
|
7950
|
+
/** Mask indicators: the outline of a selected mask group's mask (dashed; solid while the mask is selected). */
|
|
7951
|
+
mask: "#8b5cf6",
|
|
7952
|
+
maskDash: "5 3"
|
|
8073
7953
|
};
|
|
8074
7954
|
function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
|
|
8075
7955
|
return `M${fmt2(x)} ${fmt2(y - r)}L${fmt2(x + r)} ${fmt2(y)}L${fmt2(x)} ${fmt2(y + r)}L${fmt2(x - r)} ${fmt2(y)}Z`;
|
|
@@ -8318,6 +8198,32 @@ function getVectorAppearancePanel(state) {
|
|
|
8318
8198
|
};
|
|
8319
8199
|
}
|
|
8320
8200
|
var VECTOR_BLEND_MODE_MENU = VECTOR_BLEND_MODES.map((mode) => ({ mode, label: VECTOR_BLEND_MODE_LABELS[mode] }));
|
|
8201
|
+
var VECTOR_MASK_TYPE_LABELS = { clip: "Clip", alpha: "Alpha", luminance: "Luminance" };
|
|
8202
|
+
var VECTOR_MASK_TYPE_OPTIONS = [
|
|
8203
|
+
{ value: "clip", label: "Clip", title: "Clipping mask: the contents show inside the mask's outline" },
|
|
8204
|
+
{ value: "luminance", label: "Luminance", title: "Opacity mask by brightness: white shows, black hides" },
|
|
8205
|
+
{ value: "alpha", label: "Alpha", title: "Opacity mask by opacity: opaque shows, transparent hides" }
|
|
8206
|
+
];
|
|
8207
|
+
function getVectorMaskPanel(state) {
|
|
8208
|
+
const doc = state.document;
|
|
8209
|
+
if (!state.selection.length) return null;
|
|
8210
|
+
const locs = state.selection.map((id) => locateVectorNode(doc.nodes, id));
|
|
8211
|
+
let makeHint = null;
|
|
8212
|
+
if (state.selection.length < 2) makeHint = "Select two or more objects: the topmost becomes the mask.";
|
|
8213
|
+
else if (locs.some((l) => !l)) makeHint = "Select objects in the document.";
|
|
8214
|
+
else if (locs.some((l) => l.list[l.index].locked || l.list[l.index].blendStep)) makeHint = "Unlock the selected objects first.";
|
|
8215
|
+
else if (locs.some((l) => (l.parent?.id ?? null) !== (locs[0].parent?.id ?? null))) makeHint = "Select objects at one level (sharing a parent).";
|
|
8216
|
+
let group = null;
|
|
8217
|
+
const first = locs[0];
|
|
8218
|
+
const node = first ? first.list[first.index] : null;
|
|
8219
|
+
const owner = isVectorMaskGroup(node) ? node : isVectorMaskGroup(first?.parent) ? first.parent : null;
|
|
8220
|
+
if (owner && state.selection.every((id) => id === owner.id || owner.children.some((c) => c.id === id))) {
|
|
8221
|
+
const summary = vectorMaskSummary(owner);
|
|
8222
|
+
const editing = state.selection.includes(owner.id) ? "group" : state.selection.includes(summary.maskId) ? "mask" : "contents";
|
|
8223
|
+
group = { ...summary, id: owner.id, ...owner.name ? { name: owner.name } : {}, editing };
|
|
8224
|
+
}
|
|
8225
|
+
return { canMake: makeHint === null, makeHint, group };
|
|
8226
|
+
}
|
|
8321
8227
|
|
|
8322
8228
|
// src/vector/commands.ts
|
|
8323
8229
|
var EFFECT_PARAMS_DESCRIPTION = "Effect parameters (missing ones keep their value or default). trim: start, end (percent), offset (degrees), mode simultaneous|individual. roundCorners: radius. puckerBloat: amount (\u2212100\u2026100). twist: angle, center {x, y} (offset from the middle). zigzag: size, ridges, smooth. wiggle: size, detail, smooth, speed, correlation, phase, seed, time (seconds). offset: amount, join miter|round|bevel, miterLimit, copies, copyOffset. merge: mode merge|add|subtract|intersect|exclude. repeater: copies (fractions fade the last), offset, position {x, y}, anchor {x, y} (offset from the middle), rotation, scale {x, y} (percent), startOpacity, endOpacity (percent), composite below|above. envelope: style (a warp preset: arc|arcLower|arcUpper|arch|bulge|shellLower|shellUpper|flag|wave|fish|rise|fisheye|inflate|squeeze|twist, or mesh), bend, horizontal, vertical (percent, \u2212100\u2026100), orientation horizontal|vertical, rows, columns, mesh { us, vs, points } (node space; prefer vector.envelope).";
|
|
@@ -8504,7 +8410,8 @@ function createVectorCommands(controller, options = {}) {
|
|
|
8504
8410
|
...n.type === "path" && Array.isArray(n.fills) ? { fills: n.fills } : {},
|
|
8505
8411
|
...n.type === "path" && Array.isArray(n.strokes) ? { strokes: n.strokes } : {},
|
|
8506
8412
|
...n.blendMode && n.blendMode !== "normal" ? { blendMode: n.blendMode } : {},
|
|
8507
|
-
...n.filters?.length ? { filters: n.filters } : {}
|
|
8413
|
+
...n.filters?.length ? { filters: n.filters } : {},
|
|
8414
|
+
...isVectorMaskGroup(n) ? { mask: vectorMaskSummary(n) } : {}
|
|
8508
8415
|
});
|
|
8509
8416
|
if (n.type === "group") visit(n.children, depth + 1);
|
|
8510
8417
|
}
|
|
@@ -8652,6 +8559,105 @@ function createVectorCommands(controller, options = {}) {
|
|
|
8652
8559
|
const node = controller.getNode(nodeId) ?? fail(`There is no node "${nodeId}".`);
|
|
8653
8560
|
return node.filters?.find((f) => f.id === filterId) ?? fail(`Node "${nodeId}" has no filter "${filterId}"; it has ${(node.filters ?? []).map((f) => `"${f.id}" (${f.type})`).join(", ") || "none"}.`);
|
|
8654
8561
|
};
|
|
8562
|
+
const maskResult = (id) => ({ id, mask: vectorMaskSummary(controller.getNode(id)) ?? fail(`"${id}" is not a mask group.`) });
|
|
8563
|
+
const maskInputs = (params) => {
|
|
8564
|
+
const list2 = [...new Set(ids(params))];
|
|
8565
|
+
if (list2.length < 2) fail(`Masking needs at least two objects (the mask and what it masks); got ${list2.length}. Pass ids, or select more.`);
|
|
8566
|
+
for (const id of list2) if (controller.getNode(id).locked) fail(`"${id}" is locked; ask the user to unlock it, or leave it out of ids.`);
|
|
8567
|
+
if (params?.maskId !== void 0 && !list2.includes(params.maskId)) fail(`maskId "${params.maskId}" must be one of ids (${list2.map((id) => `"${id}"`).join(", ")}).`);
|
|
8568
|
+
return list2;
|
|
8569
|
+
};
|
|
8570
|
+
const maskTargets = (params) => {
|
|
8571
|
+
const list2 = ids(params);
|
|
8572
|
+
const groups = list2.map((id) => controller.getNode(id)).filter((n) => isVectorMaskGroup(n));
|
|
8573
|
+
if (!groups.length) fail(`None of ${list2.map((id) => `"${id}"`).join(", ")} is a mask group. Mask groups have a \`mask\` in vector.list; make one with vector.makeClippingMask or vector.makeOpacityMask.`);
|
|
8574
|
+
return groups.map((g) => g.id);
|
|
8575
|
+
};
|
|
8576
|
+
const MASK_INPUT = {
|
|
8577
|
+
ids: { ...IDS, description: "The objects to mask with, two or more sharing a parent (default: the selection). The topmost becomes the mask unless maskId says otherwise." },
|
|
8578
|
+
maskId: { type: "string", description: "Which of ids is the mask (default: the topmost)." },
|
|
8579
|
+
invert: { type: "boolean", description: "Show the contents only where the mask hides them (default false)." }
|
|
8580
|
+
};
|
|
8581
|
+
const maskCommands = () => [
|
|
8582
|
+
{
|
|
8583
|
+
...base,
|
|
8584
|
+
id: `${prefix}makeClippingMask`,
|
|
8585
|
+
label: "Make Clipping Mask",
|
|
8586
|
+
keywords: ["mask", "clip", "clipping", "clip path", "crop", "inside"],
|
|
8587
|
+
shortcut: "Mod+7",
|
|
8588
|
+
description: "Groups objects into a clipping mask group: the mask (the topmost object, or maskId) moves to the bottom of the new group and the others show only inside its outline (its paint is not drawn; every path in it counts, by its fill rule). Hit-testing, bounds, snapping and booleans use the clipped result. One undo step. Returns { id, mask: { type, invert, maskId, contents, enabled } }.",
|
|
8589
|
+
params: { type: "object", properties: MASK_INPUT, additionalProperties: false },
|
|
8590
|
+
undoable: true,
|
|
8591
|
+
enabled: enabledFor,
|
|
8592
|
+
run: (_ctx, params) => {
|
|
8593
|
+
const list2 = maskInputs(params);
|
|
8594
|
+
const r = controller.makeMask("clip", { ids: list2, ...params?.maskId ? { maskId: params.maskId } : {}, invert: params?.invert });
|
|
8595
|
+
if (!r.ok) fail(r.message);
|
|
8596
|
+
return maskResult(r.id);
|
|
8597
|
+
}
|
|
8598
|
+
},
|
|
8599
|
+
{
|
|
8600
|
+
...base,
|
|
8601
|
+
id: `${prefix}makeOpacityMask`,
|
|
8602
|
+
label: "Make Opacity Mask",
|
|
8603
|
+
keywords: ["mask", "opacity mask", "alpha", "luminance", "fade", "gradient mask", "soft mask"],
|
|
8604
|
+
description: "Groups objects into an opacity mask group: the mask (the topmost object, or maskId) moves to the bottom of the new group and hides the others where it is dark (luminance: white shows, black hides) or transparent (alpha). A gradient mask fades the contents. One undo step. Returns { id, mask: { type, invert, maskId, contents, enabled } }.",
|
|
8605
|
+
params: {
|
|
8606
|
+
type: "object",
|
|
8607
|
+
properties: { ...MASK_INPUT, type: { type: "string", enum: ["luminance", "alpha"], description: "luminance (by brightness, the default) or alpha (by opacity)." } },
|
|
8608
|
+
additionalProperties: false
|
|
8609
|
+
},
|
|
8610
|
+
undoable: true,
|
|
8611
|
+
enabled: enabledFor,
|
|
8612
|
+
run: (_ctx, params) => {
|
|
8613
|
+
const list2 = maskInputs(params);
|
|
8614
|
+
const r = controller.makeMask(params?.type ?? "luminance", { ids: list2, ...params?.maskId ? { maskId: params.maskId } : {}, invert: params?.invert });
|
|
8615
|
+
if (!r.ok) fail(r.message);
|
|
8616
|
+
return maskResult(r.id);
|
|
8617
|
+
}
|
|
8618
|
+
},
|
|
8619
|
+
{
|
|
8620
|
+
...base,
|
|
8621
|
+
id: `${prefix}setMaskOptions`,
|
|
8622
|
+
label: "Mask Options\u2026",
|
|
8623
|
+
keywords: ["mask", "clip", "invert", "alpha", "luminance", "opacity mask"],
|
|
8624
|
+
description: "Changes how mask groups mask: `type` clip (by outline), alpha or luminance, `invert`, and `enabled` (false hides the mask so the contents draw whole; true shows it again). Pass at least one. One undo step. Returns { groups: [{ id, mask }] }.",
|
|
8625
|
+
params: {
|
|
8626
|
+
type: "object",
|
|
8627
|
+
properties: {
|
|
8628
|
+
ids: { ...IDS, description: "Mask group ids (default: the selection)." },
|
|
8629
|
+
type: { type: "string", enum: ["clip", "alpha", "luminance"], description: "clip (the mask's outline), alpha (its opacity) or luminance (its brightness)." },
|
|
8630
|
+
invert: { type: "boolean", description: "true: show the contents where the mask hides them." },
|
|
8631
|
+
enabled: { type: "boolean", description: "false: turn the mask off (it is hidden and the contents draw whole); true: on again." }
|
|
8632
|
+
},
|
|
8633
|
+
additionalProperties: false
|
|
8634
|
+
},
|
|
8635
|
+
undoable: true,
|
|
8636
|
+
enabled: enabledFor,
|
|
8637
|
+
run: (_ctx, params) => {
|
|
8638
|
+
if (params?.type === void 0 && params?.invert === void 0 && params?.enabled === void 0) fail("Pass at least one of type, invert, enabled.");
|
|
8639
|
+
const groups = maskTargets(params);
|
|
8640
|
+
const patch = { ...params?.type ? { type: params.type } : {}, ...params?.invert !== void 0 ? { invert: params.invert } : {}, ...params?.enabled !== void 0 ? { enabled: params.enabled } : {} };
|
|
8641
|
+
check(controller.setMaskOptions(patch, { ids: groups }));
|
|
8642
|
+
return { groups: groups.map(maskResult) };
|
|
8643
|
+
}
|
|
8644
|
+
},
|
|
8645
|
+
{
|
|
8646
|
+
...base,
|
|
8647
|
+
id: `${prefix}releaseMask`,
|
|
8648
|
+
label: "Release Mask",
|
|
8649
|
+
keywords: ["mask", "clip", "release", "unmask", "remove mask"],
|
|
8650
|
+
shortcut: "Mod+Alt+7",
|
|
8651
|
+
description: "Releases mask groups: the mask goes back on top of the contents and draws as itself again; the group is replaced by its children unless it has filters, a blend mode or path effects (then it stays a plain group). One undo step. Returns { ids } of the released objects (the mask last).",
|
|
8652
|
+
params: { type: "object", properties: { ids: { ...IDS, description: "Mask group ids (default: the selection)." } }, additionalProperties: false },
|
|
8653
|
+
undoable: true,
|
|
8654
|
+
enabled: enabledFor,
|
|
8655
|
+
run: (_ctx, params) => {
|
|
8656
|
+
const groups = maskTargets(params);
|
|
8657
|
+
return { ids: controller.releaseMask({ ids: groups }) };
|
|
8658
|
+
}
|
|
8659
|
+
}
|
|
8660
|
+
];
|
|
8655
8661
|
const appearanceCommands = () => [
|
|
8656
8662
|
{
|
|
8657
8663
|
...base,
|
|
@@ -8888,6 +8894,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
8888
8894
|
}
|
|
8889
8895
|
},
|
|
8890
8896
|
...appearanceCommands(),
|
|
8897
|
+
...maskCommands(),
|
|
8891
8898
|
{
|
|
8892
8899
|
...base,
|
|
8893
8900
|
id: `${prefix}setCorners`,
|
|
@@ -9709,6 +9716,9 @@ export {
|
|
|
9709
9716
|
VECTOR_GRADIENT_TYPES,
|
|
9710
9717
|
VECTOR_GRADIENT_TYPE_LABELS,
|
|
9711
9718
|
VECTOR_INK_MAX_CORNERS,
|
|
9719
|
+
VECTOR_MASK_TYPES,
|
|
9720
|
+
VECTOR_MASK_TYPE_LABELS,
|
|
9721
|
+
VECTOR_MASK_TYPE_OPTIONS,
|
|
9712
9722
|
VECTOR_MERGE_MODES,
|
|
9713
9723
|
VECTOR_OVERLAY_THEME,
|
|
9714
9724
|
VECTOR_PATHFINDER_ACTIONS,
|
|
@@ -9814,6 +9824,7 @@ export {
|
|
|
9814
9824
|
getVectorEnvelopeTarget,
|
|
9815
9825
|
getVectorGradientAnnotator,
|
|
9816
9826
|
getVectorGradientEditor,
|
|
9827
|
+
getVectorMaskPanel,
|
|
9817
9828
|
getVectorOverlayRenderData,
|
|
9818
9829
|
getVectorSceneRenderData,
|
|
9819
9830
|
getVectorSelectionBoxGeometry,
|
|
@@ -9860,6 +9871,9 @@ export {
|
|
|
9860
9871
|
isVectorFilterType,
|
|
9861
9872
|
isVectorGradient,
|
|
9862
9873
|
isVectorLiveBoolean,
|
|
9874
|
+
isVectorMaskActive,
|
|
9875
|
+
isVectorMaskGroup,
|
|
9876
|
+
isVectorMaskType,
|
|
9863
9877
|
isVectorTextTarget,
|
|
9864
9878
|
joinSubpaths,
|
|
9865
9879
|
knifePath,
|
|
@@ -9937,6 +9951,7 @@ export {
|
|
|
9937
9951
|
sanitizeVectorFills,
|
|
9938
9952
|
sanitizeVectorFilters,
|
|
9939
9953
|
sanitizeVectorGradient,
|
|
9954
|
+
sanitizeVectorMask,
|
|
9940
9955
|
sanitizeVectorStrokeSettings,
|
|
9941
9956
|
sanitizeVectorStrokes,
|
|
9942
9957
|
sanitizeWidthProfile,
|
|
@@ -9997,6 +10012,7 @@ export {
|
|
|
9997
10012
|
vectorBoxCorners,
|
|
9998
10013
|
vectorBoxMatrix,
|
|
9999
10014
|
vectorBuilderErasedEdges,
|
|
10015
|
+
vectorChildAppearanceShapes,
|
|
10000
10016
|
vectorCornerAt,
|
|
10001
10017
|
vectorCorners,
|
|
10002
10018
|
vectorCornersAttribute,
|
|
@@ -10036,6 +10052,19 @@ export {
|
|
|
10036
10052
|
identityWarpMesh as vectorIdentityMesh,
|
|
10037
10053
|
vectorInkShapeName,
|
|
10038
10054
|
vectorLiveBooleanSteps,
|
|
10055
|
+
vectorMaskContains,
|
|
10056
|
+
vectorMaskContentBounds,
|
|
10057
|
+
vectorMaskContents,
|
|
10058
|
+
vectorMaskOwner,
|
|
10059
|
+
vectorMaskRegion,
|
|
10060
|
+
vectorMaskRegionPath,
|
|
10061
|
+
vectorMaskRegionShapes,
|
|
10062
|
+
vectorMaskRenderData,
|
|
10063
|
+
vectorMaskSource,
|
|
10064
|
+
vectorMaskSummary,
|
|
10065
|
+
vectorMaskedGeometry,
|
|
10066
|
+
vectorMaskedLocalBounds,
|
|
10067
|
+
vectorMaskedShapes,
|
|
10039
10068
|
warpMeshColumns as vectorMeshColumns,
|
|
10040
10069
|
warpMeshControlBounds as vectorMeshControlBounds,
|
|
10041
10070
|
warpMeshEditPoints as vectorMeshEditPoints,
|