@pixerate/editor 0.110.0 → 0.112.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 +3 -0
- package/dist/{chunk-DMRSJBJF.js → chunk-CZOXTEMN.js} +2709 -599
- package/dist/chunk-CZOXTEMN.js.map +1 -0
- package/dist/{chunk-MGL4U5WA.cjs → chunk-YGIMK4NX.cjs} +2697 -587
- package/dist/chunk-YGIMK4NX.cjs.map +1 -0
- package/dist/{export-DRiLzP_j.d.cts → export-DBba-buV.d.cts} +556 -20
- package/dist/{export-CO2ryu3I.d.ts → export-DNu_Jr8p.d.ts} +556 -20
- package/dist/image-editor/index.cjs +1953 -702
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +253 -9
- package/dist/image-editor/index.d.ts +253 -9
- package/dist/image-editor/index.js +1297 -46
- package/dist/image-editor/index.js.map +1 -1
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.d.cts +2 -2
- package/dist/paint-editor/index.d.ts +2 -2
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +27 -30
- package/skills/pixerate-image-editor/references/shapes-and-type.md +60 -0
- package/dist/chunk-DMRSJBJF.js.map +0 -1
- package/dist/chunk-MGL4U5WA.cjs.map +0 -1
|
@@ -17,8 +17,10 @@ import {
|
|
|
17
17
|
D50,
|
|
18
18
|
D65,
|
|
19
19
|
DEFAULT_ADJUSTMENTS_VALUES,
|
|
20
|
+
DEFAULT_AUTO_LEADING,
|
|
20
21
|
DEFAULT_DEPTH_MASK_VALUES,
|
|
21
22
|
DEFAULT_MITER_LIMIT,
|
|
23
|
+
DEFAULT_TEXT_STYLE,
|
|
22
24
|
DEFAULT_TRANSFORM_VALUES,
|
|
23
25
|
DEVELOP_FILTER_ID,
|
|
24
26
|
EMPTY_DISTANCE,
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
MAX_KELVIN,
|
|
36
38
|
MIN_KELVIN,
|
|
37
39
|
MIRED_PER_TEMPERATURE_UNIT,
|
|
40
|
+
PARAGRAPH_STYLE_FIELDS,
|
|
38
41
|
PATTERN_BLOCK_KEYS,
|
|
39
42
|
PSD_BLEND_KEYS,
|
|
40
43
|
PsdError,
|
|
@@ -44,6 +47,9 @@ import {
|
|
|
44
47
|
SHAPE_RENDER_MARGIN,
|
|
45
48
|
SMART_RENDER_MARGIN,
|
|
46
49
|
SUPPORTED_ADJUSTMENT_KEYS,
|
|
50
|
+
TEXT_ALIGNS,
|
|
51
|
+
TEXT_RENDER_MARGIN,
|
|
52
|
+
TEXT_STYLE_FIELDS,
|
|
47
53
|
TINT_SCALE,
|
|
48
54
|
TONE_LUT_SIZE,
|
|
49
55
|
TRANSFORM_ANCHORS,
|
|
@@ -64,6 +70,7 @@ import {
|
|
|
64
70
|
annotationLayerName,
|
|
65
71
|
annotationOfLayer,
|
|
66
72
|
annotationOfShape,
|
|
73
|
+
annotationOfType,
|
|
67
74
|
applyAdjustmentLayers,
|
|
68
75
|
applyAdjustments,
|
|
69
76
|
applyBoxBlur,
|
|
@@ -84,6 +91,7 @@ import {
|
|
|
84
91
|
buildToneLut,
|
|
85
92
|
calculateCropDrag,
|
|
86
93
|
canvasToImagePoint,
|
|
94
|
+
caretAt,
|
|
87
95
|
clampCropToImage,
|
|
88
96
|
colorBalanceChannel,
|
|
89
97
|
combineSelections,
|
|
@@ -100,6 +108,7 @@ import {
|
|
|
100
108
|
createAdjustmentLayer,
|
|
101
109
|
createSelectionMask,
|
|
102
110
|
createShapeLayer,
|
|
111
|
+
createTypeLayer,
|
|
103
112
|
curveLutFromLevels,
|
|
104
113
|
decodeLayerChannel,
|
|
105
114
|
decodeLayerMask,
|
|
@@ -131,8 +140,11 @@ import {
|
|
|
131
140
|
exportPsd,
|
|
132
141
|
featherCoverage,
|
|
133
142
|
filterMaskCoverage,
|
|
143
|
+
fitParagraphRuns,
|
|
144
|
+
fitRuns,
|
|
134
145
|
flattenPath,
|
|
135
146
|
flipCorners,
|
|
147
|
+
fontOf,
|
|
136
148
|
fullSelection,
|
|
137
149
|
geometryHomography,
|
|
138
150
|
getAnnotationBounds,
|
|
@@ -183,6 +195,7 @@ import {
|
|
|
183
195
|
layerTree,
|
|
184
196
|
layerTypeLabel,
|
|
185
197
|
layersFromLegacy,
|
|
198
|
+
layoutText,
|
|
186
199
|
legacyAdjustmentLayer,
|
|
187
200
|
legacyMaskOf,
|
|
188
201
|
legacyView,
|
|
@@ -215,9 +228,11 @@ import {
|
|
|
215
228
|
normalizeShapeStroke,
|
|
216
229
|
normalizeSmartFilter,
|
|
217
230
|
normalizeSubpaths,
|
|
231
|
+
normalizeText,
|
|
218
232
|
offsetLayer,
|
|
219
233
|
offsetShape,
|
|
220
234
|
offsetSmartObject,
|
|
235
|
+
paragraphRanges,
|
|
221
236
|
parseAdjustmentBlock,
|
|
222
237
|
parseBlend,
|
|
223
238
|
parseCssColor,
|
|
@@ -226,6 +241,8 @@ import {
|
|
|
226
241
|
parsePatternBlock,
|
|
227
242
|
parseSelectionMode,
|
|
228
243
|
parseVersionedDescriptor,
|
|
244
|
+
patchParagraphStyle,
|
|
245
|
+
patchTextStyle,
|
|
229
246
|
photoFilterMatrix,
|
|
230
247
|
pixelLayerBounds,
|
|
231
248
|
pixelLayers,
|
|
@@ -256,16 +273,21 @@ import {
|
|
|
256
273
|
renderShape,
|
|
257
274
|
renderShapeRect,
|
|
258
275
|
renderSmartObject,
|
|
276
|
+
renderText,
|
|
259
277
|
replaceContents,
|
|
278
|
+
replaceTextRange,
|
|
260
279
|
requireLayer,
|
|
261
280
|
requireOperation,
|
|
262
281
|
requireShapeLayer,
|
|
263
282
|
requireSmartObject,
|
|
283
|
+
requireTypeLayer,
|
|
264
284
|
requireUnlocked,
|
|
265
285
|
rerender,
|
|
266
286
|
rerenderShape,
|
|
287
|
+
rerenderText,
|
|
267
288
|
resizeSelection,
|
|
268
289
|
resolveExternalAssets,
|
|
290
|
+
restyleRange,
|
|
269
291
|
rgbToGray,
|
|
270
292
|
rgbToHsl,
|
|
271
293
|
runLayerFilters,
|
|
@@ -301,16 +323,28 @@ import {
|
|
|
301
323
|
smartContentsFile,
|
|
302
324
|
smartObjectLayers,
|
|
303
325
|
smoothCoverage,
|
|
326
|
+
spliceRuns,
|
|
304
327
|
srgbToLinear,
|
|
328
|
+
styleAt,
|
|
305
329
|
tempTintToXy,
|
|
306
330
|
temperatureToKelvin,
|
|
331
|
+
textIndexAt,
|
|
332
|
+
textMatrix,
|
|
333
|
+
textOutlines,
|
|
334
|
+
textRange,
|
|
335
|
+
toTextSpace,
|
|
307
336
|
toneCurve,
|
|
308
337
|
toneLuts,
|
|
309
338
|
transformHomography,
|
|
310
339
|
transformMap,
|
|
311
340
|
transformShapeComponents,
|
|
341
|
+
transformTypeLayer,
|
|
312
342
|
transformedBox,
|
|
313
343
|
transformedPoint,
|
|
344
|
+
typeAsPixelLayer,
|
|
345
|
+
typeFrame,
|
|
346
|
+
typeFromAnnotation,
|
|
347
|
+
typeLayerName,
|
|
314
348
|
ungroupLayer,
|
|
315
349
|
unionRect,
|
|
316
350
|
unprojectCanvasPoint,
|
|
@@ -319,6 +353,7 @@ import {
|
|
|
319
353
|
updateLayerMaskProps,
|
|
320
354
|
updateShapeLayer,
|
|
321
355
|
updateSmartFilter,
|
|
356
|
+
updateTypeLayer,
|
|
322
357
|
uvToXy,
|
|
323
358
|
versionedDescriptorBytes,
|
|
324
359
|
vibranceLinear,
|
|
@@ -334,7 +369,7 @@ import {
|
|
|
334
369
|
writePsd,
|
|
335
370
|
xyToTempTint,
|
|
336
371
|
xyToUv
|
|
337
|
-
} from "../chunk-
|
|
372
|
+
} from "../chunk-CZOXTEMN.js";
|
|
338
373
|
import {
|
|
339
374
|
DEFAULT_GLOBAL_LIGHT,
|
|
340
375
|
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
@@ -506,12 +541,15 @@ import {
|
|
|
506
541
|
WARP_STYLES,
|
|
507
542
|
WARP_STYLE_LABELS,
|
|
508
543
|
applyMatrix,
|
|
544
|
+
bezierToSubpaths,
|
|
509
545
|
dragWarpMeshPoint,
|
|
510
546
|
evalWarpMesh,
|
|
511
547
|
identityWarpMesh,
|
|
548
|
+
invertMatrix,
|
|
512
549
|
multiplyMatrices,
|
|
513
550
|
pullWarpMesh,
|
|
514
551
|
resizeWarpMesh,
|
|
552
|
+
rotateMatrix,
|
|
515
553
|
roundSubpaths,
|
|
516
554
|
translateMatrix,
|
|
517
555
|
warpMeshColumns,
|
|
@@ -700,7 +738,7 @@ function needsLegacyBase(input) {
|
|
|
700
738
|
if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;
|
|
701
739
|
return false;
|
|
702
740
|
}
|
|
703
|
-
var isPlainAnnotation = (l) => l.type === "annotation" ? l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask : l.type === "shape" && Boolean(l.annotation) && l.opacity === 1 && l.blend === (l.annotation.blend ?? "normal") && !l.clipped && !l.mask && !l.effects && (l.fillOpacity ?? 1) === 1;
|
|
741
|
+
var isPlainAnnotation = (l) => l.type === "annotation" ? l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask : (l.type === "shape" || l.type === "type") && Boolean(l.annotation) && l.opacity === 1 && l.blend === (l.annotation.blend ?? "normal") && !l.clipped && !l.mask && !l.effects && (l.fillOpacity ?? 1) === 1;
|
|
704
742
|
var overlayViews = /* @__PURE__ */ new WeakMap();
|
|
705
743
|
function overlayView(l) {
|
|
706
744
|
if (l.type === "annotation") return l;
|
|
@@ -716,7 +754,7 @@ function splitOverlayAnnotations(layers) {
|
|
|
716
754
|
let i = layers.length;
|
|
717
755
|
while (i > 0) {
|
|
718
756
|
const l = layers[i - 1];
|
|
719
|
-
if ((l.type === "annotation" || l.type === "shape" && l.annotation) && (!l.visible || isPlainAnnotation(l))) i--;
|
|
757
|
+
if ((l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) && (!l.visible || isPlainAnnotation(l))) i--;
|
|
720
758
|
else break;
|
|
721
759
|
}
|
|
722
760
|
return {
|
|
@@ -1027,6 +1065,45 @@ var ImageEditorRenderer = class {
|
|
|
1027
1065
|
}
|
|
1028
1066
|
ctx.restore();
|
|
1029
1067
|
}
|
|
1068
|
+
if (options.textEdit) {
|
|
1069
|
+
const t = options.textEdit;
|
|
1070
|
+
ctx.save();
|
|
1071
|
+
try {
|
|
1072
|
+
const scr = (p) => projectImagePoint(p, projection);
|
|
1073
|
+
const poly = (pts) => {
|
|
1074
|
+
ctx.beginPath();
|
|
1075
|
+
pts.map(scr).forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
|
|
1076
|
+
ctx.closePath();
|
|
1077
|
+
};
|
|
1078
|
+
if (t.frame) {
|
|
1079
|
+
poly(t.frame);
|
|
1080
|
+
ctx.setLineDash([4, 3]);
|
|
1081
|
+
ctx.lineWidth = 1;
|
|
1082
|
+
ctx.strokeStyle = "rgba(99,102,241,0.9)";
|
|
1083
|
+
ctx.stroke();
|
|
1084
|
+
ctx.setLineDash([]);
|
|
1085
|
+
}
|
|
1086
|
+
ctx.fillStyle = "rgba(99,102,241,0.35)";
|
|
1087
|
+
for (const q of t.selection) {
|
|
1088
|
+
poly(q);
|
|
1089
|
+
ctx.fill();
|
|
1090
|
+
}
|
|
1091
|
+
if (t.caret) {
|
|
1092
|
+
const [a, b] = t.caret.map(scr);
|
|
1093
|
+
ctx.beginPath();
|
|
1094
|
+
ctx.moveTo(a.x, a.y);
|
|
1095
|
+
ctx.lineTo(b.x, b.y);
|
|
1096
|
+
ctx.lineWidth = 3;
|
|
1097
|
+
ctx.strokeStyle = "#ffffff";
|
|
1098
|
+
ctx.stroke();
|
|
1099
|
+
ctx.lineWidth = 1.5;
|
|
1100
|
+
ctx.strokeStyle = "#111827";
|
|
1101
|
+
ctx.stroke();
|
|
1102
|
+
}
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
ctx.restore();
|
|
1106
|
+
}
|
|
1030
1107
|
if (options.freeTransform) {
|
|
1031
1108
|
const t = options.freeTransform;
|
|
1032
1109
|
ctx.save();
|
|
@@ -2441,6 +2518,159 @@ function refineSelection(pixels, mask, params = {}) {
|
|
|
2441
2518
|
return createSelectionMask(mask.width, mask.height, out);
|
|
2442
2519
|
}
|
|
2443
2520
|
|
|
2521
|
+
// src/image-editor/layers/text-shapers.ts
|
|
2522
|
+
var GENERIC = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "math", "emoji", "fangsong"]);
|
|
2523
|
+
function cssFontFamily(family) {
|
|
2524
|
+
const f = family.trim();
|
|
2525
|
+
if (GENERIC.has(f.toLowerCase()) || f.includes(",") || /^["']/.test(f)) return f;
|
|
2526
|
+
return /^[A-Za-z_-][\w -]*$/.test(f) && !/\s{2}/.test(f) ? f : `"${f.replace(/"/g, '\\"')}"`;
|
|
2527
|
+
}
|
|
2528
|
+
function cssFont(font, family = font.family) {
|
|
2529
|
+
return `${font.italic ? "italic " : ""}${font.weight} ${font.size}px ${cssFontFamily(family)}`;
|
|
2530
|
+
}
|
|
2531
|
+
function createCanvasTextShaper(options = {}) {
|
|
2532
|
+
const make = options.createCanvas ?? ((w, h) => {
|
|
2533
|
+
if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(w, h);
|
|
2534
|
+
if (typeof document !== "undefined") {
|
|
2535
|
+
const c = document.createElement("canvas");
|
|
2536
|
+
c.width = w;
|
|
2537
|
+
c.height = h;
|
|
2538
|
+
return c;
|
|
2539
|
+
}
|
|
2540
|
+
return null;
|
|
2541
|
+
});
|
|
2542
|
+
const canvas = make(1, 1);
|
|
2543
|
+
const ctx = canvas?.getContext("2d");
|
|
2544
|
+
if (!ctx || typeof ctx.measureText !== "function") return null;
|
|
2545
|
+
const fallback = options.fallbackFamily ?? "sans-serif";
|
|
2546
|
+
const available = /* @__PURE__ */ new Map();
|
|
2547
|
+
const widths = /* @__PURE__ */ new Map();
|
|
2548
|
+
const metrics = /* @__PURE__ */ new Map();
|
|
2549
|
+
const probe = "mmmmmmmmmmlli1WQ@#";
|
|
2550
|
+
const width = (font, text) => {
|
|
2551
|
+
ctx.font = font;
|
|
2552
|
+
return ctx.measureText(text).width;
|
|
2553
|
+
};
|
|
2554
|
+
const isAvailable = (family) => {
|
|
2555
|
+
const f = family.trim();
|
|
2556
|
+
if (GENERIC.has(f.toLowerCase()) || f.includes(",")) return true;
|
|
2557
|
+
let hit = available.get(f);
|
|
2558
|
+
if (hit === void 0) {
|
|
2559
|
+
const q = cssFontFamily(f);
|
|
2560
|
+
hit = width(`72px ${q}, monospace`, probe) !== width("72px monospace", probe) || width(`72px ${q}, serif`, probe) !== width("72px serif", probe);
|
|
2561
|
+
available.set(f, hit);
|
|
2562
|
+
}
|
|
2563
|
+
return hit;
|
|
2564
|
+
};
|
|
2565
|
+
return {
|
|
2566
|
+
id: "canvas",
|
|
2567
|
+
resolve(font) {
|
|
2568
|
+
const ok = isAvailable(font.family);
|
|
2569
|
+
return { family: ok ? font.family : fallback, available: ok };
|
|
2570
|
+
},
|
|
2571
|
+
metrics(font) {
|
|
2572
|
+
const key = cssFont(font);
|
|
2573
|
+
let m = metrics.get(key);
|
|
2574
|
+
if (!m) {
|
|
2575
|
+
ctx.font = key;
|
|
2576
|
+
ctx.textBaseline = "alphabetic";
|
|
2577
|
+
const t = ctx.measureText("Hg");
|
|
2578
|
+
const ascent = t.emHeightAscent ?? t.fontBoundingBoxAscent ?? font.size * 0.8;
|
|
2579
|
+
const descent = t.emHeightDescent ?? t.fontBoundingBoxDescent ?? font.size * 0.2;
|
|
2580
|
+
m = { ascent: Number.isFinite(ascent) ? ascent : font.size * 0.8, descent: Number.isFinite(descent) ? descent : font.size * 0.2 };
|
|
2581
|
+
metrics.set(key, m);
|
|
2582
|
+
}
|
|
2583
|
+
return m;
|
|
2584
|
+
},
|
|
2585
|
+
measure(text, font) {
|
|
2586
|
+
const key = `${cssFont(font)}|${text}`;
|
|
2587
|
+
let w = widths.get(key);
|
|
2588
|
+
if (!w) {
|
|
2589
|
+
ctx.font = cssFont(font);
|
|
2590
|
+
w = [0];
|
|
2591
|
+
for (let i = 1; i <= text.length; i++) w.push(ctx.measureText(text.slice(0, i)).width);
|
|
2592
|
+
if (widths.size > 2e3) widths.clear();
|
|
2593
|
+
widths.set(key, w);
|
|
2594
|
+
}
|
|
2595
|
+
return w;
|
|
2596
|
+
},
|
|
2597
|
+
draw(target, text, font, x, y) {
|
|
2598
|
+
target.font = cssFont(font);
|
|
2599
|
+
target.textBaseline = "alphabetic";
|
|
2600
|
+
target.textAlign = "left";
|
|
2601
|
+
target.fillText(text, x, y);
|
|
2602
|
+
},
|
|
2603
|
+
families() {
|
|
2604
|
+
const out = /* @__PURE__ */ new Set(["sans-serif", "serif", "monospace"]);
|
|
2605
|
+
const fonts = typeof document !== "undefined" ? document.fonts : void 0;
|
|
2606
|
+
if (fonts && typeof fonts.forEach === "function") fonts.forEach((f) => out.add(f.family.replace(/^["']|["']$/g, "")));
|
|
2607
|
+
return [...out];
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
function createOutlineTextShaper(fonts, options = {}) {
|
|
2612
|
+
if (!fonts.length) throw new Error("createOutlineTextShaper needs at least one font.");
|
|
2613
|
+
const entries = fonts.map((f) => {
|
|
2614
|
+
const e = "face" in f ? f : { face: f };
|
|
2615
|
+
const names = e.face.names;
|
|
2616
|
+
const family = e.family ?? names?.fontFamily?.en ?? Object.values(names?.fontFamily ?? {})[0] ?? "Font";
|
|
2617
|
+
const weight = e.weight ?? e.face.tables?.os2?.usWeightClass ?? 400;
|
|
2618
|
+
const italic = e.italic ?? Boolean((e.face.tables?.os2?.fsSelection ?? 0) & 1);
|
|
2619
|
+
const postScript = names?.postScriptName?.en ?? Object.values(names?.postScriptName ?? {})[0];
|
|
2620
|
+
return { face: e.face, family, key: family.toLowerCase(), weight, italic, postScript };
|
|
2621
|
+
});
|
|
2622
|
+
const fallbackKey = (options.fallback ?? entries[0].family).toLowerCase();
|
|
2623
|
+
const pick2 = (font) => {
|
|
2624
|
+
if (font.postScriptName) {
|
|
2625
|
+
const exact = entries.find((e) => e.postScript === font.postScriptName);
|
|
2626
|
+
if (exact) return { entry: exact, available: true };
|
|
2627
|
+
}
|
|
2628
|
+
const key = font.family.trim().toLowerCase();
|
|
2629
|
+
let pool = entries.filter((e) => e.key === key);
|
|
2630
|
+
const available = pool.length > 0;
|
|
2631
|
+
if (!available) pool = entries.filter((e) => e.key === fallbackKey);
|
|
2632
|
+
if (!pool.length) pool = entries;
|
|
2633
|
+
const styled = pool.filter((e) => e.italic === font.italic);
|
|
2634
|
+
const list = styled.length ? styled : pool;
|
|
2635
|
+
const entry = list.reduce((best, e) => Math.abs(e.weight - font.weight) < Math.abs(best.weight - font.weight) ? e : best);
|
|
2636
|
+
return { entry, available };
|
|
2637
|
+
};
|
|
2638
|
+
const toPath = (cmds) => {
|
|
2639
|
+
const out = [];
|
|
2640
|
+
for (const c of cmds) {
|
|
2641
|
+
if (c.type === "M" || c.type === "L") out.push({ type: c.type, x: c.x, y: c.y });
|
|
2642
|
+
else if (c.type === "C") out.push({ type: "C", x1: c.x1, y1: c.y1, x2: c.x2, y2: c.y2, x: c.x, y: c.y });
|
|
2643
|
+
else if (c.type === "Q") out.push({ type: "Q", x1: c.x1, y1: c.y1, x: c.x, y: c.y });
|
|
2644
|
+
else out.push({ type: "Z" });
|
|
2645
|
+
}
|
|
2646
|
+
return out;
|
|
2647
|
+
};
|
|
2648
|
+
return {
|
|
2649
|
+
id: options.id ?? `outline:${entries.map((e) => `${e.family}/${e.weight}${e.italic ? "i" : ""}`).join(",")}`,
|
|
2650
|
+
resolve(font) {
|
|
2651
|
+
const { entry, available } = pick2(font);
|
|
2652
|
+
return { family: entry.family, available };
|
|
2653
|
+
},
|
|
2654
|
+
metrics(font) {
|
|
2655
|
+
const { face } = pick2(font).entry;
|
|
2656
|
+
const k = font.size / face.unitsPerEm;
|
|
2657
|
+
return { ascent: face.ascender * k, descent: -face.descender * k };
|
|
2658
|
+
},
|
|
2659
|
+
measure(text, font) {
|
|
2660
|
+
const { face } = pick2(font).entry;
|
|
2661
|
+
const out = [0];
|
|
2662
|
+
for (let i = 1; i <= text.length; i++) out.push(face.getAdvanceWidth(text.slice(0, i), font.size, { kerning: true }));
|
|
2663
|
+
return out;
|
|
2664
|
+
},
|
|
2665
|
+
outline(text, font, x, y) {
|
|
2666
|
+
return toPath(pick2(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);
|
|
2667
|
+
},
|
|
2668
|
+
families() {
|
|
2669
|
+
return [...new Set(entries.map((e) => e.family))];
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2444
2674
|
// src/image-editor/free-transform.ts
|
|
2445
2675
|
var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
|
|
2446
2676
|
var fail = (message) => {
|
|
@@ -2512,7 +2742,7 @@ var FreeTransformSession = class {
|
|
|
2512
2742
|
}
|
|
2513
2743
|
const targetId = init.layerId ?? host.activeLayerId();
|
|
2514
2744
|
const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
|
|
2515
|
-
const smartTarget = (target?.type === "smart" || target?.type === "shape") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
|
|
2745
|
+
const smartTarget = (target?.type === "smart" || target?.type === "shape" || target?.type === "type") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
|
|
2516
2746
|
if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
|
|
2517
2747
|
this.content = content;
|
|
2518
2748
|
if (content !== "layer" && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
|
|
@@ -2539,7 +2769,7 @@ var FreeTransformSession = class {
|
|
|
2539
2769
|
const linked = mask?.linked !== false;
|
|
2540
2770
|
const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
|
|
2541
2771
|
if (channel === "mask" && !mask) fail(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
|
|
2542
|
-
if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape") fail(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel, shape and smart object layers, layer masks (channel mask) and selections.`);
|
|
2772
|
+
if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape" && layer.type !== "type") fail(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel, shape, type and smart object layers, layer masks (channel mask) and selections.`);
|
|
2543
2773
|
if (!layer.visible && content !== "selection-copy") fail(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
|
|
2544
2774
|
if (content !== "selection-copy") {
|
|
2545
2775
|
requireUnlocked(layer, "position", "transform it");
|
|
@@ -2561,6 +2791,26 @@ var FreeTransformSession = class {
|
|
|
2561
2791
|
this.shown = this.startLayers;
|
|
2562
2792
|
return;
|
|
2563
2793
|
}
|
|
2794
|
+
if (layer.type === "type" && !maskAlone) {
|
|
2795
|
+
if (content !== "layer") fail(`Layer "${layer.name || id}" is a type layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
|
|
2796
|
+
const frame = typeFrame(layer, host.textEnv().shaper);
|
|
2797
|
+
const m = textMatrix(layer);
|
|
2798
|
+
const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
|
|
2799
|
+
this.typeTarget = layer;
|
|
2800
|
+
this.typeLayer = true;
|
|
2801
|
+
this.box = frame;
|
|
2802
|
+
this.startGeometry = {
|
|
2803
|
+
box: frame,
|
|
2804
|
+
corners: [at(frame.x, frame.y), at(frame.x + frame.width, frame.y), at(frame.x + frame.width, frame.y + frame.height), at(frame.x, frame.y + frame.height)],
|
|
2805
|
+
warp: null
|
|
2806
|
+
};
|
|
2807
|
+
this.geometryValue = this.startGeometry;
|
|
2808
|
+
this.startInterp = this.interp;
|
|
2809
|
+
this.moves = mask && linked ? ["pixels", "mask"] : ["pixels"];
|
|
2810
|
+
if (mask && linked) this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
|
|
2811
|
+
this.shown = this.startLayers;
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2564
2814
|
if (layer.type === "shape" && !maskAlone) {
|
|
2565
2815
|
if (content !== "layer") fail(`Layer "${layer.name || id}" is a shape layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
|
|
2566
2816
|
const g = shapeGeometryBounds(layer);
|
|
@@ -2663,6 +2913,9 @@ var FreeTransformSession = class {
|
|
|
2663
2913
|
/** True when the layer is a shape layer (the transform maps its paths and renders them again). */
|
|
2664
2914
|
shapeLayer = false;
|
|
2665
2915
|
shape = null;
|
|
2916
|
+
/** True when the layer is a type layer (the transform sets its text matrix; affine only). */
|
|
2917
|
+
typeLayer = false;
|
|
2918
|
+
typeTarget = null;
|
|
2666
2919
|
geometryValue;
|
|
2667
2920
|
smart = null;
|
|
2668
2921
|
smartReduced = /* @__PURE__ */ new Map();
|
|
@@ -2711,6 +2964,9 @@ var FreeTransformSession = class {
|
|
|
2711
2964
|
};
|
|
2712
2965
|
if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
|
|
2713
2966
|
if (!transformMap(next)) fail("These corners fold the frame over itself or through the horizon; keep the quad convex.");
|
|
2967
|
+
if (this.typeTarget && (hasWarp(next) || !matrixFromCorners(next.box, next.corners))) {
|
|
2968
|
+
fail("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram, no warp); rasterize the layer (rasterize_layer) for distort, perspective or warp.");
|
|
2969
|
+
}
|
|
2714
2970
|
this.geometryValue = next;
|
|
2715
2971
|
if (preview !== false) this.render(preview);
|
|
2716
2972
|
return next;
|
|
@@ -2718,6 +2974,7 @@ var FreeTransformSession = class {
|
|
|
2718
2974
|
/** Resamples every part for the current geometry and shows it (or returns it, for commit). */
|
|
2719
2975
|
resample(final, preview = {}) {
|
|
2720
2976
|
if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };
|
|
2977
|
+
if (this.typeTarget) return { layers: this.resampleType(), selection: this.startSelection };
|
|
2721
2978
|
const g = this.geometryValue;
|
|
2722
2979
|
const identity = isIdentityTransform(g);
|
|
2723
2980
|
const warp = hasWarp(g);
|
|
@@ -2798,6 +3055,27 @@ var FreeTransformSession = class {
|
|
|
2798
3055
|
const surface = renderShape(moved, this.host.shapeEnv(), final ? {} : { clip: preview.clip });
|
|
2799
3056
|
return mapLayer(this.startLayers, layer.id, () => ({ ...moved, surface }));
|
|
2800
3057
|
}
|
|
3058
|
+
/**
|
|
3059
|
+
* A type layer with its text matrix set from the corners (affine only: scale, rotate, skew and
|
|
3060
|
+
* flips), drawn again; a linked mask follows by the change's homography.
|
|
3061
|
+
*/
|
|
3062
|
+
resampleType() {
|
|
3063
|
+
const layer = this.typeTarget;
|
|
3064
|
+
const g = this.geometryValue;
|
|
3065
|
+
if (hasWarp(g)) fail("Type layers cannot be warped here; rasterize the layer first (rasterize_layer), or transform it with scale, rotate and skew.");
|
|
3066
|
+
const m = matrixFromCorners(g.box, g.corners);
|
|
3067
|
+
if (!m) fail("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram); rasterize the layer for distort or perspective.");
|
|
3068
|
+
const next = { ...layer, transform: m };
|
|
3069
|
+
delete next.annotation;
|
|
3070
|
+
let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));
|
|
3071
|
+
const change = multiplyHomographies(homographyFromAffine(invertMatrix(textMatrix(layer))), homographyFromAffine(m));
|
|
3072
|
+
for (const part of this.parts) {
|
|
3073
|
+
if (part.what !== "mask") continue;
|
|
3074
|
+
const surface = transformSurface(part.moving, change, { interpolation: this.interp === "nearest" ? "nearest" : "bilinear" });
|
|
3075
|
+
layers = mapLayer(layers, layer.id, (l) => ({ ...l, mask: { ...l.mask, surface } }));
|
|
3076
|
+
}
|
|
3077
|
+
return layers;
|
|
3078
|
+
}
|
|
2801
3079
|
/** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */
|
|
2802
3080
|
resampleSmart(final, preview) {
|
|
2803
3081
|
const layer = this.smart;
|
|
@@ -2821,7 +3099,7 @@ var FreeTransformSession = class {
|
|
|
2821
3099
|
/** Whether the geometry (or the interpolation of a smart object) differs from where it started. */
|
|
2822
3100
|
isChanged() {
|
|
2823
3101
|
const g = this.geometryValue;
|
|
2824
|
-
if (!this.smart) return !isIdentityTransform(g);
|
|
3102
|
+
if (!this.smart && !this.typeTarget) return !isIdentityTransform(g);
|
|
2825
3103
|
const a = this.startGeometry;
|
|
2826
3104
|
const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);
|
|
2827
3105
|
const warp = (w, box) => w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : "";
|
|
@@ -2858,7 +3136,7 @@ var FreeTransformSession = class {
|
|
|
2858
3136
|
const id = this.copyId ?? this.layerId;
|
|
2859
3137
|
const layer = id ? findLayer(layers, id)?.layer : void 0;
|
|
2860
3138
|
if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
|
|
2861
|
-
else if (this.moves.includes("pixels") && (layer?.type === "smart" || layer?.type === "shape")) bounds = contentBounds(layer.surface);
|
|
3139
|
+
else if (this.moves.includes("pixels") && (layer?.type === "smart" || layer?.type === "shape" || layer?.type === "type")) bounds = contentBounds(layer.surface);
|
|
2862
3140
|
else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
|
|
2863
3141
|
else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
|
|
2864
3142
|
return {
|
|
@@ -3112,6 +3390,7 @@ var ImageEditorController = class {
|
|
|
3112
3390
|
compositor = new LayerCompositor();
|
|
3113
3391
|
constructor(options = {}) {
|
|
3114
3392
|
this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
|
|
3393
|
+
if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
|
|
3115
3394
|
this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
|
|
3116
3395
|
this.onStateChange = options.onStateChange;
|
|
3117
3396
|
this.renderer = new ImageEditorRenderer();
|
|
@@ -3818,6 +4097,21 @@ var ImageEditorController = class {
|
|
|
3818
4097
|
const b = shapeGeometryBounds(moved) ?? { x: 0, y: 0 };
|
|
3819
4098
|
return { offset: { x: Math.round(b.x), y: Math.round(b.y) }, bounds: this.getLayerBounds(id) };
|
|
3820
4099
|
}
|
|
4100
|
+
if (layer.type === "type") {
|
|
4101
|
+
const m = layer.transform ?? [1, 0, 0, 1, 0, 0];
|
|
4102
|
+
const dx = move.x !== void 0 ? move.x - m[4] : move.dx ?? 0;
|
|
4103
|
+
const dy = move.y !== void 0 ? move.y - m[5] : move.dy ?? 0;
|
|
4104
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError("x and y must be numbers.");
|
|
4105
|
+
requireUnlocked(layer, "position", "move it");
|
|
4106
|
+
const mx = Math.round(dx);
|
|
4107
|
+
const my = Math.round(dy);
|
|
4108
|
+
let moved = { ...transformTypeLayer(layer, [1, 0, 0, 1, mx, my]), surface: translateSurface(layer.surface, mx, my) };
|
|
4109
|
+
const mask = layer.mask;
|
|
4110
|
+
if (mask && mask.linked !== false && !mask.source && (mx || my)) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, mx, my) } };
|
|
4111
|
+
this.commitLayers(mapLayer(this.state.layers, id, () => moved), void 0, commitToHistory);
|
|
4112
|
+
const t = moved.transform ?? [1, 0, 0, 1, 0, 0];
|
|
4113
|
+
return { offset: { x: Math.round(t[4]), y: Math.round(t[5]) }, bounds: this.getLayerBounds(id) };
|
|
4114
|
+
}
|
|
3821
4115
|
const current = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
|
|
3822
4116
|
const x = move.x ?? current.x + (move.dx ?? 0);
|
|
3823
4117
|
const y = move.y ?? current.y + (move.dy ?? 0);
|
|
@@ -3966,6 +4260,7 @@ var ImageEditorController = class {
|
|
|
3966
4260
|
canvas: () => this.naturalSize,
|
|
3967
4261
|
pixelsOf: (layer) => this.surfaceOf(layer),
|
|
3968
4262
|
shapeEnv: () => this.shapeEnv(),
|
|
4263
|
+
textEnv: () => this.textEnv(),
|
|
3969
4264
|
capture: () => this.captureHistoryEntry(),
|
|
3970
4265
|
show: (layers, selection, active) => {
|
|
3971
4266
|
this.state.layers = layers;
|
|
@@ -4066,7 +4361,7 @@ var ImageEditorController = class {
|
|
|
4066
4361
|
if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
|
|
4067
4362
|
const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
|
|
4068
4363
|
try {
|
|
4069
|
-
const b = session.smartObject ? transformedBox(session.startGeometry) ?? session.box : session.box;
|
|
4364
|
+
const b = session.smartObject || session.typeLayer ? transformedBox(session.startGeometry) ?? session.box : session.box;
|
|
4070
4365
|
const cx = b.x + b.width / 2;
|
|
4071
4366
|
const cy = b.y + b.height / 2;
|
|
4072
4367
|
const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
|
|
@@ -4786,6 +5081,7 @@ var ImageEditorController = class {
|
|
|
4786
5081
|
} else {
|
|
4787
5082
|
if (layer.type === "smart") throw new LayerOperationError(`Layer "${layer.name || id}" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);
|
|
4788
5083
|
if (layer.type === "shape") throw new LayerOperationError(`Layer "${layer.name || id}" is a shape layer; its pixels are drawn from its paths. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);
|
|
5084
|
+
if (layer.type === "type") throw new LayerOperationError(`Layer "${layer.name || id}" is a type layer; its pixels are drawn from its text. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);
|
|
4789
5085
|
if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);
|
|
4790
5086
|
requireUnlocked(layer, "pixels", action);
|
|
4791
5087
|
}
|
|
@@ -5491,7 +5787,11 @@ var ImageEditorController = class {
|
|
|
5491
5787
|
// --- Annotations & Layers ---
|
|
5492
5788
|
/** The layer showing annotation `id`: an annotation layer, or a shape layer migrated from it. */
|
|
5493
5789
|
annotationLayer(id) {
|
|
5494
|
-
return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape") && annotationOfLayer(l)?.id === id);
|
|
5790
|
+
return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape" || l.type === "type") && annotationOfLayer(l)?.id === id);
|
|
5791
|
+
}
|
|
5792
|
+
/** What migrating an annotation needs: the canvas size, patterns and the text shaper. */
|
|
5793
|
+
annotationEnv() {
|
|
5794
|
+
return { ...this.shapeEnv(), ...this.textEnv() };
|
|
5495
5795
|
}
|
|
5496
5796
|
/**
|
|
5497
5797
|
* Adds an annotation as its own layer (top of the stack unless `aboveId` is given). Pen,
|
|
@@ -5503,7 +5803,7 @@ var ImageEditorController = class {
|
|
|
5503
5803
|
this.recordHistory();
|
|
5504
5804
|
const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
5505
5805
|
const fullAnnotation = { ...annotation, id };
|
|
5506
|
-
const layer = annotationLayerFor(fullAnnotation, this.
|
|
5806
|
+
const layer = annotationLayerFor(fullAnnotation, this.annotationEnv());
|
|
5507
5807
|
const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;
|
|
5508
5808
|
this.state.layers = loc ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1) : insertLayer(this.state.layers, layer, null);
|
|
5509
5809
|
this.state.selectedAnnotationId = id;
|
|
@@ -5523,24 +5823,14 @@ var ImageEditorController = class {
|
|
|
5523
5823
|
}
|
|
5524
5824
|
this.notify();
|
|
5525
5825
|
}
|
|
5526
|
-
/** `layer` (an annotation or
|
|
5826
|
+
/** `layer` (an annotation, or a shape or type layer migrated from one) showing `annotation` instead, keeping its other properties. */
|
|
5527
5827
|
layerForAnnotation(layer, annotation, name) {
|
|
5528
|
-
const
|
|
5529
|
-
const {
|
|
5530
|
-
const
|
|
5531
|
-
if (
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
const fresh = rerenderShape({ ...base, components: shape.components, fill: shape.fill, stroke: shape.stroke }, this.shapeEnv());
|
|
5535
|
-
delete fresh.fillRule;
|
|
5536
|
-
return { ...fresh, annotation };
|
|
5537
|
-
}
|
|
5538
|
-
const created = createShapeLayer({ ...shape, name, blend: base.blend }, this.shapeEnv(), layer.id);
|
|
5539
|
-
const { type: _t2, ...rest2 } = base;
|
|
5540
|
-
return { ...rest2, ...created, annotation };
|
|
5541
|
-
}
|
|
5542
|
-
const { type: _t, components: _c, fill: _f, stroke: _s, surface: _sf, offset: _o, fillRule: _r, fillOpacity: _fo, psd: _p, ...rest } = props;
|
|
5543
|
-
return { ...rest, type: "annotation", name, blend: layer.type === "shape" ? "normal" : layer.blend, annotation };
|
|
5828
|
+
const fresh = annotationLayerFor(annotation, this.annotationEnv());
|
|
5829
|
+
const keep = { id: layer.id, visible: layer.visible, opacity: layer.opacity };
|
|
5830
|
+
for (const k of ["clipped", "mask", "locks", "effects"]) if (layer[k] !== void 0) keep[k] = layer[k];
|
|
5831
|
+
if (fresh.type !== "annotation" && layer.type !== "annotation" && layer.fillOpacity !== void 0) keep.fillOpacity = layer.fillOpacity;
|
|
5832
|
+
const blend = fresh.type === "annotation" ? layer.type === "annotation" ? layer.blend : "normal" : layer.type === "annotation" && layer.blend !== "normal" ? layer.blend : fresh.blend;
|
|
5833
|
+
return { ...fresh, ...keep, name, blend };
|
|
5544
5834
|
}
|
|
5545
5835
|
removeAnnotation(id) {
|
|
5546
5836
|
this.recordHistory();
|
|
@@ -5555,7 +5845,7 @@ var ImageEditorController = class {
|
|
|
5555
5845
|
clearAnnotations() {
|
|
5556
5846
|
this.recordHistory();
|
|
5557
5847
|
let layers = this.state.layers;
|
|
5558
|
-
for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
|
|
5848
|
+
for (const l of flattenLayers(layers)) if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) layers = mapLayer(layers, l.id, () => null);
|
|
5559
5849
|
this.state.layers = layers;
|
|
5560
5850
|
this.state.selectedAnnotationId = null;
|
|
5561
5851
|
this.notify();
|
|
@@ -5794,6 +6084,233 @@ var ImageEditorController = class {
|
|
|
5794
6084
|
this.commitLayers(setLayerMask(this.state.layers, target.id, { surface, defaultValue: 0 }));
|
|
5795
6085
|
return target.id;
|
|
5796
6086
|
}
|
|
6087
|
+
// --- Type layers ---
|
|
6088
|
+
textShaperValue;
|
|
6089
|
+
/** The shaper that lays out and draws type layers (`null` when there is none here). */
|
|
6090
|
+
getTextShaper() {
|
|
6091
|
+
if (this.textShaperValue === void 0) this.textShaperValue = createCanvasTextShaper({ createCanvas: (w, h) => this.renderer.createCanvas(w, h) });
|
|
6092
|
+
return this.textShaperValue;
|
|
6093
|
+
}
|
|
6094
|
+
/** Sets the text shaper (fonts) type layers lay out and draw with; existing layers redraw on their next edit. */
|
|
6095
|
+
setTextShaper(shaper) {
|
|
6096
|
+
this.textShaperValue = shaper;
|
|
6097
|
+
this.notify();
|
|
6098
|
+
}
|
|
6099
|
+
/** What type layers lay out and draw with. */
|
|
6100
|
+
textEnv() {
|
|
6101
|
+
return { ...this.naturalSize, shaper: this.getTextShaper(), createCanvas: (w, h) => this.renderer.createCanvas(w, h) };
|
|
6102
|
+
}
|
|
6103
|
+
/** Font families the shaper offers (generic families at least). */
|
|
6104
|
+
listFonts() {
|
|
6105
|
+
return this.getTextShaper()?.families?.() ?? ["sans-serif", "serif", "monospace"];
|
|
6106
|
+
}
|
|
6107
|
+
typeInfo(id) {
|
|
6108
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6109
|
+
const ranges = (runs) => {
|
|
6110
|
+
let at = 0;
|
|
6111
|
+
return runs.map((r) => {
|
|
6112
|
+
const out = { start: at, end: at + r.length, style: JSON.parse(JSON.stringify(r.style)) };
|
|
6113
|
+
at += r.length;
|
|
6114
|
+
return out;
|
|
6115
|
+
});
|
|
6116
|
+
};
|
|
6117
|
+
const shaper = this.getTextShaper();
|
|
6118
|
+
let lines = [];
|
|
6119
|
+
let missing = layer.missingFonts ?? [];
|
|
6120
|
+
if (shaper) {
|
|
6121
|
+
const layout = layoutText(layer, shaper);
|
|
6122
|
+
const r = (n) => Math.round(n * 1e3) / 1e3;
|
|
6123
|
+
lines = layout.lines.map((l) => ({ start: l.start, end: l.end, baseline: r(l.baseline), width: r(l.width) }));
|
|
6124
|
+
if (layer.rendering === "stored") missing = layout.missingFonts;
|
|
6125
|
+
}
|
|
6126
|
+
const info = {
|
|
6127
|
+
id: layer.id,
|
|
6128
|
+
name: layer.name,
|
|
6129
|
+
text: layer.text,
|
|
6130
|
+
runs: ranges(layer.runs),
|
|
6131
|
+
paragraphs: ranges(layer.paragraphs),
|
|
6132
|
+
frame: { ...layer.frame },
|
|
6133
|
+
transform: [...layer.transform ?? [1, 0, 0, 1, 0, 0]],
|
|
6134
|
+
lines,
|
|
6135
|
+
bounds: this.getLayerBounds(id),
|
|
6136
|
+
missingFonts: [...missing],
|
|
6137
|
+
rendering: layer.rendering === "stored" ? "stored" : "live"
|
|
6138
|
+
};
|
|
6139
|
+
if (layer.annotation) info.annotationId = layer.annotation.id;
|
|
6140
|
+
return info;
|
|
6141
|
+
}
|
|
6142
|
+
/** A type layer's text, runs, paragraphs, frame, lines and bounds. */
|
|
6143
|
+
getTextLayer(id) {
|
|
6144
|
+
return this.typeInfo(id);
|
|
6145
|
+
}
|
|
6146
|
+
/**
|
|
6147
|
+
* Adds a type layer (one undo step) and makes it active: point text at `x`, `y`, or paragraph
|
|
6148
|
+
* text wrapped in `box`. Refused without a text shaper. Returns its id.
|
|
6149
|
+
*/
|
|
6150
|
+
addTextLayer(init) {
|
|
6151
|
+
const text = normalizeText(init.text);
|
|
6152
|
+
if (!Number.isFinite(init.x) || !Number.isFinite(init.y)) throw new LayerOperationError("x and y must be numbers (document pixels).");
|
|
6153
|
+
const style = patchTextStyle({ ...DEFAULT_TEXT_STYLE, color: this.state.paint.foreground }, init.style ?? {});
|
|
6154
|
+
const paragraph = patchParagraphStyle({}, init.paragraph ?? {});
|
|
6155
|
+
let frame = { kind: "point" };
|
|
6156
|
+
if (init.box) {
|
|
6157
|
+
const { width, height } = init.box;
|
|
6158
|
+
if (!(Number.isFinite(width) && width >= 1 && Number.isFinite(height) && height >= 1)) throw new LayerOperationError("box.width and box.height must be at least 1 pixel.");
|
|
6159
|
+
frame = { kind: "box", width, height };
|
|
6160
|
+
}
|
|
6161
|
+
if (init.rotation !== void 0 && !Number.isFinite(init.rotation)) throw new LayerOperationError("rotation must be a number of degrees.");
|
|
6162
|
+
if (init.opacity !== void 0 && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError("opacity must be between 0 and 1.");
|
|
6163
|
+
if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
|
|
6164
|
+
const t = [1, 0, 0, 1, init.x, init.y];
|
|
6165
|
+
const transform = init.rotation ? multiplyMatrices(t, rotateMatrix(init.rotation, { x: init.x, y: init.y })) : t;
|
|
6166
|
+
const layer = createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());
|
|
6167
|
+
if (init.opacity !== void 0) layer.opacity = init.opacity;
|
|
6168
|
+
if (init.blend !== void 0) layer.blend = init.blend;
|
|
6169
|
+
const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
|
|
6170
|
+
this.commitLayers(next, () => {
|
|
6171
|
+
this.state.activeLayerId = layer.id;
|
|
6172
|
+
this.state.activeTarget = "pixels";
|
|
6173
|
+
});
|
|
6174
|
+
return layer.id;
|
|
6175
|
+
}
|
|
6176
|
+
/**
|
|
6177
|
+
* Replaces text [start, end) (default all of it) with `text`; inserted text takes the style at
|
|
6178
|
+
* `start`. One undo step unless `commitToHistory` is false (typing). Returns the layer.
|
|
6179
|
+
*/
|
|
6180
|
+
setText(id, text, range = {}, commitToHistory = true) {
|
|
6181
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6182
|
+
const insert = normalizeText(text);
|
|
6183
|
+
const [start, end] = textRange(layer.text, range.start, range.end);
|
|
6184
|
+
const { layers } = updateTypeLayer(this.state.layers, id, (l) => replaceTextRange(l, start, end, insert), this.textEnv(), "edit its text");
|
|
6185
|
+
this.commitLayers(layers, void 0, commitToHistory);
|
|
6186
|
+
return this.typeInfo(id);
|
|
6187
|
+
}
|
|
6188
|
+
/** Sets character style fields over [start, end) (default all), one undo step. Returns the layer. */
|
|
6189
|
+
setTextStyle(id, style, range = {}, commitToHistory = true) {
|
|
6190
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6191
|
+
const [start, end] = textRange(layer.text, range.start, range.end);
|
|
6192
|
+
if (!Object.keys(style).length) throw new LayerOperationError("Give at least one style field to set.");
|
|
6193
|
+
patchTextStyle(DEFAULT_TEXT_STYLE, style);
|
|
6194
|
+
const { layers } = updateTypeLayer(
|
|
6195
|
+
this.state.layers,
|
|
6196
|
+
id,
|
|
6197
|
+
(l) => {
|
|
6198
|
+
if (start === end) {
|
|
6199
|
+
if (l.text.length) throw new LayerOperationError("The range is empty; give start < end (or leave both out for the whole text).");
|
|
6200
|
+
return { runs: [{ length: 0, style: patchTextStyle(styleAt(l.runs, 0, DEFAULT_TEXT_STYLE), style) }] };
|
|
6201
|
+
}
|
|
6202
|
+
return { runs: restyleRange(l.runs, start, end, (s) => patchTextStyle(s, style)) };
|
|
6203
|
+
},
|
|
6204
|
+
this.textEnv(),
|
|
6205
|
+
"restyle its text"
|
|
6206
|
+
);
|
|
6207
|
+
this.commitLayers(layers, void 0, commitToHistory);
|
|
6208
|
+
return this.typeInfo(id);
|
|
6209
|
+
}
|
|
6210
|
+
/** Sets paragraph style fields on every paragraph that [start, end) touches (default all), one undo step. */
|
|
6211
|
+
setParagraphStyle(id, style, range = {}, commitToHistory = true) {
|
|
6212
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6213
|
+
const [start, end] = textRange(layer.text, range.start, range.end);
|
|
6214
|
+
if (!Object.keys(style).length) throw new LayerOperationError("Give at least one paragraph field to set.");
|
|
6215
|
+
patchParagraphStyle({}, style);
|
|
6216
|
+
const { layers } = updateTypeLayer(
|
|
6217
|
+
this.state.layers,
|
|
6218
|
+
id,
|
|
6219
|
+
(l) => {
|
|
6220
|
+
let at = 0;
|
|
6221
|
+
const paragraphs = l.paragraphs.map((p) => {
|
|
6222
|
+
const a = at;
|
|
6223
|
+
at += p.length;
|
|
6224
|
+
const touches = a < end && at > start || start === end && start >= a && (start < at || at === l.text.length);
|
|
6225
|
+
return touches || !l.text.length ? { length: p.length, style: patchParagraphStyle(p.style, style) } : p;
|
|
6226
|
+
});
|
|
6227
|
+
return { paragraphs };
|
|
6228
|
+
},
|
|
6229
|
+
this.textEnv(),
|
|
6230
|
+
"restyle its paragraphs"
|
|
6231
|
+
);
|
|
6232
|
+
this.commitLayers(layers, void 0, commitToHistory);
|
|
6233
|
+
return this.typeInfo(id);
|
|
6234
|
+
}
|
|
6235
|
+
/**
|
|
6236
|
+
* Point text ↔ paragraph text: `{ kind: 'box', width, height }` wraps the text in a box (its
|
|
6237
|
+
* top-left where the first line's top-left is now), `{ kind: 'point' }` sets it on one line per
|
|
6238
|
+
* paragraph. Also resizes a box. One undo step.
|
|
6239
|
+
*/
|
|
6240
|
+
setTextFrame(id, frame, commitToHistory = true) {
|
|
6241
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6242
|
+
if (frame.kind === "box" && !(Number.isFinite(frame.width) && frame.width >= 1 && Number.isFinite(frame.height) && frame.height >= 1)) throw new LayerOperationError("width and height must be at least 1 pixel.");
|
|
6243
|
+
if (frame.kind !== "box" && frame.kind !== "point") throw new LayerOperationError("kind must be point or box.");
|
|
6244
|
+
const shaper = this.getTextShaper();
|
|
6245
|
+
const { layers } = updateTypeLayer(
|
|
6246
|
+
this.state.layers,
|
|
6247
|
+
id,
|
|
6248
|
+
(l) => {
|
|
6249
|
+
if (frame.kind === "box" && l.frame.kind === "box") return { frame: { kind: "box", width: frame.width, height: frame.height } };
|
|
6250
|
+
if (!shaper) throw new LayerOperationError("Changing between point and paragraph text needs a text shaper to measure the text.");
|
|
6251
|
+
const lines = layoutText(l, shaper).lines;
|
|
6252
|
+
const first = lines[0];
|
|
6253
|
+
const dx = frame.kind === "box" ? first ? first.x : 0 : 0;
|
|
6254
|
+
const dy = frame.kind === "box" ? first ? first.baseline - first.ascent : 0 : first ? first.baseline : 0;
|
|
6255
|
+
const m = textMatrix(l);
|
|
6256
|
+
const shifted = [m[0], m[1], m[2], m[3], m[0] * dx + m[2] * dy + m[4], m[1] * dx + m[3] * dy + m[5]];
|
|
6257
|
+
const nextFrame = frame.kind === "box" ? { kind: "box", width: frame.width, height: frame.height } : { kind: "point" };
|
|
6258
|
+
return { frame: nextFrame, transform: shifted };
|
|
6259
|
+
},
|
|
6260
|
+
this.textEnv(),
|
|
6261
|
+
"change its frame"
|
|
6262
|
+
);
|
|
6263
|
+
this.commitLayers(layers, void 0, commitToHistory);
|
|
6264
|
+
return this.typeInfo(id);
|
|
6265
|
+
}
|
|
6266
|
+
/**
|
|
6267
|
+
* Convert to Shape: the text's glyph outlines become a shape layer with the same id, name and
|
|
6268
|
+
* layer properties (needs a shaper that gives outlines). The fill is the first run's colour;
|
|
6269
|
+
* `warnings` says when other colours were dropped. One undo step.
|
|
6270
|
+
*/
|
|
6271
|
+
convertTextToShape(id) {
|
|
6272
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6273
|
+
requireUnlocked(layer, "all", "convert it");
|
|
6274
|
+
const shaper = this.getTextShaper();
|
|
6275
|
+
if (!shaper) throw new LayerOperationError("Convert to Shape needs a text shaper that gives glyph outlines; none is set here.");
|
|
6276
|
+
const parts = textOutlines(layer, shaper);
|
|
6277
|
+
const subpaths = parts.flatMap((p) => bezierToSubpaths(p.path));
|
|
6278
|
+
if (!subpaths.length) throw new LayerOperationError("The text draws nothing (it is empty or only spaces), so there is no shape to make.");
|
|
6279
|
+
const warnings = [];
|
|
6280
|
+
const colors = new Set(layer.runs.filter((r) => r.length > 0).map((r) => `${r.style.color}/${r.style.opacity ?? 1}`));
|
|
6281
|
+
if (colors.size > 1) warnings.push("The text had several colours; the shape takes the first one.");
|
|
6282
|
+
const missing = layoutText(layer, shaper).missingFonts;
|
|
6283
|
+
if (missing.length) warnings.push(`Missing fonts were drawn with a fallback: ${missing.join(", ")}.`);
|
|
6284
|
+
const first = layer.runs.find((r) => r.length > 0)?.style ?? DEFAULT_TEXT_STYLE;
|
|
6285
|
+
const fill = first.opacity !== void 0 && first.opacity !== 1 ? { type: "color", color: first.color, opacity: first.opacity } : { type: "color", color: first.color };
|
|
6286
|
+
const shape = createShapeLayer({ components: [{ op: "combine", subpaths }], fill, stroke: null, name: layer.name }, this.shapeEnv(), layer.id);
|
|
6287
|
+
const keep = { visible: layer.visible, opacity: layer.opacity, blend: layer.blend };
|
|
6288
|
+
for (const k of ["clipped", "mask", "locks", "effects", "fillOpacity"]) if (layer[k] !== void 0) keep[k] = layer[k];
|
|
6289
|
+
this.commitLayers(mapLayer(this.state.layers, id, () => ({ ...shape, ...keep })));
|
|
6290
|
+
return { layerId: id, warnings };
|
|
6291
|
+
}
|
|
6292
|
+
/** The layout of a type layer (lines, characters, bounds in text space), or `null` without a shaper. */
|
|
6293
|
+
getTextLayout(id) {
|
|
6294
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6295
|
+
const shaper = this.getTextShaper();
|
|
6296
|
+
return shaper ? layoutText(layer, shaper) : null;
|
|
6297
|
+
}
|
|
6298
|
+
/** The text index nearest a document point (for clicks on a type layer). */
|
|
6299
|
+
textIndexAt(id, point) {
|
|
6300
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6301
|
+
const layout = this.getTextLayout(id);
|
|
6302
|
+
return layout ? textIndexAt(layout, toTextSpace(layer, point)) : layer.text.length;
|
|
6303
|
+
}
|
|
6304
|
+
/** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */
|
|
6305
|
+
textCaret(id, index) {
|
|
6306
|
+
const layer = requireTypeLayer(this.state.layers, id);
|
|
6307
|
+
const layout = this.getTextLayout(id);
|
|
6308
|
+
if (!layout) return null;
|
|
6309
|
+
const c = caretAt(layout, Math.max(0, Math.min(layer.text.length, index)));
|
|
6310
|
+
const m = textMatrix(layer);
|
|
6311
|
+
const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
|
|
6312
|
+
return { top: at(c.x, c.top), bottom: at(c.x, c.bottom), line: c.line };
|
|
6313
|
+
}
|
|
5797
6314
|
// --- Shape layers ---
|
|
5798
6315
|
/** What shape rendering needs: the canvas size and the document's patterns. */
|
|
5799
6316
|
shapeEnv() {
|
|
@@ -5970,11 +6487,11 @@ var ImageEditorController = class {
|
|
|
5970
6487
|
}
|
|
5971
6488
|
} else if (layer.type === "smart") {
|
|
5972
6489
|
return this.rasterizeSmartObject(id);
|
|
5973
|
-
} else if (layer.type === "shape") {
|
|
5974
|
-
next = shapeAsPixelLayer(layer);
|
|
6490
|
+
} else if (layer.type === "shape" || layer.type === "type") {
|
|
6491
|
+
next = layer.type === "shape" ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);
|
|
5975
6492
|
if (this.state.selectedAnnotationId && this.state.selectedAnnotationId === layer.annotation?.id) this.state.selectedAnnotationId = null;
|
|
5976
6493
|
} else {
|
|
5977
|
-
throw new LayerOperationError(`Only shape and annotation layers, smart objects and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
|
|
6494
|
+
throw new LayerOperationError(`Only shape, type and annotation layers, smart objects and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
|
|
5978
6495
|
}
|
|
5979
6496
|
this.commitLayers(mapLayer(this.state.layers, id, () => next));
|
|
5980
6497
|
return id;
|
|
@@ -5997,7 +6514,7 @@ var ImageEditorController = class {
|
|
|
5997
6514
|
s.sourceUrl = null;
|
|
5998
6515
|
s.imageDimensions = { width: result.width, height: result.height };
|
|
5999
6516
|
if (!this.samplePixelsOverride) this.samplePixels = null;
|
|
6000
|
-
s.layers = result
|
|
6517
|
+
s.layers = this.checkPsdFonts(result);
|
|
6001
6518
|
s.crop = null;
|
|
6002
6519
|
s.transform = { ...DEFAULT_TRANSFORM_VALUES };
|
|
6003
6520
|
s.outputOpacity = 1;
|
|
@@ -6022,6 +6539,30 @@ var ImageEditorController = class {
|
|
|
6022
6539
|
summary: result.summary
|
|
6023
6540
|
};
|
|
6024
6541
|
}
|
|
6542
|
+
/**
|
|
6543
|
+
* Type layers whose fonts the shaper lacks: they keep Photoshop's pixels, are marked with their
|
|
6544
|
+
* `missingFonts`, and a warning names them (an edit draws them with a fallback).
|
|
6545
|
+
*/
|
|
6546
|
+
checkPsdFonts(result) {
|
|
6547
|
+
const shaper = this.getTextShaper();
|
|
6548
|
+
const types = flattenLayers(result.layers).filter((l) => l.type === "type");
|
|
6549
|
+
if (!types.length) return result.layers;
|
|
6550
|
+
let layers = result.layers;
|
|
6551
|
+
const missing = /* @__PURE__ */ new Map();
|
|
6552
|
+
for (const l of types) {
|
|
6553
|
+
const fonts = shaper ? layoutText(l, shaper).missingFonts : [...new Set(l.runs.map((r) => r.style.fontFamily))];
|
|
6554
|
+
if (!fonts.length) continue;
|
|
6555
|
+
layers = mapLayer(layers, l.id, () => ({ ...l, missingFonts: fonts }));
|
|
6556
|
+
for (const f of fonts) missing.set(f, [...missing.get(f) ?? [], l.name]);
|
|
6557
|
+
}
|
|
6558
|
+
for (const [font, names] of missing) {
|
|
6559
|
+
result.warnings.push(
|
|
6560
|
+
shaper ? `The font "${font}" is missing here: the type shows Photoshop's pixels until it is edited, then a fallback font (${names.length === 1 ? "layer" : "layers"} ${names.map((n) => `"${n}"`).join(", ")}).` : `Type layers show Photoshop's pixels; nothing here can draw text, so they cannot be edited (${names.length === 1 ? "layer" : "layers"} ${names.map((n) => `"${n}"`).join(", ")}).`
|
|
6561
|
+
);
|
|
6562
|
+
if (!shaper) break;
|
|
6563
|
+
}
|
|
6564
|
+
return layers;
|
|
6565
|
+
}
|
|
6025
6566
|
/**
|
|
6026
6567
|
* The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment
|
|
6027
6568
|
* layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked
|
|
@@ -6099,9 +6640,9 @@ var ImageEditorController = class {
|
|
|
6099
6640
|
if (mask2) out2.mask = mask2;
|
|
6100
6641
|
return out2;
|
|
6101
6642
|
}
|
|
6102
|
-
if (layer.type === "shape") {
|
|
6103
|
-
|
|
6104
|
-
const out2 =
|
|
6643
|
+
if (layer.type === "shape" || layer.type === "type") {
|
|
6644
|
+
if (layer.type === "type" && layer.missingFonts?.length) add2(`Type layers drawn with a fallback for missing fonts (${layer.missingFonts.join(", ")}) keep the font names, so Photoshop may draw them differently`, layer.name);
|
|
6645
|
+
const out2 = { ...layer };
|
|
6105
6646
|
const mask2 = bakeMask(layer);
|
|
6106
6647
|
if (mask2) out2.mask = mask2;
|
|
6107
6648
|
else delete out2.mask;
|
|
@@ -6203,7 +6744,7 @@ var ImageEditorController = class {
|
|
|
6203
6744
|
const bg = developTarget(layers, null);
|
|
6204
6745
|
if (bg && bg.source !== "document") layers = mapLayer(layers, bg.id, () => ({ ...bg, source: "document" }));
|
|
6205
6746
|
if (clearAnnotations) {
|
|
6206
|
-
for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
|
|
6747
|
+
for (const l of flattenLayers(layers)) if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) layers = mapLayer(layers, l.id, () => null);
|
|
6207
6748
|
this.state.selectedAnnotationId = null;
|
|
6208
6749
|
}
|
|
6209
6750
|
this.state.layers = layers;
|
|
@@ -6279,7 +6820,8 @@ var ImageEditorController = class {
|
|
|
6279
6820
|
problems,
|
|
6280
6821
|
smartContents: Array.isArray(data.smartContents) ? data.smartContents : [],
|
|
6281
6822
|
canvas,
|
|
6282
|
-
pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null
|
|
6823
|
+
pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null,
|
|
6824
|
+
text: this.textEnv()
|
|
6283
6825
|
});
|
|
6284
6826
|
this.lastLoadProblems = problems;
|
|
6285
6827
|
s.crop = data.crop ? { ...data.crop } : null;
|
|
@@ -6482,7 +7024,7 @@ function collectStateTiles(layers, into) {
|
|
|
6482
7024
|
for (const l of flattenLayers(layers)) {
|
|
6483
7025
|
if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
|
|
6484
7026
|
if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);
|
|
6485
|
-
if (l.type === "shape") collectTiles(l.surface, into);
|
|
7027
|
+
if (l.type === "shape" || l.type === "type") collectTiles(l.surface, into);
|
|
6486
7028
|
if (l.type === "smart") {
|
|
6487
7029
|
collectTiles(l.surface, into);
|
|
6488
7030
|
if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);
|
|
@@ -6531,7 +7073,7 @@ function resizePixels(px2, width, height) {
|
|
|
6531
7073
|
function withoutAnnotations(layers) {
|
|
6532
7074
|
const out = [];
|
|
6533
7075
|
for (const l of layers) {
|
|
6534
|
-
if (l.type === "annotation" || l.type === "shape" && l.annotation) continue;
|
|
7076
|
+
if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) continue;
|
|
6535
7077
|
out.push(l.type === "group" ? { ...l, children: withoutAnnotations(l.children) } : l);
|
|
6536
7078
|
}
|
|
6537
7079
|
return out;
|
|
@@ -6721,7 +7263,7 @@ function loadImageEditorDocument(input) {
|
|
|
6721
7263
|
if (state.selection === void 0) delete state.selection;
|
|
6722
7264
|
return { state, report: result.report, unknown: result.unknown };
|
|
6723
7265
|
}
|
|
6724
|
-
var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape"]);
|
|
7266
|
+
var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape", "type"]);
|
|
6725
7267
|
function cleanLayers(value, dropped) {
|
|
6726
7268
|
const out = [];
|
|
6727
7269
|
for (const raw of value) {
|
|
@@ -7781,6 +8323,18 @@ function describeLayer(layer, activeId, boundsOf) {
|
|
|
7781
8323
|
};
|
|
7782
8324
|
if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
|
|
7783
8325
|
break;
|
|
8326
|
+
case "type":
|
|
8327
|
+
out.bounds = boundsOf(layer);
|
|
8328
|
+
out.fillOpacity = round3(layer.fillOpacity ?? 1);
|
|
8329
|
+
out.text = {
|
|
8330
|
+
text: layer.text.length > 80 ? `${layer.text.slice(0, 80)}\u2026` : layer.text,
|
|
8331
|
+
frame: layer.frame.kind,
|
|
8332
|
+
runs: layer.runs.length,
|
|
8333
|
+
rendering: layer.rendering === "stored" ? "stored" : "live",
|
|
8334
|
+
missingFonts: layer.missingFonts ?? []
|
|
8335
|
+
};
|
|
8336
|
+
if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
|
|
8337
|
+
break;
|
|
7784
8338
|
case "smart":
|
|
7785
8339
|
out.bounds = boundsOf(layer);
|
|
7786
8340
|
out.fillOpacity = round3(layer.fillOpacity ?? 1);
|
|
@@ -7826,7 +8380,7 @@ function createImageLayerTools(controller) {
|
|
|
7826
8380
|
const listLayers = {
|
|
7827
8381
|
name: "list_layers",
|
|
7828
8382
|
title: "List image layers",
|
|
7829
|
-
description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, shape, smart, annotation), name, document bounds (pixel, shape and smart layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), a shape summary (component operations and kinds, fill and stroke paint types; details from get_shape_layer), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
|
|
8383
|
+
description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, shape, type, smart, annotation), name, document bounds (pixel, shape, type and smart layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), a shape summary (component operations and kinds, fill and stroke paint types; details from get_shape_layer), a text summary (the text, point or box frame, run count, live or stored pixels, missing fonts; details from get_text_layer), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
|
|
7830
8384
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
7831
8385
|
annotations: { title: "List image layers", ...READ2 },
|
|
7832
8386
|
handler: () => {
|
|
@@ -8170,7 +8724,7 @@ function createImageLayerTools(controller) {
|
|
|
8170
8724
|
const rasterize = {
|
|
8171
8725
|
name: "rasterize_layer",
|
|
8172
8726
|
title: "Rasterize a layer",
|
|
8173
|
-
description: "Rasterize Layer: turns a shape layer into a pixel layer holding what it shows now (same id, name, opacity, blend, fill, mask and effects; its paths are gone, so later transforms resample pixels), turns an annotation layer into a pixel layer (needs a browser), rasterizes a smart object, or bakes a pixel layer's filters (develop, blur) into its pixels. One undo step. Returns the layer.",
|
|
8727
|
+
description: "Rasterize Layer: turns a shape or type layer into a pixel layer holding what it shows now (same id, name, opacity, blend, fill, mask and effects; its paths are gone, so later transforms resample pixels), turns an annotation layer into a pixel layer (needs a browser), rasterizes a smart object, or bakes a pixel layer's filters (develop, blur) into its pixels. One undo step. Returns the layer.",
|
|
8174
8728
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID2 }, required: ["layerId"], additionalProperties: false },
|
|
8175
8729
|
annotations: { title: "Rasterize a layer", ...DESTRUCTIVE },
|
|
8176
8730
|
handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) })
|
|
@@ -8501,7 +9055,7 @@ function createImageEffectTools(controller) {
|
|
|
8501
9055
|
const add2 = {
|
|
8502
9056
|
name: "add_layer_effect",
|
|
8503
9057
|
title: "Add a layer effect",
|
|
8504
|
-
description: "Adds a live effect to a pixel, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
|
|
9058
|
+
description: "Adds a live effect to a pixel, shape, type, smart object, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
|
|
8505
9059
|
inputSchema: {
|
|
8506
9060
|
type: "object",
|
|
8507
9061
|
$defs: DEFS,
|
|
@@ -8975,7 +9529,7 @@ function createImagePsdTools(controller) {
|
|
|
8975
9529
|
const open = {
|
|
8976
9530
|
name: "import_psd",
|
|
8977
9531
|
title: "Open a PSD as the document",
|
|
8978
|
-
description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels);
|
|
9532
|
+
description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels); shape layers come in as live shapes (paths, path operations, fill, stroke, live rectangles, ellipses and lines); type layers come in as live text (runs, paragraphs, frame, transform) showing Photoshop\u2019s pixels until edited, with a warning for fonts missing here; fill layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).",
|
|
8979
9533
|
inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
|
|
8980
9534
|
annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE4 },
|
|
8981
9535
|
handler: ({ data }) => {
|
|
@@ -8994,7 +9548,7 @@ function createImagePsdTools(controller) {
|
|
|
8994
9548
|
const save = {
|
|
8995
9549
|
name: "export_psd",
|
|
8996
9550
|
title: "Export the document as a PSD",
|
|
8997
|
-
description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). Smart objects are written as Photoshop smart objects (contents embedded once per linked set, placement, warp, smart filters and filter mask; a Develop smart filter has no Photoshop filter and is left out of the list). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
|
|
9551
|
+
description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). Smart objects are written as Photoshop smart objects (contents embedded once per linked set, placement, warp, smart filters and filter mask; a Develop smart filter has no Photoshop filter and is left out of the list). Shape layers are written as Photoshop shape layers (path, fill, stroke, live shapes) and type layers as Photoshop type layers (text, runs, paragraphs, frame; blocks opened from a PSD are written back unchanged while the layer is). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
|
|
8998
9552
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
8999
9553
|
annotations: { title: "Export the document as a PSD", ...READ5 },
|
|
9000
9554
|
handler: async () => {
|
|
@@ -10019,6 +10573,176 @@ function createImageShapeTools(controller) {
|
|
|
10019
10573
|
return [get, add2, update, setComponent, addComponent, removeComponent, merge];
|
|
10020
10574
|
}
|
|
10021
10575
|
|
|
10576
|
+
// src/image-editor/type-tools.ts
|
|
10577
|
+
var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
10578
|
+
var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
10579
|
+
var WRITE_IDEMPOTENT6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
10580
|
+
var DESTRUCTIVE7 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
10581
|
+
var LAYER_ID6 = { type: "string", description: "Type layer id from list_layers (type type) or add_text_layer." };
|
|
10582
|
+
var START = { type: "integer", minimum: 0, description: "Start of the range in UTF-16 code units, as get_text_layer counts them (default 0)." };
|
|
10583
|
+
var END = { type: "integer", minimum: 0, description: "End of the range, exclusive (default the end of the text)." };
|
|
10584
|
+
var STYLE = {
|
|
10585
|
+
fontFamily: { type: "string", description: "Font family name (list_fonts shows what is available; a missing family falls back, with a warning)." },
|
|
10586
|
+
fontWeight: { type: "integer", minimum: 1, maximum: 1e3, description: "Weight 100..900 (400 regular, 700 bold)." },
|
|
10587
|
+
italic: { type: "boolean", description: "Italic." },
|
|
10588
|
+
fontSize: { type: "number", minimum: 0.5, maximum: 5e3, description: "Size in document pixels." },
|
|
10589
|
+
color: { type: "string", description: "Colour (#rrggbb, or a CSS colour; its alpha becomes opacity)." },
|
|
10590
|
+
opacity: { type: ["number", "null"], minimum: 0, maximum: 1, description: "Text opacity 0..1 (null resets to 1)." },
|
|
10591
|
+
tracking: { type: ["number", "null"], minimum: -1e3, maximum: 1e4, description: "Letter spacing in 1/1000 em (null resets to 0)." },
|
|
10592
|
+
leading: { type: ["number", "null"], minimum: 0, maximum: 1e4, description: "Baseline-to-baseline distance in pixels (null = auto, the paragraph autoLeading \xD7 size)." },
|
|
10593
|
+
baselineShift: { type: ["number", "null"], minimum: -5e3, maximum: 5e3, description: "Pixels; positive raises the text (null resets to 0)." },
|
|
10594
|
+
underline: { type: "boolean", description: "Underline." },
|
|
10595
|
+
strikethrough: { type: "boolean", description: "Strike through." }
|
|
10596
|
+
};
|
|
10597
|
+
var PARAGRAPH = {
|
|
10598
|
+
align: { type: "string", enum: ["left", "center", "right", "justify"], description: "Alignment (justify stretches paragraph-box lines but the last)." },
|
|
10599
|
+
autoLeading: { type: ["number", "null"], minimum: 0.1, maximum: 10, description: "Auto leading as a multiple of the font size (default 1.2)." },
|
|
10600
|
+
firstLineIndent: { type: ["number", "null"], description: "First-line indent in pixels." },
|
|
10601
|
+
startIndent: { type: ["number", "null"], description: "Left indent in pixels." },
|
|
10602
|
+
endIndent: { type: ["number", "null"], description: "Right indent in pixels." },
|
|
10603
|
+
spaceBefore: { type: ["number", "null"], description: "Space above the paragraph in pixels (not before the first)." },
|
|
10604
|
+
spaceAfter: { type: ["number", "null"], description: "Space below the paragraph in pixels." }
|
|
10605
|
+
};
|
|
10606
|
+
function pick(args, keys) {
|
|
10607
|
+
const out = {};
|
|
10608
|
+
for (const k of keys) if (args[k] !== void 0) out[k] = args[k];
|
|
10609
|
+
return out;
|
|
10610
|
+
}
|
|
10611
|
+
function createImageTypeTools(controller) {
|
|
10612
|
+
const typeId = (id) => {
|
|
10613
|
+
if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a type layer id from list_layers).");
|
|
10614
|
+
requireTypeLayer(controller.getLayers(), id);
|
|
10615
|
+
return id;
|
|
10616
|
+
};
|
|
10617
|
+
const layerSummary = (id) => {
|
|
10618
|
+
const s = controller.getState();
|
|
10619
|
+
const loc = findLayer(s.layers, id);
|
|
10620
|
+
return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;
|
|
10621
|
+
};
|
|
10622
|
+
const range = (args) => ({ start: args.start, end: args.end });
|
|
10623
|
+
const listFonts = {
|
|
10624
|
+
name: "list_fonts",
|
|
10625
|
+
title: "List fonts",
|
|
10626
|
+
description: "Returns the font families type layers can use here (generic families such as sans-serif always work). Changes nothing.",
|
|
10627
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
10628
|
+
annotations: { title: "List fonts", ...READ9 },
|
|
10629
|
+
handler: () => ({ families: controller.listFonts(), shaper: controller.getTextShaper()?.id ?? null })
|
|
10630
|
+
};
|
|
10631
|
+
const get = {
|
|
10632
|
+
name: "get_text_layer",
|
|
10633
|
+
title: "Read a type layer",
|
|
10634
|
+
description: "Returns a type layer\u2019s text, its character runs and paragraphs as [start, end) ranges in UTF-16 code units with their styles, the frame (point text, or a paragraph box with its size), the text-to-document transform, the laid-out lines (range, baseline, width), the rendered bounds, missing fonts and whether the pixels are live or stored from a file. Call before editing text you did not write, to get the ranges. Changes nothing.",
|
|
10635
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
|
|
10636
|
+
annotations: { title: "Read a type layer", ...READ9 },
|
|
10637
|
+
handler: ({ layerId }) => controller.getTextLayer(typeId(layerId))
|
|
10638
|
+
};
|
|
10639
|
+
const add2 = {
|
|
10640
|
+
name: "add_text_layer",
|
|
10641
|
+
title: "Add a type layer",
|
|
10642
|
+
description: "Adds a live type layer and makes it active: point text whose origin (x, y) is the first line\u2019s baseline at its alignment edge, or paragraph text wrapped in box {width, height} with its top-left at (x, y). Line breaks in text start paragraphs. Style and paragraph settings apply to the whole text (change ranges later with set_text_style). Default style: 24 px sans-serif in the foreground colour. One undo step. Returns the layer and the text layer, including missingFonts.",
|
|
10643
|
+
inputSchema: {
|
|
10644
|
+
type: "object",
|
|
10645
|
+
properties: {
|
|
10646
|
+
text: { type: "string", description: "The text; \\n starts a new paragraph." },
|
|
10647
|
+
x: { type: "number", description: "Document x of the origin, pixels." },
|
|
10648
|
+
y: { type: "number", description: "Document y of the origin (point text: the first baseline), pixels." },
|
|
10649
|
+
box: {
|
|
10650
|
+
type: "object",
|
|
10651
|
+
description: "Make it paragraph (area) text wrapped in this box, pixels.",
|
|
10652
|
+
properties: { width: { type: "number", minimum: 1, description: "Box width, pixels." }, height: { type: "number", minimum: 1, description: "Box height, pixels." } },
|
|
10653
|
+
required: ["width", "height"],
|
|
10654
|
+
additionalProperties: false
|
|
10655
|
+
},
|
|
10656
|
+
style: { type: "object", description: "Character style.", properties: STYLE, additionalProperties: false },
|
|
10657
|
+
paragraph: { type: "object", description: "Paragraph style.", properties: PARAGRAPH, additionalProperties: false },
|
|
10658
|
+
rotation: { type: "number", description: "Degrees clockwise about the origin (default 0)." },
|
|
10659
|
+
name: { type: "string", description: "Layer name (default the start of the text)." },
|
|
10660
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0..1 (default 1)." },
|
|
10661
|
+
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
|
|
10662
|
+
aboveId: { type: "string", description: "Put the layer directly above this layer (default above the active layer)." },
|
|
10663
|
+
belowId: { type: "string", description: "Put the layer directly below this layer." },
|
|
10664
|
+
parentId: { type: "string", description: "Put the layer inside this group." },
|
|
10665
|
+
index: { type: "integer", minimum: 0, description: "Position among the siblings, 0 = bottom." }
|
|
10666
|
+
},
|
|
10667
|
+
required: ["text", "x", "y"],
|
|
10668
|
+
additionalProperties: false
|
|
10669
|
+
},
|
|
10670
|
+
annotations: { title: "Add a type layer", ...WRITE8 },
|
|
10671
|
+
handler: (args) => {
|
|
10672
|
+
const init = pick(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
|
|
10673
|
+
if (init.blend !== void 0) init.blend = init.blend;
|
|
10674
|
+
const id = controller.addTextLayer(init);
|
|
10675
|
+
return { layer: layerSummary(id), text: controller.getTextLayer(id) };
|
|
10676
|
+
}
|
|
10677
|
+
};
|
|
10678
|
+
const setText = {
|
|
10679
|
+
name: "set_text",
|
|
10680
|
+
title: "Edit text",
|
|
10681
|
+
description: "Replaces the text in [start, end) (default all of it) with text; inserted text takes the style of the character at start. Use start = end to insert. Returns the text layer with its new runs.",
|
|
10682
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID6, text: { type: "string", description: "Replacement text (\\n starts a paragraph; empty deletes the range)." }, start: START, end: END }, required: ["layerId", "text"], additionalProperties: false },
|
|
10683
|
+
annotations: { title: "Edit text", ...WRITE8 },
|
|
10684
|
+
handler: (args) => controller.setText(typeId(args.layerId), args.text, range(args))
|
|
10685
|
+
};
|
|
10686
|
+
const setStyle = {
|
|
10687
|
+
name: "set_text_style",
|
|
10688
|
+
title: "Style a range of text",
|
|
10689
|
+
description: "Sets character style fields (font, weight, italic, size, colour, tracking, leading, baseline shift, underline, strike-through) over [start, end) of a type layer (default all of it); fields not given keep their values per run. Returns the text layer with its runs and missingFonts.",
|
|
10690
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...STYLE }, required: ["layerId"], additionalProperties: false },
|
|
10691
|
+
annotations: { title: "Style a range of text", ...WRITE_IDEMPOTENT6 },
|
|
10692
|
+
handler: (args) => {
|
|
10693
|
+
const style = pick(args, Object.keys(STYLE));
|
|
10694
|
+
if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one style field: ${Object.keys(STYLE).join(", ")}.`);
|
|
10695
|
+
return controller.setTextStyle(typeId(args.layerId), style, range(args));
|
|
10696
|
+
}
|
|
10697
|
+
};
|
|
10698
|
+
const setParagraph = {
|
|
10699
|
+
name: "set_paragraph_style",
|
|
10700
|
+
title: "Style paragraphs",
|
|
10701
|
+
description: "Sets paragraph fields (alignment, auto leading, indents, spacing) on every paragraph that [start, end) touches (default all). Returns the text layer.",
|
|
10702
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...PARAGRAPH }, required: ["layerId"], additionalProperties: false },
|
|
10703
|
+
annotations: { title: "Style paragraphs", ...WRITE_IDEMPOTENT6 },
|
|
10704
|
+
handler: (args) => {
|
|
10705
|
+
const style = pick(args, Object.keys(PARAGRAPH));
|
|
10706
|
+
if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one paragraph field: ${Object.keys(PARAGRAPH).join(", ")}.`);
|
|
10707
|
+
return controller.setParagraphStyle(typeId(args.layerId), style, range(args));
|
|
10708
|
+
}
|
|
10709
|
+
};
|
|
10710
|
+
const setFrame = {
|
|
10711
|
+
name: "set_text_frame",
|
|
10712
|
+
title: "Point or paragraph text",
|
|
10713
|
+
description: "Converts between point text (kind point: one line per paragraph) and paragraph text (kind box: wrapped in width \xD7 height pixels, its top-left where the first line starts), or resizes a paragraph box. The first line stays in place. Returns the text layer with its new lines.",
|
|
10714
|
+
inputSchema: {
|
|
10715
|
+
type: "object",
|
|
10716
|
+
properties: {
|
|
10717
|
+
layerId: LAYER_ID6,
|
|
10718
|
+
kind: { type: "string", enum: ["point", "box"], description: "point or box." },
|
|
10719
|
+
width: { type: "number", minimum: 1, description: "Box width in pixels (kind box)." },
|
|
10720
|
+
height: { type: "number", minimum: 1, description: "Box height in pixels (kind box)." }
|
|
10721
|
+
},
|
|
10722
|
+
required: ["layerId", "kind"],
|
|
10723
|
+
additionalProperties: false
|
|
10724
|
+
},
|
|
10725
|
+
annotations: { title: "Point or paragraph text", ...WRITE_IDEMPOTENT6 },
|
|
10726
|
+
handler: ({ layerId, kind, width, height }) => {
|
|
10727
|
+
const id = typeId(layerId);
|
|
10728
|
+
if (kind === "box" && (width === void 0 || height === void 0)) throw new LayerOperationError("kind box needs width and height in pixels.");
|
|
10729
|
+
return controller.setTextFrame(id, kind === "box" ? { kind: "box", width, height } : { kind: "point" });
|
|
10730
|
+
}
|
|
10731
|
+
};
|
|
10732
|
+
const toShape = {
|
|
10733
|
+
name: "convert_text_to_shape",
|
|
10734
|
+
title: "Convert text to a shape",
|
|
10735
|
+
description: "Type \u203A Convert to Shape: the text\u2019s glyph outlines become a shape layer (same id, name, mask, effects, opacity, blend) that can be edited with the shape tools; the text is no longer editable. Needs a font shaper that gives outlines (the browser\u2019s canvas cannot). The fill is the first run\u2019s colour. One undo step. Returns the layer and warnings.",
|
|
10736
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
|
|
10737
|
+
annotations: { title: "Convert text to a shape", ...DESTRUCTIVE7 },
|
|
10738
|
+
handler: ({ layerId }) => {
|
|
10739
|
+
const { layerId: id, warnings } = controller.convertTextToShape(typeId(layerId));
|
|
10740
|
+
return { layer: layerSummary(id), shape: controller.getShapeLayer(id), warnings };
|
|
10741
|
+
}
|
|
10742
|
+
};
|
|
10743
|
+
return [listFonts, get, add2, setText, setStyle, setParagraph, setFrame, toShape];
|
|
10744
|
+
}
|
|
10745
|
+
|
|
10022
10746
|
// src/image-editor/tools.ts
|
|
10023
10747
|
var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
|
|
10024
10748
|
function adjustmentsSummary(adjustments) {
|
|
@@ -10259,6 +10983,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
10259
10983
|
...createImageTransformTools(controller),
|
|
10260
10984
|
...createImageSmartObjectTools(controller),
|
|
10261
10985
|
...createImageShapeTools(controller),
|
|
10986
|
+
...createImageTypeTools(controller),
|
|
10262
10987
|
...createImageEffectTools(controller),
|
|
10263
10988
|
...createImagePaintTools(controller),
|
|
10264
10989
|
...createPigmentTools(),
|
|
@@ -10561,6 +11286,470 @@ function applyRegionBlur(imageData, regions, radius = 10) {
|
|
|
10561
11286
|
return imageData;
|
|
10562
11287
|
}
|
|
10563
11288
|
|
|
11289
|
+
// src/image-editor/layers/shape-type-ui.ts
|
|
11290
|
+
var DEFAULT_SHAPE_TOOL_OPTIONS = {
|
|
11291
|
+
kind: "rect",
|
|
11292
|
+
fill: "#4f46e5",
|
|
11293
|
+
stroke: null,
|
|
11294
|
+
strokeWidth: 2,
|
|
11295
|
+
radius: 0,
|
|
11296
|
+
sides: 6,
|
|
11297
|
+
points: 5,
|
|
11298
|
+
innerRatio: 0.5,
|
|
11299
|
+
weight: 4,
|
|
11300
|
+
arrow: false
|
|
11301
|
+
};
|
|
11302
|
+
var SHAPE_KIND_LABELS = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
|
|
11303
|
+
var DEFAULT_TYPE_TOOL_OPTIONS = { fontFamily: "sans-serif", fontSize: 32, fontWeight: 400, italic: false, color: "#111827", align: "left" };
|
|
11304
|
+
function liveShapeForDrag(o, a, b, mods = {}) {
|
|
11305
|
+
if (o.kind === "line") {
|
|
11306
|
+
let x2 = b.x;
|
|
11307
|
+
let y2 = b.y;
|
|
11308
|
+
if (mods.shift) {
|
|
11309
|
+
const ang = Math.round(Math.atan2(b.y - a.y, b.x - a.x) / (Math.PI / 4)) * (Math.PI / 4);
|
|
11310
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
11311
|
+
x2 = a.x + Math.cos(ang) * len;
|
|
11312
|
+
y2 = a.y + Math.sin(ang) * len;
|
|
11313
|
+
}
|
|
11314
|
+
if (Math.hypot(x2 - a.x, y2 - a.y) < 1) return null;
|
|
11315
|
+
const head = o.arrow ? { endArrow: { width: o.weight * 4, length: o.weight * 4 } } : {};
|
|
11316
|
+
return { kind: "line", x1: a.x, y1: a.y, x2, y2, weight: o.weight, ...head };
|
|
11317
|
+
}
|
|
11318
|
+
let w = b.x - a.x;
|
|
11319
|
+
let h = b.y - a.y;
|
|
11320
|
+
if (mods.shift) {
|
|
11321
|
+
const s = Math.max(Math.abs(w), Math.abs(h));
|
|
11322
|
+
w = Math.sign(w || 1) * s;
|
|
11323
|
+
h = Math.sign(h || 1) * s;
|
|
11324
|
+
}
|
|
11325
|
+
let x = a.x;
|
|
11326
|
+
let y = a.y;
|
|
11327
|
+
if (mods.alt) {
|
|
11328
|
+
x = a.x - w;
|
|
11329
|
+
y = a.y - h;
|
|
11330
|
+
w *= 2;
|
|
11331
|
+
h *= 2;
|
|
11332
|
+
}
|
|
11333
|
+
const box = { x: Math.min(x, x + w), y: Math.min(y, y + h), width: Math.abs(w), height: Math.abs(h) };
|
|
11334
|
+
if (box.width < 1 || box.height < 1) return null;
|
|
11335
|
+
switch (o.kind) {
|
|
11336
|
+
case "rect":
|
|
11337
|
+
return { kind: "rect", ...box, ...o.radius > 0 ? { radius: o.radius } : {} };
|
|
11338
|
+
case "ellipse":
|
|
11339
|
+
return { kind: "ellipse", ...box };
|
|
11340
|
+
case "polygon":
|
|
11341
|
+
return { kind: "polygon", ...box, sides: o.sides };
|
|
11342
|
+
case "star":
|
|
11343
|
+
return { kind: "star", ...box, points: o.points, innerRatio: o.innerRatio };
|
|
11344
|
+
}
|
|
11345
|
+
return null;
|
|
11346
|
+
}
|
|
11347
|
+
var ShapeDrawGesture = class {
|
|
11348
|
+
constructor(controller, options) {
|
|
11349
|
+
this.controller = controller;
|
|
11350
|
+
this.options = options;
|
|
11351
|
+
}
|
|
11352
|
+
controller;
|
|
11353
|
+
options;
|
|
11354
|
+
start = null;
|
|
11355
|
+
layerId = null;
|
|
11356
|
+
error = null;
|
|
11357
|
+
get active() {
|
|
11358
|
+
return this.start !== null;
|
|
11359
|
+
}
|
|
11360
|
+
/** The shape layer drawn by the last drag (until the next one starts). */
|
|
11361
|
+
get drawnLayerId() {
|
|
11362
|
+
return this.layerId;
|
|
11363
|
+
}
|
|
11364
|
+
down(p) {
|
|
11365
|
+
this.error = null;
|
|
11366
|
+
this.start = { x: p.x, y: p.y };
|
|
11367
|
+
this.layerId = null;
|
|
11368
|
+
}
|
|
11369
|
+
move(p, mods = {}) {
|
|
11370
|
+
if (!this.start) return;
|
|
11371
|
+
const o = this.options();
|
|
11372
|
+
const shape = liveShapeForDrag(o, this.start, p, mods);
|
|
11373
|
+
if (!shape) return;
|
|
11374
|
+
try {
|
|
11375
|
+
if (!this.layerId) {
|
|
11376
|
+
const isLine = o.kind === "line";
|
|
11377
|
+
this.layerId = this.controller.addShapeLayer({
|
|
11378
|
+
shape,
|
|
11379
|
+
fill: isLine ? o.stroke ?? o.fill ?? "#000000" : o.fill,
|
|
11380
|
+
stroke: !isLine && o.stroke ? { width: o.strokeWidth, paint: o.stroke } : null
|
|
11381
|
+
});
|
|
11382
|
+
} else this.controller.setShapeComponent(this.layerId, 0, { shape }, false);
|
|
11383
|
+
} catch (e) {
|
|
11384
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
11385
|
+
this.start = null;
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
11388
|
+
up() {
|
|
11389
|
+
this.start = null;
|
|
11390
|
+
return this.layerId;
|
|
11391
|
+
}
|
|
11392
|
+
cancel() {
|
|
11393
|
+
this.start = null;
|
|
11394
|
+
}
|
|
11395
|
+
};
|
|
11396
|
+
function applyShapeOptions(controller, layerId, change) {
|
|
11397
|
+
const info = controller.getShapeLayer(layerId);
|
|
11398
|
+
const style = {};
|
|
11399
|
+
if ("fill" in change) style.fill = change.fill ?? null;
|
|
11400
|
+
if ("stroke" in change || "strokeWidth" in change) {
|
|
11401
|
+
const color = "stroke" in change ? change.stroke : info.stroke?.paint.type === "color" ? info.stroke.paint.color : null;
|
|
11402
|
+
style.stroke = color ? { width: change.strokeWidth ?? info.stroke?.width ?? 2, paint: color } : null;
|
|
11403
|
+
}
|
|
11404
|
+
if (Object.keys(style).length) controller.setShapeStyle(layerId, style);
|
|
11405
|
+
const live = info.components[0]?.live;
|
|
11406
|
+
if (!live) return;
|
|
11407
|
+
const shape = {};
|
|
11408
|
+
if (live.kind === "rect" && change.radius !== void 0) shape.radius = change.radius;
|
|
11409
|
+
if (live.kind === "polygon" && change.sides !== void 0) shape.sides = change.sides;
|
|
11410
|
+
if (live.kind === "star" && change.points !== void 0) shape.points = change.points;
|
|
11411
|
+
if (live.kind === "star" && change.innerRatio !== void 0) shape.innerRatio = change.innerRatio;
|
|
11412
|
+
if (live.kind === "line" && change.weight !== void 0) shape.weight = change.weight;
|
|
11413
|
+
if (live.kind === "line" && change.arrow !== void 0) shape.endArrow = change.arrow ? { width: (live.weight || 1) * 4, length: (live.weight || 1) * 4 } : null;
|
|
11414
|
+
if (Object.keys(shape).length) controller.setShapeComponent(layerId, 0, { shape });
|
|
11415
|
+
}
|
|
11416
|
+
var TextEditSession = class {
|
|
11417
|
+
constructor(controller, layerId, index) {
|
|
11418
|
+
this.controller = controller;
|
|
11419
|
+
this.layerId = layerId;
|
|
11420
|
+
const n = this.text.length;
|
|
11421
|
+
this.anchor = this.focus = Math.max(0, Math.min(n, index ?? n));
|
|
11422
|
+
}
|
|
11423
|
+
controller;
|
|
11424
|
+
layerId;
|
|
11425
|
+
anchor;
|
|
11426
|
+
focus;
|
|
11427
|
+
changed = false;
|
|
11428
|
+
listeners = /* @__PURE__ */ new Set();
|
|
11429
|
+
error = null;
|
|
11430
|
+
get layer() {
|
|
11431
|
+
const l = findLayer(this.controller.getLayers(), this.layerId)?.layer;
|
|
11432
|
+
if (!l || l.type !== "type") throw new Error("The type layer being edited is gone.");
|
|
11433
|
+
return l;
|
|
11434
|
+
}
|
|
11435
|
+
get text() {
|
|
11436
|
+
return this.layer.text;
|
|
11437
|
+
}
|
|
11438
|
+
get start() {
|
|
11439
|
+
return Math.min(this.anchor, this.focus);
|
|
11440
|
+
}
|
|
11441
|
+
get end() {
|
|
11442
|
+
return Math.max(this.anchor, this.focus);
|
|
11443
|
+
}
|
|
11444
|
+
/** Whether the layer still exists (it can be deleted or undone away while editing). */
|
|
11445
|
+
get alive() {
|
|
11446
|
+
return findLayer(this.controller.getLayers(), this.layerId)?.layer.type === "type";
|
|
11447
|
+
}
|
|
11448
|
+
subscribe(fn) {
|
|
11449
|
+
this.listeners.add(fn);
|
|
11450
|
+
return () => this.listeners.delete(fn);
|
|
11451
|
+
}
|
|
11452
|
+
emit() {
|
|
11453
|
+
for (const l of this.listeners) l();
|
|
11454
|
+
}
|
|
11455
|
+
layout() {
|
|
11456
|
+
return this.controller.getTextLayout(this.layerId);
|
|
11457
|
+
}
|
|
11458
|
+
run(fn) {
|
|
11459
|
+
this.error = null;
|
|
11460
|
+
try {
|
|
11461
|
+
const out = fn(!this.changed);
|
|
11462
|
+
this.changed = true;
|
|
11463
|
+
return out;
|
|
11464
|
+
} catch (e) {
|
|
11465
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
11466
|
+
return void 0;
|
|
11467
|
+
} finally {
|
|
11468
|
+
this.emit();
|
|
11469
|
+
}
|
|
11470
|
+
}
|
|
11471
|
+
/** Replaces the selection with `text` (typing and pasting). */
|
|
11472
|
+
insert(text) {
|
|
11473
|
+
if (!text) return;
|
|
11474
|
+
const s = this.start;
|
|
11475
|
+
const e = this.end;
|
|
11476
|
+
const done = this.run((commit) => this.controller.setText(this.layerId, text, { start: s, end: e }, commit));
|
|
11477
|
+
if (done) this.anchor = this.focus = s + text.replace(/\r\n?/g, "\n").length;
|
|
11478
|
+
}
|
|
11479
|
+
/** Backspace: the selection, else the character before the caret. */
|
|
11480
|
+
deleteBackward() {
|
|
11481
|
+
if (this.start === this.end && this.start === 0) return;
|
|
11482
|
+
const s = this.start === this.end ? Math.max(0, this.start - this.surrogate(this.start - 1, -1)) : this.start;
|
|
11483
|
+
const e = this.end;
|
|
11484
|
+
if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
|
|
11485
|
+
}
|
|
11486
|
+
/** Delete: the selection, else the character after the caret. */
|
|
11487
|
+
deleteForward() {
|
|
11488
|
+
const n = this.text.length;
|
|
11489
|
+
if (this.start === this.end && this.end >= n) return;
|
|
11490
|
+
const s = this.start;
|
|
11491
|
+
const e = this.start === this.end ? Math.min(n, this.end + this.surrogate(this.end, 1)) : this.end;
|
|
11492
|
+
if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
|
|
11493
|
+
}
|
|
11494
|
+
/** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */
|
|
11495
|
+
surrogate(i, dir) {
|
|
11496
|
+
const t = this.text;
|
|
11497
|
+
const c = t.charCodeAt(i);
|
|
11498
|
+
if (dir < 0 && c >= 56320 && c <= 57343 && i > 0) return 2;
|
|
11499
|
+
if (dir > 0 && c >= 55296 && c <= 56319 && i + 1 < t.length) return 2;
|
|
11500
|
+
return 1;
|
|
11501
|
+
}
|
|
11502
|
+
/** Moves the caret (Shift extends the selection). */
|
|
11503
|
+
move(to, extend = false) {
|
|
11504
|
+
const n = this.text.length;
|
|
11505
|
+
let f = this.focus;
|
|
11506
|
+
const collapsedJump = !extend && this.start !== this.end;
|
|
11507
|
+
if (to === "left") f = collapsedJump ? this.start : Math.max(0, f - this.surrogate(f - 1, -1));
|
|
11508
|
+
else if (to === "right") f = collapsedJump ? this.end : Math.min(n, f + this.surrogate(f, 1));
|
|
11509
|
+
else if (to === "start") f = 0;
|
|
11510
|
+
else if (to === "finish") f = n;
|
|
11511
|
+
else {
|
|
11512
|
+
const layout = this.layout();
|
|
11513
|
+
if (layout && layout.lines.length) {
|
|
11514
|
+
const c = caretAt(layout, f);
|
|
11515
|
+
const line = layout.lines[c.line];
|
|
11516
|
+
if (to === "home") f = line.start;
|
|
11517
|
+
else if (to === "end") {
|
|
11518
|
+
const last = line.chars[line.chars.length - 1];
|
|
11519
|
+
f = last ? Math.min(n, last.index + 1) : line.start;
|
|
11520
|
+
if (last && this.text[last.index] === " " && c.line < layout.lines.length - 1) f = last.index;
|
|
11521
|
+
} else {
|
|
11522
|
+
const target = layout.lines[c.line + (to === "up" ? -1 : 1)];
|
|
11523
|
+
if (!target) f = to === "up" ? 0 : n;
|
|
11524
|
+
else {
|
|
11525
|
+
const hit = target.chars.find((ch) => c.x < ch.x + ch.advance / 2);
|
|
11526
|
+
const last = target.chars[target.chars.length - 1];
|
|
11527
|
+
f = hit ? hit.index : last ? last.index + 1 : target.start;
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11530
|
+
}
|
|
11531
|
+
}
|
|
11532
|
+
this.focus = f;
|
|
11533
|
+
if (!extend) this.anchor = f;
|
|
11534
|
+
this.emit();
|
|
11535
|
+
}
|
|
11536
|
+
selectAll() {
|
|
11537
|
+
this.anchor = 0;
|
|
11538
|
+
this.focus = this.text.length;
|
|
11539
|
+
this.emit();
|
|
11540
|
+
}
|
|
11541
|
+
/** Places the caret at a document point (Shift-click extends). */
|
|
11542
|
+
pointAt(p, extend = false) {
|
|
11543
|
+
this.focus = this.controller.textIndexAt(this.layerId, p);
|
|
11544
|
+
if (!extend) this.anchor = this.focus;
|
|
11545
|
+
this.emit();
|
|
11546
|
+
}
|
|
11547
|
+
/** Handles a key; returns `true` when it was the session's (the caller then prevents the default). */
|
|
11548
|
+
handleKey(e) {
|
|
11549
|
+
const mod2 = e.metaKey || e.ctrlKey;
|
|
11550
|
+
if (mod2 && e.key.toLowerCase() === "a") {
|
|
11551
|
+
this.selectAll();
|
|
11552
|
+
return true;
|
|
11553
|
+
}
|
|
11554
|
+
switch (e.key) {
|
|
11555
|
+
case "Backspace":
|
|
11556
|
+
this.deleteBackward();
|
|
11557
|
+
return true;
|
|
11558
|
+
case "Delete":
|
|
11559
|
+
this.deleteForward();
|
|
11560
|
+
return true;
|
|
11561
|
+
case "Enter":
|
|
11562
|
+
this.insert("\n");
|
|
11563
|
+
return true;
|
|
11564
|
+
case "ArrowLeft":
|
|
11565
|
+
this.move(mod2 ? "home" : "left", e.shiftKey);
|
|
11566
|
+
return true;
|
|
11567
|
+
case "ArrowRight":
|
|
11568
|
+
this.move(mod2 ? "end" : "right", e.shiftKey);
|
|
11569
|
+
return true;
|
|
11570
|
+
case "ArrowUp":
|
|
11571
|
+
this.move(mod2 ? "start" : "up", e.shiftKey);
|
|
11572
|
+
return true;
|
|
11573
|
+
case "ArrowDown":
|
|
11574
|
+
this.move(mod2 ? "finish" : "down", e.shiftKey);
|
|
11575
|
+
return true;
|
|
11576
|
+
case "Home":
|
|
11577
|
+
this.move("home", e.shiftKey);
|
|
11578
|
+
return true;
|
|
11579
|
+
case "End":
|
|
11580
|
+
this.move("end", e.shiftKey);
|
|
11581
|
+
return true;
|
|
11582
|
+
case "Tab":
|
|
11583
|
+
this.insert(" ");
|
|
11584
|
+
return true;
|
|
11585
|
+
}
|
|
11586
|
+
if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
|
|
11587
|
+
this.insert(e.key);
|
|
11588
|
+
return true;
|
|
11589
|
+
}
|
|
11590
|
+
return false;
|
|
11591
|
+
}
|
|
11592
|
+
/** Sets character style fields on the selection, or on the whole text when nothing is selected. */
|
|
11593
|
+
setStyle(style) {
|
|
11594
|
+
const s = this.start;
|
|
11595
|
+
const e = this.end;
|
|
11596
|
+
const range = s === e ? {} : { start: s, end: e };
|
|
11597
|
+
this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
|
|
11598
|
+
}
|
|
11599
|
+
/** Sets paragraph fields on the paragraphs the selection touches. */
|
|
11600
|
+
setParagraph(style) {
|
|
11601
|
+
const s = this.start;
|
|
11602
|
+
const e = this.end;
|
|
11603
|
+
this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
|
|
11604
|
+
}
|
|
11605
|
+
/** The style at the caret (or the selection's start), for the options bar. */
|
|
11606
|
+
styleHere() {
|
|
11607
|
+
const layer = this.layer;
|
|
11608
|
+
const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
|
|
11609
|
+
return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
|
|
11610
|
+
}
|
|
11611
|
+
info() {
|
|
11612
|
+
return this.controller.getTextLayer(this.layerId);
|
|
11613
|
+
}
|
|
11614
|
+
/** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
|
|
11615
|
+
overlay() {
|
|
11616
|
+
const layer = this.layer;
|
|
11617
|
+
const layout = this.layout();
|
|
11618
|
+
const m = textMatrix(layer);
|
|
11619
|
+
const at = (x, y) => applyMatrix(m, { x, y });
|
|
11620
|
+
let frame = null;
|
|
11621
|
+
if (layer.frame.kind === "box") frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];
|
|
11622
|
+
else if (layout) {
|
|
11623
|
+
const b = layout.bounds;
|
|
11624
|
+
frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];
|
|
11625
|
+
}
|
|
11626
|
+
if (!layout) return { caret: null, selection: [], frame };
|
|
11627
|
+
const selection = [];
|
|
11628
|
+
if (this.start !== this.end) {
|
|
11629
|
+
for (const l of layout.lines) {
|
|
11630
|
+
const chars = l.chars.filter((c2) => c2.index >= this.start && c2.index < this.end);
|
|
11631
|
+
if (!chars.length) continue;
|
|
11632
|
+
const x0 = chars[0].x;
|
|
11633
|
+
const x1 = chars[chars.length - 1].x + chars[chars.length - 1].advance;
|
|
11634
|
+
const top = l.baseline - l.ascent;
|
|
11635
|
+
const bottom = l.baseline + l.descent;
|
|
11636
|
+
selection.push([at(x0, top), at(x1, top), at(x1, bottom), at(x0, bottom)]);
|
|
11637
|
+
}
|
|
11638
|
+
}
|
|
11639
|
+
const c = caretAt(layout, this.focus);
|
|
11640
|
+
return { caret: [at(c.x, c.top), at(c.x, c.bottom)], selection, frame };
|
|
11641
|
+
}
|
|
11642
|
+
/** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
|
|
11643
|
+
close() {
|
|
11644
|
+
if (!this.alive) return false;
|
|
11645
|
+
if (!this.text.trim().length) {
|
|
11646
|
+
try {
|
|
11647
|
+
this.controller.removeLayer(this.layerId);
|
|
11648
|
+
} catch {
|
|
11649
|
+
}
|
|
11650
|
+
return false;
|
|
11651
|
+
}
|
|
11652
|
+
if (this.layer.name === "Text" && this.changed) {
|
|
11653
|
+
try {
|
|
11654
|
+
this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
|
|
11655
|
+
} catch {
|
|
11656
|
+
}
|
|
11657
|
+
}
|
|
11658
|
+
return true;
|
|
11659
|
+
}
|
|
11660
|
+
};
|
|
11661
|
+
function typeLayerAt(controller, p, slop = 4) {
|
|
11662
|
+
const layers = [...flattenTypeLayers(controller.getLayers())].reverse();
|
|
11663
|
+
for (const l of layers) {
|
|
11664
|
+
if (!l.visible) continue;
|
|
11665
|
+
const inv = invert(textMatrix(l));
|
|
11666
|
+
const q = applyMatrix(inv, p);
|
|
11667
|
+
let b = null;
|
|
11668
|
+
if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
|
|
11669
|
+
else {
|
|
11670
|
+
const layout = controller.getTextLayout(l.id);
|
|
11671
|
+
if (layout) b = layout.bounds;
|
|
11672
|
+
}
|
|
11673
|
+
if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;
|
|
11674
|
+
}
|
|
11675
|
+
return null;
|
|
11676
|
+
}
|
|
11677
|
+
function flattenTypeLayers(layers) {
|
|
11678
|
+
const out = [];
|
|
11679
|
+
const walk = (list) => {
|
|
11680
|
+
for (const l of list) {
|
|
11681
|
+
if (l.type === "type") out.push(l);
|
|
11682
|
+
else if (l.type === "group") walk(l.children);
|
|
11683
|
+
}
|
|
11684
|
+
};
|
|
11685
|
+
walk(layers);
|
|
11686
|
+
return out;
|
|
11687
|
+
}
|
|
11688
|
+
function invert(m) {
|
|
11689
|
+
const [a, b, c, d, e, f] = m;
|
|
11690
|
+
const det = a * d - b * c || 1;
|
|
11691
|
+
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
11692
|
+
}
|
|
11693
|
+
var TypeToolGesture = class {
|
|
11694
|
+
constructor(controller, options) {
|
|
11695
|
+
this.controller = controller;
|
|
11696
|
+
this.options = options;
|
|
11697
|
+
}
|
|
11698
|
+
controller;
|
|
11699
|
+
options;
|
|
11700
|
+
session = null;
|
|
11701
|
+
dragStart = null;
|
|
11702
|
+
error = null;
|
|
11703
|
+
down(p, mods = {}) {
|
|
11704
|
+
this.error = null;
|
|
11705
|
+
const hit = typeLayerAt(this.controller, p);
|
|
11706
|
+
if (this.session && this.session.alive && hit === this.session.layerId) {
|
|
11707
|
+
this.session.pointAt(p, mods.shift);
|
|
11708
|
+
this.dragStart = null;
|
|
11709
|
+
return;
|
|
11710
|
+
}
|
|
11711
|
+
this.finish();
|
|
11712
|
+
if (hit) {
|
|
11713
|
+
this.controller.setActiveLayer(hit);
|
|
11714
|
+
this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
|
|
11715
|
+
this.dragStart = null;
|
|
11716
|
+
return;
|
|
11717
|
+
}
|
|
11718
|
+
this.dragStart = { x: p.x, y: p.y };
|
|
11719
|
+
}
|
|
11720
|
+
/** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */
|
|
11721
|
+
up(p) {
|
|
11722
|
+
const start = this.dragStart;
|
|
11723
|
+
this.dragStart = null;
|
|
11724
|
+
if (!start) return this.session;
|
|
11725
|
+
const o = this.options();
|
|
11726
|
+
const w = Math.abs(p.x - start.x);
|
|
11727
|
+
const h = Math.abs(p.y - start.y);
|
|
11728
|
+
try {
|
|
11729
|
+
const box = w >= 8 && h >= 8 ? { width: w, height: h } : void 0;
|
|
11730
|
+
const id = this.controller.addTextLayer({
|
|
11731
|
+
text: "",
|
|
11732
|
+
x: box ? Math.min(start.x, p.x) : start.x,
|
|
11733
|
+
y: box ? Math.min(start.y, p.y) : start.y,
|
|
11734
|
+
...box ? { box } : {},
|
|
11735
|
+
style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
|
|
11736
|
+
paragraph: o.align !== "left" ? { align: o.align } : {},
|
|
11737
|
+
name: "Text"
|
|
11738
|
+
});
|
|
11739
|
+
this.session = new TextEditSession(this.controller, id, 0);
|
|
11740
|
+
} catch (e) {
|
|
11741
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
11742
|
+
}
|
|
11743
|
+
return this.session;
|
|
11744
|
+
}
|
|
11745
|
+
/** Ends the open session (an empty new layer is removed). */
|
|
11746
|
+
finish() {
|
|
11747
|
+
const s = this.session;
|
|
11748
|
+
this.session = null;
|
|
11749
|
+
s?.close();
|
|
11750
|
+
}
|
|
11751
|
+
};
|
|
11752
|
+
|
|
10564
11753
|
// src/image-editor/layers/panel.ts
|
|
10565
11754
|
function layerPanelRows(layers, activeId) {
|
|
10566
11755
|
const rows = [];
|
|
@@ -10601,7 +11790,18 @@ function layerBadge(layer) {
|
|
|
10601
11790
|
return "so";
|
|
10602
11791
|
case "shape":
|
|
10603
11792
|
return "shp";
|
|
11793
|
+
case "type":
|
|
11794
|
+
return "T";
|
|
11795
|
+
}
|
|
11796
|
+
}
|
|
11797
|
+
function layerHasThumbnail(layer) {
|
|
11798
|
+
return layer.type === "pixel" || layer.type === "shape" || layer.type === "type";
|
|
11799
|
+
}
|
|
11800
|
+
function layerWarning(layer) {
|
|
11801
|
+
if (layer.type === "type" && layer.missingFonts?.length) {
|
|
11802
|
+
return `Missing ${layer.missingFonts.length === 1 ? "font" : "fonts"}: ${layer.missingFonts.join(", ")}. ${layer.rendering === "stored" ? "Showing the saved pixels until the text is edited." : "Drawn with a fallback font."}`;
|
|
10604
11803
|
}
|
|
11804
|
+
return null;
|
|
10605
11805
|
}
|
|
10606
11806
|
function dropZone(fraction, isGroup) {
|
|
10607
11807
|
if (isGroup && fraction > 0.3 && fraction < 0.7) return "inside";
|
|
@@ -10640,7 +11840,7 @@ function layerMenu(controller) {
|
|
|
10640
11840
|
{ action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
|
|
10641
11841
|
{ action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
|
|
10642
11842
|
{ action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
|
|
10643
|
-
{ action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
|
|
11843
|
+
{ action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || active?.type === "type" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
|
|
10644
11844
|
{ action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
|
|
10645
11845
|
{ action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
|
|
10646
11846
|
{ action: "smart-via-copy", label: "New Smart Object via Copy", enabled: active?.type === "smart" },
|
|
@@ -11573,6 +12773,7 @@ export {
|
|
|
11573
12773
|
D50,
|
|
11574
12774
|
D65,
|
|
11575
12775
|
DEFAULT_ADJUSTMENTS,
|
|
12776
|
+
DEFAULT_AUTO_LEADING,
|
|
11576
12777
|
DEFAULT_BLENDER,
|
|
11577
12778
|
DEFAULT_BRUSH,
|
|
11578
12779
|
DEFAULT_DEPTH_MASK,
|
|
@@ -11581,8 +12782,11 @@ export {
|
|
|
11581
12782
|
DEFAULT_MIXER,
|
|
11582
12783
|
DEFAULT_REFINE_EDGE_PARAMS,
|
|
11583
12784
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
12785
|
+
DEFAULT_SHAPE_TOOL_OPTIONS,
|
|
11584
12786
|
DEFAULT_SMUDGE,
|
|
12787
|
+
DEFAULT_TEXT_STYLE,
|
|
11585
12788
|
DEFAULT_TRANSFORM,
|
|
12789
|
+
DEFAULT_TYPE_TOOL_OPTIONS,
|
|
11586
12790
|
DEPTH_MASK_CONTROLS,
|
|
11587
12791
|
DEVELOP_FILTER_ID,
|
|
11588
12792
|
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
@@ -11631,6 +12835,7 @@ export {
|
|
|
11631
12835
|
MixerStroke,
|
|
11632
12836
|
PAINTERLY_BLEND_MODES,
|
|
11633
12837
|
PAINT_TOOLS,
|
|
12838
|
+
PARAGRAPH_STYLE_FIELDS,
|
|
11634
12839
|
PATTERN_BLOCK_KEYS,
|
|
11635
12840
|
PIGMENT_BANDS,
|
|
11636
12841
|
PIGMENT_LAMBDA_MIN,
|
|
@@ -11650,23 +12855,30 @@ export {
|
|
|
11650
12855
|
SELECTION_MODIFY_OPS,
|
|
11651
12856
|
SELECTION_TOOLS,
|
|
11652
12857
|
SHADOW_AMBIENT,
|
|
12858
|
+
SHAPE_KIND_LABELS,
|
|
11653
12859
|
SHAPE_OPERATIONS,
|
|
11654
12860
|
SHAPE_RENDER_MARGIN,
|
|
11655
12861
|
SMART_RENDER_MARGIN,
|
|
11656
12862
|
SUPPORTED_ADJUSTMENT_KEYS,
|
|
11657
12863
|
SelectionGesture,
|
|
12864
|
+
ShapeDrawGesture,
|
|
11658
12865
|
SmudgeTip,
|
|
11659
12866
|
StrokeCoverage,
|
|
11660
12867
|
SurfaceDraft,
|
|
12868
|
+
TEXT_ALIGNS,
|
|
12869
|
+
TEXT_RENDER_MARGIN,
|
|
12870
|
+
TEXT_STYLE_FIELDS,
|
|
11661
12871
|
TILE,
|
|
11662
12872
|
TILE_SHIFT,
|
|
11663
12873
|
TINT_SCALE,
|
|
11664
12874
|
TONE_LUT_SIZE,
|
|
11665
12875
|
TONE_RANGE_PRESETS,
|
|
11666
12876
|
TRANSFORM_ANCHORS,
|
|
12877
|
+
TextEditSession,
|
|
11667
12878
|
TileMedium,
|
|
11668
12879
|
TiledSurface,
|
|
11669
12880
|
TransformTooLargeError,
|
|
12881
|
+
TypeToolGesture,
|
|
11670
12882
|
VIBRANCE_GREY,
|
|
11671
12883
|
WARP_GRID_OPTIONS,
|
|
11672
12884
|
WARP_STYLES,
|
|
@@ -11691,6 +12903,7 @@ export {
|
|
|
11691
12903
|
annotationLayerName,
|
|
11692
12904
|
annotationOfLayer,
|
|
11693
12905
|
annotationOfShape,
|
|
12906
|
+
annotationOfType,
|
|
11694
12907
|
applyAdjustmentLayers,
|
|
11695
12908
|
applyAdjustments,
|
|
11696
12909
|
applyBoxBlur,
|
|
@@ -11704,6 +12917,7 @@ export {
|
|
|
11704
12917
|
applyProcess2Adjustments,
|
|
11705
12918
|
applyRegionBlur,
|
|
11706
12919
|
applyRegionMosaic,
|
|
12920
|
+
applyShapeOptions,
|
|
11707
12921
|
applySmartFilters,
|
|
11708
12922
|
applyStrokeToTile,
|
|
11709
12923
|
backgroundLayer,
|
|
@@ -11733,6 +12947,7 @@ export {
|
|
|
11733
12947
|
calculateCropDrag,
|
|
11734
12948
|
canvasCompositeOperation,
|
|
11735
12949
|
canvasToImagePoint,
|
|
12950
|
+
caretAt,
|
|
11736
12951
|
changedTiles,
|
|
11737
12952
|
clampControlValue,
|
|
11738
12953
|
clampCropToImage,
|
|
@@ -11760,6 +12975,7 @@ export {
|
|
|
11760
12975
|
coveringRect,
|
|
11761
12976
|
createAdjustmentLayer,
|
|
11762
12977
|
createBlenderScratch,
|
|
12978
|
+
createCanvasTextShaper,
|
|
11763
12979
|
createImageCaptureSurface,
|
|
11764
12980
|
createImageEditTools,
|
|
11765
12981
|
createImageEditorToolProvider,
|
|
@@ -11771,10 +12987,15 @@ export {
|
|
|
11771
12987
|
createImageShapeTools,
|
|
11772
12988
|
createImageSmartObjectTools,
|
|
11773
12989
|
createImageTransformTools,
|
|
12990
|
+
createImageTypeTools,
|
|
12991
|
+
createOutlineTextShaper,
|
|
11774
12992
|
createPigment,
|
|
11775
12993
|
createPigmentTools,
|
|
11776
12994
|
createSelectionMask,
|
|
11777
12995
|
createShapeLayer,
|
|
12996
|
+
createTypeLayer,
|
|
12997
|
+
cssFont,
|
|
12998
|
+
cssFontFamily,
|
|
11778
12999
|
curveLut,
|
|
11779
13000
|
curveLutFromLevels,
|
|
11780
13001
|
curvePointToBox,
|
|
@@ -11843,9 +13064,12 @@ export {
|
|
|
11843
13064
|
findLayer,
|
|
11844
13065
|
findLayerControl,
|
|
11845
13066
|
finishTile,
|
|
13067
|
+
fitParagraphRuns,
|
|
13068
|
+
fitRuns,
|
|
11846
13069
|
flattenLayers,
|
|
11847
13070
|
flattenPath,
|
|
11848
13071
|
flipCorners,
|
|
13072
|
+
fontOf,
|
|
11849
13073
|
formatControlValue,
|
|
11850
13074
|
fullSelection,
|
|
11851
13075
|
gaussianBlur,
|
|
@@ -11926,6 +13150,7 @@ export {
|
|
|
11926
13150
|
layerEffectsBadge,
|
|
11927
13151
|
layerEffectsOf,
|
|
11928
13152
|
layerEffectsPanel,
|
|
13153
|
+
layerHasThumbnail,
|
|
11929
13154
|
layerLabel,
|
|
11930
13155
|
layerMaskCoverage,
|
|
11931
13156
|
layerMaskFromSelection,
|
|
@@ -11935,7 +13160,9 @@ export {
|
|
|
11935
13160
|
layerPanelRows,
|
|
11936
13161
|
layerTree,
|
|
11937
13162
|
layerTypeLabel,
|
|
13163
|
+
layerWarning,
|
|
11938
13164
|
layersFromLegacy,
|
|
13165
|
+
layoutText,
|
|
11939
13166
|
legacyAdjustmentLayer,
|
|
11940
13167
|
legacyMaskOf,
|
|
11941
13168
|
legacyView,
|
|
@@ -11945,6 +13172,7 @@ export {
|
|
|
11945
13172
|
linearSrgbToXy,
|
|
11946
13173
|
linearToSrgb,
|
|
11947
13174
|
liveComponent,
|
|
13175
|
+
liveShapeForDrag,
|
|
11948
13176
|
liveShapeSubpaths,
|
|
11949
13177
|
loadImageEditorDocument,
|
|
11950
13178
|
loadMixer,
|
|
@@ -11995,11 +13223,13 @@ export {
|
|
|
11995
13223
|
normalizeSmudge,
|
|
11996
13224
|
normalizeStoredAdjustments,
|
|
11997
13225
|
normalizeSubpaths,
|
|
13226
|
+
normalizeText,
|
|
11998
13227
|
offsetLayer,
|
|
11999
13228
|
offsetShape,
|
|
12000
13229
|
offsetSmartObject,
|
|
12001
13230
|
openPsdFile,
|
|
12002
13231
|
packRgba,
|
|
13232
|
+
paragraphRanges,
|
|
12003
13233
|
parseAdjustmentBlock,
|
|
12004
13234
|
parseBlend,
|
|
12005
13235
|
parseColor,
|
|
@@ -12011,6 +13241,8 @@ export {
|
|
|
12011
13241
|
parsePatternBlock,
|
|
12012
13242
|
parseSelectionMode,
|
|
12013
13243
|
parseVersionedDescriptor,
|
|
13244
|
+
patchParagraphStyle,
|
|
13245
|
+
patchTextStyle,
|
|
12014
13246
|
photoFilterMatrix,
|
|
12015
13247
|
pigment8,
|
|
12016
13248
|
pixelLayerBounds,
|
|
@@ -12066,19 +13298,24 @@ export {
|
|
|
12066
13298
|
renderShape,
|
|
12067
13299
|
renderShapeRect,
|
|
12068
13300
|
renderSmartObject,
|
|
13301
|
+
renderText,
|
|
12069
13302
|
replaceContents,
|
|
13303
|
+
replaceTextRange,
|
|
12070
13304
|
requireLayer,
|
|
12071
13305
|
requireOperation,
|
|
12072
13306
|
requireShapeLayer,
|
|
12073
13307
|
requireSmartObject,
|
|
13308
|
+
requireTypeLayer,
|
|
12074
13309
|
requireUnlocked,
|
|
12075
13310
|
rerender,
|
|
12076
13311
|
rerenderShape,
|
|
13312
|
+
rerenderText,
|
|
12077
13313
|
resizePixels,
|
|
12078
13314
|
resizeSelection,
|
|
12079
13315
|
resizeWarpMesh,
|
|
12080
13316
|
resolveExternalAssets,
|
|
12081
13317
|
resolveMosaicRegionBounds,
|
|
13318
|
+
restyleRange,
|
|
12082
13319
|
rgbToGray,
|
|
12083
13320
|
rgbToHsl,
|
|
12084
13321
|
rgbToReflectance,
|
|
@@ -12125,16 +13362,22 @@ export {
|
|
|
12125
13362
|
smoothCoverage,
|
|
12126
13363
|
smoothPoints,
|
|
12127
13364
|
solidSurface,
|
|
13365
|
+
spliceRuns,
|
|
12128
13366
|
splitOverlayAnnotations,
|
|
12129
13367
|
srgb8ToLab,
|
|
12130
13368
|
srgbToLinear,
|
|
12131
13369
|
standardBlendMode,
|
|
13370
|
+
styleAt,
|
|
12132
13371
|
sumHistograms,
|
|
12133
13372
|
surfaceBytes,
|
|
12134
13373
|
surfaceFromPixels,
|
|
12135
13374
|
surfacesEqual,
|
|
12136
13375
|
tempTintToXy,
|
|
12137
13376
|
temperatureToKelvin,
|
|
13377
|
+
textIndexAt,
|
|
13378
|
+
textMatrix,
|
|
13379
|
+
textOutlines,
|
|
13380
|
+
textRange,
|
|
12138
13381
|
tileCoord,
|
|
12139
13382
|
tileKey,
|
|
12140
13383
|
tileRect,
|
|
@@ -12143,6 +13386,7 @@ export {
|
|
|
12143
13386
|
tipReach,
|
|
12144
13387
|
tipShape,
|
|
12145
13388
|
toHex,
|
|
13389
|
+
toTextSpace,
|
|
12146
13390
|
toneCurve,
|
|
12147
13391
|
toneLuts,
|
|
12148
13392
|
toneRange,
|
|
@@ -12150,10 +13394,16 @@ export {
|
|
|
12150
13394
|
transformMap,
|
|
12151
13395
|
transformShapeComponents,
|
|
12152
13396
|
transformSurface,
|
|
13397
|
+
transformTypeLayer,
|
|
12153
13398
|
transformedBounds,
|
|
12154
13399
|
transformedBox,
|
|
12155
13400
|
transformedPoint,
|
|
12156
13401
|
translateSurface,
|
|
13402
|
+
typeAsPixelLayer,
|
|
13403
|
+
typeFrame,
|
|
13404
|
+
typeFromAnnotation,
|
|
13405
|
+
typeLayerAt,
|
|
13406
|
+
typeLayerName,
|
|
12157
13407
|
ungroupLayer,
|
|
12158
13408
|
uniformTile,
|
|
12159
13409
|
unionRect,
|
|
@@ -12163,6 +13413,7 @@ export {
|
|
|
12163
13413
|
updateLayerMaskProps,
|
|
12164
13414
|
updateShapeLayer,
|
|
12165
13415
|
updateSmartFilter,
|
|
13416
|
+
updateTypeLayer,
|
|
12166
13417
|
uvToXy,
|
|
12167
13418
|
validateEffectGradient,
|
|
12168
13419
|
validateLayerEffect,
|