@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.
@@ -369,7 +369,7 @@ import {
369
369
  writePsd,
370
370
  xyToTempTint,
371
371
  xyToUv
372
- } from "../chunk-HHZ6ZJTW.js";
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.layers;
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
- add2(layer.type === "shape" ? "Shape layers were written as pixel layers" : "Type layers were written as pixel layers", layer.name);
6582
- const out2 = layer.type === "shape" ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);
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); type, fill and shape 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).",
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,