@pixerate/editor 0.118.0 → 0.119.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 +1 -0
- package/dist/animation-editor/index.cjs +54 -54
- package/dist/animation-editor/index.d.cts +3 -3
- package/dist/animation-editor/index.d.ts +3 -3
- package/dist/animation-editor/index.js +2 -2
- package/dist/{chunk-2J3LS7JZ.cjs → chunk-4S2AXNQF.cjs} +221 -186
- package/dist/chunk-4S2AXNQF.cjs.map +1 -0
- package/dist/{chunk-7LNZAWSJ.js → chunk-CSHEI3YT.js} +625 -451
- package/dist/chunk-CSHEI3YT.js.map +1 -0
- package/dist/{chunk-M7O4BJRH.cjs → chunk-KGNUFO4T.cjs} +36 -36
- package/dist/{chunk-M7O4BJRH.cjs.map → chunk-KGNUFO4T.cjs.map} +1 -1
- package/dist/{chunk-R2JE6SA7.js → chunk-KZLQY4CD.js} +42 -7
- package/dist/chunk-KZLQY4CD.js.map +1 -0
- package/dist/{chunk-L2T6EJ5S.cjs → chunk-L3PXFMOF.cjs} +38 -25
- package/dist/chunk-L3PXFMOF.cjs.map +1 -0
- package/dist/{chunk-FODKPD44.cjs → chunk-LI3KTY7K.cjs} +724 -550
- package/dist/chunk-LI3KTY7K.cjs.map +1 -0
- package/dist/{chunk-IVSNUXW7.js → chunk-T7IZDNSL.js} +37 -24
- package/dist/chunk-T7IZDNSL.js.map +1 -0
- package/dist/{chunk-T2P5TIZ6.js → chunk-TDQDMBUR.js} +2 -2
- package/dist/{chunk-BYAWOAST.cjs → chunk-TS5IEUW6.cjs} +26 -25
- package/dist/chunk-TS5IEUW6.cjs.map +1 -0
- package/dist/{chunk-VD2UDVF7.js → chunk-UMVW2Q66.js} +21 -5
- package/dist/chunk-UMVW2Q66.js.map +1 -0
- package/dist/{chunk-EBSYZRSW.cjs → chunk-WMRIG4XQ.cjs} +21 -5
- package/dist/chunk-WMRIG4XQ.cjs.map +1 -0
- package/dist/{chunk-JHXMNOMF.js → chunk-YHW26LS2.js} +14 -13
- package/dist/chunk-YHW26LS2.js.map +1 -0
- package/dist/{export-DMOuzgZZ.d.cts → export-B_jO2PrH.d.cts} +18 -4
- package/dist/{export-Dul1SbPv.d.ts → export-Ds8Ccw0K.d.ts} +18 -4
- package/dist/{flow-4-OC-UkQ.d.cts → flow-A2Dj-69w.d.cts} +21 -9
- package/dist/{flow-Cb7lkPm8.d.ts → flow-C95atNoQ.d.ts} +21 -9
- package/dist/image-editor/index.cjs +1104 -979
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +5 -5
- package/dist/image-editor/index.d.ts +5 -5
- package/dist/image-editor/index.js +157 -32
- package/dist/image-editor/index.js.map +1 -1
- package/dist/paint/index.cjs +5 -3
- package/dist/paint/index.cjs.map +1 -1
- package/dist/paint/index.d.cts +2 -2
- package/dist/paint/index.d.ts +2 -2
- package/dist/paint/index.js +4 -2
- package/dist/paint-editor/index.cjs +147 -147
- package/dist/paint-editor/index.d.cts +4 -4
- package/dist/paint-editor/index.d.ts +4 -4
- package/dist/paint-editor/index.js +4 -4
- package/dist/raster/index.cjs +2 -2
- package/dist/raster/index.js +1 -1
- package/dist/{svg-DlEuhwoX.d.ts → svg-CTxrB5xO.d.ts} +5 -1
- package/dist/{svg-CVdFc80-.d.cts → svg-hK0WQc7-.d.cts} +5 -1
- package/dist/{types-Tfly4Aya.d.cts → types-Dq-8qgoo.d.cts} +45 -3
- package/dist/{types-CDLGhAZp.d.ts → types-TrSSh_RB.d.ts} +45 -3
- package/dist/vector/index.cjs +1312 -1009
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +103 -5
- package/dist/vector/index.d.ts +103 -5
- package/dist/vector/index.js +317 -14
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/references/photoshop-files.md +5 -2
- package/skills/pixerate-vector/SKILL.md +13 -1
- package/skills/pixerate-vector/references/commands.md +1 -0
- package/dist/chunk-2J3LS7JZ.cjs.map +0 -1
- package/dist/chunk-7LNZAWSJ.js.map +0 -1
- package/dist/chunk-BYAWOAST.cjs.map +0 -1
- package/dist/chunk-EBSYZRSW.cjs.map +0 -1
- package/dist/chunk-FODKPD44.cjs.map +0 -1
- package/dist/chunk-IVSNUXW7.js.map +0 -1
- package/dist/chunk-JHXMNOMF.js.map +0 -1
- package/dist/chunk-L2T6EJ5S.cjs.map +0 -1
- package/dist/chunk-R2JE6SA7.js.map +0 -1
- package/dist/chunk-VD2UDVF7.js.map +0 -1
- /package/dist/{chunk-T2P5TIZ6.js.map → chunk-TDQDMBUR.js.map} +0 -0
package/dist/vector/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
reverseMorphContour,
|
|
41
41
|
subpathToMorphContour,
|
|
42
42
|
turnMorphContour
|
|
43
|
-
} from "../chunk-
|
|
43
|
+
} from "../chunk-TDQDMBUR.js";
|
|
44
44
|
import {
|
|
45
45
|
DEFAULT_VECTOR_FONT,
|
|
46
46
|
IDENTITY_MATRIX,
|
|
@@ -83,6 +83,8 @@ import {
|
|
|
83
83
|
VECTOR_STROKE_GRADIENT_MODES,
|
|
84
84
|
VECTOR_STROKE_SETTING_KEYS,
|
|
85
85
|
VECTOR_SYMBOL_MAX_DEPTH,
|
|
86
|
+
VECTOR_SYMMETRY_MODES,
|
|
87
|
+
VECTOR_SYMMETRY_MODE_LABELS,
|
|
86
88
|
VECTOR_TEXT_ALIGNS,
|
|
87
89
|
VECTOR_TOOLS,
|
|
88
90
|
VECTOR_TRANSFORM_HANDLES,
|
|
@@ -200,6 +202,7 @@ import {
|
|
|
200
202
|
isVectorMaskType,
|
|
201
203
|
isVectorMeshGradient,
|
|
202
204
|
isVectorShadedPaint,
|
|
205
|
+
isVectorSymmetryGroup,
|
|
203
206
|
knifePath,
|
|
204
207
|
layoutVectorText,
|
|
205
208
|
lineSubpath,
|
|
@@ -284,6 +287,7 @@ import {
|
|
|
284
287
|
sanitizeVectorShadedPaint,
|
|
285
288
|
sanitizeVectorStrokeSettings,
|
|
286
289
|
sanitizeVectorStrokes,
|
|
290
|
+
sanitizeVectorSymmetry,
|
|
287
291
|
sanitizeVectorTextFields,
|
|
288
292
|
sanitizeVectorTextFrame,
|
|
289
293
|
sanitizeWidthProfile,
|
|
@@ -316,6 +320,7 @@ import {
|
|
|
316
320
|
transformVectorNode,
|
|
317
321
|
transformVectorPaint,
|
|
318
322
|
transformVectorShadedPaint,
|
|
323
|
+
transformVectorSymmetry,
|
|
319
324
|
translateAnchor,
|
|
320
325
|
translateMatrix,
|
|
321
326
|
trimIntervals,
|
|
@@ -429,6 +434,9 @@ import {
|
|
|
429
434
|
vectorStrokes,
|
|
430
435
|
vectorSymbolUses,
|
|
431
436
|
vectorSymbolsUsedIn,
|
|
437
|
+
vectorSymmetryAxes,
|
|
438
|
+
vectorSymmetryDrawn,
|
|
439
|
+
vectorSymmetryMatrices,
|
|
432
440
|
vectorTargetPaint,
|
|
433
441
|
vectorTextBoxOutline,
|
|
434
442
|
vectorTextCaret,
|
|
@@ -467,7 +475,7 @@ import {
|
|
|
467
475
|
withVectorStrokes,
|
|
468
476
|
withVectorTargetPaint,
|
|
469
477
|
zigzagPaths
|
|
470
|
-
} from "../chunk-
|
|
478
|
+
} from "../chunk-CSHEI3YT.js";
|
|
471
479
|
import {
|
|
472
480
|
bezierPathBounds,
|
|
473
481
|
bezierPathToSvg,
|
|
@@ -2371,6 +2379,46 @@ function shadingOverlay(state) {
|
|
|
2371
2379
|
}
|
|
2372
2380
|
return out;
|
|
2373
2381
|
}
|
|
2382
|
+
function symmetryOverlay(state) {
|
|
2383
|
+
const ids = /* @__PURE__ */ new Set([...state.selection, ...state.symmetryTarget ? [state.symmetryTarget] : []]);
|
|
2384
|
+
const out = [];
|
|
2385
|
+
for (const id of ids) {
|
|
2386
|
+
const n2 = findVectorNode(state.document.nodes, id);
|
|
2387
|
+
if (!isVectorSymmetryGroup(n2)) continue;
|
|
2388
|
+
const world = vectorWorldMatrix(state.document.nodes, id);
|
|
2389
|
+
if (!world) continue;
|
|
2390
|
+
const inv = invertMatrix(world);
|
|
2391
|
+
const corners2 = [
|
|
2392
|
+
{ x: 0, y: 0 },
|
|
2393
|
+
{ x: state.document.width, y: 0 },
|
|
2394
|
+
{ x: 0, y: state.document.height },
|
|
2395
|
+
{ x: state.document.width, y: state.document.height }
|
|
2396
|
+
].map((p) => applyMatrix(inv, p));
|
|
2397
|
+
const { transform: _t, ...own } = n2;
|
|
2398
|
+
const b = unionBoxes([vectorNodeBounds(own), boundsFromCorners(corners2)]);
|
|
2399
|
+
if (!b) continue;
|
|
2400
|
+
const pad = Math.max(b.width, b.height) * 0.05;
|
|
2401
|
+
const lines = vectorSymmetryAxes(n2.symmetry, { x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad }).map(([a, c]) => {
|
|
2402
|
+
const p = toScreen(state, applyMatrix(world, a));
|
|
2403
|
+
const q = toScreen(state, applyMatrix(world, c));
|
|
2404
|
+
return { x1: p.x, y1: p.y, x2: q.x, y2: q.y };
|
|
2405
|
+
});
|
|
2406
|
+
out.push({ nodeId: id, drawing: state.symmetryTarget === id, lines });
|
|
2407
|
+
}
|
|
2408
|
+
return out;
|
|
2409
|
+
}
|
|
2410
|
+
function unionBoxes(list) {
|
|
2411
|
+
const ok = list.filter((b) => !!b);
|
|
2412
|
+
if (!ok.length) return null;
|
|
2413
|
+
const x = Math.min(...ok.map((b) => b.x));
|
|
2414
|
+
const y = Math.min(...ok.map((b) => b.y));
|
|
2415
|
+
return { x, y, width: Math.max(...ok.map((b) => b.x + b.width)) - x, height: Math.max(...ok.map((b) => b.y + b.height)) - y };
|
|
2416
|
+
}
|
|
2417
|
+
function boundsFromCorners(pts) {
|
|
2418
|
+
const xs = pts.map((p) => p.x);
|
|
2419
|
+
const ys = pts.map((p) => p.y);
|
|
2420
|
+
return { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) };
|
|
2421
|
+
}
|
|
2374
2422
|
function connectorSiteOverlay(state) {
|
|
2375
2423
|
if (state.tool !== "connector" || !state.pointer) return [];
|
|
2376
2424
|
const drawing = state.interaction?.type === "draw" && state.interaction.tool === "connector" ? state.interaction.nodeId : void 0;
|
|
@@ -2643,6 +2691,7 @@ function getVectorOverlayRenderData(state) {
|
|
|
2643
2691
|
frames: frameLabels(state),
|
|
2644
2692
|
connectorSites: connectorSiteOverlay(state),
|
|
2645
2693
|
shading: shadingOverlay(state),
|
|
2694
|
+
symmetry: symmetryOverlay(state),
|
|
2646
2695
|
trackingPoints: (state.trackingPoints ?? []).map((p) => toScreen(state, p)),
|
|
2647
2696
|
typedEntry: state.typedEntry ? { text: state.typedEntry, ...state.pointer ? toScreen(state, state.pointer) : { x: 0, y: 0 } } : null
|
|
2648
2697
|
};
|
|
@@ -3663,9 +3712,9 @@ function nodeGroup(node, notes) {
|
|
|
3663
3712
|
if (node.filters?.length) notes.push(`${label(node)}: filters (shadows, glows, blurs) are not part of Lottie (left out).`);
|
|
3664
3713
|
const m = nodeMatrix(node);
|
|
3665
3714
|
let items = [];
|
|
3666
|
-
const live = (node.effects ?? []).some((e) => e.enabled !== false && (e.type === "wiggle" || e.type === "envelope"));
|
|
3667
|
-
if (node.type === "text" || node.type === "instance" || live || isVectorMaskGroup(node) || isVectorFrame(node)) {
|
|
3668
|
-
if (live) notes.push(`${label(node)}: wiggle and
|
|
3715
|
+
const live = (node.effects ?? []).some((e) => e.enabled !== false && (e.type === "wiggle" || e.type === "envelope" || e.type === "contour"));
|
|
3716
|
+
if (node.type === "text" || node.type === "instance" || live || isVectorMaskGroup(node) || isVectorFrame(node) || isVectorSymmetryGroup(node)) {
|
|
3717
|
+
if (live) notes.push(`${label(node)}: wiggle, envelope and contour effects have no Lottie equivalent (exported as drawn).`);
|
|
3669
3718
|
if (isVectorMaskGroup(node) && node.mask?.type !== "clip") notes.push(`${label(node)}: opacity masks are not exported (the contents are drawn whole).`);
|
|
3670
3719
|
const drawn = isVectorFrame(node) ? vectorFrameDrawn(node) : node;
|
|
3671
3720
|
const shapes = isVectorMaskGroup(drawn) && drawn.mask?.type === "clip" ? vectorAppearanceShapes({ ...drawn, transform: void 0 }) : null;
|
|
@@ -6899,6 +6948,7 @@ var VectorEditorController = class _VectorEditorController {
|
|
|
6899
6948
|
trackingPoints: this.trackingPoints,
|
|
6900
6949
|
typedEntry: this.typed,
|
|
6901
6950
|
shadingPoint: this.getShadingSelection(),
|
|
6951
|
+
symmetryTarget: this.symmetryTarget && this.doc.nodes.some((n2) => n2.id === this.symmetryTarget) ? this.symmetryTarget : null,
|
|
6902
6952
|
canUndo: h.canUndo,
|
|
6903
6953
|
canRedo: h.canRedo,
|
|
6904
6954
|
undoLabel: h.undoName,
|
|
@@ -9202,6 +9252,155 @@ var VectorEditorController = class _VectorEditorController {
|
|
|
9202
9252
|
return { ok: true, id };
|
|
9203
9253
|
}
|
|
9204
9254
|
// -----------------------------------------------------------------------------------------
|
|
9255
|
+
// Live symmetry groups
|
|
9256
|
+
/** The symmetry group new pen, pencil, shape and text art goes into (see `enterSymmetry`), or null. */
|
|
9257
|
+
symmetryTarget = null;
|
|
9258
|
+
/** The symmetry group drawn art goes into, while it is entered and still a top-level, unturned symmetry group. */
|
|
9259
|
+
drawingTarget() {
|
|
9260
|
+
const id = this.symmetryTarget;
|
|
9261
|
+
if (!id) return null;
|
|
9262
|
+
const n2 = this.doc.nodes.find((x) => x.id === id);
|
|
9263
|
+
if (!isVectorSymmetryGroup(n2) || n2.transform || n2.locked) {
|
|
9264
|
+
this.symmetryTarget = null;
|
|
9265
|
+
return null;
|
|
9266
|
+
}
|
|
9267
|
+
return n2;
|
|
9268
|
+
}
|
|
9269
|
+
/** Adds a node a drawing tool makes: into the entered symmetry group, else on top. */
|
|
9270
|
+
addDrawn(node) {
|
|
9271
|
+
const target = this.tool === "frame" || this.tool === "connector" ? null : this.drawingTarget();
|
|
9272
|
+
if (!target) return this.setDoc([...this.doc.nodes, node]);
|
|
9273
|
+
this.setDoc(this.doc.nodes.map((n2) => n2.id === target.id && n2.type === "group" ? { ...n2, children: [...n2.children, node] } : n2));
|
|
9274
|
+
}
|
|
9275
|
+
/**
|
|
9276
|
+
* Make Symmetry: puts objects that share a parent (default: the selection) into a live symmetry
|
|
9277
|
+
* group that draws them with their mirrored or rotated copies, the axes through `center`
|
|
9278
|
+
* (default: the objects' centre, or the artboard's when there are none, making an empty group to
|
|
9279
|
+
* draw into). One undo step; selects the group (and enters it when empty). Returns its id.
|
|
9280
|
+
*/
|
|
9281
|
+
makeSymmetry(input, options = {}) {
|
|
9282
|
+
const symmetry = sanitizeVectorSymmetry({ ...input, center: input.center ?? { x: 0, y: 0 } });
|
|
9283
|
+
if (!symmetry) return { ok: false, message: `Unknown symmetry "${input.mode}": use ${VECTOR_SYMMETRY_MODES.join(", ")}.` };
|
|
9284
|
+
if (input.copies !== void 0 && !(input.copies >= 2 && input.copies <= 64)) return { ok: false, message: "copies must be from 2 to 64." };
|
|
9285
|
+
const ids = [...new Set(options.ids ?? this.selectedRoots())];
|
|
9286
|
+
for (const id2 of ids) {
|
|
9287
|
+
const n2 = this.getNode(id2);
|
|
9288
|
+
if (!n2) return { ok: false, message: `There is no node "${id2}".` };
|
|
9289
|
+
if (n2.locked) return { ok: false, message: `"${id2}" is locked; unlock it first.` };
|
|
9290
|
+
}
|
|
9291
|
+
const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
|
|
9292
|
+
const parent = locs[0]?.parent ?? null;
|
|
9293
|
+
if (locs.some((l) => (l.parent?.id ?? null) !== (parent?.id ?? null))) return { ok: false, message: "The objects must share a parent (pick objects at one level)." };
|
|
9294
|
+
const b = unionBounds(ids.map((id2) => vectorNodeBounds(this.getNode(id2))));
|
|
9295
|
+
const center = input.center ?? (b ? { x: b.x + b.width / 2, y: b.y + b.height / 2 } : { x: this.doc.width / 2, y: this.doc.height / 2 });
|
|
9296
|
+
const id = this.newId();
|
|
9297
|
+
const group = { id, type: "group", name: input.name ?? "Symmetry", symmetry: { ...symmetry, center }, children: [] };
|
|
9298
|
+
const order = ids.slice().sort((a, c) => locs[ids.indexOf(a)].index - locs[ids.indexOf(c)].index);
|
|
9299
|
+
const set = new Set(order);
|
|
9300
|
+
this.change("Make Symmetry", () => {
|
|
9301
|
+
if (!order.length) this.setDoc([...this.doc.nodes, group]);
|
|
9302
|
+
else {
|
|
9303
|
+
const regroup = (list) => {
|
|
9304
|
+
const members = order.map((m) => list.find((n2) => n2.id === m));
|
|
9305
|
+
const top = Math.max(...members.map((m) => list.indexOf(m)));
|
|
9306
|
+
const out = [];
|
|
9307
|
+
list.forEach((n2, i) => {
|
|
9308
|
+
if (i === top) out.push({ ...group, children: members });
|
|
9309
|
+
if (!set.has(n2.id)) out.push(n2);
|
|
9310
|
+
});
|
|
9311
|
+
return out;
|
|
9312
|
+
};
|
|
9313
|
+
if (!parent) this.setDoc(regroup(this.doc.nodes));
|
|
9314
|
+
else this.setDoc(mapTree(this.doc.nodes, (n2) => n2.id === parent.id && n2.type === "group" ? { ...n2, children: regroup(n2.children) } : n2));
|
|
9315
|
+
}
|
|
9316
|
+
this.selection = [id];
|
|
9317
|
+
this.anchors = [];
|
|
9318
|
+
this.editingId = null;
|
|
9319
|
+
if (!order.length && !parent) this.symmetryTarget = id;
|
|
9320
|
+
});
|
|
9321
|
+
return { ok: true, id };
|
|
9322
|
+
}
|
|
9323
|
+
/** Symmetry groups among `ids` (default: the selection, or the group holding it). */
|
|
9324
|
+
symmetryTargets(ids) {
|
|
9325
|
+
const out = /* @__PURE__ */ new Map();
|
|
9326
|
+
for (const id of ids ?? this.selection) {
|
|
9327
|
+
const n2 = this.getNode(id);
|
|
9328
|
+
if (isVectorSymmetryGroup(n2)) out.set(n2.id, n2);
|
|
9329
|
+
else if (!ids) {
|
|
9330
|
+
const p = locateVectorNode(this.doc.nodes, id)?.parent;
|
|
9331
|
+
if (isVectorSymmetryGroup(p)) out.set(p.id, p);
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
return [...out.values()];
|
|
9335
|
+
}
|
|
9336
|
+
/** Changes symmetry groups (default: the selected one, or the one holding the selection): mode, centre (the group's space), rotation, radial copies and mirroring. One undo step (merged while `merge` is the same key). */
|
|
9337
|
+
setSymmetry(patch, options = {}) {
|
|
9338
|
+
if (options.ids) {
|
|
9339
|
+
for (const id of options.ids) if (!isVectorSymmetryGroup(this.getNode(id))) return { ok: false, message: `"${id}" is not a symmetry group (make one with vector.makeSymmetry).` };
|
|
9340
|
+
}
|
|
9341
|
+
const groups = this.symmetryTargets(options.ids);
|
|
9342
|
+
if (!groups.length) return { ok: false, message: "Select a symmetry group (or something in one)." };
|
|
9343
|
+
if (patch.mode !== void 0 && !VECTOR_SYMMETRY_MODES.includes(patch.mode)) return { ok: false, message: `Unknown symmetry "${patch.mode}": use ${VECTOR_SYMMETRY_MODES.join(", ")}.` };
|
|
9344
|
+
if (patch.copies !== void 0 && !(patch.copies >= 2 && patch.copies <= 64)) return { ok: false, message: "copies must be from 2 to 64." };
|
|
9345
|
+
const next = new Map(groups.map((g) => [g.id, sanitizeVectorSymmetry({ ...g.symmetry, ...patch })]));
|
|
9346
|
+
if ([...next.values()].some((v) => !v)) return { ok: false, message: "center must be { x, y } numbers." };
|
|
9347
|
+
this.change(
|
|
9348
|
+
"Symmetry",
|
|
9349
|
+
() => {
|
|
9350
|
+
this.setDoc(mapTree(this.doc.nodes, (n2) => next.has(n2.id) && n2.type === "group" ? { ...n2, symmetry: next.get(n2.id) } : n2));
|
|
9351
|
+
},
|
|
9352
|
+
options.merge
|
|
9353
|
+
);
|
|
9354
|
+
return { ok: true };
|
|
9355
|
+
}
|
|
9356
|
+
/**
|
|
9357
|
+
* Release Symmetry: the groups stop repeating their children. With `expand`, the copies become
|
|
9358
|
+
* real objects in the group (new ids); otherwise they go. One undo step. Returns the ids of the
|
|
9359
|
+
* objects added by expanding.
|
|
9360
|
+
*/
|
|
9361
|
+
releaseSymmetry(options = {}) {
|
|
9362
|
+
const groups = this.symmetryTargets(options.ids);
|
|
9363
|
+
if (!groups.length) return { ok: false, message: "Select a symmetry group (or something in one)." };
|
|
9364
|
+
const added = [];
|
|
9365
|
+
const next = /* @__PURE__ */ new Map();
|
|
9366
|
+
for (const g of groups) {
|
|
9367
|
+
const { symmetry: _s, ...rest } = g;
|
|
9368
|
+
if (!options.expand) {
|
|
9369
|
+
next.set(g.id, rest);
|
|
9370
|
+
continue;
|
|
9371
|
+
}
|
|
9372
|
+
const drawn = vectorSymmetryDrawn(g);
|
|
9373
|
+
const fresh = (n2) => {
|
|
9374
|
+
const id = this.newId();
|
|
9375
|
+
added.push(id);
|
|
9376
|
+
const { locked: _l, ...plain2 } = n2;
|
|
9377
|
+
return plain2.type === "group" ? { ...plain2, id, children: plain2.children.map(fresh) } : { ...plain2, id };
|
|
9378
|
+
};
|
|
9379
|
+
next.set(g.id, { ...rest, children: [...g.children, ...drawn.children.slice(g.children.length).map(fresh)] });
|
|
9380
|
+
}
|
|
9381
|
+
this.change(options.expand ? "Expand Symmetry" : "Release Symmetry", () => {
|
|
9382
|
+
this.setDoc(mapTree(this.doc.nodes, (n2) => next.get(n2.id) ?? n2));
|
|
9383
|
+
if (this.symmetryTarget && next.has(this.symmetryTarget)) this.symmetryTarget = null;
|
|
9384
|
+
});
|
|
9385
|
+
return { ok: true, ids: added.filter((id) => !!this.getNode(id)) };
|
|
9386
|
+
}
|
|
9387
|
+
/** Enters a top-level symmetry group: new pen, pencil, shape and text art goes into it (mirrored as it is drawn) until `exitSymmetry` or Escape. */
|
|
9388
|
+
enterSymmetry(id) {
|
|
9389
|
+
const n2 = this.doc.nodes.find((x) => x.id === id);
|
|
9390
|
+
if (!isVectorSymmetryGroup(n2)) return { ok: false, message: `"${id}" is not a top-level symmetry group.` };
|
|
9391
|
+
if (n2.transform) return { ok: false, message: "Drawing into a turned symmetry group is not supported; draw beside it and move the art in." };
|
|
9392
|
+
this.symmetryTarget = id;
|
|
9393
|
+
this.selection = [id];
|
|
9394
|
+
this.notify();
|
|
9395
|
+
return { ok: true };
|
|
9396
|
+
}
|
|
9397
|
+
/** Stops drawing into the entered symmetry group. */
|
|
9398
|
+
exitSymmetry() {
|
|
9399
|
+
if (!this.symmetryTarget) return;
|
|
9400
|
+
this.symmetryTarget = null;
|
|
9401
|
+
this.notify();
|
|
9402
|
+
}
|
|
9403
|
+
// -----------------------------------------------------------------------------------------
|
|
9205
9404
|
// PDF
|
|
9206
9405
|
/**
|
|
9207
9406
|
* The document as a vector PDF (see `vectorDocumentToPdf`): one page of the artboard, or a page
|
|
@@ -11673,7 +11872,7 @@ var VectorEditorController = class _VectorEditorController {
|
|
|
11673
11872
|
}
|
|
11674
11873
|
const pt2 = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
|
|
11675
11874
|
const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt2.x, y: pt2.y }], closed: false }] };
|
|
11676
|
-
this.
|
|
11875
|
+
this.addDrawn(node);
|
|
11677
11876
|
this.pen = { nodeId: node.id, subpath: 0 };
|
|
11678
11877
|
this.selection = [node.id];
|
|
11679
11878
|
this.editingId = node.id;
|
|
@@ -11694,7 +11893,7 @@ var VectorEditorController = class _VectorEditorController {
|
|
|
11694
11893
|
strokeLinejoin: this.defaults.strokeLinejoin ?? "round",
|
|
11695
11894
|
subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
|
|
11696
11895
|
};
|
|
11697
|
-
this.
|
|
11896
|
+
this.addDrawn(node);
|
|
11698
11897
|
const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
|
|
11699
11898
|
lazy.push([p.x, p.y]);
|
|
11700
11899
|
const mode = this.editing.pencilPressure;
|
|
@@ -11712,7 +11911,7 @@ var VectorEditorController = class _VectorEditorController {
|
|
|
11712
11911
|
const kind = this.tool === "frame" ? "rect" : this.tool;
|
|
11713
11912
|
const node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });
|
|
11714
11913
|
if (this.tool === "frame") Object.assign(node, { fill: "#ffffff", stroke: "#a3a3a3", strokeWidth: 1 / this.viewport.zoom });
|
|
11715
|
-
this.
|
|
11914
|
+
this.addDrawn(node);
|
|
11716
11915
|
this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before, targets };
|
|
11717
11916
|
this.interaction = { type: "draw", tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };
|
|
11718
11917
|
this.notify(true);
|
|
@@ -12855,7 +13054,7 @@ var VectorEditorController = class _VectorEditorController {
|
|
|
12855
13054
|
this.endTextEdit();
|
|
12856
13055
|
const before = this.snapshot();
|
|
12857
13056
|
if (replace) this.setDoc(mapTree(this.doc.nodes, (n2) => n2.id === replace ? node : n2));
|
|
12858
|
-
else this.
|
|
13057
|
+
else this.addDrawn(node);
|
|
12859
13058
|
this.selection = [node.id];
|
|
12860
13059
|
this.anchors = [];
|
|
12861
13060
|
this.textEdit = { nodeId: node.id, anchor: 0, focus: 0, before, key: `text-edit:${node.id}:${++this.textEditCount}`, created: true };
|
|
@@ -13409,6 +13608,7 @@ var VectorEditorController = class _VectorEditorController {
|
|
|
13409
13608
|
case "Escape":
|
|
13410
13609
|
if (this.drag) this.cancelDrag();
|
|
13411
13610
|
else if (this.trackingPoints.length) this.clearTracking();
|
|
13611
|
+
else if (this.symmetryTarget && !this.pen) this.exitSymmetry();
|
|
13412
13612
|
else if (this.widthEdit) this.closeWidthPointEdit();
|
|
13413
13613
|
else if (this.tool === "width" && this.widthPoints.length) {
|
|
13414
13614
|
this.widthPoints = [];
|
|
@@ -13602,11 +13802,12 @@ function pencilPressureProfile(points, pressures) {
|
|
|
13602
13802
|
}
|
|
13603
13803
|
|
|
13604
13804
|
// src/vector/commands.ts
|
|
13605
|
-
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).";
|
|
13805
|
+
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. contour: direction inside|outside|center, steps, offset, fillColor / strokeColor (end colours, null keeps the colour), acceleration (\u2212100\u2026100), join. 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).";
|
|
13606
13806
|
var fail = (message) => {
|
|
13607
13807
|
throw new Error(message);
|
|
13608
13808
|
};
|
|
13609
13809
|
var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
|
|
13810
|
+
var PARENT_ID = { type: "string", description: "A group to add into, on top of its children (default: the document). Coordinates are the group's own space (the document's for an unturned top-level group)." };
|
|
13610
13811
|
var ANCHORS = {
|
|
13611
13812
|
type: "array",
|
|
13612
13813
|
items: {
|
|
@@ -13746,6 +13947,11 @@ function createVectorCommands(controller, options = {}) {
|
|
|
13746
13947
|
const prefix = options.prefix ?? "vector.";
|
|
13747
13948
|
const group = options.group ?? "Vector";
|
|
13748
13949
|
const base = { group, ...options.scope ? { scope: options.scope } : {} };
|
|
13950
|
+
const parentOf = (parentId) => {
|
|
13951
|
+
if (parentId === void 0) return {};
|
|
13952
|
+
if (controller.getNode(parentId)?.type !== "group") fail(`"${parentId}" is not a group to add into.`);
|
|
13953
|
+
return { parentId };
|
|
13954
|
+
};
|
|
13749
13955
|
const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
|
|
13750
13956
|
const ids = (params) => {
|
|
13751
13957
|
const list2 = params?.ids ?? controller.getState().selection;
|
|
@@ -13792,7 +13998,8 @@ function createVectorCommands(controller, options = {}) {
|
|
|
13792
13998
|
...isVectorMaskGroup(n2) ? { mask: vectorMaskSummary(n2) } : {},
|
|
13793
13999
|
...isVectorFrame(n2) ? { frameRect: n2.frame } : {},
|
|
13794
14000
|
...n2.resizing ? { resizing: n2.resizing } : {},
|
|
13795
|
-
...n2.type === "path" && n2.connector ? { connector: n2.connector } : {}
|
|
14001
|
+
...n2.type === "path" && n2.connector ? { connector: n2.connector } : {},
|
|
14002
|
+
...n2.type === "group" && n2.symmetry ? { symmetry: n2.symmetry } : {}
|
|
13796
14003
|
});
|
|
13797
14004
|
if (n2.type === "group") visit(n2.children, depth + 1);
|
|
13798
14005
|
}
|
|
@@ -14673,6 +14880,89 @@ function createVectorCommands(controller, options = {}) {
|
|
|
14673
14880
|
issues: state.constraintIssues
|
|
14674
14881
|
};
|
|
14675
14882
|
};
|
|
14883
|
+
const SYMMETRY_PROPS = {
|
|
14884
|
+
mode: { type: "string", enum: VECTOR_SYMMETRY_MODES, description: "vertical (mirror left / right), horizontal (top / bottom), dual (four quarters), diagonal, or radial (turns round the centre)." },
|
|
14885
|
+
center: { type: "object", properties: { x: { type: "number", description: "Document units (the group's space)." }, y: { type: "number", description: "Document units (the group's space)." } }, required: ["x", "y"], additionalProperties: false, description: "Where the axes cross." },
|
|
14886
|
+
rotation: { type: "number", description: "Turn of the axes, degrees clockwise (default 0)." },
|
|
14887
|
+
copies: { type: "integer", minimum: 2, maximum: 64, description: "Radial: how many turns round the circle (default 6)." },
|
|
14888
|
+
mirror: { type: "boolean", description: "Radial: mirror each turn too, like a kaleidoscope (default false)." }
|
|
14889
|
+
};
|
|
14890
|
+
const symmetryResult = (id) => {
|
|
14891
|
+
const n2 = controller.getNode(id);
|
|
14892
|
+
return n2?.type === "group" && n2.symmetry ? { id, symmetry: n2.symmetry, children: n2.children.map((c) => c.id) } : fail(`"${id}" is not a symmetry group.`);
|
|
14893
|
+
};
|
|
14894
|
+
const symmetryCommands = () => [
|
|
14895
|
+
{
|
|
14896
|
+
...base,
|
|
14897
|
+
id: `${prefix}makeSymmetry`,
|
|
14898
|
+
label: "Make Symmetry\u2026",
|
|
14899
|
+
keywords: ["symmetry", "mirror", "kaleidoscope", "radial", "mandala", "reflect", "live symmetry"],
|
|
14900
|
+
description: "Puts objects sharing a parent (`ids`, default the selection) into a live symmetry group: it draws them with their mirrored or rotated copies, which follow every edit of the originals. center defaults to the objects' centre (the artboard's with no objects: an empty group to add art to with parentId). One undo step. Returns { id, symmetry, children }.",
|
|
14901
|
+
params: { type: "object", properties: { ids: { ...IDS, description: "Objects to repeat (default: the selection; [] for an empty group)." }, ...SYMMETRY_PROPS, name: { type: "string", description: 'Group name (default "Symmetry").' } }, required: ["mode"], additionalProperties: false },
|
|
14902
|
+
undoable: true,
|
|
14903
|
+
run: (_ctx, params) => {
|
|
14904
|
+
const { ids: list2, ...input } = params;
|
|
14905
|
+
const r = controller.makeSymmetry(input, list2 ? { ids: list2 } : {});
|
|
14906
|
+
if (!r.ok) fail(r.message);
|
|
14907
|
+
return symmetryResult(r.id);
|
|
14908
|
+
}
|
|
14909
|
+
},
|
|
14910
|
+
{
|
|
14911
|
+
...base,
|
|
14912
|
+
id: `${prefix}setSymmetry`,
|
|
14913
|
+
label: "Symmetry Options\u2026",
|
|
14914
|
+
keywords: ["symmetry", "mirror", "radial", "axis", "copies"],
|
|
14915
|
+
description: "Changes symmetry groups (`ids`, default the selected one or the one holding the selection): mode, center, rotation, copies, mirror. Pass at least one. One undo step. Returns { groups }.",
|
|
14916
|
+
params: { type: "object", properties: { ids: { ...IDS, description: "Symmetry group ids (default: the selection)." }, ...SYMMETRY_PROPS }, additionalProperties: false },
|
|
14917
|
+
undoable: true,
|
|
14918
|
+
run: (_ctx, params) => {
|
|
14919
|
+
const { ids: list2, ...patch } = params ?? {};
|
|
14920
|
+
if (!Object.keys(patch).length) fail("Pass at least one of mode, center, rotation, copies, mirror.");
|
|
14921
|
+
check(controller.setSymmetry(patch, list2 ? { ids: list2 } : {}));
|
|
14922
|
+
const ids2 = list2 ?? controller.getState().selection.filter((id) => controller.getNode(id)?.type === "group");
|
|
14923
|
+
return { groups: ids2.filter((id) => controller.getNode(id)?.symmetry).map(symmetryResult) };
|
|
14924
|
+
}
|
|
14925
|
+
},
|
|
14926
|
+
{
|
|
14927
|
+
...base,
|
|
14928
|
+
id: `${prefix}releaseSymmetry`,
|
|
14929
|
+
label: "Release Symmetry",
|
|
14930
|
+
keywords: ["symmetry", "release", "expand", "bake"],
|
|
14931
|
+
description: "Stops symmetry groups repeating their children. With expand: true the copies become real, editable objects in the group (new ids); otherwise they go. One undo step. Returns { ids } of the objects expanding added.",
|
|
14932
|
+
params: { type: "object", properties: { ids: { ...IDS, description: "Symmetry group ids (default: the selection)." }, expand: { type: "boolean", description: "Keep the copies as real objects (default false)." } }, additionalProperties: false },
|
|
14933
|
+
undoable: true,
|
|
14934
|
+
enabled: enabledFor,
|
|
14935
|
+
run: (_ctx, params) => {
|
|
14936
|
+
const r = controller.releaseSymmetry({ ...params?.ids ? { ids: params.ids } : {}, ...params?.expand ? { expand: true } : {} });
|
|
14937
|
+
check(r);
|
|
14938
|
+
return { ids: r.ids ?? [] };
|
|
14939
|
+
}
|
|
14940
|
+
},
|
|
14941
|
+
{
|
|
14942
|
+
...base,
|
|
14943
|
+
id: `${prefix}enterSymmetry`,
|
|
14944
|
+
label: "Draw Inside Symmetry",
|
|
14945
|
+
keywords: ["symmetry", "draw inside", "mirror drawing"],
|
|
14946
|
+
description: "Hands the canvas to the user drawing inside a top-level symmetry group: new pen, pencil, shape and text art goes into it, mirrored as it is drawn, until vector.exitSymmetry or Escape. Returns { id }.",
|
|
14947
|
+
params: { type: "object", properties: { id: { type: "string", description: "The symmetry group." } }, required: ["id"], additionalProperties: false },
|
|
14948
|
+
run: (_ctx, params) => {
|
|
14949
|
+
check(controller.enterSymmetry(params.id));
|
|
14950
|
+
return { id: params.id };
|
|
14951
|
+
}
|
|
14952
|
+
},
|
|
14953
|
+
{
|
|
14954
|
+
...base,
|
|
14955
|
+
id: `${prefix}exitSymmetry`,
|
|
14956
|
+
label: "Stop Drawing Inside Symmetry",
|
|
14957
|
+
keywords: ["symmetry", "exit"],
|
|
14958
|
+
description: "New art goes on top of the document again (no symmetry group is entered). Returns { ok: true }.",
|
|
14959
|
+
params: { type: "object", properties: {}, additionalProperties: false },
|
|
14960
|
+
run: () => {
|
|
14961
|
+
controller.exitSymmetry();
|
|
14962
|
+
return { ok: true };
|
|
14963
|
+
}
|
|
14964
|
+
}
|
|
14965
|
+
];
|
|
14676
14966
|
const pdfCommands = () => [
|
|
14677
14967
|
{
|
|
14678
14968
|
...base,
|
|
@@ -15431,12 +15721,16 @@ function createVectorCommands(controller, options = {}) {
|
|
|
15431
15721
|
points: { type: "integer", minimum: 3 },
|
|
15432
15722
|
innerRatio: { type: "number", minimum: 0, maximum: 1 },
|
|
15433
15723
|
name: { type: "string" },
|
|
15724
|
+
parentId: PARENT_ID,
|
|
15434
15725
|
...STYLE_PROPS
|
|
15435
15726
|
},
|
|
15436
15727
|
required: ["kind", "x", "y", "width", "height"]
|
|
15437
15728
|
},
|
|
15438
15729
|
undoable: true,
|
|
15439
|
-
run: (_ctx, params) =>
|
|
15730
|
+
run: (_ctx, params) => {
|
|
15731
|
+
const { parentId, ...input } = params;
|
|
15732
|
+
return { id: controller.addShape(input, parentOf(parentId)) };
|
|
15733
|
+
}
|
|
15440
15734
|
},
|
|
15441
15735
|
{
|
|
15442
15736
|
...base,
|
|
@@ -15444,10 +15738,10 @@ function createVectorCommands(controller, options = {}) {
|
|
|
15444
15738
|
label: "Add Path\u2026",
|
|
15445
15739
|
keywords: ["svg", "path data", "bezier", "curve"],
|
|
15446
15740
|
description: "Adds a path from SVG path data (`d`), in document units.",
|
|
15447
|
-
params: { type: "object", properties: { d: { type: "string" }, name: { type: "string" }, ...STYLE_PROPS }, required: ["d"] },
|
|
15741
|
+
params: { type: "object", properties: { d: { type: "string" }, name: { type: "string" }, parentId: PARENT_ID, ...STYLE_PROPS }, required: ["d"] },
|
|
15448
15742
|
undoable: true,
|
|
15449
15743
|
run: (_ctx, params) => {
|
|
15450
|
-
const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
|
|
15744
|
+
const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} }, parentOf(params.parentId));
|
|
15451
15745
|
const node = controller.getNode(id);
|
|
15452
15746
|
if (node?.type === "path" && !node.subpaths.length) {
|
|
15453
15747
|
controller.undo();
|
|
@@ -15504,6 +15798,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
15504
15798
|
...traceCommands(),
|
|
15505
15799
|
...lottieCommands(),
|
|
15506
15800
|
...pdfCommands(),
|
|
15801
|
+
...symmetryCommands(),
|
|
15507
15802
|
{
|
|
15508
15803
|
...base,
|
|
15509
15804
|
id: `${prefix}setCorners`,
|
|
@@ -16495,6 +16790,8 @@ export {
|
|
|
16495
16790
|
VECTOR_STROKE_SETTING_KEYS,
|
|
16496
16791
|
VECTOR_STYLE_KEYS,
|
|
16497
16792
|
VECTOR_SYMBOL_MAX_DEPTH,
|
|
16793
|
+
VECTOR_SYMMETRY_MODES,
|
|
16794
|
+
VECTOR_SYMMETRY_MODE_LABELS,
|
|
16498
16795
|
VECTOR_TEXT_ALIGNS,
|
|
16499
16796
|
VECTOR_TEXT_ALIGN_LABELS,
|
|
16500
16797
|
VECTOR_TEXT_ALIGN_OPTIONS,
|
|
@@ -16680,6 +16977,7 @@ export {
|
|
|
16680
16977
|
isVectorMaskType,
|
|
16681
16978
|
isVectorMeshGradient,
|
|
16682
16979
|
isVectorShadedPaint,
|
|
16980
|
+
isVectorSymmetryGroup,
|
|
16683
16981
|
isVectorTextTarget,
|
|
16684
16982
|
joinSubpaths,
|
|
16685
16983
|
knifePath,
|
|
@@ -16788,6 +17086,7 @@ export {
|
|
|
16788
17086
|
sanitizeVectorShadedPaint,
|
|
16789
17087
|
sanitizeVectorStrokeSettings,
|
|
16790
17088
|
sanitizeVectorStrokes,
|
|
17089
|
+
sanitizeVectorSymmetry,
|
|
16791
17090
|
sanitizeVectorTextFields,
|
|
16792
17091
|
sanitizeVectorTextFrame,
|
|
16793
17092
|
sanitizeWidthProfile,
|
|
@@ -16831,6 +17130,7 @@ export {
|
|
|
16831
17130
|
transformVectorNode,
|
|
16832
17131
|
transformVectorPaint,
|
|
16833
17132
|
transformVectorShadedPaint,
|
|
17133
|
+
transformVectorSymmetry,
|
|
16834
17134
|
translateAnchor,
|
|
16835
17135
|
translateMatrix,
|
|
16836
17136
|
trimIntervals,
|
|
@@ -16995,6 +17295,9 @@ export {
|
|
|
16995
17295
|
vectorStrokes,
|
|
16996
17296
|
vectorSymbolUses,
|
|
16997
17297
|
vectorSymbolsUsedIn,
|
|
17298
|
+
vectorSymmetryAxes,
|
|
17299
|
+
vectorSymmetryDrawn,
|
|
17300
|
+
vectorSymmetryMatrices,
|
|
16998
17301
|
vectorTargetPaint,
|
|
16999
17302
|
vectorTextBoxOutline,
|
|
17000
17303
|
vectorTextCaret,
|