@pixerate/editor 0.191.0 → 0.193.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.
Files changed (55) hide show
  1. package/dist/animation-editor/index.cjs +56 -56
  2. package/dist/animation-editor/index.js +2 -2
  3. package/dist/{chunk-ZQGZWFPU.cjs → chunk-3T5VCDIV.cjs} +97 -5
  4. package/dist/{chunk-RQ2EM5TY.js → chunk-5OC22IEO.js} +97 -5
  5. package/dist/{chunk-EGZM52EO.cjs → chunk-BRKVNXDZ.cjs} +354 -125
  6. package/dist/{chunk-XCDGM7TD.cjs → chunk-FR2Q2LVF.cjs} +35 -35
  7. package/dist/{chunk-LPIKXPBM.js → chunk-GHCXLQ47.js} +262 -33
  8. package/dist/{chunk-VRXXAXUT.js → chunk-MLC7HUHO.js} +1 -1
  9. package/dist/{chunk-2UR4OX3B.cjs → chunk-UYXL6ZWQ.cjs} +114 -97
  10. package/dist/{chunk-TXYXF32N.js → chunk-ZIDUPSP4.js} +35 -18
  11. package/dist/image-editor/controller.d.cts +31 -2
  12. package/dist/image-editor/controller.d.ts +31 -2
  13. package/dist/image-editor/index.cjs +1064 -923
  14. package/dist/image-editor/index.js +204 -63
  15. package/dist/image-editor/layers/model.d.cts +7 -2
  16. package/dist/image-editor/layers/model.d.ts +7 -2
  17. package/dist/image-editor/layers/serialize.d.cts +2 -0
  18. package/dist/image-editor/layers/serialize.d.ts +2 -0
  19. package/dist/image-editor/layers/shape-type-ui.d.cts +8 -2
  20. package/dist/image-editor/layers/shape-type-ui.d.ts +8 -2
  21. package/dist/image-editor/layers/text.d.cts +6 -2
  22. package/dist/image-editor/layers/text.d.ts +6 -2
  23. package/dist/image-editor/psd/type.d.cts +2 -1
  24. package/dist/image-editor/psd/type.d.ts +2 -1
  25. package/dist/image-editor/selection/tool-options.d.cts +7 -1
  26. package/dist/image-editor/selection/tool-options.d.ts +7 -1
  27. package/dist/image-editor/tools.d.cts +1 -1
  28. package/dist/image-editor/tools.d.ts +1 -1
  29. package/dist/image-editor/type-tools.d.cts +2 -2
  30. package/dist/image-editor/type-tools.d.ts +2 -2
  31. package/dist/paint-editor/index.cjs +7 -7
  32. package/dist/paint-editor/index.js +2 -2
  33. package/dist/text/layout.d.cts +93 -12
  34. package/dist/text/layout.d.ts +93 -12
  35. package/dist/text/opentype.d.cts +13 -3
  36. package/dist/text/opentype.d.ts +13 -3
  37. package/dist/text/shapers.d.cts +12 -0
  38. package/dist/text/shapers.d.ts +12 -0
  39. package/dist/text/types.d.cts +36 -2
  40. package/dist/text/types.d.ts +36 -2
  41. package/dist/vector/controller.d.cts +8 -0
  42. package/dist/vector/controller.d.ts +8 -0
  43. package/dist/vector/index.cjs +1039 -967
  44. package/dist/vector/index.js +90 -18
  45. package/dist/vector/text.d.cts +5 -3
  46. package/dist/vector/text.d.ts +5 -3
  47. package/dist/vector/textPanel.d.cts +6 -0
  48. package/dist/vector/textPanel.d.ts +6 -0
  49. package/dist/vector/types.d.cts +9 -0
  50. package/dist/vector/types.d.ts +9 -0
  51. package/package.json +1 -1
  52. package/skills/pixerate-image-editor/SKILL.md +4 -2
  53. package/skills/pixerate-image-editor/references/shapes-and-type.md +19 -5
  54. package/skills/pixerate-image-selection-masking/SKILL.md +2 -0
  55. package/skills/pixerate-vector/SKILL.md +9 -3
