@pixerate/editor 0.108.0 → 0.109.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 (74) hide show
  1. package/dist/animation/index.cjs +5 -5
  2. package/dist/animation/index.cjs.map +1 -1
  3. package/dist/animation/index.js +9 -9
  4. package/dist/animation-editor/index.cjs +59 -59
  5. package/dist/animation-editor/index.cjs.map +1 -1
  6. package/dist/animation-editor/index.d.cts +2 -1
  7. package/dist/animation-editor/index.d.ts +2 -1
  8. package/dist/animation-editor/index.js +29 -29
  9. package/dist/canvas/index.cjs +27 -27
  10. package/dist/canvas/index.cjs.map +1 -1
  11. package/dist/canvas/index.js +11 -11
  12. package/dist/{chunk-2FGRTYJN.cjs → chunk-477UVMNR.cjs} +3 -3
  13. package/dist/{chunk-2FGRTYJN.cjs.map → chunk-477UVMNR.cjs.map} +1 -1
  14. package/dist/{chunk-T3QZ6S47.cjs → chunk-AZTZRLDI.cjs} +40 -2
  15. package/dist/chunk-AZTZRLDI.cjs.map +1 -0
  16. package/dist/{chunk-HUQ7EHVI.js → chunk-BLWCAQCW.js} +723 -1975
  17. package/dist/chunk-BLWCAQCW.js.map +1 -0
  18. package/dist/{chunk-G4BIYE6R.js → chunk-CFMWALBM.js} +39 -1
  19. package/dist/chunk-CFMWALBM.js.map +1 -0
  20. package/dist/{chunk-FRD3FUXI.cjs → chunk-D37BX2ZA.cjs} +2081 -460
  21. package/dist/chunk-D37BX2ZA.cjs.map +1 -0
  22. package/dist/chunk-JLYX4V57.cjs +2001 -0
  23. package/dist/chunk-JLYX4V57.cjs.map +1 -0
  24. package/dist/chunk-KB2YBCVS.js +2001 -0
  25. package/dist/chunk-KB2YBCVS.js.map +1 -0
  26. package/dist/{chunk-SOROZ2BF.js → chunk-RE77QYUF.js} +1938 -317
  27. package/dist/chunk-RE77QYUF.js.map +1 -0
  28. package/dist/{chunk-UTEVPQTS.cjs → chunk-W3GXSUSQ.cjs} +746 -1998
  29. package/dist/chunk-W3GXSUSQ.cjs.map +1 -0
  30. package/dist/{chunk-JK6M6V7G.js → chunk-Z3YWVUWT.js} +4 -4
  31. package/dist/{export-a9bnF7Cl.d.ts → export-BLPLaE6Z.d.ts} +417 -16
  32. package/dist/{export-JGWQEPJn.d.cts → export-DSUc4fQD.d.cts} +417 -16
  33. package/dist/image-editor/index.cjs +1527 -1001
  34. package/dist/image-editor/index.cjs.map +1 -1
  35. package/dist/image-editor/index.d.cts +73 -9
  36. package/dist/image-editor/index.d.ts +73 -9
  37. package/dist/image-editor/index.js +814 -288
  38. package/dist/image-editor/index.js.map +1 -1
  39. package/dist/paint-editor/index.cjs +56 -53
  40. package/dist/paint-editor/index.cjs.map +1 -1
  41. package/dist/paint-editor/index.d.cts +5 -2
  42. package/dist/paint-editor/index.d.ts +5 -2
  43. package/dist/paint-editor/index.js +6 -3
  44. package/dist/paint-editor/index.js.map +1 -1
  45. package/dist/raster/index.cjs +4 -2
  46. package/dist/raster/index.cjs.map +1 -1
  47. package/dist/raster/index.d.cts +3 -129
  48. package/dist/raster/index.d.ts +3 -129
  49. package/dist/raster/index.js +3 -1
  50. package/dist/{compositor-D92URMlm.d.cts → render-C-fO_mes.d.cts} +135 -1
  51. package/dist/{compositor-Kt_M4ruo.d.ts → render-OLIMk_v0.d.ts} +135 -1
  52. package/dist/{svg-SseJTIrA.d.ts → svg-D_cfkhlB.d.cts} +3 -629
  53. package/dist/{svg-ClVIG4T7.d.cts → svg-wIaKLrU0.d.ts} +3 -629
  54. package/dist/types-DtL_qLFz.d.ts +630 -0
  55. package/dist/types-DyQq-WPY.d.cts +630 -0
  56. package/dist/vector/index.cjs +563 -563
  57. package/dist/vector/index.cjs.map +1 -1
  58. package/dist/vector/index.d.cts +4 -2
  59. package/dist/vector/index.d.ts +4 -2
  60. package/dist/vector/index.js +69 -69
  61. package/dist/video-editor/index.cjs +3 -3
  62. package/dist/video-editor/index.js +2 -2
  63. package/package.json +1 -1
  64. package/dist/chunk-DWGZUSES.cjs +0 -694
  65. package/dist/chunk-DWGZUSES.cjs.map +0 -1
  66. package/dist/chunk-FRD3FUXI.cjs.map +0 -1
  67. package/dist/chunk-G4BIYE6R.js.map +0 -1
  68. package/dist/chunk-HUQ7EHVI.js.map +0 -1
  69. package/dist/chunk-KKREERP2.js +0 -694
  70. package/dist/chunk-KKREERP2.js.map +0 -1
  71. package/dist/chunk-SOROZ2BF.js.map +0 -1
  72. package/dist/chunk-T3QZ6S47.cjs.map +0 -1
  73. package/dist/chunk-UTEVPQTS.cjs.map +0 -1
  74. /package/dist/{chunk-JK6M6V7G.js.map → chunk-Z3YWVUWT.js.map} +0 -0
