@pixerate/editor 0.111.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/dist/{chunk-HHZ6ZJTW.js → chunk-CZOXTEMN.js} +1772 -456
- package/dist/chunk-CZOXTEMN.js.map +1 -0
- package/dist/{chunk-PHUMLSCW.cjs → chunk-YGIMK4NX.cjs} +1810 -494
- package/dist/chunk-YGIMK4NX.cjs.map +1 -0
- package/dist/{export-DoDhCszr.d.cts → export-DBba-buV.d.cts} +40 -13
- package/dist/{export-DBNVwb6h.d.ts → export-DNu_Jr8p.d.ts} +40 -13
- package/dist/image-editor/index.cjs +1166 -619
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +159 -3
- package/dist/image-editor/index.d.ts +159 -3
- package/dist/image-editor/index.js +554 -7
- package/dist/image-editor/index.js.map +1 -1
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.d.cts +1 -1
- package/dist/paint-editor/index.d.ts +1 -1
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-HHZ6ZJTW.js.map +0 -1
- package/dist/chunk-PHUMLSCW.cjs.map +0 -1
|
@@ -369,7 +369,7 @@ import {
|
|
|
369
369
|
writePsd,
|
|
370
370
|
xyToTempTint,
|
|
371
371
|
xyToUv
|
|
372
|
-
} from "../chunk-
|
|
372
|
+
} from "../chunk-CZOXTEMN.js";
|
|
373
373
|
import {
|
|
374
374
|
DEFAULT_GLOBAL_LIGHT,
|
|
375
375
|
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
@@ -1065,6 +1065,45 @@ var ImageEditorRenderer = class {
|
|
|
1065
1065
|
}
|
|
1066
1066
|
ctx.restore();
|
|
1067
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
|
+
}
|
|
1068
1107
|
if (options.freeTransform) {
|
|
1069
1108
|
const t = options.freeTransform;
|
|
1070
1109
|
ctx.save();
|
|
@@ -6475,7 +6514,7 @@ var ImageEditorController = class {
|
|
|
6475
6514
|
s.sourceUrl = null;
|
|
6476
6515
|
s.imageDimensions = { width: result.width, height: result.height };
|
|
6477
6516
|
if (!this.samplePixelsOverride) this.samplePixels = null;
|
|
6478
|
-
s.layers = result
|
|
6517
|
+
s.layers = this.checkPsdFonts(result);
|
|
6479
6518
|
s.crop = null;
|
|
6480
6519
|
s.transform = { ...DEFAULT_TRANSFORM_VALUES };
|
|
6481
6520
|
s.outputOpacity = 1;
|
|
@@ -6500,6 +6539,30 @@ var ImageEditorController = class {
|
|
|
6500
6539
|
summary: result.summary
|
|
6501
6540
|
};
|
|
6502
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
|
+
}
|
|
6503
6566
|
/**
|
|
6504
6567
|
* The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment
|
|
6505
6568
|
* layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked
|
|
@@ -6578,8 +6641,8 @@ var ImageEditorController = class {
|
|
|
6578
6641
|
return out2;
|
|
6579
6642
|
}
|
|
6580
6643
|
if (layer.type === "shape" || layer.type === "type") {
|
|
6581
|
-
|
|
6582
|
-
const out2 =
|
|
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 };
|
|
6583
6646
|
const mask2 = bakeMask(layer);
|
|
6584
6647
|
if (mask2) out2.mask = mask2;
|
|
6585
6648
|
else delete out2.mask;
|
|
@@ -8992,7 +9055,7 @@ function createImageEffectTools(controller) {
|
|
|
8992
9055
|
const add2 = {
|
|
8993
9056
|
name: "add_layer_effect",
|
|
8994
9057
|
title: "Add a layer effect",
|
|
8995
|
-
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.",
|
|
8996
9059
|
inputSchema: {
|
|
8997
9060
|
type: "object",
|
|
8998
9061
|
$defs: DEFS,
|
|
@@ -9466,7 +9529,7 @@ function createImagePsdTools(controller) {
|
|
|
9466
9529
|
const open = {
|
|
9467
9530
|
name: "import_psd",
|
|
9468
9531
|
title: "Open a PSD as the document",
|
|
9469
|
-
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).",
|
|
9470
9533
|
inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
|
|
9471
9534
|
annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE4 },
|
|
9472
9535
|
handler: ({ data }) => {
|
|
@@ -9485,7 +9548,7 @@ function createImagePsdTools(controller) {
|
|
|
9485
9548
|
const save = {
|
|
9486
9549
|
name: "export_psd",
|
|
9487
9550
|
title: "Export the document as a PSD",
|
|
9488
|
-
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.",
|
|
9489
9552
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
9490
9553
|
annotations: { title: "Export the document as a PSD", ...READ5 },
|
|
9491
9554
|
handler: async () => {
|
|
@@ -11223,6 +11286,470 @@ function applyRegionBlur(imageData, regions, radius = 10) {
|
|
|
11223
11286
|
return imageData;
|
|
11224
11287
|
}
|
|
11225
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
|
+
|
|
11226
11753
|
// src/image-editor/layers/panel.ts
|
|
11227
11754
|
function layerPanelRows(layers, activeId) {
|
|
11228
11755
|
const rows = [];
|
|
@@ -11267,6 +11794,15 @@ function layerBadge(layer) {
|
|
|
11267
11794
|
return "T";
|
|
11268
11795
|
}
|
|
11269
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."}`;
|
|
11803
|
+
}
|
|
11804
|
+
return null;
|
|
11805
|
+
}
|
|
11270
11806
|
function dropZone(fraction, isGroup) {
|
|
11271
11807
|
if (isGroup && fraction > 0.3 && fraction < 0.7) return "inside";
|
|
11272
11808
|
return fraction < 0.5 ? "above" : "below";
|
|
@@ -12246,9 +12782,11 @@ export {
|
|
|
12246
12782
|
DEFAULT_MIXER,
|
|
12247
12783
|
DEFAULT_REFINE_EDGE_PARAMS,
|
|
12248
12784
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
12785
|
+
DEFAULT_SHAPE_TOOL_OPTIONS,
|
|
12249
12786
|
DEFAULT_SMUDGE,
|
|
12250
12787
|
DEFAULT_TEXT_STYLE,
|
|
12251
12788
|
DEFAULT_TRANSFORM,
|
|
12789
|
+
DEFAULT_TYPE_TOOL_OPTIONS,
|
|
12252
12790
|
DEPTH_MASK_CONTROLS,
|
|
12253
12791
|
DEVELOP_FILTER_ID,
|
|
12254
12792
|
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
@@ -12317,11 +12855,13 @@ export {
|
|
|
12317
12855
|
SELECTION_MODIFY_OPS,
|
|
12318
12856
|
SELECTION_TOOLS,
|
|
12319
12857
|
SHADOW_AMBIENT,
|
|
12858
|
+
SHAPE_KIND_LABELS,
|
|
12320
12859
|
SHAPE_OPERATIONS,
|
|
12321
12860
|
SHAPE_RENDER_MARGIN,
|
|
12322
12861
|
SMART_RENDER_MARGIN,
|
|
12323
12862
|
SUPPORTED_ADJUSTMENT_KEYS,
|
|
12324
12863
|
SelectionGesture,
|
|
12864
|
+
ShapeDrawGesture,
|
|
12325
12865
|
SmudgeTip,
|
|
12326
12866
|
StrokeCoverage,
|
|
12327
12867
|
SurfaceDraft,
|
|
@@ -12334,9 +12874,11 @@ export {
|
|
|
12334
12874
|
TONE_LUT_SIZE,
|
|
12335
12875
|
TONE_RANGE_PRESETS,
|
|
12336
12876
|
TRANSFORM_ANCHORS,
|
|
12877
|
+
TextEditSession,
|
|
12337
12878
|
TileMedium,
|
|
12338
12879
|
TiledSurface,
|
|
12339
12880
|
TransformTooLargeError,
|
|
12881
|
+
TypeToolGesture,
|
|
12340
12882
|
VIBRANCE_GREY,
|
|
12341
12883
|
WARP_GRID_OPTIONS,
|
|
12342
12884
|
WARP_STYLES,
|
|
@@ -12375,6 +12917,7 @@ export {
|
|
|
12375
12917
|
applyProcess2Adjustments,
|
|
12376
12918
|
applyRegionBlur,
|
|
12377
12919
|
applyRegionMosaic,
|
|
12920
|
+
applyShapeOptions,
|
|
12378
12921
|
applySmartFilters,
|
|
12379
12922
|
applyStrokeToTile,
|
|
12380
12923
|
backgroundLayer,
|
|
@@ -12607,6 +13150,7 @@ export {
|
|
|
12607
13150
|
layerEffectsBadge,
|
|
12608
13151
|
layerEffectsOf,
|
|
12609
13152
|
layerEffectsPanel,
|
|
13153
|
+
layerHasThumbnail,
|
|
12610
13154
|
layerLabel,
|
|
12611
13155
|
layerMaskCoverage,
|
|
12612
13156
|
layerMaskFromSelection,
|
|
@@ -12616,6 +13160,7 @@ export {
|
|
|
12616
13160
|
layerPanelRows,
|
|
12617
13161
|
layerTree,
|
|
12618
13162
|
layerTypeLabel,
|
|
13163
|
+
layerWarning,
|
|
12619
13164
|
layersFromLegacy,
|
|
12620
13165
|
layoutText,
|
|
12621
13166
|
legacyAdjustmentLayer,
|
|
@@ -12627,6 +13172,7 @@ export {
|
|
|
12627
13172
|
linearSrgbToXy,
|
|
12628
13173
|
linearToSrgb,
|
|
12629
13174
|
liveComponent,
|
|
13175
|
+
liveShapeForDrag,
|
|
12630
13176
|
liveShapeSubpaths,
|
|
12631
13177
|
loadImageEditorDocument,
|
|
12632
13178
|
loadMixer,
|
|
@@ -12856,6 +13402,7 @@ export {
|
|
|
12856
13402
|
typeAsPixelLayer,
|
|
12857
13403
|
typeFrame,
|
|
12858
13404
|
typeFromAnnotation,
|
|
13405
|
+
typeLayerAt,
|
|
12859
13406
|
typeLayerName,
|
|
12860
13407
|
ungroupLayer,
|
|
12861
13408
|
uniformTile,
|