@@ -2,7 +2,7 @@ import {
2
2
  loadOpenTypeFont,
3
3
  parseOpenTypeFont,
4
4
  traceField
5
- } from "../chunk-RQ2EM5TY.js";
5
+ } from "../chunk-5OC22IEO.js";
6
6
  import {
7
7
  ADJUSTMENT_KEYS,
8
8
  ADJUSTMENT_KINDS,
@@ -46,6 +46,7 @@ import {
46
46
  SMART_RENDER_MARGIN,
47
47
  SUPPORTED_ADJUSTMENT_KEYS,
48
48
  TEXT_ALIGNS,
49
+ TEXT_ORIENTATIONS,
49
50
  TEXT_RENDER_MARGIN,
50
51
  TEXT_STYLE_FIELDS,
51
52
  TINT_SCALE,
@@ -339,6 +340,7 @@ import {
339
340
  tempTintToXy,
340
341
  temperatureToKelvin,
341
342
  textMatrix,
343
+ textOrientation,
342
344
  textOutlines,
343
345
  textRange,
344
346
  toTextSpace,
@@ -384,7 +386,7 @@ import {
384
386
  writePsd,
385
387
  xyToTempTint,
386
388
  xyToUv
387
- } from "../chunk-TXYXF32N.js";
389
+ } from "../chunk-ZIDUPSP4.js";
388
390
  import {
389
391
  parseCssColor
390
392
  } from "../chunk-3CI7FRMK.js";
@@ -622,6 +624,7 @@ import {
622
624
  identityWarpMesh,
623
625
  invertMatrix,
624
626
  layoutText,
627
+ lineToTextSpace,
625
628
  multiplyMatrices,
626
629
  paragraphRanges,
627
630
  pullWarpMesh,
@@ -632,7 +635,10 @@ import {
632
635
  spliceRuns,
633
636
  styleAt,
634
637
  subpathsToBezier,
638
+ textArrowStep,
635
639
  textIndexAt,
640
+ textLineMove,
641
+ textSelectionQuads,
636
642
  translateMatrix,
637
643
  warpMeshColumns,
638
644
  warpMeshEditPoints,
@@ -640,7 +646,7 @@ import {
640
646
  warpMeshParamAt,
641
647
  warpMeshRows,
642
648
  warpPresetMesh
643
- } from "../chunk-LPIKXPBM.js";
649
+ } from "../chunk-GHCXLQ47.js";
644
650
  import "../chunk-YXKLVD56.js";
645
651
  import "../chunk-M6DNZNVY.js";
646
652
  import {
@@ -2704,7 +2710,11 @@ var TextEditSession = class {
2704
2710
  if (dir > 0 && c >= 55296 && c <= 56319 && i + 1 < t.length) return 2;
2705
2711
  return 1;
2706
2712
  }
2707
- /** Moves the caret (Shift extends the selection). */
2713
+ /**
2714
+ * Moves the caret (Shift extends the selection): `left` / `right` along the line, `up` / `down`
2715
+ * between lines, `home` / `end` of the line, `start` / `finish` of the text. Arrow keys of vertical
2716
+ * text map onto these through `textArrowStep` in `handleKey`.
2717
+ */
2708
2718
  move(to, extend = false) {
2709
2719
  const n = this.text.length;
2710
2720
  let f = this.focus;
@@ -2723,15 +2733,7 @@ var TextEditSession = class {
2723
2733
  const last = line.chars[line.chars.length - 1];
2724
2734
  f = last ? Math.min(n, last.index + 1) : line.start;
2725
2735
  if (last && this.text[last.index] === " " && c.line < layout.lines.length - 1) f = last.index;
2726
- } else {
2727
- const target = layout.lines[c.line + (to === "up" ? -1 : 1)];
2728
- if (!target) f = to === "up" ? 0 : n;
2729
- else {
2730
- const hit = target.chars.find((ch) => c.x < ch.x + ch.advance / 2);
2731
- const last = target.chars[target.chars.length - 1];
2732
- f = hit ? hit.index : last ? last.index + 1 : target.start;
2733
- }
2734
- }
2736
+ } else f = textLineMove(layout, f, to === "up" ? -1 : 1, n);
2735
2737
  }
2736
2738
  }
2737
2739
  this.focus = f;
@@ -2767,17 +2769,16 @@ var TextEditSession = class {
2767
2769
  this.insert("\n");
2768
2770
  return true;
2769
2771
  case "ArrowLeft":
2770
- this.move(mod2 ? "home" : "left", e.shiftKey);
2771
- return true;
2772
2772
  case "ArrowRight":
2773
- this.move(mod2 ? "end" : "right", e.shiftKey);
2774
- return true;
2775
2773
  case "ArrowUp":
2776
- this.move(mod2 ? "start" : "up", e.shiftKey);
2777
- return true;
2778
- case "ArrowDown":
2779
- this.move(mod2 ? "finish" : "down", e.shiftKey);
2774
+ case "ArrowDown": {
2775
+ const step = textArrowStep(this.layer.orientation, e.key);
2776
+ if (step === "previous") this.move(mod2 ? "home" : "left", e.shiftKey);
2777
+ else if (step === "next") this.move(mod2 ? "end" : "right", e.shiftKey);
2778
+ else if (step === "line-before") this.move(mod2 ? "start" : "up", e.shiftKey);
2779
+ else this.move(mod2 ? "finish" : "down", e.shiftKey);
2780
2780
  return true;
2781
+ }
2781
2782
  case "Home":
2782
2783
  this.move("home", e.shiftKey);
2783
2784
  return true;
@@ -2829,20 +2830,9 @@ var TextEditSession = class {
2829
2830
  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)];
2830
2831
  }
2831
2832
  if (!layout) return { caret: null, selection: [], frame };
2832
- const selection = [];
2833
- if (this.start !== this.end) {
2834
- for (const l of layout.lines) {
2835
- const chars = l.chars.filter((c2) => c2.index >= this.start && c2.index < this.end);
2836
- if (!chars.length) continue;
2837
- const x0 = chars[0].x;
2838
- const x1 = chars[chars.length - 1].x + chars[chars.length - 1].advance;
2839
- const top = l.baseline - l.ascent;
2840
- const bottom = l.baseline + l.descent;
2841
- selection.push([at(x0, top), at(x1, top), at(x1, bottom), at(x0, bottom)]);
2842
- }
2843
- }
2833
+ const selection = textSelectionQuads(layout, this.start, this.end).map((q) => q.map((p) => at(p.x, p.y)));
2844
2834
  const c = caretAt(layout, this.focus);
2845
- return { caret: [at(c.x, c.top), at(c.x, c.bottom)], selection, frame };
2835
+ return { caret: [at(c.from.x, c.from.y), at(c.to.x, c.to.y)], selection, frame };
2846
2836
  }