@@ -10,12 +10,17 @@ import {
10
10
  ADJUSTMENT_KINDS,
11
11
  ADJUSTMENT_LUT_SIZE,
12
12
  AS_SHOT_KELVIN,
13
+ BACKGROUND_LAYER_ID,
14
+ BACKGROUND_LAYER_NAME,
15
+ BLUR_FILTER_ID,
13
16
  BROWSER_IMAGE_TYPES,
14
17
  D50,
15
18
  D65,
16
19
  DEFAULT_ADJUSTMENTS_VALUES,
17
20
  DEFAULT_DEPTH_MASK_VALUES,
21
+ DEFAULT_MITER_LIMIT,
18
22
  DEFAULT_TRANSFORM_VALUES,
23
+ DEVELOP_FILTER_ID,
19
24
  EMPTY_DISTANCE,
20
25
  FREE_TRANSFORM_MODES,
21
26
  HUE_RANGE_LABELS,
@@ -23,6 +28,7 @@ import {
23
28
  LATEST_PROCESS_VERSION,
24
29
  LAYERED_SCHEMA_VERSION,
25
30
  LEGACY_PROCESS_VERSION,
31
+ LIVE_SHAPE_KINDS,
26
32
  LUMA_709,
27
33
  LayerCompositor,
28
34
  LayerOperationError,
@@ -34,6 +40,8 @@ import {
34
40
  PsdError,
35
41
  SELECTION_MODES,
36
42
  SELECTION_MODIFY_OPS,
43
+ SHAPE_OPERATIONS,
44
+ SHAPE_RENDER_MARGIN,
37
45
  SMART_RENDER_MARGIN,
38
46
  SUPPORTED_ADJUSTMENT_KEYS,
39
47
  TINT_SCALE,
@@ -49,7 +57,13 @@ import {
49
57
  adjustmentKindLabel,
50
58
  adjustmentLayersKey,
51
59
  adjustmentNodeAsLayer,
60
+ adjustmentNodeFromLegacy,
52
61
  anchorPoint,
62
+ annotationBlend,
63
+ annotationLayerFor,
64
+ annotationLayerName,
65
+ annotationOfLayer,
66
+ annotationOfShape,
53
67
  applyAdjustmentLayers,
54
68
  applyAdjustments,
55
69
  applyBoxBlur,
@@ -60,6 +74,7 @@ import {
60
74
  applyParams,
61
75
  applyProcess2Adjustments,
62
76
  applySmartFilters,
77
+ backgroundLayer,
63
78
  blackWhiteGray,
64
79
  blurFilter,
65
80
  borderCoverage,
@@ -73,6 +88,7 @@ import {
73
88
  colorBalanceChannel,
74
89
  combineSelections,
75
90
  compileAdjustmentLayer,
91
+ componentPath,
76
92
  computeSelectionBounds,
77
93
  contentsBox,
78
94
  contentsSurface,
@@ -83,6 +99,7 @@ import {
83
99
  coverageToByte,
84
100
  createAdjustmentLayer,
85
101
  createSelectionMask,
102
+ createShapeLayer,
86
103
  curveLutFromLevels,
87
104
  decodeLayerChannel,
88
105
  decodeLayerMask,
@@ -91,7 +108,9 @@ import {
91
108
  decodePngDataUrl,
92
109
  decodeSelectionRle,
93
110
  defaultAdjustmentParams,
111
+ defaultShapeName,
94
112
  depthMaskAlpha,
113
+ depthRasterMask,
95
114
  describeTransform,
96
115
  deserializeEffects,
97
116
  deserializeLayer,
@@ -99,6 +118,8 @@ import {
99
118
  deserializePatterns,
100
119
  deserializeSelection,
101
120
  developFilter,
121
+ developFromAdjustments,
122
+ developTarget,
102
123
  distanceTransform,
103
124
  documentContents,
104
125
  duplicateLayer,
@@ -110,6 +131,7 @@ import {
110
131
  exportPsd,
111
132
  featherCoverage,
112
133
  filterMaskCoverage,
134
+ flattenPath,
113
135
  flipCorners,
114
136
  fullSelection,
115
137
  geometryHomography,
@@ -120,6 +142,7 @@ import {
120
142
  getViewportProjection,
121
143
  greyMorphology,
122
144
  groupLayers,
145
+ hasResampledLegacyMask,
123
146
  hasSurface,
124
147
  hasWarp,
125
148
  hashMaskContent,
@@ -141,8 +164,11 @@ import {
141
164
  isActiveFilter,
142
165
  isAdjustmentKind,
143
166
  isContentsDecoded,
167
+ isDefaultDepthMask,
144
168
  isIdentityTransform,
169
+ isLegacyShape,
145
170
  isQuarterTurnSwapped,
171
+ isRenderedLayer,
146
172
  isSelectionEmpty,
147
173
  isSelectionMode,
148
174
  isSpatialFilter,
@@ -156,12 +182,20 @@ import {
156
182
  layerName,
157
183
  layerTree,
158
184
  layerTypeLabel,
185
+ layersFromLegacy,
186
+ legacyAdjustmentLayer,
187
+ legacyMaskOf,
188
+ legacyView,
159
189
  levelsValue,
160
190
  linearSrgbToXy,
161
191
  linearToSrgb,
192
+ liveComponent,
193
+ liveShapeSubpaths,
162
194
  mapCropHandle,
195
+ mapSubpaths,
163
196
  matrixFromCorners,
164
197
  matrixFromParams,
198
+ mergeShapeComponents,
165
199
  modifySelection,
166
200
  moveLayer,
167
201
  moveSmartFilter,
@@ -174,13 +208,19 @@ import {
174
208
  normalizeAngle,
175
209
  normalizeFilter,
176
210
  normalizeLayerMask,
211
+ normalizeLiveShape,
177
212
  normalizePlacement,
178
213
  normalizeProcessVersion,
214
+ normalizeShapePaint,
215
+ normalizeShapeStroke,
179
216
  normalizeSmartFilter,
217
+ normalizeSubpaths,
180
218
  offsetLayer,
219
+ offsetShape,
181
220
  offsetSmartObject,
182
221
  parseAdjustmentBlock,
183
222
  parseBlend,
223
+ parseCssColor,
184
224
  parseLfx2,
185
225
  parseLrfx,
186
226
  parsePatternBlock,
@@ -203,6 +243,8 @@ import {
203
243
  psdToLayers,
204
244
  rangeLightness,
205
245
  rasterContents,
246
+ rasterMaskFromLegacy,
247
+ rasterizeCoverage,
206
248
  rasterizeSmartObject,
207
249
  readPsd,
208
250
  referencePoint,
@@ -211,12 +253,17 @@ import {
211
253
  removeLayerEffect,
212
254
  removeLayerFilter,
213
255
  removeSmartFilter,
256
+ renderShape,
257
+ renderShapeRect,
214
258
  renderSmartObject,
215
259
  replaceContents,
216
260
  requireLayer,
261
+ requireOperation,
262
+ requireShapeLayer,
217
263
  requireSmartObject,
218
264
  requireUnlocked,
219
265
  rerender,
266
+ rerenderShape,
220
267
  resizeSelection,
221
268
  resolveExternalAssets,
222
269
  rgbToGray,
@@ -244,6 +291,12 @@ import {
244
291
  setSmartFilterMask,
245
292
  setSmartFiltersEnabled,
246
293
  setSmartPlacement,
294
+ shapeAsPixelLayer,
295
+ shapeFromAnnotation,
296
+ shapeGeometryBounds,
297
+ shapeLayers,
298
+ shapeOutline,
299
+ shapeRenderClip,
247
300
  shiftCoverage,
248
301
  smartContentsFile,
249
302
  smartObjectLayers,
@@ -255,6 +308,7 @@ import {
255
308
  toneLuts,
256
309
  transformHomography,
257
310
  transformMap,
311
+ transformShapeComponents,
258
312
  transformedBox,
259
313
  transformedPoint,
260
314
  ungroupLayer,
@@ -263,6 +317,7 @@ import {
263
317
  updateLayerEffect,
264
318
  updateLayerFilter,
265
319
  updateLayerMaskProps,
320
+ updateShapeLayer,
266
321
  updateSmartFilter,
267
322
  uvToXy,
268
323
  versionedDescriptorBytes,
@@ -279,7 +334,7 @@ import {
279
334
  writePsd,
280
335
  xyToTempTint,
281
336
  xyToUv
282
- } from "../chunk-SOROZ2BF.js";
337
+ } from "../chunk-RE77QYUF.js";
283
338
  import {
284
339
  DEFAULT_GLOBAL_LIGHT,
285
340
  DIRECT_GAUSSIAN_MAX_SIGMA,
@@ -343,7 +398,7 @@ import {
343
398
  validateLayerEffect,
344
399
  validateLayerEffects,
345
400
  warpSurface
346
- } from "../chunk-G4BIYE6R.js";
401
+ } from "../chunk-CFMWALBM.js";
347
402
  import {
348
403
  applyHomography,
349
404
  boxQuad,
@@ -444,6 +499,7 @@ import {
444
499
  multiplyMatrices,
445
500
  pullWarpMesh,
446
501
  resizeWarpMesh,
502
+ roundSubpaths,
447
503
  translateMatrix,
448
504
  warpMeshColumns,
449
505
  warpMeshEditPoints,
@@ -451,7 +507,10 @@ import {
451
507
  warpMeshParamAt,
452
508
  warpMeshRows,
453
509
  warpPresetMesh
454
- } from "../chunk-KKREERP2.js";
510
+ } from "../chunk-BLWCAQCW.js";
511
+ import "../chunk-YCSFS3GK.js";
512
+ import "../chunk-ZRJMJIQU.js";
513
+ import "../chunk-TGD52FEZ.js";
455
514
 
456
515
  // src/image-editor/histogram.ts
457
516
  var HISTOGRAM_BINS = 256;
@@ -554,249 +613,6 @@ function quickMaskPixels(mask, style = {}) {
554
613
  return out;
555
614
  }
556
615
 
557
- // src/image-editor/layers/migrate.ts
558
- var BACKGROUND_LAYER_ID = "background";
559
- var BACKGROUND_LAYER_NAME = "Background";
560
- var DEVELOP_FILTER_ID = "develop";
561
- var BLUR_FILTER_ID = "blur";
562
- var legacyMaskMemo = /* @__PURE__ */ new WeakMap();
563
- function rasterMaskFromLegacy(mask, width, height) {
564
- const sel = decodeLayerMask(mask);
565
- if (!sel) return null;
566
- const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
567
- const surface = surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);
568
- legacyMaskMemo.set(surface, { width: mask.width, height: mask.height, rle: mask.rle });
569
- const out = { surface, defaultValue: 0 };
570
- if (mask.density !== void 0 && mask.density !== 1) out.density = mask.density;
571
- if (mask.feather) out.feather = mask.feather;
572
- if (mask.enabled === false) out.enabled = false;
573
- return out;
574
- }
575
- function hasResampledLegacyMask(layers, width, height) {
576
- for (const l of layers) {
577
- if (l.type !== "adjustment" || !l.mask) continue;
578
- const memo = legacyMaskMemo.get(l.mask.surface);
579
- if (memo && (memo.width !== width || memo.height !== height)) return true;
580
- }
581
- return false;
582
- }
583
- function legacyMaskOf(mask, width, height) {
584
- let memo = legacyMaskMemo.get(mask.surface);
585
- if (!memo) {
586
- const data = readRegion(mask.surface, { x: 0, y: 0, width, height });
587
- memo = { width, height, rle: encodeSelectionRle(new Uint8Array(data.buffer, data.byteOffset, data.length)) };
588
- legacyMaskMemo.set(mask.surface, memo);
589
- }
590
- return normalizeLayerMask({ ...memo, density: mask.density, feather: mask.feather, enabled: mask.enabled });
591
- }
592
- function sameJson(a, b) {
593
- return JSON.stringify(a) === JSON.stringify(b);
594
- }
595
- function isDefaultDepthMask(mask) {
596
- if (!mask) return true;
597
- return sameJson({ ...DEFAULT_DEPTH_MASK_VALUES, ...mask, depthRange: [...mask.depthRange ?? []] }, DEFAULT_DEPTH_MASK_VALUES);
598
- }
599
- function developFromAdjustments(adjustments) {
600
- return {
601
- id: DEVELOP_FILTER_ID,
602
- kind: "develop",
603
- enabled: true,
604
- brightness: adjustments.brightness ?? 0,
605
- contrast: adjustments.contrast ?? 0,
606
- saturation: adjustments.saturation ?? 0,
607
- exposure: adjustments.exposure ?? 0,
608
- temperature: adjustments.temperature ?? 0,
609
- tint: adjustments.tint ?? 0,
610
- process: normalizeProcessVersion(adjustments.process)
611
- };
612
- }
613
- function annotationLayerName(annotation) {
614
- switch (annotation.type) {
615
- case "pen":
616
- return "Pen";
617
- case "rect":
618
- return "Rectangle";
619
- case "circle":
620
- return "Ellipse";
621
- case "arrow":
622
- return "Arrow";
623
- case "line":
624
- return "Line";
625
- case "text":
626
- return annotation.text ? annotation.text.slice(0, 32) : "Text";
627
- case "image":
628
- return "Image";
629
- }
630
- }
631
- function annotationLayerFor(annotation) {
632
- return {
633
- type: "annotation",
634
- id: annotation.id,
635
- name: annotationLayerName(annotation),
636
- visible: true,
637
- opacity: 1,
638
- blend: "normal",
639
- annotation
640
- };
641
- }
642
- function adjustmentNodeFromLegacy(layer, width, height) {
643
- const node = {
644
- type: "adjustment",
645
- id: layer.id,
646
- name: layer.name ?? "",
647
- visible: layer.enabled !== false,
648
- opacity: layer.opacity ?? 1,
649
- blend: layer.blend ?? "normal",
650
- kind: layer.kind,
651
- params: JSON.parse(JSON.stringify(layer.params))
652
- };
653
- if (layer.mask) {
654
- const mask = rasterMaskFromLegacy(layer.mask, width, height);
655
- if (mask) node.mask = mask;
656
- }
657
- return node;
658
- }
659
- function backgroundLayer(filters = [], mask) {
660
- const layer = {
661
- type: "pixel",
662
- id: BACKGROUND_LAYER_ID,
663
- name: BACKGROUND_LAYER_NAME,
664
- visible: true,
665
- opacity: 1,
666
- blend: "normal",
667
- surface: emptySurface(4),
668
- offset: { x: 0, y: 0 },
669
- source: "document",
670
- filters
671
- };
672
- if (mask) layer.mask = mask;
673
- return layer;
674
- }
675
- function depthRasterMask(source) {
676
- const out = {
677
- surface: emptySurface(1, 255),
678
- defaultValue: 255,
679
- source: { ...source, depthRange: [...source.depthRange] }
680
- };
681
- if (!source.enabled) out.enabled = false;
682
- return out;
683
- }
684
- function layersFromLegacy(parts, width, height) {
685
- const stored = parts.adjustments;
686
- const adjustments = stored ? { ...DEFAULT_ADJUSTMENTS_VALUES, ...stored, process: normalizeProcessVersion(stored.process) } : { ...DEFAULT_ADJUSTMENTS_VALUES };
687
- const adjLayers = normalizeAdjustmentLayers(adjustments.layers);
688
- const filters = [developFromAdjustments(adjustments)];
689
- const blur = typeof adjustments.blur === "number" && adjustments.blur > 0 ? adjustments.blur : 0;
690
- let legacyOutputBlur;
691
- if (blur > 0) {
692
- if (adjLayers.length) legacyOutputBlur = blur;
693
- else filters.push({ id: BLUR_FILTER_ID, kind: "blur", enabled: true, radius: blur });
694
- }
695
- const depth = parts.depthMask;
696
- const mask = depth && !isDefaultDepthMask(depth) ? depthRasterMask({ ...DEFAULT_DEPTH_MASK_VALUES, ...depth }) : void 0;
697
- const layers = [backgroundLayer(filters, mask)];
698
- for (const l of adjLayers) layers.push(adjustmentNodeFromLegacy(l, width, height));
699
- for (const a of parts.annotations ?? []) layers.push(annotationLayerFor(JSON.parse(JSON.stringify(a))));
700
- const out = { layers, outputOpacity: typeof adjustments.opacity === "number" ? adjustments.opacity : 1 };
701
- if (legacyOutputBlur !== void 0) out.legacyOutputBlur = legacyOutputBlur;
702
- return out;
703
- }
704
- function legacyAdjustmentLayer(node, width, height) {
705
- const out = {
706
- id: node.id,
707
- kind: node.kind,
708
- enabled: node.visible,
709
- opacity: node.opacity,
710
- blend: node.blend,
711
- params: node.params
712
- };
713
- if (node.name) out.name = node.name;
714
- if (node.mask && !node.mask.source) out.mask = legacyMaskOf(node.mask, width, height);
715
- return out;
716
- }
717
- function developTarget(layers, activeLayerId) {
718
- const pixels = [];
719
- const walk = (list) => {
720
- for (const l of list) {
721
- if (l.type === "pixel") pixels.push(l);
722
- else if (l.type === "group") walk(l.children);
723
- }
724
- };
725
- walk(layers);
726
- if (activeLayerId) {
727
- const active = pixels.find((l) => l.id === activeLayerId);
728
- if (active) return active;
729
- }
730
- return pixels.find((l) => l.source === "document") ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;
731
- }
732
- var isPlain = (l) => l.visible && l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask && !l.locks && !l.effects;
733
- function legacyView(stack, width, height, activeLayerId = null) {
734
- const { layers } = stack;
735
- const target = developTarget(layers, activeLayerId);
736
- const develop = target ? developFilter(target) : void 0;
737
- const blur = stack.legacyOutputBlur ?? (target ? blurFilter(target)?.radius : void 0) ?? 0;
738
- const adjustments = {
739
- ...DEFAULT_ADJUSTMENTS_VALUES,
740
- ...develop ? {
741
- brightness: develop.brightness,
742
- contrast: develop.contrast,
743
- saturation: develop.saturation,
744
- exposure: develop.exposure,
745
- temperature: develop.temperature,
746
- tint: develop.tint ?? 0
747
- } : {},
748
- blur,
749
- opacity: stack.outputOpacity,
750
- process: develop ? normalizeProcessVersion(develop.process) : DEFAULT_ADJUSTMENTS_VALUES.process
751
- };
752
- const adjNodes = [];
753
- const annotations = [];
754
- const walk = (list) => {
755
- for (const l of list) {
756
- if (l.type === "adjustment") adjNodes.push(l);
757
- else if (l.type === "annotation") annotations.push(l.annotation);
758
- else if (l.type === "group") walk(l.children);
759
- }
760
- };
761
- walk(layers);
762
- if (adjNodes.length) adjustments.layers = adjNodes.map((n) => legacyAdjustmentLayer(n, width, height));
763
- const depthSource = target?.mask?.source;
764
- const depthMask = depthSource ? { ...depthSource, depthRange: [...depthSource.depthRange] } : { ...DEFAULT_DEPTH_MASK_VALUES, depthRange: [...DEFAULT_DEPTH_MASK_VALUES.depthRange] };
765
- return { adjustments, depthMask, annotations, exact: isLegacyShape(stack) };
766
- }
767
- function isLegacyShape(stack) {
768
- const { layers } = stack;
769
- const bg = layers[0];
770
- if (!bg || bg.type !== "pixel" || bg.source !== "document") return false;
771
- if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;
772
- if (!bg.visible || bg.opacity !== 1 || bg.blend !== "normal" || bg.clipped || bg.locks || bg.effects) return false;
773
- if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;
774
- const filters = bg.filters ?? [];
775
- if (filters.length < 1 || filters.length > 2) return false;
776
- const [develop, blur] = filters;
777
- if (develop.kind !== "develop" || !develop.enabled || develop.id !== DEVELOP_FILTER_ID) return false;
778
- if (blur && (blur.kind !== "blur" || !blur.enabled || blur.id !== BLUR_FILTER_ID || !(blur.radius > 0))) return false;
779
- if (bg.mask) {
780
- const m = bg.mask;
781
- if (!m.source || m.density !== void 0 || m.feather !== void 0) return false;
782
- if (m.enabled !== false !== m.source.enabled) return false;
783
- }
784
- let i = 1;
785
- let adjustments = 0;
786
- for (; i < layers.length && layers[i].type === "adjustment"; i++) {
787
- const l = layers[i];
788
- if (l.clipped || l.locks || l.mask?.source || l.effects) return false;
789
- adjustments++;
790
- }
791
- for (; i < layers.length; i++) {
792
- const l = layers[i];
793
- if (l.type !== "annotation" || !isPlain(l) || l.id !== l.annotation.id || l.name !== annotationLayerName(l.annotation)) return false;
794
- }
795
- if (blur && adjustments) return false;
796
- if (stack.legacyOutputBlur !== void 0 && (!adjustments || !(stack.legacyOutputBlur > 0))) return false;
797
- return true;
798
- }
799
-
800
616
  // src/image-editor/layers/render.ts
801
617
  function isQuarterTurn(rotate) {
802
618
  return normalizeAngle(rotate) % 90 === 0;
@@ -871,17 +687,28 @@ function needsLegacyBase(input) {
871
687
  if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;
872
688
  return false;
873
689
  }
874
- var isPlainAnnotation = (l) => l.type === "annotation" && l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask;
690
+ var isPlainAnnotation = (l) => l.type === "annotation" ? l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask : l.type === "shape" && Boolean(l.annotation) && l.opacity === 1 && l.blend === (l.annotation.blend ?? "normal") && !l.clipped && !l.mask && !l.effects && (l.fillOpacity ?? 1) === 1;
691
+ var overlayViews = /* @__PURE__ */ new WeakMap();
692
+ function overlayView(l) {
693
+ if (l.type === "annotation") return l;
694
+ let view = overlayViews.get(l);
695
+ if (!view) {
696
+ const shape = l;
697
+ view = { type: "annotation", id: shape.id, name: shape.name, visible: shape.visible, opacity: 1, blend: "normal", annotation: shape.annotation };
698
+ overlayViews.set(l, view);
699
+ }
700
+ return view;
701
+ }
875
702
  function splitOverlayAnnotations(layers) {
876
703
  let i = layers.length;
877
704
  while (i > 0) {
878
705
  const l = layers[i - 1];
879
- if (l.type === "annotation" && (!l.visible || isPlainAnnotation(l))) i--;
706
+ if ((l.type === "annotation" || l.type === "shape" && l.annotation) && (!l.visible || isPlainAnnotation(l))) i--;
880
707
  else break;
881
708
  }
882
709
  return {
883
710
  composite: layers.slice(0, i),
884
- overlay: layers.slice(i).filter((l) => l.visible)
711
+ overlay: layers.slice(i).filter((l) => l.visible).map(overlayView)
885
712
  };
886
713
  }
887
714
  function coveringRect(region) {
@@ -2672,7 +2499,7 @@ var FreeTransformSession = class {
2672
2499
  }
2673
2500
  const targetId = init.layerId ?? host.activeLayerId();
2674
2501
  const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
2675
- const smartTarget = target?.type === "smart" && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
2502
+ const smartTarget = (target?.type === "smart" || target?.type === "shape") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
2676
2503
  if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
2677
2504
  this.content = content;
2678
2505
  if (content !== "layer" && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
@@ -2699,7 +2526,7 @@ var FreeTransformSession = class {
2699
2526
  const linked = mask?.linked !== false;
2700
2527
  const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
2701
2528
  if (channel === "mask" && !mask) fail(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
2702
- if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart") fail(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel layers, smart objects, layer masks (channel mask) and selections.`);
2529
+ if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape") fail(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel, shape and smart object layers, layer masks (channel mask) and selections.`);
2703
2530
  if (!layer.visible && content !== "selection-copy") fail(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
2704
2531
  if (content !== "selection-copy") {
2705
2532
  requireUnlocked(layer, "position", "transform it");
@@ -2721,7 +2548,21 @@ var FreeTransformSession = class {
2721
2548
  this.shown = this.startLayers;
2722
2549
  return;
2723
2550
  }
2724
- if (maskAlone) {
2551
+ if (layer.type === "shape" && !maskAlone) {
2552
+ if (content !== "layer") fail(`Layer "${layer.name || id}" is a shape layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
2553
+ const g = shapeGeometryBounds(layer);
2554
+ if (!g) fail(`Shape layer "${layer.name || id}" has no path to transform.`);
2555
+ const w = Math.max(g.width, 1);
2556
+ const h = Math.max(g.height, 1);
2557
+ box = { x: g.x + (g.width - w) / 2, y: g.y + (g.height - h) / 2, width: w, height: h };
2558
+ this.shape = layer;
2559
+ this.shapeLayer = true;
2560
+ moves.push("pixels");
2561
+ if (mask && linked) {
2562
+ this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
2563
+ moves.push("mask");
2564
+ }
2565
+ } else if (maskAlone) {
2725
2566
  if (content === "selection-copy") fail("Layer via Copy copies pixels; transform a mask with content layer or selection.");
2726
2567
  if (content === "selection") {
2727
2568
  const { lifted, rest } = liftMask(mask.surface, sel, o);
@@ -2806,6 +2647,9 @@ var FreeTransformSession = class {
2806
2647
  startGeometry;
2807
2648
  /** True when the layer is a smart object (the transform sets its placement). */
2808
2649
  smartObject = false;
2650
+ /** True when the layer is a shape layer (the transform maps its paths and renders them again). */
2651
+ shapeLayer = false;
2652
+ shape = null;
2809
2653
  geometryValue;
2810
2654
  smart = null;
2811
2655
  smartReduced = /* @__PURE__ */ new Map();
@@ -2871,6 +2715,7 @@ var FreeTransformSession = class {
2871
2715
  let layers = this.startLayers;
2872
2716
  let selection = this.startSelection;
2873
2717
  const targetId = this.copyId ?? this.layerId;
2718
+ if (this.shape) layers = this.resampleShape(final, preview, warp ? f : null, h);
2874
2719
  for (const part of this.parts) {
2875
2720
  const o = part.offset;
2876
2721
  let moved;
@@ -2922,6 +2767,24 @@ var FreeTransformSession = class {
2922
2767
  }
2923
2768
  return { layers, selection };
2924
2769
  }
2770
+ /**
2771
+ * A shape with its paths mapped through the current geometry and rendered again (live shapes
2772
+ * stay live under an affine map; perspective and warps turn them into plain paths). Previews
2773
+ * render only the visible part.
2774
+ */
2775
+ resampleShape(final, preview, warpMap, h) {
2776
+ const layer = this.shape;
2777
+ let components = layer.components;
2778
+ if (!isIdentityTransform(this.geometryValue)) {
2779
+ const affine = !warpMap && h && Math.abs(h[6]) < 1e-12 && Math.abs(h[7]) < 1e-12 && Math.abs(h[8] - 1) < 1e-12 ? [h[0], h[3], h[1], h[4], h[2], h[5]] : void 0;
2780
+ const map = warpMap ?? ((p) => h ? applyHomography(h, p) : p);
2781
+ components = transformShapeComponents(layer.components, (p) => map(p) ?? p, affine);
2782
+ }
2783
+ const moved = { ...layer, components };
2784
+ delete moved.annotation;
2785
+ const surface = renderShape(moved, this.host.shapeEnv(), final ? {} : { clip: preview.clip });
2786
+ return mapLayer(this.startLayers, layer.id, () => ({ ...moved, surface }));
2787
+ }
2925
2788
  /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */
2926
2789
  resampleSmart(final, preview) {
2927
2790
  const layer = this.smart;
@@ -2982,7 +2845,7 @@ var FreeTransformSession = class {
2982
2845
  const id = this.copyId ?? this.layerId;
2983
2846
  const layer = id ? findLayer(layers, id)?.layer : void 0;
2984
2847
  if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
2985
- else if (this.moves.includes("pixels") && layer?.type === "smart") bounds = contentBounds(layer.surface);
2848
+ else if (this.moves.includes("pixels") && (layer?.type === "smart" || layer?.type === "shape")) bounds = contentBounds(layer.surface);
2986
2849
  else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
2987
2850
  else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
2988
2851
  return {
@@ -3601,7 +3464,7 @@ var ImageEditorController = class {
3601
3464
  */
3602
3465
  getLayerPixels(id) {
3603
3466
  const layer = this.getLayer(id);
3604
- if (!layer || layer.type !== "pixel" && layer.type !== "smart") return null;
3467
+ if (!layer || !hasSurface(layer)) return null;
3605
3468
  const surface = this.surfaceOf(layer);
3606
3469
  const b = contentBounds(surface);
3607
3470
  if (!b) return null;
@@ -3614,7 +3477,7 @@ var ImageEditorController = class {
3614
3477
  */
3615
3478
  getLayerBounds(id) {
3616
3479
  const layer = this.getLayer(id);
3617
- if (!layer || layer.type !== "pixel" && layer.type !== "smart") return null;
3480
+ if (!layer || !hasSurface(layer)) return null;
3618
3481
  const surface = this.surfaceOf(layer);
3619
3482
  let b = this.boundsCache.get(surface);
3620
3483
  if (b === void 0) {
@@ -3629,7 +3492,7 @@ var ImageEditorController = class {
3629
3492
  */
3630
3493
  getLayerDataURL(id) {
3631
3494
  const layer = this.getLayer(id);
3632
- if (!layer || layer.type !== "pixel" && layer.type !== "smart") return null;
3495
+ if (!layer || !hasSurface(layer)) return null;
3633
3496
  const surface = this.surfaceOf(layer);
3634
3497
  const enc = encodeSurface(surface);
3635
3498
  return enc ? enc.url : null;
@@ -3674,9 +3537,9 @@ var ImageEditorController = class {
3674
3537
  }
3675
3538
  return String(id);
3676
3539
  }
3677
- /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */
3540
+ /** The pixels a pixel layer, smart object or shape composites (the decoded source for the Background). */
3678
3541
  surfaceOf(layer) {
3679
- if (layer.type === "smart" || layer.source !== "document") return layer.surface;
3542
+ if (layer.type !== "pixel" || layer.source !== "document") return layer.surface;
3680
3543
  return this.sourceSurface() ?? layer.surface;
3681
3544
  }
3682
3545
  /**
@@ -3859,7 +3722,7 @@ var ImageEditorController = class {
3859
3722
  removeLayer(id) {
3860
3723
  const next = removeLayer(this.state.layers, id);
3861
3724
  this.commitLayers(next, () => {
3862
- if (this.state.selectedAnnotationId && !flattenLayers(next).some((l) => l.type === "annotation" && l.annotation.id === this.state.selectedAnnotationId)) {
3725
+ if (this.state.selectedAnnotationId && !flattenLayers(next).some((l) => annotationOfLayer(l)?.id === this.state.selectedAnnotationId)) {
3863
3726
  this.state.selectedAnnotationId = null;
3864
3727
  }
3865
3728
  });
@@ -3929,6 +3792,19 @@ var ImageEditorController = class {
3929
3792
  const moved = findLayer(this.state.layers, id).layer;
3930
3793
  return { offset: { x: Math.round(moved.placement.corners[0].x), y: Math.round(moved.placement.corners[0].y) }, bounds: this.getLayerBounds(id) };
3931
3794
  }
3795
+ if (layer.type === "shape") {
3796
+ const g = shapeGeometryBounds(layer) ?? { x: 0, y: 0 };
3797
+ const dx = move.x !== void 0 ? move.x - g.x : move.dx ?? 0;
3798
+ const dy = move.y !== void 0 ? move.y - g.y : move.dy ?? 0;
3799
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError("x and y must be numbers.");
3800
+ requireUnlocked(layer, "position", "move it");
3801
+ let moved = offsetShape(layer, dx, dy, this.shapeEnv());
3802
+ const mask = layer.mask;
3803
+ if (mask && mask.linked !== false && !mask.source && (Math.round(dx) || Math.round(dy))) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, Math.round(dx), Math.round(dy)) } };
3804
+ this.commitLayers(mapLayer(this.state.layers, id, () => moved), void 0, commitToHistory);
3805
+ const b = shapeGeometryBounds(moved) ?? { x: 0, y: 0 };
3806
+ return { offset: { x: Math.round(b.x), y: Math.round(b.y) }, bounds: this.getLayerBounds(id) };
3807
+ }
3932
3808
  const current = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
3933
3809
  const x = move.x ?? current.x + (move.dx ?? 0);
3934
3810
  const y = move.y ?? current.y + (move.dy ?? 0);
@@ -4076,6 +3952,7 @@ var ImageEditorController = class {
4076
3952
  selection: () => this.state.selection?.bounds ? this.state.selection : null,
4077
3953
  canvas: () => this.naturalSize,
4078
3954
  pixelsOf: (layer) => this.surfaceOf(layer),
3955
+ shapeEnv: () => this.shapeEnv(),
4079
3956
  capture: () => this.captureHistoryEntry(),
4080
3957
  show: (layers, selection, active) => {
4081
3958
  this.state.layers = layers;
@@ -4680,7 +4557,7 @@ var ImageEditorController = class {
4680
4557
  paintBounds(layer) {
4681
4558
  const { width, height } = this.naturalSize;
4682
4559
  const canvas = { x: 0, y: 0, width, height };
4683
- if (layer.type === "pixel") return this.getLayerBounds(layer.id) ?? { x: 0, y: 0, width: 0, height: 0 };
4560
+ if (hasSurface(layer)) return this.getLayerBounds(layer.id) ?? { x: 0, y: 0, width: 0, height: 0 };
4684
4561
  if (layer.type === "annotation") {
4685
4562
  const b = getAnnotationBounds(layer.annotation);
4686
4563
  const pad = 4 + ("strokeWidth" in layer.annotation ? layer.annotation.strokeWidth : 0);
@@ -4791,7 +4668,7 @@ var ImageEditorController = class {
4791
4668
  const k = Math.min(1, side / Math.max(width, height));
4792
4669
  const tw = Math.max(1, Math.round(width * k));
4793
4670
  const th = Math.max(1, Math.round(height * k));
4794
- const surface = options.mask ? layer.mask && !layer.mask.source ? layer.mask.surface : null : layer.type === "pixel" || layer.type === "smart" ? this.surfaceOf(layer) : null;
4671
+ const surface = options.mask ? layer.mask && !layer.mask.source ? layer.mask.surface : null : hasSurface(layer) ? this.surfaceOf(layer) : null;
4795
4672
  if (!surface) return null;
4796
4673
  const ox = layer.type === "pixel" ? layer.offset.x : 0;
4797
4674
  const oy = layer.type === "pixel" ? layer.offset.y : 0;
@@ -4895,6 +4772,7 @@ var ImageEditorController = class {
4895
4772
  requireUnlocked(layer, "all", action);
4896
4773
  } else {
4897
4774
  if (layer.type === "smart") throw new LayerOperationError(`Layer "${layer.name || id}" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);
4775
+ if (layer.type === "shape") throw new LayerOperationError(`Layer "${layer.name || id}" is a shape layer; its pixels are drawn from its paths. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);
4898
4776
  if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);
4899
4777
  requireUnlocked(layer, "pixels", action);
4900
4778
  }
@@ -5598,17 +5476,21 @@ var ImageEditorController = class {
5598
5476
  this.notify();
5599
5477
  }
5600
5478
  // --- Annotations & Layers ---
5479
+ /** The layer showing annotation `id`: an annotation layer, or a shape layer migrated from it. */
5601
5480
  annotationLayer(id) {
5602
- return flattenLayers(this.state.layers).find(
5603
- (l) => l.type === "annotation" && l.annotation.id === id
5604
- );
5481
+ return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape") && annotationOfLayer(l)?.id === id);
5605
5482
  }
5606
- /** Adds an annotation as its own layer (top of the stack unless `aboveId` is given). */
5483
+ /**
5484
+ * Adds an annotation as its own layer (top of the stack unless `aboveId` is given). Pen,
5485
+ * rectangle, ellipse, line and arrow annotations become shape layers that remember the
5486
+ * annotation (so `annotations` and `updateAnnotation` keep working); text and images stay
5487
+ * annotation layers.
5488
+ */
5607
5489
  addAnnotation(annotation, placement = {}) {
5608
5490
  this.recordHistory();
5609
5491
  const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
5610
5492
  const fullAnnotation = { ...annotation, id };
5611
- const layer = annotationLayerFor(fullAnnotation);
5493
+ const layer = annotationLayerFor(fullAnnotation, this.shapeEnv());
5612
5494
  const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;
5613
5495
  this.state.layers = loc ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1) : insertLayer(this.state.layers, layer, null);
5614
5496
  this.state.selectedAnnotationId = id;
@@ -5620,13 +5502,33 @@ var ImageEditorController = class {
5620
5502
  this.recordHistory();
5621
5503
  }
5622
5504
  const layer = this.annotationLayer(id);
5623
- if (layer) {
5624
- const annotation = { ...layer.annotation, ...partial, id };
5625
- const name = layer.name === annotationLayerName(layer.annotation) ? annotationLayerName(annotation) : layer.name;
5626
- this.state.layers = mapLayer(this.state.layers, layer.id, () => ({ ...layer, annotation, name }));
5505
+ const current = layer ? annotationOfLayer(layer) : null;
5506
+ if (layer && current) {
5507
+ const annotation = { ...current, ...partial, id };
5508
+ const name = layer.name === annotationLayerName(current) ? annotationLayerName(annotation) : layer.name;
5509
+ this.state.layers = mapLayer(this.state.layers, layer.id, () => this.layerForAnnotation(layer, annotation, name));
5627
5510
  }
5628
5511
  this.notify();
5629
5512
  }
5513
+ /** `layer` (an annotation or migrated shape layer) showing `annotation` instead, keeping its other properties. */
5514
+ layerForAnnotation(layer, annotation, name) {
5515
+ const shape = shapeFromAnnotation(annotation);
5516
+ const { annotation: _old, ...props } = layer;
5517
+ const blend = annotation.blend && annotation.blend !== "normal" ? annotation.blend : "normal";
5518
+ if (shape) {
5519
+ const base = { ...props, name, blend: layer.type === "annotation" && layer.blend !== "normal" ? layer.blend : blend };
5520
+ if (layer.type === "shape") {
5521
+ const fresh = rerenderShape({ ...base, components: shape.components, fill: shape.fill, stroke: shape.stroke }, this.shapeEnv());
5522
+ delete fresh.fillRule;
5523
+ return { ...fresh, annotation };
5524
+ }
5525
+ const created = createShapeLayer({ ...shape, name, blend: base.blend }, this.shapeEnv(), layer.id);
5526
+ const { type: _t2, ...rest2 } = base;
5527
+ return { ...rest2, ...created, annotation };
5528
+ }
5529
+ const { type: _t, components: _c, fill: _f, stroke: _s, surface: _sf, offset: _o, fillRule: _r, fillOpacity: _fo, psd: _p, ...rest } = props;
5530
+ return { ...rest, type: "annotation", name, blend: layer.type === "shape" ? "normal" : layer.blend, annotation };
5531
+ }
5630
5532
  removeAnnotation(id) {
5631
5533
  this.recordHistory();
5632
5534
  const layer = this.annotationLayer(id);
@@ -5636,10 +5538,11 @@ var ImageEditorController = class {
5636
5538
  }
5637
5539
  this.notify();
5638
5540
  }
5541
+ /** Deletes every annotation layer and every shape layer that still is the annotation it was migrated from. */
5639
5542
  clearAnnotations() {
5640
5543
  this.recordHistory();
5641
5544
  let layers = this.state.layers;
5642
- for (const l of flattenLayers(layers)) if (l.type === "annotation") layers = mapLayer(layers, l.id, () => null);
5545
+ for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
5643
5546
  this.state.layers = layers;
5644
5547
  this.state.selectedAnnotationId = null;
5645
5548
  this.notify();
@@ -5878,6 +5781,142 @@ var ImageEditorController = class {
5878
5781
  this.commitLayers(setLayerMask(this.state.layers, target.id, { surface, defaultValue: 0 }));
5879
5782
  return target.id;
5880
5783
  }
5784
+ // --- Shape layers ---
5785
+ /** What shape rendering needs: the canvas size and the document's patterns. */
5786
+ shapeEnv() {
5787
+ return { ...this.naturalSize, pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null };
5788
+ }
5789
+ /** One component from tool or UI input (validated). */
5790
+ componentFrom(spec, path) {
5791
+ const op = requireOperation(spec.op, `${path}op`);
5792
+ if (spec.shape !== void 0 && spec.subpaths !== void 0) throw new LayerOperationError(`Give ${path}shape or ${path}subpaths, not both.`);
5793
+ if (spec.shape !== void 0) return liveComponent(normalizeLiveShape(spec.shape, `${path}shape`), op);
5794
+ if (spec.subpaths !== void 0) return { op, subpaths: normalizeSubpaths(spec.subpaths, `${path}subpaths`) };
5795
+ throw new LayerOperationError(`${path}shape (a live shape: { kind: rect | ellipse | polygon | star | line, \u2026 }) or ${path}subpaths (anchors or SVG path data) is required.`);
5796
+ }
5797
+ shapeInfo(id) {
5798
+ const layer = requireShapeLayer(this.state.layers, id);
5799
+ const r = (n) => Math.round(n * 1e3) / 1e3;
5800
+ const box = (b) => b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null;
5801
+ const info = {
5802
+ id: layer.id,
5803
+ name: layer.name,
5804
+ components: layer.components.map((c, index) => ({
5805
+ index,
5806
+ op: c.op,
5807
+ live: c.live ? JSON.parse(JSON.stringify(c.live)) : null,
5808
+ subpaths: roundSubpaths(c.subpaths),
5809
+ bounds: box(shapeGeometryBounds({ components: [c] }))
5810
+ })),
5811
+ fillRule: layer.fillRule ?? "nonzero",
5812
+ fill: layer.fill ? JSON.parse(JSON.stringify(layer.fill)) : null,
5813
+ stroke: layer.stroke ? JSON.parse(JSON.stringify(layer.stroke)) : null,
5814
+ geometryBounds: box(shapeGeometryBounds(layer)),
5815
+ bounds: this.getLayerBounds(id)
5816
+ };
5817
+ if (layer.annotation) info.annotationId = layer.annotation.id;
5818
+ return info;
5819
+ }
5820
+ /** A shape layer's components, fill, stroke and bounds. */
5821
+ getShapeLayer(id) {
5822
+ return this.shapeInfo(id);
5823
+ }
5824
+ /**
5825
+ * Adds a shape layer (one undo step) and makes it active: a live shape or subpaths (or several
5826
+ * `components`), filled with `fill` (default the foreground colour, except for a stroke-only
5827
+ * open path) and stroked with `stroke`. Goes above the active layer unless placed. Returns its id.
5828
+ */
5829
+ addShapeLayer(init) {
5830
+ const specs = init.components?.length ? init.components : [init];
5831
+ if (init.components?.length && (init.shape !== void 0 || init.subpaths !== void 0)) throw new LayerOperationError("Give components, or shape / subpaths, not both.");
5832
+ const components = specs.map((c, i) => this.componentFrom(c, init.components?.length ? `components[${i}].` : ""));
5833
+ const stroke = init.stroke ? normalizeShapeStroke(init.stroke, "stroke") : null;
5834
+ const openOnly = components.every((c) => !c.live && c.subpaths.every((sp) => !sp.closed));
5835
+ const fill = init.fill === void 0 ? stroke && openOnly ? null : normalizeShapePaint(this.state.paint.foreground, "fill") : init.fill === null ? null : normalizeShapePaint(init.fill, "fill");
5836
+ if (!fill && !stroke) throw new LayerOperationError("A shape with neither fill nor stroke draws nothing; give fill or stroke.");
5837
+ if (init.fillRule !== void 0 && init.fillRule !== "nonzero" && init.fillRule !== "evenodd") throw new LayerOperationError("fillRule must be nonzero or evenodd.");
5838
+ 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.");
5839
+ if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
5840
+ const layer = createShapeLayer({ components, fill, stroke, fillRule: init.fillRule, name: init.name, opacity: init.opacity, blend: init.blend }, this.shapeEnv());
5841
+ const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
5842
+ this.commitLayers(next, () => {
5843
+ this.state.activeLayerId = layer.id;
5844
+ this.state.activeTarget = "pixels";
5845
+ });
5846
+ return layer.id;
5847
+ }
5848
+ /**
5849
+ * Sets a shape's fill, stroke or fill rule (one undo step unless `commitToHistory` is false):
5850
+ * `fill` / `stroke` replace them (`null` removes); `stroke` given as a partial object changes
5851
+ * only those settings of the current stroke. Returns the shape.
5852
+ */
5853
+ setShapeStyle(id, changes, commitToHistory = true) {
5854
+ const layer = requireShapeLayer(this.state.layers, id);
5855
+ const fill = changes.fill === void 0 ? layer.fill : changes.fill === null ? null : normalizeShapePaint(changes.fill, "fill");
5856
+ const stroke = changes.stroke === void 0 ? layer.stroke : changes.stroke === null ? null : normalizeShapeStroke(changes.stroke, "stroke", layer.stroke);
5857
+ if (changes.fillRule !== void 0 && changes.fillRule !== "nonzero" && changes.fillRule !== "evenodd") throw new LayerOperationError("fillRule must be nonzero or evenodd.");
5858
+ const fillRule = changes.fillRule ?? layer.fillRule ?? "nonzero";
5859
+ const { layers } = updateShapeLayer(this.state.layers, id, () => ({ fill, stroke, fillRule: fillRule === "nonzero" ? void 0 : fillRule }), this.shapeEnv(), "restyle it");
5860
+ this.commitLayers(layers, void 0, commitToHistory);
5861
+ return this.shapeInfo(id);
5862
+ }
5863
+ componentIndex(layer, index) {
5864
+ if (!Number.isInteger(index) || index < 0 || index >= layer.components.length) {
5865
+ throw new LayerOperationError(`componentIndex must be 0..${layer.components.length - 1} (get_shape_layer lists the components), got ${String(index)}.`);
5866
+ }
5867
+ return index;
5868
+ }
5869
+ /**
5870
+ * Changes one component (one undo step unless `commitToHistory` is false): its live shape
5871
+ * (fields not given keep their values when the kind stays the same), its subpaths (which drops
5872
+ * the live shape) or its operation. Returns the shape.
5873
+ */
5874
+ setShapeComponent(id, index, change, commitToHistory = true) {
5875
+ const layer = requireShapeLayer(this.state.layers, id);
5876
+ const i = this.componentIndex(layer, index);
5877
+ const current = layer.components[i];
5878
+ let shape = change.shape;
5879
+ if (shape && current.live && shape.kind === void 0) shape = { ...current.live, ...shape };
5880
+ else if (shape && current.live && shape.kind === current.live.kind) shape = { ...current.live, ...shape };
5881
+ const next = shape !== void 0 || change.subpaths !== void 0 ? this.componentFrom({ ...change, shape, op: change.op ?? current.op }, "") : { ...current, op: requireOperation(change.op) };
5882
+ const components = layer.components.map((c, k) => k === i ? next : c);
5883
+ const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), "edit its path");
5884
+ this.commitLayers(layers, void 0, commitToHistory);
5885
+ return this.shapeInfo(id);
5886
+ }
5887
+ /** Adds a component to a shape (path operations), on top unless `index` is given (one undo step). Returns the shape. */
5888
+ addShapeComponent(id, spec, index) {
5889
+ const layer = requireShapeLayer(this.state.layers, id);
5890
+ const c = this.componentFrom(spec, "");
5891
+ const at = index === void 0 ? layer.components.length : index;
5892
+ if (!Number.isInteger(at) || at < 0 || at > layer.components.length) throw new LayerOperationError(`index must be 0..${layer.components.length}, got ${String(index)}.`);
5893
+ const components = [...layer.components.slice(0, at), c, ...layer.components.slice(at)];
5894
+ const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), "edit its path");
5895
+ this.commitLayers(layers);
5896
+ return this.shapeInfo(id);
5897
+ }
5898
+ /** Removes one component of a shape (one undo step); the last one cannot go (delete the layer instead). Returns the shape. */
5899
+ removeShapeComponent(id, index) {
5900
+ const layer = requireShapeLayer(this.state.layers, id);
5901
+ const i = this.componentIndex(layer, index);
5902
+ if (layer.components.length === 1) throw new LayerOperationError("This is the shape's only component; delete the layer instead (delete_layer).");
5903
+ const components = layer.components.filter((_, k) => k !== i);
5904
+ const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components }), this.shapeEnv(), "edit its path");
5905
+ this.commitLayers(layers);
5906
+ return this.shapeInfo(id);
5907
+ }
5908
+ /**
5909
+ * Merge Shape Components: the area the components combine to becomes one plain component (live
5910
+ * shapes become paths), as one undo step. Returns the shape.
5911
+ */
5912
+ mergeShapeComponents(id) {
5913
+ const layer = requireShapeLayer(this.state.layers, id);
5914
+ if (layer.components.length < 2) throw new LayerOperationError("The shape has one component; there is nothing to merge.");
5915
+ const components = mergeShapeComponents(layer.components, layer.fillRule);
5916
+ const { layers } = updateShapeLayer(this.state.layers, id, () => ({ components, fillRule: void 0 }), this.shapeEnv(), "edit its path");
5917
+ this.commitLayers(layers);
5918
+ return this.shapeInfo(id);
5919
+ }
5881
5920
  /**
5882
5921
  * Rasterize Layer: an annotation layer becomes a pixel layer with the same id, name and
5883
5922
  * properties (needs a canvas), and a pixel layer's filters are baked into its pixels. One undo
@@ -5918,8 +5957,11 @@ var ImageEditorController = class {
5918
5957
  }
5919
5958
  } else if (layer.type === "smart") {
5920
5959
  return this.rasterizeSmartObject(id);
5960
+ } else if (layer.type === "shape") {
5961
+ next = shapeAsPixelLayer(layer);
5962
+ if (this.state.selectedAnnotationId && this.state.selectedAnnotationId === layer.annotation?.id) this.state.selectedAnnotationId = null;
5921
5963
  } else {
5922
- throw new LayerOperationError(`Only annotation layers, smart objects and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
5964
+ throw new LayerOperationError(`Only shape and annotation layers, smart objects and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
5923
5965
  }
5924
5966
  this.commitLayers(mapLayer(this.state.layers, id, () => next));
5925
5967
  return id;
@@ -6044,6 +6086,14 @@ var ImageEditorController = class {
6044
6086
  if (mask2) out2.mask = mask2;
6045
6087
  return out2;
6046
6088
  }
6089
+ if (layer.type === "shape") {
6090
+ add2("Shape layers were written as pixel layers", layer.name);
6091
+ const out2 = shapeAsPixelLayer(layer);
6092
+ const mask2 = bakeMask(layer);
6093
+ if (mask2) out2.mask = mask2;
6094
+ else delete out2.mask;
6095
+ return out2;
6096
+ }
6047
6097
  if (layer.type === "smart") {
6048
6098
  const out2 = { ...layer };
6049
6099
  const mask2 = bakeMask(layer);
@@ -6120,16 +6170,27 @@ var ImageEditorController = class {
6120
6170
  );
6121
6171
  }
6122
6172
  layers = resizeLegacyMasks(layers, prev, next.dimensions);
6173
+ const env = { ...next.dimensions, pattern: (pid) => this.state.patterns?.find((p) => p.id === pid) ?? null };
6123
6174
  for (const l of flattenLayers(layers)) {
6124
6175
  if (l.type === "annotation") {
6125
6176
  layers = mapLayer(layers, l.id, () => ({ ...l, annotation: scaleAnnotation(l.annotation, sx, sy) }));
6177
+ } else if (l.type === "shape") {
6178
+ const a = l.annotation;
6179
+ const scaled = a ? scaleAnnotation(a, sx, sy) : null;
6180
+ const fromAnnotation = scaled ? shapeFromAnnotation(scaled) : null;
6181
+ const m = [sx, 0, 0, sy, 0, 0];
6182
+ const components = fromAnnotation?.components ?? transformShapeComponents(l.components, (p) => ({ x: p.x * sx, y: p.y * sy }), m);
6183
+ const moved = { ...l, components, ...fromAnnotation ? { fill: fromAnnotation.fill, stroke: fromAnnotation.stroke } : {} };
6184
+ if (scaled && fromAnnotation) moved.annotation = scaled;
6185
+ else delete moved.annotation;
6186
+ layers = mapLayer(layers, l.id, () => rerenderShape(moved, env));
6126
6187
  }
6127
6188
  }
6128
6189
  }
6129
6190
  const bg = developTarget(layers, null);
6130
6191
  if (bg && bg.source !== "document") layers = mapLayer(layers, bg.id, () => ({ ...bg, source: "document" }));
6131
6192
  if (clearAnnotations) {
6132
- for (const l of flattenLayers(layers)) if (l.type === "annotation") layers = mapLayer(layers, l.id, () => null);
6193
+ for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
6133
6194
  this.state.selectedAnnotationId = null;
6134
6195
  }
6135
6196
  this.state.layers = layers;
@@ -6199,7 +6260,14 @@ var ImageEditorController = class {
6199
6260
  if (isLayered(data)) {
6200
6261
  const problems = [];
6201
6262
  const canvas = typeof data.width === "number" && data.width > 0 && typeof data.height === "number" && data.height > 0 ? { width: data.width, height: data.height } : this.naturalSize;
6202
- s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems, smartContents: Array.isArray(data.smartContents) ? data.smartContents : [], canvas });
6263
+ const storedPatterns = deserializePatterns(data.patterns, []);
6264
+ s.layers = deserializeLayers(data.layers, {
6265
+ assets: data.assets ?? {},
6266
+ problems,
6267
+ smartContents: Array.isArray(data.smartContents) ? data.smartContents : [],
6268
+ canvas,
6269
+ pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null
6270
+ });
6203
6271
  this.lastLoadProblems = problems;
6204
6272
  s.crop = data.crop ? { ...data.crop } : null;
6205
6273
  s.transform = { ...data.transform };
@@ -6401,6 +6469,7 @@ function collectStateTiles(layers, into) {
6401
6469
  for (const l of flattenLayers(layers)) {
6402
6470
  if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
6403
6471
  if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);
6472
+ if (l.type === "shape") collectTiles(l.surface, into);
6404
6473
  if (l.type === "smart") {
6405
6474
  collectTiles(l.surface, into);
6406
6475
  if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);
@@ -6449,7 +6518,7 @@ function resizePixels(px2, width, height) {
6449
6518
  function withoutAnnotations(layers) {
6450
6519
  const out = [];
6451
6520
  for (const l of layers) {
6452
- if (l.type === "annotation") continue;
6521
+ if (l.type === "annotation" || l.type === "shape" && l.annotation) continue;
6453
6522
  out.push(l.type === "group" ? { ...l, children: withoutAnnotations(l.children) } : l);
6454
6523
  }
6455
6524
  return out;
@@ -6639,7 +6708,7 @@ function loadImageEditorDocument(input) {
6639
6708
  if (state.selection === void 0) delete state.selection;
6640
6709
  return { state, report: result.report, unknown: result.unknown };
6641
6710
  }
6642
- var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart"]);
6711
+ var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape"]);
6643
6712
  function cleanLayers(value, dropped) {
6644
6713
  const out = [];
6645
6714
  for (const raw of value) {
@@ -7431,7 +7500,7 @@ function createImageEditTools(controller) {
7431
7500
  return { layer: layers[index], index };
7432
7501
  };
7433
7502
  const layerJson = (id) => JSON.parse(JSON.stringify(controller.getAdjustmentLayer(id)));
7434
- const geometry = () => {
7503
+ const geometry2 = () => {
7435
7504
  const s = controller.getState();
7436
7505
  const region = s.crop ?? { x: 0, y: 0, ...s.imageDimensions };
7437
7506
  const out = getRotatedSize(region.width, region.height, s.transform);
@@ -7456,7 +7525,7 @@ function createImageEditTools(controller) {
7456
7525
  description: "Returns the source image size in pixels, the crop box (natural image pixels, or null for the whole image), rotation in degrees, flips, the output size after crop and rotation, the number of adjustment layers and whether undo / redo are available.",
7457
7526
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
7458
7527
  annotations: { title: "Read image geometry", ...READ },
7459
- handler: () => ({ ...geometry(), layers: controller.getAdjustmentLayers().length, ...history() })
7528
+ handler: () => ({ ...geometry2(), layers: controller.getAdjustmentLayers().length, ...history() })
7460
7529
  };
7461
7530
  const getHistogram = {
7462
7531
  name: "get_histogram",
@@ -7560,7 +7629,7 @@ function createImageEditTools(controller) {
7560
7629
  throw new Error(`The crop box (${x}, ${y}, ${width} \xD7 ${height}) goes outside the ${image.width} \xD7 ${image.height} image.`);
7561
7630
  }
7562
7631
  controller.setCrop({ x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) });
7563
- return geometry();
7632
+ return geometry2();
7564
7633
  }
7565
7634
  };
7566
7635
  const resetCrop = {
@@ -7572,7 +7641,7 @@ function createImageEditTools(controller) {
7572
7641
  handler: () => {
7573
7642
  if (!controller.getState().crop) throw new Error("The image is not cropped.");
7574
7643
  controller.resetCrop();
7575
- return geometry();
7644
+ return geometry2();
7576
7645
  }
7577
7646
  };
7578
7647
  const setRotation = {
@@ -7589,7 +7658,7 @@ function createImageEditTools(controller) {
7589
7658
  handler: ({ degrees }) => {
7590
7659
  requireImage();
7591
7660
  controller.setRotation(degrees);
7592
- return geometry();
7661
+ return geometry2();
7593
7662
  }
7594
7663
  };
7595
7664
  const flipImage = {
@@ -7607,7 +7676,7 @@ function createImageEditTools(controller) {
7607
7676
  requireImage();
7608
7677
  if (axis === "horizontal") controller.flipHorizontal();
7609
7678
  else controller.flipVertical();
7610
- return geometry();
7679
+ return geometry2();
7611
7680
  }
7612
7681
  };
7613
7682
  const undo = {
@@ -7689,6 +7758,16 @@ function describeLayer(layer, activeId, boundsOf) {
7689
7758
  out.collapsed = Boolean(layer.collapsed);
7690
7759
  out.children = layer.children.map((c) => describeLayer(c, activeId, boundsOf));
7691
7760
  break;
7761
+ case "shape":
7762
+ out.bounds = boundsOf(layer);
7763
+ out.fillOpacity = round3(layer.fillOpacity ?? 1);
7764
+ out.shape = {
7765
+ components: layer.components.map((c) => ({ op: c.op, kind: c.live?.kind ?? "path" })),
7766
+ fill: layer.fill ? layer.fill.type : null,
7767
+ stroke: layer.stroke ? { width: layer.stroke.width, paint: layer.stroke.paint.type } : null
7768
+ };
7769
+ if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
7770
+ break;
7692
7771
  case "smart":
7693
7772
  out.bounds = boundsOf(layer);
7694
7773
  out.fillOpacity = round3(layer.fillOpacity ?? 1);
@@ -7734,7 +7813,7 @@ function createImageLayerTools(controller) {
7734
7813
  const listLayers = {
7735
7814
  name: "list_layers",
7736
7815
  title: "List image layers",
7737
- description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
7816
+ description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, shape, smart, annotation), name, document bounds (pixel, shape and smart layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), a shape summary (component operations and kinds, fill and stroke paint types; details from get_shape_layer), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
7738
7817
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
7739
7818
  annotations: { title: "List image layers", ...READ2 },
7740
7819
  handler: () => {
@@ -8078,7 +8157,7 @@ function createImageLayerTools(controller) {
8078
8157
  const rasterize = {
8079
8158
  name: "rasterize_layer",
8080
8159
  title: "Rasterize a layer",
8081
- description: "Rasterize Layer: turns an annotation layer into a pixel layer (same id, name, opacity, blend and mask; needs a browser) or bakes a pixel layer's filters (develop, blur) into its pixels. One undo step. Returns the layer.",
8160
+ description: "Rasterize Layer: turns a shape layer into a pixel layer holding what it shows now (same id, name, opacity, blend, fill, mask and effects; its paths are gone, so later transforms resample pixels), turns an annotation layer into a pixel layer (needs a browser), rasterizes a smart object, or bakes a pixel layer's filters (develop, blur) into its pixels. One undo step. Returns the layer.",
8082
8161
  inputSchema: { type: "object", properties: { layerId: LAYER_ID2 }, required: ["layerId"], additionalProperties: false },
8083
8162
  annotations: { title: "Rasterize a layer", ...DESTRUCTIVE },
8084
8163
  handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) })
@@ -8181,7 +8260,7 @@ var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true,
8181
8260
  var WRITE3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
8182
8261
  var WRITE_IDEMPOTENT3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
8183
8262
  var DESTRUCTIVE2 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
8184
- var LAYER_ID3 = { type: "string", description: "Layer id from list_layers (a pixel, annotation or group layer)." };
8263
+ var LAYER_ID3 = { type: "string", description: "Layer id from list_layers (a pixel, shape, smart object, annotation or group layer)." };
8185
8264
  var EFFECT_ID = { type: "string", description: "Effect id from list_layer_effects or add_layer_effect." };
8186
8265
  var BLEND_IDS = BLEND_MODES.map((m) => m.id).filter((id) => id !== "pigment");
8187
8266
  var DEFS = {
@@ -8603,9 +8682,13 @@ var PaintGesture = class {
8603
8682
  if (tool === "move") {
8604
8683
  const layer = this.controller.getActiveLayer();
8605
8684
  if (!layer) throw new Error("Select a layer to move in the Layers panel.");
8606
- if (layer.type !== "pixel") throw new Error(`Only pixel layers move; "${layer.name}" is a ${layer.type} layer.`);
8685
+ if (layer.type !== "pixel" && layer.type !== "shape" && layer.type !== "smart") throw new Error(`Only pixel, shape and smart object layers move; "${layer.name}" is a ${layer.type} layer.`);
8607
8686
  if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer "${layer.name}" has its position locked.`);
8608
- this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };
8687
+ const origin = layer.type === "pixel" ? { ...layer.offset } : layer.type === "smart" ? { ...layer.placement.corners[0] } : (() => {
8688
+ const b = shapeGeometryBounds(layer);
8689
+ return b ? { x: b.x, y: b.y } : { x: 0, y: 0 };
8690
+ })();
8691
+ this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin, moved: false };
8609
8692
  return null;
8610
8693
  }
8611
8694
  if (tool === "brush" || tool === "eraser" || tool === "mixer") {
@@ -9520,6 +9603,409 @@ function createImageSmartObjectTools(controller) {
9520
9603
  return [getSmart, convert, rasterize, setTransform, replace, exportContents, copy, editContents, closeContents, addFilter, updateFilter, removeFilter, moveFilter, setEnabled, setMask];
9521
9604
  }
9522
9605
 
9606
+ // src/image-editor/shape-tools.ts
9607
+ var READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9608
+ var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
9609
+ var WRITE_IDEMPOTENT5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9610
+ var DESTRUCTIVE6 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
9611
+ var LAYER_ID5 = { type: "string", description: "Shape layer id from list_layers (type shape) or add_shape_layer." };
9612
+ var COMPONENT_INDEX = { type: "integer", minimum: 0, description: "Component index from get_shape_layer (0 = bottom component)." };
9613
+ var OP = {
9614
+ type: "string",
9615
+ enum: ["combine", "subtract", "intersect", "exclude"],
9616
+ description: "How the component combines with the components below it (Photoshop path operations; default combine). The bottom component always combines."
9617
+ };
9618
+ var NUM = (description) => ({ type: "number", description });
9619
+ var MATRIX = {
9620
+ type: "array",
9621
+ items: { type: "number", description: "Matrix entry." },
9622
+ minItems: 6,
9623
+ maxItems: 6,
9624
+ description: "Optional affine map from the shape's own frame to the document [a, b, c, d, e, f] (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f), e.g. a rotation. Default identity."
9625
+ };
9626
+ var BOX = {
9627
+ x: NUM("Left of the bounding box, document pixels."),
9628
+ y: NUM("Top of the bounding box, document pixels."),
9629
+ width: NUM("Width in pixels."),
9630
+ height: NUM("Height in pixels."),
9631
+ transform: { $ref: "#/$defs/matrix" }
9632
+ };
9633
+ var DEFS2 = {
9634
+ matrix: MATRIX,
9635
+ point: {
9636
+ type: "object",
9637
+ description: "A point in document pixels.",
9638
+ properties: { x: NUM("Pixels from the left."), y: NUM("Pixels from the top.") },
9639
+ required: ["x", "y"],
9640
+ additionalProperties: false
9641
+ },
9642
+ arrowhead: {
9643
+ type: "object",
9644
+ description: "An arrowhead whose tip is the line end: a triangle, notched by concavity.",
9645
+ properties: {
9646
+ width: NUM("Width across the base, pixels."),
9647
+ length: NUM("Length from base to tip, pixels."),
9648
+ concavity: { type: "number", minimum: -0.5, maximum: 0.5, description: "Notch of the base towards the tip as a fraction of length (default 0)." }
9649
+ },
9650
+ required: ["width", "length"],
9651
+ additionalProperties: false
9652
+ },
9653
+ shape: {
9654
+ description: "A live shape (stays editable as that shape): rect (with optional corner radii), ellipse, polygon, star, or line (a filled bar with optional arrowheads). Sizes are document pixels.",
9655
+ anyOf: [
9656
+ {
9657
+ type: "object",
9658
+ description: "Rectangle.",
9659
+ properties: {
9660
+ kind: { const: "rect", description: "Rectangle." },
9661
+ ...BOX,
9662
+ radius: { type: "number", minimum: 0, description: "One corner radius for all corners, pixels." },
9663
+ radii: { type: "array", items: { type: "number", minimum: 0, description: "Radius in pixels." }, minItems: 4, maxItems: 4, description: "Corner radii [top-left, top-right, bottom-right, bottom-left] in pixels." }
9664
+ },
9665
+ required: ["kind", "x", "y", "width", "height"],
9666
+ additionalProperties: false
9667
+ },
9668
+ { type: "object", description: "Ellipse inscribed in the box.", properties: { kind: { const: "ellipse", description: "Ellipse." }, ...BOX }, required: ["kind", "x", "y", "width", "height"], additionalProperties: false },
9669
+ {
9670
+ type: "object",
9671
+ description: "Regular polygon inscribed in the box, first vertex at the top.",
9672
+ properties: { kind: { const: "polygon", description: "Polygon." }, ...BOX, sides: { type: "integer", minimum: 3, maximum: 100, description: "Number of sides (default 6)." } },
9673
+ required: ["kind", "x", "y", "width", "height"],
9674
+ additionalProperties: false
9675
+ },
9676
+ {
9677
+ type: "object",
9678
+ description: "Star inscribed in the box, first point at the top.",
9679
+ properties: {
9680
+ kind: { const: "star", description: "Star." },
9681
+ ...BOX,
9682
+ points: { type: "integer", minimum: 3, maximum: 100, description: "Number of points (default 5)." },
9683
+ innerRatio: { type: "number", minimum: 0.01, maximum: 1, description: "Inner radius over outer radius (default 0.5)." }
9684
+ },
9685
+ required: ["kind", "x", "y", "width", "height"],
9686
+ additionalProperties: false
9687
+ },
9688
+ {
9689
+ type: "object",
9690
+ description: "Line drawn as a filled bar.",
9691
+ properties: {
9692
+ kind: { const: "line", description: "Line." },
9693
+ x1: NUM("Start x, document pixels."),
9694
+ y1: NUM("Start y, document pixels."),
9695
+ x2: NUM("End x, document pixels."),
9696
+ y2: NUM("End y, document pixels."),
9697
+ weight: { type: "number", minimum: 0, description: "Thickness in pixels (default 1)." },
9698
+ startArrow: { anyOf: [{ $ref: "#/$defs/arrowhead" }, { type: "null", description: "No arrowhead." }], description: "Arrowhead at the start, or null." },
9699
+ endArrow: { anyOf: [{ $ref: "#/$defs/arrowhead" }, { type: "null", description: "No arrowhead." }], description: "Arrowhead at the end, or null." },
9700
+ transform: { $ref: "#/$defs/matrix" }
9701
+ },
9702
+ required: ["kind", "x1", "y1", "x2", "y2"],
9703
+ additionalProperties: false
9704
+ }
9705
+ ]
9706
+ },
9707
+ subpaths: {
9708
+ description: "A free path: SVG path data (absolute or relative M, L, C, Q, A, Z) or subpaths with anchors. Handles are absolute document positions.",
9709
+ anyOf: [
9710
+ { type: "string", description: "SVG path data, document pixels." },
9711
+ {
9712
+ type: "array",
9713
+ minItems: 1,
9714
+ description: "Subpaths.",
9715
+ items: {
9716
+ type: "object",
9717
+ description: "One subpath.",
9718
+ properties: {
9719
+ anchors: {
9720
+ type: "array",
9721
+ minItems: 1,
9722
+ description: "Anchors in order.",
9723
+ items: {
9724
+ type: "object",
9725
+ description: "An anchor with optional handles.",
9726
+ properties: {
9727
+ x: NUM("Anchor x, document pixels."),
9728
+ y: NUM("Anchor y, document pixels."),
9729
+ in: { $ref: "#/$defs/point", description: "Handle of the segment arriving here (absolute)." },
9730
+ out: { $ref: "#/$defs/point", description: "Handle of the segment leaving here (absolute)." },
9731
+ corner: { type: "number", minimum: 0, description: "Live corner radius at this anchor, pixels." }
9732
+ },
9733
+ required: ["x", "y"],
9734
+ additionalProperties: false
9735
+ }
9736
+ },
9737
+ closed: { type: "boolean", description: "Joins the last anchor back to the first (default true)." }
9738
+ },
9739
+ required: ["anchors"],
9740
+ additionalProperties: false
9741
+ }
9742
+ }
9743
+ ]
9744
+ },
9745
+ gradient: {
9746
+ type: "object",
9747
+ description: "A gradient: colour stops, optional opacity stops and its layout (aligned with the shape bounds by default).",
9748
+ properties: {
9749
+ stops: {
9750
+ type: "array",
9751
+ minItems: 1,
9752
+ description: "Colour stops.",
9753
+ items: {
9754
+ type: "object",
9755
+ description: "A colour stop.",
9756
+ properties: { position: { type: "number", minimum: 0, maximum: 1, description: "0..1 along the gradient." }, color: { type: "string", description: "Colour (#rrggbb or a CSS colour)." } },
9757
+ required: ["position", "color"],
9758
+ additionalProperties: false
9759
+ }
9760
+ },
9761
+ opacityStops: {
9762
+ type: "array",
9763
+ description: "Opacity stops (default opaque).",
9764
+ items: {
9765
+ type: "object",
9766
+ description: "An opacity stop.",
9767
+ properties: { position: { type: "number", minimum: 0, maximum: 1, description: "0..1 along the gradient." }, opacity: { type: "number", minimum: 0, maximum: 1, description: "0..1." } },
9768
+ required: ["position", "opacity"],
9769
+ additionalProperties: false
9770
+ }
9771
+ },
9772
+ style: { type: "string", enum: ["linear", "radial", "angle", "reflected", "diamond"], description: "Geometry (default linear)." },
9773
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Degrees, 90 = bottom to top (default 90)." },
9774
+ scale: { type: "number", minimum: 0.1, maximum: 1.5, description: "1 = 100 % (default 1)." },
9775
+ reverse: { type: "boolean", description: "Swap the ends (default false)." },
9776
+ align: { type: "boolean", description: "Lay out over the shape bounds (true, default) or the canvas." },
9777
+ offset: { $ref: "#/$defs/point", description: "Centre offset as fractions of the bounds (default 0, 0)." }
9778
+ },
9779
+ required: ["stops"],
9780
+ additionalProperties: false
9781
+ },
9782
+ paint: {
9783
+ description: "What a fill or stroke paints with: a colour string (#rrggbb, #rrggbbaa, rgb(), a CSS name; alpha becomes opacity), or an object of type color, gradient or pattern.",
9784
+ anyOf: [
9785
+ { type: "string", description: "A colour." },
9786
+ {
9787
+ type: "object",
9788
+ description: "Solid colour.",
9789
+ properties: { type: { const: "color", description: "Solid colour." }, color: { type: "string", description: "Colour." }, opacity: { type: "number", minimum: 0, maximum: 1, description: "0..1 (default 1)." } },
9790
+ required: ["type", "color"],
9791
+ additionalProperties: false
9792
+ },
9793
+ {
9794
+ type: "object",
9795
+ description: "Gradient.",
9796
+ properties: { type: { const: "gradient", description: "Gradient." }, gradient: { $ref: "#/$defs/gradient" }, opacity: { type: "number", minimum: 0, maximum: 1, description: "0..1 (default 1)." } },
9797
+ required: ["type", "gradient"],
9798
+ additionalProperties: false
9799
+ },
9800
+ {
9801
+ type: "object",
9802
+ description: "Pattern (list_patterns gives ids).",
9803
+ properties: {
9804
+ type: { const: "pattern", description: "Pattern." },
9805
+ patternId: { type: "string", description: "A document or built-in pattern id." },
9806
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "1 = 100 % (default 1)." },
9807
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Degrees counter-clockwise (default 0)." },
9808
+ link: { type: "boolean", description: "Tile from the shape\u2019s top-left (default true) instead of the canvas origin." },
9809
+ phase: { $ref: "#/$defs/point", description: "Origin offset in pixels." },
9810
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "0..1 (default 1)." }
9811
+ },
9812
+ required: ["type", "patternId"],
9813
+ additionalProperties: false
9814
+ }
9815
+ ]
9816
+ },
9817
+ stroke: {
9818
+ type: "object",
9819
+ description: "The shape stroke (not the layer-effect stroke). Unset settings keep their current values (or the defaults).",
9820
+ properties: {
9821
+ width: { type: "number", minimum: 0, maximum: 1e4, description: "Width in pixels (required for a new stroke)." },
9822
+ paint: { $ref: "#/$defs/paint" },
9823
+ color: { type: "string", description: "Shorthand for paint as a colour." },
9824
+ align: { type: "string", enum: ["inside", "center", "outside"], description: "Where it sits on the outline (default center; open paths always centre)." },
9825
+ cap: { type: "string", enum: ["butt", "round", "square"], description: "Line ends (default butt)." },
9826
+ join: { type: "string", enum: ["miter", "round", "bevel"], description: "Corners (default miter)." },
9827
+ miterLimit: { type: "number", minimum: 1, maximum: 500, description: "Miter length over width above which a corner is bevelled (default 100)." },
9828
+ dashes: { type: "array", maxItems: 32, items: { type: "number", minimum: 0, description: "Length in multiples of the width." }, description: "Dash and gap lengths in multiples of the stroke width ([] = solid)." },
9829
+ dashOffset: { type: "number", description: "Start offset into the dashes, multiples of the width (default 0)." }
9830
+ },
9831
+ additionalProperties: false
9832
+ }
9833
+ };
9834
+ var GEOMETRY2 = {
9835
+ shape: { $ref: "#/$defs/shape", description: "A live shape: { kind: rect | ellipse | polygon | star | line, \u2026 } in document pixels. Give shape or subpaths." },
9836
+ subpaths: { $ref: "#/$defs/subpaths", description: "A free path: SVG path data or subpaths with anchors, document pixels. Give shape or subpaths." }
9837
+ };
9838
+ function geometry(args) {
9839
+ const out = {};
9840
+ if (args.shape !== void 0) out.shape = args.shape;
9841
+ if (args.subpaths !== void 0) out.subpaths = args.subpaths;
9842
+ if (args.op !== void 0) out.op = args.op;
9843
+ return out;
9844
+ }
9845
+ function createImageShapeTools(controller) {
9846
+ const shapeId = (id) => {
9847
+ if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a shape layer id from list_layers).");
9848
+ requireShapeLayer(controller.getLayers(), id);
9849
+ return id;
9850
+ };
9851
+ const layerSummary = (id) => {
9852
+ const s = controller.getState();
9853
+ const loc = findLayer(s.layers, id);
9854
+ return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;
9855
+ };
9856
+ const get = {
9857
+ name: "get_shape_layer",
9858
+ title: "Read a shape layer",
9859
+ description: "Returns a shape layer\u2019s components bottom to top (index, path operation, the live shape it was drawn as or null, its subpaths with absolute anchors and handles, bounds), the fill rule, fill and stroke paints and settings, the path bounds and the rendered bounds (stroke included). Changes nothing.",
9860
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, required: ["layerId"], additionalProperties: false },
9861
+ annotations: { title: "Read a shape layer", ...READ8 },
9862
+ handler: ({ layerId }) => controller.getShapeLayer(shapeId(layerId))
9863
+ };
9864
+ const add2 = {
9865
+ name: "add_shape_layer",
9866
+ title: "Add a shape layer",
9867
+ description: "Adds a vector shape layer and makes it active: a live shape (rect, ellipse, polygon, star, line), a free path (SVG path data or anchors), or several components combined with path operations. fill defaults to the foreground colour (none for an open path with a stroke); give stroke for an outline. It renders sharp at any size, can be moved and transformed without losing quality, and takes masks, effects, clipping and blend modes like any layer. Goes above the active layer unless placed. One undo step. Returns the layer and the shape.",
9868
+ inputSchema: {
9869
+ type: "object",
9870
+ $defs: DEFS2,
9871
+ properties: {
9872
+ ...GEOMETRY2,
9873
+ components: {
9874
+ type: "array",
9875
+ minItems: 1,
9876
+ description: "Several components instead of shape / subpaths, bottom first, each with its path operation.",
9877
+ items: { type: "object", description: "One component.", properties: { ...GEOMETRY2, op: OP }, additionalProperties: false }
9878
+ },
9879
+ fill: { anyOf: [{ $ref: "#/$defs/paint" }, { type: "null", description: "No fill." }], description: "Fill paint, or null for none (default the foreground colour)." },
9880
+ stroke: { anyOf: [{ $ref: "#/$defs/stroke" }, { type: "null", description: "No stroke." }], description: "Stroke { width, paint, \u2026 }, or null / omitted for none." },
9881
+ fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component (default nonzero)." },
9882
+ name: { type: "string", description: "Layer name (default by kind: Rectangle, Ellipse, \u2026)." },
9883
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0..1 (default 1)." },
9884
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
9885
+ aboveId: { type: "string", description: "Put the layer directly above this layer (default above the active layer)." },
9886
+ belowId: { type: "string", description: "Put the layer directly below this layer." },
9887
+ parentId: { type: "string", description: "Put the layer inside this group." },
9888
+ index: { type: "integer", minimum: 0, description: "Position among the siblings, 0 = bottom." }
9889
+ },
9890
+ additionalProperties: false
9891
+ },
9892
+ annotations: { title: "Add a shape layer", ...WRITE7 },
9893
+ handler: (args) => {
9894
+ const init = { ...geometry(args) };
9895
+ if (args.components !== void 0) init.components = args.components.map(geometry);
9896
+ for (const k of ["fill", "stroke", "fillRule", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]) {
9897
+ if (args[k] !== void 0) init[k] = args[k];
9898
+ }
9899
+ if (init.blend !== void 0) init.blend = init.blend;
9900
+ const id = controller.addShapeLayer(init);
9901
+ return { layer: layerSummary(id), shape: controller.getShapeLayer(id) };
9902
+ }
9903
+ };
9904
+ const update = {
9905
+ name: "update_shape",
9906
+ title: "Restyle a shape",
9907
+ description: "Changes a shape layer\u2019s fill, stroke or fill rule and renders it again: fill replaces the fill (null removes it); stroke changes only the settings given (null removes the stroke; a new stroke needs width and paint). Geometry is untouched (set_shape_component edits paths). One undo step. Returns the shape.",
9908
+ inputSchema: {
9909
+ type: "object",
9910
+ $defs: DEFS2,
9911
+ properties: {
9912
+ layerId: LAYER_ID5,
9913
+ fill: { anyOf: [{ $ref: "#/$defs/paint" }, { type: "null", description: "No fill." }], description: "New fill paint, or null for none." },
9914
+ stroke: { anyOf: [{ $ref: "#/$defs/stroke" }, { type: "null", description: "No stroke." }], description: "Stroke settings to change, or null to remove the stroke." },
9915
+ fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component." }
9916
+ },
9917
+ required: ["layerId"],
9918
+ additionalProperties: false
9919
+ },
9920
+ annotations: { title: "Restyle a shape", ...WRITE_IDEMPOTENT5 },
9921
+ handler: ({ layerId, fill, stroke, fillRule }) => {
9922
+ const id = shapeId(layerId);
9923
+ if (fill === void 0 && stroke === void 0 && fillRule === void 0) throw new LayerOperationError("Give fill, stroke or fillRule to change.");
9924
+ return controller.setShapeStyle(id, { fill, stroke, fillRule });
9925
+ }
9926
+ };
9927
+ const setComponent = {
9928
+ name: "set_shape_component",
9929
+ title: "Edit a shape component",
9930
+ description: "Changes one component of a shape layer: shape sets its live shape (fields you leave out keep their values when the kind stays the same, e.g. only radii), subpaths replaces its path (which ends the live shape), op changes its path operation. Positions are document pixels. One undo step. Returns the shape.",
9931
+ inputSchema: {
9932
+ type: "object",
9933
+ $defs: DEFS2,
9934
+ properties: {
9935
+ layerId: LAYER_ID5,
9936
+ componentIndex: COMPONENT_INDEX,
9937
+ shape: {
9938
+ type: "object",
9939
+ description: "Live shape fields to set (see add_shape_layer\u2019s shape): kind plus its fields; without kind, the fields change the current live shape.",
9940
+ properties: {
9941
+ kind: { type: "string", enum: ["rect", "ellipse", "polygon", "star", "line"], description: "Shape kind (default the current one)." },
9942
+ x: NUM("Left of the box, pixels."),
9943
+ y: NUM("Top of the box, pixels."),
9944
+ width: NUM("Box width, pixels."),
9945
+ height: NUM("Box height, pixels."),
9946
+ radius: { type: "number", minimum: 0, description: "rect: one corner radius, pixels." },
9947
+ radii: { type: "array", items: { type: "number", minimum: 0, description: "Radius in pixels." }, minItems: 4, maxItems: 4, description: "rect: [top-left, top-right, bottom-right, bottom-left] radii, pixels." },
9948
+ sides: { type: "integer", minimum: 3, maximum: 100, description: "polygon: number of sides." },
9949
+ points: { type: "integer", minimum: 3, maximum: 100, description: "star: number of points." },
9950
+ innerRatio: { type: "number", minimum: 0.01, maximum: 1, description: "star: inner over outer radius." },
9951
+ x1: NUM("line: start x."),
9952
+ y1: NUM("line: start y."),
9953
+ x2: NUM("line: end x."),
9954
+ y2: NUM("line: end y."),
9955
+ weight: { type: "number", minimum: 0, description: "line: thickness in pixels." },
9956
+ startArrow: { anyOf: [{ $ref: "#/$defs/arrowhead" }, { type: "null", description: "No arrowhead." }], description: "line: start arrowhead or null." },
9957
+ endArrow: { anyOf: [{ $ref: "#/$defs/arrowhead" }, { type: "null", description: "No arrowhead." }], description: "line: end arrowhead or null." },
9958
+ transform: { $ref: "#/$defs/matrix" }
9959
+ },
9960
+ additionalProperties: false
9961
+ },
9962
+ subpaths: { $ref: "#/$defs/subpaths", description: "A new path for the component (SVG path data or anchors); ends its live shape." },
9963
+ op: OP
9964
+ },
9965
+ required: ["layerId", "componentIndex"],
9966
+ additionalProperties: false
9967
+ },
9968
+ annotations: { title: "Edit a shape component", ...WRITE_IDEMPOTENT5 },
9969
+ handler: ({ layerId, componentIndex, ...rest }) => {
9970
+ const id = shapeId(layerId);
9971
+ const change = geometry(rest);
9972
+ if (change.shape === void 0 && change.subpaths === void 0 && change.op === void 0) throw new LayerOperationError("Give shape, subpaths or op to change.");
9973
+ return controller.setShapeComponent(id, componentIndex, change);
9974
+ }
9975
+ };
9976
+ const addComponent = {
9977
+ name: "add_shape_component",
9978
+ title: "Add a shape component",
9979
+ description: "Adds a component to a shape layer and combines it with the components below it by op: combine (union), subtract, intersect or exclude, as Photoshop\u2019s path operations. Goes on top unless index is given. One undo step. Returns the shape.",
9980
+ inputSchema: {
9981
+ type: "object",
9982
+ $defs: DEFS2,
9983
+ properties: { layerId: LAYER_ID5, ...GEOMETRY2, op: OP, index: { type: "integer", minimum: 0, description: "Position among the components, 0 = bottom (default the top)." } },
9984
+ required: ["layerId"],
9985
+ additionalProperties: false
9986
+ },
9987
+ annotations: { title: "Add a shape component", ...WRITE7 },
9988
+ handler: ({ layerId, index, ...rest }) => controller.addShapeComponent(shapeId(layerId), geometry(rest), index)
9989
+ };
9990
+ const removeComponent = {
9991
+ name: "remove_shape_component",
9992
+ title: "Delete a shape component",
9993
+ description: "Deletes one component of a shape layer (the last one cannot go: delete the layer instead). One undo step. Returns the shape.",
9994
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID5, componentIndex: COMPONENT_INDEX }, required: ["layerId", "componentIndex"], additionalProperties: false },
9995
+ annotations: { title: "Delete a shape component", ...DESTRUCTIVE6 },
9996
+ handler: ({ layerId, componentIndex }) => controller.removeShapeComponent(shapeId(layerId), componentIndex)
9997
+ };
9998
+ const merge = {
9999
+ name: "merge_shape_components",
10000
+ title: "Merge shape components",
10001
+ description: "Merge Shape Components: the area a shape\u2019s components combine to (after their path operations) becomes one plain component; live shapes become paths. The look is unchanged. One undo step. Returns the shape.",
10002
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, required: ["layerId"], additionalProperties: false },
10003
+ annotations: { title: "Merge shape components", ...DESTRUCTIVE6 },
10004
+ handler: ({ layerId }) => controller.mergeShapeComponents(shapeId(layerId))
10005
+ };
10006
+ return [get, add2, update, setComponent, addComponent, removeComponent, merge];
10007
+ }
10008
+
9523
10009
  // src/image-editor/tools.ts
9524
10010
  var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
9525
10011
  function adjustmentsSummary(adjustments) {
@@ -9759,6 +10245,7 @@ function createImageEditorToolProvider(controller, options = {}) {
9759
10245
  ...createImageLayerTools(controller),
9760
10246
  ...createImageTransformTools(controller),
9761
10247
  ...createImageSmartObjectTools(controller),
10248
+ ...createImageShapeTools(controller),
9762
10249
  ...createImageEffectTools(controller),
9763
10250
  ...createImagePaintTools(controller),
9764
10251
  ...createPigmentTools(),
@@ -10099,6 +10586,8 @@ function layerBadge(layer) {
10099
10586
  return layer.annotation.type === "text" ? "T" : "ann";
10100
10587
  case "smart":
10101
10588
  return "so";
10589
+ case "shape":
10590
+ return "shp";
10102
10591
  }
10103
10592
  }
10104
10593
  function dropZone(fraction, isGroup) {
@@ -10138,7 +10627,7 @@ function layerMenu(controller) {
10138
10627
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
10139
10628
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
10140
10629
  { action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
10141
- { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
10630
+ { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
10142
10631
  { action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
10143
10632
  { action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
10144
10633
  { action: "smart-via-copy", label: "New Smart Object via Copy", enabled: active?.type === "smart" },
@@ -11074,6 +11563,7 @@ export {
11074
11563
  DEFAULT_BRUSH,
11075
11564
  DEFAULT_DEPTH_MASK,
11076
11565
  DEFAULT_GLOBAL_LIGHT,
11566
+ DEFAULT_MITER_LIMIT,
11077
11567
  DEFAULT_MIXER,
11078
11568
  DEFAULT_REFINE_EDGE_PARAMS,
11079
11569
  DEFAULT_SELECTION_TOOL_OPTIONS,
@@ -11107,6 +11597,7 @@ export {
11107
11597
  LAYER_EFFECT_KINDS,
11108
11598
  LAYER_EFFECT_MENU,
11109
11599
  LEGACY_PROCESS_VERSION,
11600
+ LIVE_SHAPE_KINDS,
11110
11601
  LUMA_709,
11111
11602
  LayerCompositor,
11112
11603
  LayerEffectError,
@@ -11143,6 +11634,8 @@ export {
11143
11634
  SELECTION_MODIFY_OPS,
11144
11635
  SELECTION_TOOLS,
11145
11636
  SHADOW_AMBIENT,
11637
+ SHAPE_OPERATIONS,
11638
+ SHAPE_RENDER_MARGIN,
11146
11639
  SMART_RENDER_MARGIN,
11147
11640
  SUPPORTED_ADJUSTMENT_KEYS,
11148
11641
  SelectionGesture,
@@ -11175,8 +11668,11 @@ export {
11175
11668
  adjustmentNodeFromLegacy,
11176
11669
  adler32,
11177
11670
  anchorPoint,
11671
+ annotationBlend,
11178
11672
  annotationLayerFor,
11179
11673
  annotationLayerName,
11674
+ annotationOfLayer,
11675
+ annotationOfShape,
11180
11676
  applyAdjustmentLayers,
11181
11677
  applyAdjustments,
11182
11678
  applyBoxBlur,
@@ -11226,6 +11722,7 @@ export {
11226
11722
  colorRange,
11227
11723
  combineSelections,
11228
11724
  compileAdjustmentLayer,
11725
+ componentPath,
11229
11726
  compositeSurfaceOver,
11230
11727
  computeHistogram,
11231
11728
  computeSelectionBounds,
@@ -11250,11 +11747,13 @@ export {
11250
11747
  createImagePaintTools,
11251
11748
  createImagePsdTools,
11252
11749
  createImageSelectionTools,
11750
+ createImageShapeTools,
11253
11751
  createImageSmartObjectTools,
11254
11752
  createImageTransformTools,
11255
11753
  createPigment,
11256
11754
  createPigmentTools,
11257
11755
  createSelectionMask,
11756
+ createShapeLayer,
11258
11757
  curveLut,
11259
11758
  curveLutFromLevels,
11260
11759
  curvePointToBox,
@@ -11271,6 +11770,7 @@ export {
11271
11770
  defaultAdjustmentParams,
11272
11771
  defaultEffectGradient,
11273
11772
  defaultLayerEffect,
11773
+ defaultShapeName,
11274
11774
  deflate,
11275
11775
  deflateRaw,
11276
11776
  depthMaskAlpha,
@@ -11322,6 +11822,7 @@ export {
11322
11822
  findLayerControl,
11323
11823
  finishTile,
11324
11824
  flattenLayers,
11825
+ flattenPath,
11325
11826
  flipCorners,
11326
11827
  formatControlValue,
11327
11828
  fullSelection,
@@ -11387,6 +11888,7 @@ export {
11387
11888
  isPng,
11388
11889
  isQuarterTurn,
11389
11890
  isQuarterTurnSwapped,
11891
+ isRenderedLayer,
11390
11892
  isSelectionEmpty,
11391
11893
  isSelectionMode,
11392
11894
  isSeparableBlendMode,
@@ -11420,15 +11922,19 @@ export {
11420
11922
  levelsValue,
11421
11923
  linearSrgbToXy,
11422
11924
  linearToSrgb,
11925
+ liveComponent,
11926
+ liveShapeSubpaths,
11423
11927
  loadImageEditorDocument,
11424
11928
  loadMixer,
11425
11929
  magicWand,
11426
11930
  mapCropHandle,
11427
11931
  mapLayer,
11932
+ mapSubpaths,
11428
11933
  maskSummary,
11429
11934
  matrixFromCorners,
11430
11935
  matrixFromParams,
11431
11936
  mergeLayerEffect,
11937
+ mergeShapeComponents,
11432
11938
  migrateSerializedImageEditorState,
11433
11939
  migrateToLayeredState,
11434
11940
  mixPigment8,
@@ -11453,20 +11959,26 @@ export {
11453
11959
  normalizeFilter,
11454
11960
  normalizeLayerEffects,
11455
11961
  normalizeLayerMask,
11962
+ normalizeLiveShape,
11456
11963
  normalizeMixer,
11457
11964
  normalizePlacement,
11458
11965
  normalizeProcessVersion,
11459
11966
  normalizeRect,
11460
11967
  normalizeRefineEdgeParams,
11968
+ normalizeShapePaint,
11969
+ normalizeShapeStroke,
11461
11970
  normalizeSmartFilter,
11462
11971
  normalizeStoredAdjustments,
11972
+ normalizeSubpaths,
11463
11973
  offsetLayer,
11974
+ offsetShape,
11464
11975
  offsetSmartObject,
11465
11976
  openPsdFile,
11466
11977
  packRgba,
11467
11978
  parseAdjustmentBlock,
11468
11979
  parseBlend,
11469
11980
  parseColor,
11981
+ parseCssColor,
11470
11982
  parseDataUrl,
11471
11983
  parseEffectColor,
11472
11984
  parseLfx2,
@@ -11502,6 +12014,7 @@ export {
11502
12014
  rangeLightness,
11503
12015
  rasterContents,
11504
12016
  rasterMaskFromLegacy,
12017
+ rasterizeCoverage,
11505
12018
  rasterizeDab,
11506
12019
  rasterizeSmartObject,
11507
12020
  readAdjustmentControls,
@@ -11524,12 +12037,17 @@ export {
11524
12037
  removeSmartFilter,
11525
12038
  renderLayerEffects,
11526
12039
  renderLayeredBase,
12040
+ renderShape,
12041
+ renderShapeRect,
11527
12042
  renderSmartObject,
11528
12043
  replaceContents,
11529
12044
  requireLayer,
12045
+ requireOperation,
12046
+ requireShapeLayer,
11530
12047
  requireSmartObject,
11531
12048
  requireUnlocked,
11532
12049
  rerender,
12050
+ rerenderShape,
11533
12051
  resizePixels,
11534
12052
  resizeSelection,
11535
12053
  resizeWarpMesh,
@@ -11569,6 +12087,12 @@ export {
11569
12087
  setSmartFilterMask,
11570
12088
  setSmartFiltersEnabled,
11571
12089
  setSmartPlacement,
12090
+ shapeAsPixelLayer,
12091
+ shapeFromAnnotation,
12092
+ shapeGeometryBounds,
12093
+ shapeLayers,
12094
+ shapeOutline,
12095
+ shapeRenderClip,
11572
12096
  shiftCoverage,
11573
12097
  smartFilterRows,
11574
12098
  smartObjectLayers,
@@ -11598,6 +12122,7 @@ export {
11598
12122
  toneRange,
11599
12123
  transformHomography,
11600
12124
  transformMap,
12125
+ transformShapeComponents,
11601
12126
  transformSurface,
11602
12127
  transformedBounds,
11603
12128
  transformedBox,
@@ -11610,6 +12135,7 @@ export {
11610
12135
  updateLayerEffect,
11611
12136
  updateLayerFilter,
11612
12137
  updateLayerMaskProps,
12138
+ updateShapeLayer,
11613
12139
  updateSmartFilter,
11614
12140
  uvToXy,
11615
12141
  validateEffectGradient,