2847
2837
  /** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
2848
2838
  close() {
@@ -2939,6 +2929,7 @@ var TypeToolGesture = class {
2939
2929
  ...box ? { box } : {},
2940
2930
  style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
2941
2931
  paragraph: o.align !== "left" ? { align: o.align } : {},
2932
+ ...o.orientation === "vertical" ? { orientation: "vertical" } : {},
2942
2933
  name: "Text"
2943
2934
  });
2944
2935
  this.session = new TextEditSession(this.controller, id, 0);
@@ -9328,6 +9319,7 @@ var ImageEditorController = class {
9328
9319
  runs: ranges(layer.runs),
9329
9320
  paragraphs: ranges(layer.paragraphs),
9330
9321
  frame: { ...layer.frame },
9322
+ orientation: layer.orientation ?? "horizontal",
9331
9323
  transform: [...layer.transform ?? [1, 0, 0, 1, 0, 0]],
9332
9324
  lines,
9333
9325
  bounds: this.getLayerBounds(id),
@@ -9346,6 +9338,55 @@ var ImageEditorController = class {
9346
9338
  * text wrapped in `box`. Refused without a text shaper. Returns its id.
9347
9339
  */
9348
9340
  addTextLayer(init) {
9341
+ const layer = this.layoutTypeLayer(init);
9342
+ 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.");
9343
+ if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
9344
+ if (init.opacity !== void 0) layer.opacity = init.opacity;
9345
+ if (init.blend !== void 0) layer.blend = init.blend;
9346
+ const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
9347
+ this.commitLayers(next, () => {
9348
+ this.state.activeLayerId = layer.id;
9349
+ this.state.activeTarget = "pixels";
9350
+ });
9351
+ return layer.id;
9352
+ }
9353
+ /**
9354
+ * Type Mask (horizontal or vertical): a selection in the shape of `text`, laid out and drawn as
9355
+ * `addTextLayer` would place it (font, size, tracking, box, rotation), combined with the current
9356
+ * selection by `mode`. Vertical type is laid out as vertical type layers are: columns down from
9357
+ * (`x`, `y`), right to left, CJK upright and Latin turned. No layer is added. One undo step. Returns whether anything is selected.
9358
+ */
9359
+ selectTextShape(init) {
9360
+ const vertical = init.orientation === "vertical";
9361
+ if (init.orientation !== void 0 && init.orientation !== "horizontal" && !vertical) throw new LayerOperationError("orientation must be horizontal or vertical.");
9362
+ if (init.feather !== void 0 && !(init.feather >= 0 && init.feather <= 1e3)) throw new LayerOperationError("feather must be from 0 to 1000 px.");
9363
+ const text = normalizeText(init.text);
9364
+ if (!text.trim()) throw new LayerOperationError("The text is empty, so there is nothing to select; give some characters.");
9365
+ const layer = this.layoutTypeLayer({ ...init, text, orientation: vertical ? "vertical" : "horizontal" });
9366
+ const { width, height } = this.naturalSize;
9367
+ const b = contentBounds(layer.surface);
9368
+ const x0 = Math.max(0, b?.x ?? 0);
9369
+ const y0 = Math.max(0, b?.y ?? 0);
9370
+ const x1 = Math.min(width, b ? b.x + b.width : 0);
9371
+ const y1 = Math.min(height, b ? b.y + b.height : 0);
9372
+ let mask;
9373
+ if (!b || x1 <= x0 || y1 <= y0) mask = createSelectionMask(width, height);
9374
+ else {
9375
+ const rect = { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
9376
+ const rgba = readRegion(layer.surface, rect);
9377
+ const bytes = new Uint8Array(rect.width * rect.height);
9378
+ const hard = init.antiAlias === false;
9379
+ for (let i = 0; i < bytes.length; i++) {
9380
+ const a = rgba[i * 4 + 3];
9381
+ bytes[i] = hard ? a >= 128 ? 255 : 0 : a;
9382
+ }
9383
+ mask = selectionFromRegion(width, height, rect, bytes);
9384
+ }
9385
+ if ((init.feather ?? 0) > 0 && mask.bounds) mask = modifySelection(mask, "feather", init.feather);
9386
+ return this.setSelection(mask.bounds ? mask : null, init.mode ?? "replace");
9387
+ }
9388
+ /** A type layer laid out and drawn from `init` (not added): the shared part of `addTextLayer` and the type masks. */
9389
+ layoutTypeLayer(init) {
9349
9390
  const text = normalizeText(init.text);
9350
9391
  if (!Number.isFinite(init.x) || !Number.isFinite(init.y)) throw new LayerOperationError("x and y must be numbers (document pixels).");
9351
9392
  const style = patchTextStyle({ ...DEFAULT_TEXT_STYLE, color: this.state.paint.foreground }, init.style ?? {});
@@ -9356,20 +9397,11 @@ var ImageEditorController = class {
9356
9397
  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.");
9357
9398
  frame = { kind: "box", width, height };
9358
9399
  }
9400
+ const orientation = init.orientation === void 0 ? "horizontal" : textOrientation(init.orientation);
9359
9401
  if (init.rotation !== void 0 && !Number.isFinite(init.rotation)) throw new LayerOperationError("rotation must be a number of degrees.");
9360
- 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.");
9361
- if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
9362
9402
  const t = [1, 0, 0, 1, init.x, init.y];
9363
9403
  const transform = init.rotation ? multiplyMatrices(t, rotateMatrix(init.rotation, { x: init.x, y: init.y })) : t;
9364
- const layer = createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());
9365
- if (init.opacity !== void 0) layer.opacity = init.opacity;
9366
- if (init.blend !== void 0) layer.blend = init.blend;
9367
- const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
9368
- this.commitLayers(next, () => {
9369
- this.state.activeLayerId = layer.id;
9370
- this.state.activeTarget = "pixels";
9371
- });
9372
- return layer.id;
9404
+ return createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, orientation, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());
9373
9405
  }
9374
9406
  /**
9375
9407
  * Replaces text [start, end) (default all of it) with `text`; inserted text takes the style at
@@ -9444,14 +9476,27 @@ var ImageEditorController = class {
9444
9476
  this.state.layers,
9445
9477
  id,
9446
9478
  (l) => {
9447
- if (frame.kind === "box" && l.frame.kind === "box") return { frame: { kind: "box", width: frame.width, height: frame.height } };
9448
- if (!shaper) throw new LayerOperationError("Changing between point and paragraph text needs a text shaper to measure the text.");
9449
- const lines = layoutText(l, shaper).lines;
9450
- const first = lines[0];
9451
- const dx = frame.kind === "box" ? first ? first.x : 0 : 0;
9452
- const dy = frame.kind === "box" ? first ? first.baseline - first.ascent : 0 : first ? first.baseline : 0;
9453
9479
  const m = textMatrix(l);
9454
- 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]];
9480
+ const shift = (dx2, dy2) => [m[0], m[1], m[2], m[3], m[0] * dx2 + m[2] * dy2 + m[4], m[1] * dx2 + m[3] * dy2 + m[5]];
9481
+ if (frame.kind === "box" && l.frame.kind === "box") {
9482
+ const resized = { frame: { kind: "box", width: frame.width, height: frame.height } };
9483
+ if (l.orientation === "vertical" && frame.width !== l.frame.width) resized.transform = shift(l.frame.width - frame.width, 0);
9484
+ return resized;
9485
+ }
9486
+ if (!shaper) throw new LayerOperationError("Changing between point and paragraph text needs a text shaper to measure the text.");
9487
+ const layout = layoutText(l, shaper);
9488
+ const first = layout.lines[0];
9489
+ let dx;
9490
+ let dy;
9491
+ if (l.orientation === "vertical") {
9492
+ const at = !first ? { x: 0, y: 0 } : frame.kind === "box" ? lineToTextSpace(layout, { x: first.x, y: first.baseline - first.ascent }) : lineToTextSpace(layout, { x: 0, y: first.baseline });
9493
+ dx = frame.kind === "box" ? at.x - frame.width : at.x;
9494
+ dy = at.y;
9495
+ } else {
9496
+ dx = frame.kind === "box" ? first ? first.x : 0 : 0;
9497
+ dy = frame.kind === "box" ? first ? first.baseline - first.ascent : 0 : first ? first.baseline : 0;
9498
+ }
9499
+ const shifted = shift(dx, dy);
9455
9500
  const nextFrame = frame.kind === "box" ? { kind: "box", width: frame.width, height: frame.height } : { kind: "point" };
9456
9501
  return { frame: nextFrame, transform: shifted };
9457
9502
  },
@@ -9461,6 +9506,25 @@ var ImageEditorController = class {
9461
9506
  this.commitLayers(layers, void 0, commitToHistory);
9462
9507
  return this.typeInfo(id);
9463
9508
  }
9509
+ /**
9510
+ * Horizontal ↔ vertical type (Type › Orientation): the text, styles, frame and origin stay; the
9511
+ * lines become columns running down, stacked right to left, or back. One undo step. Returns the
9512
+ * layer.
9513
+ */
9514
+ setTextOrientation(id, orientation, commitToHistory = true) {
9515
+ const next = textOrientation(orientation);
9516
+ const layer = requireTypeLayer(this.state.layers, id);
9517
+ if ((layer.orientation ?? "horizontal") === next) return this.typeInfo(id);
9518
+ const { layers } = updateTypeLayer(
9519
+ this.state.layers,
9520
+ id,
9521
+ () => ({ orientation: next === "vertical" ? "vertical" : void 0 }),
9522
+ this.textEnv(),
9523
+ "change its orientation"
9524
+ );
9525
+ this.commitLayers(layers, void 0, commitToHistory);
9526
+ return this.typeInfo(id);
9527
+ }
9464
9528
  /**
9465
9529
  * Convert to Shape: the text's glyph outlines become a shape layer with the same id, name and
9466
9530
  * layer properties (needs a shaper that gives outlines). The fill is the first run's colour;
@@ -9506,8 +9570,8 @@ var ImageEditorController = class {
9506
9570
  if (!layout) return null;
9507
9571
  const c = caretAt(layout, Math.max(0, Math.min(layer.text.length, index)));
9508
9572
  const m = textMatrix(layer);
9509
- const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
9510
- return { top: at(c.x, c.top), bottom: at(c.x, c.bottom), line: c.line };
9573
+ const at = (p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
9574
+ return { top: at(c.from), bottom: at(c.to), line: c.line };
9511
9575
  }
9512
9576
  // --- Shape layers ---
9513
9577
  /** What shape rendering needs: the canvas size and the document's patterns. */
@@ -11255,8 +11319,8 @@ var DEPTH_MASK_CONTROLS = [
11255
11319
  // src/image-editor/playback.ts
11256
11320
  var IMAGE_PLAYBACK_KIND = "layer";
11257
11321
  var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
11258
- var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection)$/;
11259
- var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|convert_text_to_shape)$/;
11322
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
11323
+ var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|set_text_orientation|convert_text_to_shape)$/;
11260
11324
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
11261
11325
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
11262
11326
  function imagePlaybackPanel(tool) {
@@ -11376,7 +11440,9 @@ var SELECTION_TOOLS = [
11376
11440
  { id: "column", label: "Single Column" },
11377
11441
  { id: "lasso", label: "Lasso" },
11378
11442
  { id: "polygon", label: "Polygonal Lasso" },
11379
- { id: "range", label: "Color Range" }
11443
+ { id: "range", label: "Color Range" },
11444
+ { id: "type", label: "Type Mask" },
11445
+ { id: "type-vertical", label: "Vertical Type Mask" }
11380
11446
  ];
11381
11447
  var COLOR_RANGE_PRESET_LABELS = {
11382
11448
  reds: "Reds",
@@ -11407,7 +11473,10 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
11407
11473
  feather: 0,
11408
11474
  fuzziness: 40,
11409
11475
  localized: 0,
11410
- rangeInvert: false
11476
+ rangeInvert: false,
11477
+ typeText: "Type",
11478
+ typeSize: 72,
11479
+ typeFamily: "sans-serif"
11411
11480
  };
11412
11481
  function selectionModeFromModifiers(modifiers, fallback) {
11413
11482
  if (modifiers.shiftKey && modifiers.altKey) return "intersect";
@@ -12852,6 +12921,7 @@ function describeLayer(layer, activeId, boundsOf) {
12852
12921
  out.text = {
12853
12922
  text: layer.text.length > 80 ? `${layer.text.slice(0, 80)}\u2026` : layer.text,
12854
12923
  frame: layer.frame.kind,
12924
+ ...layer.orientation === "vertical" ? { orientation: "vertical" } : {},
12855
12925
  runs: layer.runs.length,
12856
12926
  rendering: layer.rendering === "stored" ? "stored" : "live",
12857
12927
  missingFonts: layer.missingFonts ?? []
@@ -13789,6 +13859,24 @@ var SelectionGesture = class {
13789
13859
  }
13790
13860
  return;
13791
13861
  }
13862
+ if (options.tool === "type" || options.tool === "type-vertical") {
13863
+ this.start = null;
13864
+ try {
13865
+ this.controller.selectTextShape({
13866
+ text: options.typeText || "Type",
13867
+ x: point.x,
13868
+ y: point.y,
13869
+ orientation: options.tool === "type-vertical" ? "vertical" : "horizontal",
13870
+ style: { fontSize: options.typeSize ?? 72, fontFamily: options.typeFamily || "sans-serif" },
13871
+ mode: selectionModeFromModifiers(modifiers, options.mode),
13872
+ feather: options.feather ?? 0,
13873
+ antiAlias: options.antiAlias
13874
+ });
13875
+ } catch (err) {
13876
+ this.error = err instanceof Error ? err.message : String(err);
13877
+ }
13878
+ return;
13879
+ }
13792
13880
  if (options.tool === "wand" || options.tool === "range") {
13793
13881
  this.start = null;
13794
13882
  const common = {
@@ -16185,7 +16273,13 @@ var STYLE = {
16185
16273
  leading: { type: ["number", "null"], minimum: 0, maximum: 1e4, description: "Baseline-to-baseline distance in pixels (null = auto, the paragraph autoLeading \xD7 size)." },
16186
16274
  baselineShift: { type: ["number", "null"], minimum: -5e3, maximum: 5e3, description: "Pixels; positive raises the text (null resets to 0)." },
16187
16275
  underline: { type: "boolean", description: "Underline." },
16188
- strikethrough: { type: "boolean", description: "Strike through." }
16276
+ strikethrough: { type: "boolean", description: "Strike through." },
16277
+ upright: { type: "boolean", description: "Vertical text only: set Latin and other horizontal-script characters upright, one under the other, instead of turned sideways (default false)." }
16278
+ };
16279
+ var ORIENTATION = {
16280
+ type: "string",
16281
+ enum: ["horizontal", "vertical"],
16282
+ description: "horizontal (default): lines left to right, stacked down. vertical: columns top to bottom, stacked right to left; CJK stands upright, Latin turns sideways unless styled upright."
16189
16283
  };
16190
16284
  var PARAGRAPH = {
16191
16285
  align: { type: "string", enum: ["left", "center", "right", "justify"], description: "Alignment (justify stretches paragraph-box lines but the last)." },
@@ -16224,7 +16318,7 @@ function createImageTypeTools(controller) {
16224
16318
  const get = {
16225
16319
  name: "get_text_layer",
16226
16320
  title: "Read a type layer",
16227
- 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.",
16321
+ 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 orientation (horizontal or vertical), 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.",
16228
16322
  inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
16229
16323
  annotations: { title: "Read a type layer", ...READ11 },
16230
16324
  handler: ({ layerId }) => controller.getTextLayer(typeId(layerId))
@@ -16232,7 +16326,7 @@ function createImageTypeTools(controller) {
16232
16326
  const add2 = {
16233
16327
  name: "add_text_layer",
16234
16328
  title: "Add a type layer",
16235
- 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.",
16329
+ 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). orientation vertical sets columns running down from (x, y) (the top of the first column\u2019s centre line; a box fills from its right edge), stacked right to left. 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.",
16236
16330
  inputSchema: {
16237
16331
  type: "object",
16238
16332
  properties: {
@@ -16248,6 +16342,7 @@ function createImageTypeTools(controller) {
16248
16342
  },
16249
16343
  style: { type: "object", description: "Character style.", properties: STYLE, additionalProperties: false },
16250
16344
  paragraph: { type: "object", description: "Paragraph style.", properties: PARAGRAPH, additionalProperties: false },
16345
+ orientation: ORIENTATION,
16251
16346
  rotation: { type: "number", description: "Degrees clockwise about the origin (default 0)." },
16252
16347
  name: { type: "string", description: "Layer name (default the start of the text)." },
16253
16348
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0..1 (default 1)." },
@@ -16262,7 +16357,7 @@ function createImageTypeTools(controller) {
16262
16357
  },
16263
16358
  annotations: { title: "Add a type layer", ...WRITE9 },
16264
16359
  handler: (args) => {
16265
- const init = pick2(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
16360
+ const init = pick2(args, ["text", "x", "y", "box", "style", "paragraph", "orientation", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
16266
16361
  if (init.blend !== void 0) init.blend = init.blend;
16267
16362
  const id = controller.addTextLayer(init);
16268
16363
  return { layer: layerSummary(id), text: controller.getTextLayer(id) };
@@ -16322,6 +16417,14 @@ function createImageTypeTools(controller) {
16322
16417
  return controller.setTextFrame(id, kind === "box" ? { kind: "box", width, height } : { kind: "point" });
16323
16418
  }
16324
16419
  };
16420
+ const setOrientation = {
16421
+ name: "set_text_orientation",
16422
+ title: "Horizontal or vertical type",
16423
+ description: "Type \u203A Orientation: sets a type layer horizontal (lines left to right) or vertical (columns top to bottom, right to left). The text, styles, frame and origin stay. One undo step. Returns the text layer with its orientation and new lines (columns).",
16424
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, orientation: { ...ORIENTATION, description: "horizontal or vertical." } }, required: ["layerId", "orientation"], additionalProperties: false },
16425
+ annotations: { title: "Horizontal or vertical type", ...WRITE_IDEMPOTENT6 },
16426
+ handler: ({ layerId, orientation }) => controller.setTextOrientation(typeId(layerId), orientation)
16427
+ };
16325
16428
  const toShape = {
16326
16429
  name: "convert_text_to_shape",
16327
16430
  title: "Convert text to a shape",
@@ -16333,7 +16436,43 @@ function createImageTypeTools(controller) {
16333
16436
  return { layer: layerSummary(id), shape: controller.getShapeLayer(id), warnings };
16334
16437
  }
16335
16438
  };
16336
- return [listFonts, get, add2, setText, setStyle, setParagraph, setFrame, toShape];
16439
+ const typeMask = {
16440
+ name: "select_text_shape",
16441
+ title: "Select the shape of text",
16442
+ description: "Horizontal / Vertical Type Mask: selects the shape of text laid out as add_text_layer would place it (font family, size, weight, tracking, box, rotation; colour is ignored), combined with the current selection by mode. No layer is added. Vertical lays the text out as vertical type: columns down from (x, y), stacked right to left, CJK upright and Latin turned sideways. Use it to cut text out of an image (layer_via_copy), fill letters with a photo (add_layer_mask), or mask an adjustment. Check fonts with list_fonts first. One undo step. Returns the selection.",
16443
+ inputSchema: {
16444
+ type: "object",
16445
+ properties: {
16446
+ text: { type: "string", minLength: 1, description: "The text; \\n starts a new line (vertical: a new column)." },
16447
+ x: { type: "number", description: "Document x of the origin, pixels (vertical: the first column\u2019s centre)." },
16448
+ y: { type: "number", description: "Document y of the origin, pixels (horizontal: the first baseline; vertical: the top of the first column)." },
16449
+ orientation: { ...ORIENTATION, description: "horizontal (default) or vertical: columns top to bottom, right to left, as vertical type layers (CJK upright, Latin turned sideways unless styled upright)." },
16450
+ box: {
16451
+ type: "object",
16452
+ description: "Wrap in this box, pixels (vertical: columns fill it from its right edge).",
16453
+ properties: { width: { type: "number", minimum: 1, description: "Box width, pixels." }, height: { type: "number", minimum: 1, description: "Box height, pixels." } },
16454
+ required: ["width", "height"],
16455
+ additionalProperties: false
16456
+ },
16457
+ style: { type: "object", description: "Character style (size in px, family, weight, tracking\u2026; colour is ignored).", properties: STYLE, additionalProperties: false },
16458
+ paragraph: { type: "object", description: "Paragraph style (alignment, leading\u2026).", properties: PARAGRAPH, additionalProperties: false },
16459
+ rotation: { type: "number", description: "Degrees clockwise about the origin (default 0)." },
16460
+ mode: { type: "string", enum: ["replace", "add", "subtract", "intersect"], description: "How it combines with the current selection (default replace)." },
16461
+ feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather radius in px (default 0)." },
16462
+ antiAlias: { type: "boolean", description: "Soft glyph edges (default true)." }
16463
+ },
16464
+ required: ["text", "x", "y"],
16465
+ additionalProperties: false
16466
+ },
16467
+ annotations: { title: "Select the shape of text", ...WRITE9 },
16468
+ handler: (args) => {
16469
+ const init = pick2(args, ["text", "x", "y", "orientation", "box", "style", "paragraph", "rotation", "mode", "feather", "antiAlias"]);
16470
+ controller.selectTextShape(init);
16471
+ const sel = controller.getSelection();
16472
+ return { empty: !sel, bounds: sel?.bounds ?? null, area: Math.round(sel ? selectionArea(sel) : 0) };
16473
+ }
16474
+ };
16475
+ return [listFonts, get, add2, setText, setStyle, setParagraph, setFrame, setOrientation, toShape, typeMask];
16337
16476
  }
16338
16477
 
16339
16478
  // src/image-editor/tools.ts
@@ -18472,6 +18611,7 @@ export {
18472
18611
  StrokeCoverage,
18473
18612
  SurfaceDraft,
18474
18613
  TEXT_ALIGNS,
18614
+ TEXT_ORIENTATIONS,
18475
18615
  TEXT_RENDER_MARGIN,
18476
18616
  TEXT_STYLE_FIELDS,
18477
18617
  TILE,
@@ -19205,6 +19345,7 @@ export {
19205
19345
  temperatureToKelvin,
19206
19346
  textIndexAt,
19207
19347
  textMatrix,
19348
+ textOrientation,
19208
19349
  textOutlines,
19209
19350
  textRange,
19210
19351
  tileCoord,
@@ -298,8 +298,8 @@ export interface ShapeLayer extends LayerBase {
298
298
  annotation?: Annotation;
299
299
  psd?: ShapePsdData;
300
300
  }
301
- export type { TextStyle, TextRun, TextAlign, ParagraphStyle, ParagraphRun } from '../../text/types.cjs';
302
- export { TEXT_ALIGNS } from '../../text/types.cjs';
301
+ export type { TextStyle, TextRun, TextAlign, TextOrientation, ParagraphStyle, ParagraphRun } from '../../text/types.cjs';
302
+ export { TEXT_ALIGNS, TEXT_ORIENTATIONS } from '../../text/types.cjs';
303
303
  /** PSD data of a type layer kept for round trips (see the PSD module). */
304
304
  export interface TypePsdData {
305
305
  /** The `TySh` block as read: written back while the layer is unchanged, else patched. */
@@ -338,6 +338,11 @@ export interface TypeLayer extends LayerBase {
338
338
  width: number;
339
339
  height: number;
340
340
  };
341
+ /**
342
+ * Missing = horizontal. `vertical`: columns running down, stacked right to left (point text:
343
+ * (0, 0) is the top of the first column's centre line; a box fills from its right edge).
344
+ */
345
+ orientation?: 'vertical';
341
346
  /** Text space → document (missing = identity). */
342
347
  transform?: AffineMatrix;
343
348
  /** Default true. */
@@ -298,8 +298,8 @@ export interface ShapeLayer extends LayerBase {
298
298
  annotation?: Annotation;
299
299
  psd?: ShapePsdData;
300
300
  }
301
- export type { TextStyle, TextRun, TextAlign, ParagraphStyle, ParagraphRun } from '../../text/types.js';
302
- export { TEXT_ALIGNS } from '../../text/types.js';
301
+ export type { TextStyle, TextRun, TextAlign, TextOrientation, ParagraphStyle, ParagraphRun } from '../../text/types.js';
302
+ export { TEXT_ALIGNS, TEXT_ORIENTATIONS } from '../../text/types.js';
303
303
  /** PSD data of a type layer kept for round trips (see the PSD module). */
304
304
  export interface TypePsdData {
305
305
  /** The `TySh` block as read: written back while the layer is unchanged, else patched. */
@@ -338,6 +338,11 @@ export interface TypeLayer extends LayerBase {
338
338
  width: number;
339
339
  height: number;
340
340
  };
341
+ /**
342
+ * Missing = horizontal. `vertical`: columns running down, stacked right to left (point text:
343
+ * (0, 0) is the top of the first column's centre line; a box fills from its right edge).
344
+ */
345
+ orientation?: 'vertical';
341
346
  /** Text space → document (missing = identity). */
342
347
  transform?: AffineMatrix;
343
348
  /** Default true. */
@@ -195,6 +195,8 @@ export interface SerializedTypeLayer extends SerializedLayerBase {
195
195
  runs: TextRun[];
196
196
  paragraphs: ParagraphRun[];
197
197
  frame: TypeLayer['frame'];
198
+ /** Missing: horizontal. */
199
+ orientation?: 'vertical';
198
200
  transform?: number[];
199
201
  antiAlias?: false;
200
202
  fillOpacity?: number;
@@ -195,6 +195,8 @@ export interface SerializedTypeLayer extends SerializedLayerBase {
195
195
  runs: TextRun[];
196
196
  paragraphs: ParagraphRun[];
197
197
  frame: TypeLayer['frame'];
198
+ /** Missing: horizontal. */
199
+ orientation?: 'vertical';
198
200
  transform?: number[];
199
201
  antiAlias?: false;
200
202
  fillOpacity?: number;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { ImageEditorController, TypeLayerInfo } from '../controller.cjs';
8
8
  import type { ImagePoint } from '../types.cjs';
9
- import { type LiveShapeKind, type ParagraphStyle, type TextAlign, type TextStyle } from './model.cjs';
9
+ import { type LiveShapeKind, type ParagraphStyle, type TextAlign, type TextOrientation, type TextStyle } from './model.cjs';
10
10
  /** What the shape tool draws next (and what its options bar edits on a selected shape). */
11
11
  export interface ShapeToolOptions {
12
12
  kind: LiveShapeKind;
@@ -36,6 +36,8 @@ export interface TypeToolOptions {
36
36
  italic: boolean;
37
37
  color: string;
38
38
  align: TextAlign;
39
+ /** Horizontal Type or Vertical Type tool (missing = horizontal): what new text is set as. */
40
+ orientation?: TextOrientation;
39
41
  }
40
42
  export declare const DEFAULT_TYPE_TOOL_OPTIONS: TypeToolOptions;
41
43
  /** A live shape for a drag from `a` to `b` (Shift: square / circle / 45° line; Alt: from the centre). */
@@ -107,7 +109,11 @@ export declare class TextEditSession {
107
109
  deleteForward(): void;
108
110
  /** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */
109
111
  private surrogate;
110
- /** Moves the caret (Shift extends the selection). */
112
+ /**
113
+ * Moves the caret (Shift extends the selection): `left` / `right` along the line, `up` / `down`
114
+ * between lines, `home` / `end` of the line, `start` / `finish` of the text. Arrow keys of vertical
115
+ * text map onto these through `textArrowStep` in `handleKey`.
116
+ */
111
117
  move(to: 'left' | 'right' | 'up' | 'down' | 'home' | 'end' | 'start' | 'finish', extend?: boolean): void;
112
118
  selectAll(): void;
113
119
  /** Places the caret at a document point (Shift-click extends). */
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { ImageEditorController, TypeLayerInfo } from '../controller.js';
8
8
  import type { ImagePoint } from '../types.js';
9
- import { type LiveShapeKind, type ParagraphStyle, type TextAlign, type TextStyle } from './model.js';
9
+ import { type LiveShapeKind, type ParagraphStyle, type TextAlign, type TextOrientation, type TextStyle } from './model.js';
10
10
  /** What the shape tool draws next (and what its options bar edits on a selected shape). */
11
11
  export interface ShapeToolOptions {
12
12
  kind: LiveShapeKind;
@@ -36,6 +36,8 @@ export interface TypeToolOptions {
36
36
  italic: boolean;
37
37
  color: string;
38
38
  align: TextAlign;
39
+ /** Horizontal Type or Vertical Type tool (missing = horizontal): what new text is set as. */
40
+ orientation?: TextOrientation;
39
41
  }
40
42
  export declare const DEFAULT_TYPE_TOOL_OPTIONS: TypeToolOptions;
41
43
  /** A live shape for a drag from `a` to `b` (Shift: square / circle / 45° line; Alt: from the centre). */
@@ -107,7 +109,11 @@ export declare class TextEditSession {
107
109
  deleteForward(): void;
108
110
  /** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */
109
111
  private surrogate;
110
- /** Moves the caret (Shift extends the selection). */
112
+ /**
113
+ * Moves the caret (Shift extends the selection): `left` / `right` along the line, `up` / `down`
114
+ * between lines, `home` / `end` of the line, `start` / `finish` of the text. Arrow keys of vertical
115
+ * text map onto these through `textArrowStep` in `handleKey`.
116
+ */
111
117
  move(to: 'left' | 'right' | 'up' | 'down' | 'home' | 'end' | 'start' | 'finish', extend?: boolean): void;
112
118
  selectAll(): void;
113
119
  /** Places the caret at a document point (Shift-click extends). */