@pixerate/editor 0.101.0 → 0.102.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.
@@ -68,7 +68,7 @@ import {
68
68
  validateLayerEffect,
69
69
  validateLayerEffects,
70
70
  warpSurface
71
- } from "../chunk-WIR377F6.js";
71
+ } from "../chunk-67FRIOPZ.js";
72
72
  import {
73
73
  affineFromHomography,
74
74
  applyHomography,
@@ -166,6 +166,7 @@ import {
166
166
  identityWarpMesh,
167
167
  invertMatrix,
168
168
  isValidWarpMesh,
169
+ mapWarpMeshPoints,
169
170
  matrixAbout,
170
171
  multiplyMatrices,
171
172
  pullWarpMesh,
@@ -2429,7 +2430,13 @@ function pixelLayerBounds(layer, surface = layer.surface) {
2429
2430
  return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height };
2430
2431
  }
2431
2432
  function layerTypeLabel(type) {
2432
- return type === "pixel" ? "Pixel layer" : type === "adjustment" ? "Adjustment layer" : type === "group" ? "Group" : "Annotation";
2433
+ return type === "pixel" ? "Pixel layer" : type === "adjustment" ? "Adjustment layer" : type === "group" ? "Group" : type === "smart" ? "Smart object" : "Annotation";
2434
+ }
2435
+ function hasSurface(layer) {
2436
+ return layer.type === "pixel" || layer.type === "smart";
2437
+ }
2438
+ function smartObjectLayers(layers) {
2439
+ return flattenLayers(layers).filter((l) => l.type === "smart");
2433
2440
  }
2434
2441
 
2435
2442
  // src/image-editor/layers/compositor.ts
@@ -2485,6 +2492,13 @@ function depthMaskAlpha(depth, options) {
2485
2492
  v += lo;
2486
2493
  return v < 0 ? 0 : v > 255 ? 255 : v;
2487
2494
  }
2495
+ function runLayerFilters(data, width, height, filters) {
2496
+ for (const f of filters) {
2497
+ if (!isActiveFilter(f)) continue;
2498
+ if (f.kind === "develop") applyPixelFilters(data, [f]);
2499
+ else if (f.kind === "blur") applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));
2500
+ }
2501
+ }
2488
2502
  var filterSpecCache = /* @__PURE__ */ new WeakMap();
2489
2503
  function pixelFilterSpec(layer) {
2490
2504
  const list2 = layer.filters;
@@ -2497,12 +2511,7 @@ function pixelFilterSpec(layer) {
2497
2511
  spec = {
2498
2512
  key: JSON.stringify(filters),
2499
2513
  spatial,
2500
- apply: (data, width, height) => {
2501
- for (const f of filters) {
2502
- if (f.kind === "develop") applyPixelFilters(data, [f]);
2503
- else if (f.kind === "blur") applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));
2504
- }
2505
- }
2514
+ apply: (data, width, height) => runLayerFilters(data, width, height, filters)
2506
2515
  };
2507
2516
  }
2508
2517
  filterSpecCache.set(list2, spec);
@@ -2519,6 +2528,8 @@ var PHOTO_PROFILE = {
2519
2528
  contentBlend: (l) => l.type === "annotation" && l.blend === "normal" && l.annotation.blend ? l.annotation.blend : l.blend,
2520
2529
  layerKey: (l) => l.type === "pixel" ? [l.filters ?? null, l.source ?? null] : l.type === "adjustment" ? [l.kind, l.params] : l.type === "annotation" ? l.annotation : void 0,
2521
2530
  pixelFilters: (l) => l.type === "pixel" ? pixelFilterSpec(l) : null,
2531
+ /** A smart object draws its rendered pixels (contents → placement → smart filters) like a pixel layer. */
2532
+ isPixelLike: (l) => l.type === "smart",
2522
2533
  maskSourcePlane: (source, W, H, sources) => {
2523
2534
  const opts = source;
2524
2535
  const depth = sources.depthMap?.(opts) ?? null;
@@ -4607,330 +4618,162 @@ function refineSelection(pixels, mask, params = {}) {
4607
4618
  return createSelectionMask(mask.width, mask.height, out);
4608
4619
  }
4609
4620
 
4610
- // src/image-editor/layers/serialize.ts
4611
- var LAYERED_SCHEMA_VERSION = 3;
4612
- function serializePatterns(patterns) {
4613
- return patterns.map((p) => ({ id: p.id, name: p.name, png: pngDataUrl(p.width, p.height, p.data) }));
4621
+ // src/image-editor/transform.ts
4622
+ var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
4623
+ var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
4624
+ var ANCHOR_FRACTION = {
4625
+ "top-left": [0, 0],
4626
+ top: [0.5, 0],
4627
+ "top-right": [1, 0],
4628
+ left: [0, 0.5],
4629
+ center: [0.5, 0.5],
4630
+ right: [1, 0.5],
4631
+ "bottom-left": [0, 1],
4632
+ bottom: [0.5, 1],
4633
+ "bottom-right": [1, 1]
4634
+ };
4635
+ function isTransformAnchor(value) {
4636
+ return typeof value === "string" && value in ANCHOR_FRACTION;
4614
4637
  }
4615
- function deserializePatterns(raw, problems = []) {
4616
- if (!Array.isArray(raw)) return [];
4617
- const out = [];
4618
- for (const p of raw) {
4619
- if (!p || typeof p !== "object" || typeof p.id !== "string" || typeof p.png !== "string") continue;
4620
- try {
4621
- const png = decodePngDataUrl(p.png, 4);
4622
- out.push({ id: p.id, name: typeof p.name === "string" ? p.name : p.id, width: png.width, height: png.height, data: png.data });
4623
- } catch (e) {
4624
- problems.push(`Pattern "${p.name ?? p.id}" could not be decoded (${e instanceof Error ? e.message : String(e)}).`);
4625
- }
4626
- }
4627
- return out;
4638
+ function identityTransform(box) {
4639
+ return { box: { ...box }, corners: boxQuad(box) };
4628
4640
  }
4629
- var encodedCache = /* @__PURE__ */ new WeakMap();
4630
- function encodeSurface(surface) {
4631
- if (encodedCache.has(surface)) return encodedCache.get(surface);
4632
- const b = contentBounds(surface);
4633
- let out = null;
4634
- if (b) {
4635
- const data = readRegion(surface, b);
4636
- out = { url: pngDataUrl(b.width, b.height, data, surface.channels), x: b.x, y: b.y };
4637
- }
4638
- encodedCache.set(surface, out);
4639
- return out;
4641
+ function cornersFromMatrix(box, m) {
4642
+ const q2 = boxQuad(box);
4643
+ return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
4640
4644
  }
4641
- function baseFields(layer, ctx) {
4642
- const out = {
4643
- id: layer.id,
4644
- name: layer.name,
4645
- visible: layer.visible,
4646
- opacity: layer.opacity,
4647
- blend: layer.blend
4648
- };
4649
- if (layer.clipped) out.clipped = true;
4650
- if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
4651
- if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
4652
- if (layer.effects) out.effects = serializeEffects(layer.effects);
4653
- return out;
4645
+ function matrixFromCorners(box, corners, eps = 1e-6) {
4646
+ const [tl, tr, br, bl] = corners;
4647
+ const size = 1 + Math.max(box.width, box.height);
4648
+ if (Math.abs(tl.x + br.x - tr.x - bl.x) > eps * size || Math.abs(tl.y + br.y - tr.y - bl.y) > eps * size) return null;
4649
+ const h = rectToQuad(box, corners);
4650
+ return h ? affineFromHomography(h, 1e-9) : null;
4654
4651
  }
4655
- function serializeEffects(effects) {
4656
- const out = { enabled: effects.enabled, items: JSON.parse(JSON.stringify(effects.items)) };
4657
- if (effects.scale !== void 0 && effects.scale !== 1) out.scale = effects.scale;
4658
- if (effects.psd) out.psd = { key: effects.psd.key, data: bytesToBase64(effects.psd.data) };
4659
- return out;
4652
+ function transformHomography(geometry) {
4653
+ const h = rectToQuad(geometry.box, geometry.corners);
4654
+ if (!h) return null;
4655
+ return homographyKeepsQuad(h, boxQuad(geometry.box)) ? h : null;
4660
4656
  }
4661
- function deserializeEffects(raw) {
4662
- if (!raw || typeof raw !== "object") return void 0;
4663
- const r = raw;
4664
- let psd;
4665
- const p = r.psd;
4666
- if (p && typeof p.key === "string" && typeof p.data === "string") {
4667
- try {
4668
- psd = { key: p.key, data: base64ToBytes(p.data) };
4669
- } catch {
4670
- psd = void 0;
4671
- }
4672
- }
4673
- return normalizeLayerEffects({ ...r, psd });
4657
+ function hasWarp(geometry) {
4658
+ const w = geometry.warp;
4659
+ if (!w) return false;
4660
+ const id = identityWarpMesh(geometry.box, w.us.length - 1, w.vs.length - 1);
4661
+ if (id.points.length !== w.points.length || id.us.some((u, i) => Math.abs(u - w.us[i]) > 1e-12) || id.vs.some((v, i) => Math.abs(v - w.vs[i]) > 1e-12)) return true;
4662
+ return w.points.some((p, i) => Math.abs(p.x - id.points[i].x) > 1e-6 || Math.abs(p.y - id.points[i].y) > 1e-6);
4674
4663
  }
4675
- function serializeMask(mask, hint, ctx) {
4676
- const out = { defaultValue: mask.defaultValue };
4677
- if (!mask.source) {
4678
- const enc = encodeSurface(mask.surface);
4679
- if (enc) {
4680
- out.asset = ctx.addAsset(enc.url, hint);
4681
- out.x = enc.x;
4682
- out.y = enc.y;
4683
- }
4684
- } else {
4685
- out.source = { ...mask.source, depthRange: [...mask.source.depthRange] };
4686
- }
4687
- if (mask.density !== void 0 && mask.density !== 1) out.density = mask.density;
4688
- if (mask.feather) out.feather = mask.feather;
4689
- if (mask.enabled === false) out.enabled = false;
4690
- if (mask.linked === false) out.linked = false;
4691
- return out;
4664
+ function isIdentityTransform(geometry) {
4665
+ if (hasWarp(geometry)) return false;
4666
+ const q2 = boxQuad(geometry.box);
4667
+ return geometry.corners.every((c, i) => Math.abs(c.x - q2[i].x) < 1e-6 && Math.abs(c.y - q2[i].y) < 1e-6);
4692
4668
  }
4693
- function serializeLayer(layer, ctx) {
4694
- const base = baseFields(layer, ctx);
4695
- switch (layer.type) {
4696
- case "pixel": {
4697
- const out = { ...base, type: "pixel", offset: { ...layer.offset } };
4698
- if (layer.source === "document") out.source = "document";
4699
- else {
4700
- const enc = encodeSurface(layer.surface);
4701
- if (enc) {
4702
- out.asset = ctx.addAsset(enc.url, layer.id);
4703
- out.x = enc.x;
4704
- out.y = enc.y;
4705
- }
4706
- }
4707
- if (layer.fillOpacity !== void 0 && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;
4708
- if (layer.filters?.length) out.filters = JSON.parse(JSON.stringify(layer.filters));
4709
- return out;
4710
- }
4711
- case "adjustment":
4712
- return { ...base, type: "adjustment", kind: layer.kind, params: JSON.parse(JSON.stringify(layer.params)) };
4713
- case "group": {
4714
- const out = { ...base, type: "group", children: layer.children.map((c) => serializeLayer(c, ctx)) };
4715
- if (layer.collapsed) out.collapsed = true;
4716
- return out;
4717
- }
4718
- case "annotation":
4719
- return { ...base, type: "annotation", annotation: JSON.parse(JSON.stringify(layer.annotation)) };
4720
- }
4669
+ function transformMap(geometry) {
4670
+ const h = transformHomography(geometry);
4671
+ if (!h) return null;
4672
+ const { box } = geometry;
4673
+ const warp = hasWarp(geometry) ? geometry.warp : null;
4674
+ if (!warp) return (p) => applyHomography(h, p);
4675
+ return (p) => applyHomography(h, evalWarpMesh(warp, (p.x - box.x) / box.width, (p.y - box.y) / box.height));
4721
4676
  }
4722
- function serializeLayers(layers) {
4723
- const assets = {};
4724
- const ctx = {
4725
- addAsset(url, hint) {
4726
- let id = hint;
4727
- for (let n = 2; id in assets && assets[id] !== url; n++) id = `${hint}-${n}`;
4728
- assets[id] = url;
4729
- return id;
4730
- }
4731
- };
4732
- return { layers: layers.map((l) => serializeLayer(l, ctx)), assets };
4677
+ function transformedPoint(geometry, fx, fy) {
4678
+ const f = transformMap(geometry);
4679
+ const { box } = geometry;
4680
+ return f ? f({ x: box.x + fx * box.width, y: box.y + fy * box.height }) : null;
4733
4681
  }
4734
- var externalRefs = /* @__PURE__ */ new WeakMap();
4735
- async function serializeLayersExternal(layers, store) {
4736
- const inline = serializeLayers(layers);
4737
- let refs = externalRefs.get(store);
4738
- if (!refs) {
4739
- refs = /* @__PURE__ */ new Map();
4740
- externalRefs.set(store, refs);
4741
- }
4742
- const assets = {};
4743
- for (const [id, url] of Object.entries(inline.assets)) {
4744
- let ref = refs.get(url);
4745
- if (!ref) {
4746
- const parsed = parseDataUrl(url);
4747
- const png = decodePngHeader(parsed.bytes);
4748
- ref = await store.put(new Blob([parsed.bytes], { type: "image/png" }), { id, width: png.width, height: png.height });
4749
- if (typeof ref !== "string" || !ref) throw new Error(`The asset store returned no reference for "${id}".`);
4750
- refs.set(url, ref);
4682
+ function anchorPoint(geometry, anchor) {
4683
+ const [fx, fy] = ANCHOR_FRACTION[anchor];
4684
+ const h = rectToQuad(geometry.box, geometry.corners);
4685
+ const p = { x: geometry.box.x + fx * geometry.box.width, y: geometry.box.y + fy * geometry.box.height };
4686
+ return (h && applyHomography(h, p)) ?? p;
4687
+ }
4688
+ function transformedBox(geometry) {
4689
+ const f = transformMap(geometry);
4690
+ if (!f) return null;
4691
+ const { box } = geometry;
4692
+ const pts = [];
4693
+ const n = hasWarp(geometry) ? 16 : 1;
4694
+ for (let i = 0; i <= n; i++) {
4695
+ const t = i / n;
4696
+ for (const [fx, fy] of [[t, 0], [1, t], [1 - t, 1], [0, 1 - t]]) {
4697
+ const p = f({ x: box.x + fx * box.width, y: box.y + fy * box.height });
4698
+ if (!p) return null;
4699
+ pts.push(p);
4751
4700
  }
4752
- assets[id] = ref;
4753
4701
  }
4754
- return { layers: inline.layers, assets };
4702
+ const xs = pts.map((p) => p.x);
4703
+ const ys = pts.map((p) => p.y);
4704
+ const x = Math.min(...xs);
4705
+ const y = Math.min(...ys);
4706
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
4755
4707
  }
4756
- function decodePngHeader(bytes) {
4757
- const u32 = (o) => (bytes[o] << 24 | bytes[o + 1] << 16 | bytes[o + 2] << 8 | bytes[o + 3]) >>> 0;
4758
- return { width: u32(16), height: u32(20) };
4708
+ function referencePoint(box, reference) {
4709
+ if (reference && typeof reference === "object") return { x: reference.x, y: reference.y };
4710
+ const [fx, fy] = ANCHOR_FRACTION[reference ?? "center"];
4711
+ return { x: box.x + fx * box.width, y: box.y + fy * box.height };
4759
4712
  }
4760
- async function resolveExternalAssets(assets, store, problems) {
4761
- const out = {};
4762
- for (const [id, ref] of Object.entries(assets)) {
4763
- if (ref.startsWith("data:")) {
4764
- out[id] = ref;
4765
- continue;
4766
- }
4767
- if (!store) {
4768
- problems.push(`Asset "${id}" is external (${ref}) and no asset store was given.`);
4769
- continue;
4770
- }
4771
- try {
4772
- const blob = await store.get(ref);
4773
- const bytes = new Uint8Array(await blob.arrayBuffer());
4774
- out[id] = `data:image/png;base64,${bytesToBase64(bytes)}`;
4775
- } catch (err) {
4776
- problems.push(`Asset "${id}" (${ref}) could not be fetched: ${err.message}`);
4777
- }
4778
- }
4779
- return out;
4713
+ function matrixFromParams(box, params) {
4714
+ const sx = (params.scaleX ?? 100) / 100;
4715
+ const sy = (params.scaleY ?? 100) / 100;
4716
+ const tx = Math.tan((params.skewX ?? 0) * Math.PI / 180);
4717
+ const ty = Math.tan((params.skewY ?? 0) * Math.PI / 180);
4718
+ const ref = referencePoint(box, params.reference);
4719
+ const linear = multiplyMatrices(multiplyMatrices([1, ty, tx, 1, 0, 0], [sx, 0, 0, sy, 0, 0]), rotateMatrix(params.rotate ?? 0));
4720
+ return multiplyMatrices(matrixAbout(linear, ref), translateMatrix(params.dx ?? 0, params.dy ?? 0));
4780
4721
  }
4781
- function decodePngDataUrl(url, channels) {
4782
- const parsed = parseDataUrl(url);
4783
- if (!parsed) throw new Error("Asset is not a data URL.");
4784
- const png = decodePng(parsed.bytes, { keepGray: channels === 1 });
4785
- if (png.channels === channels) return png;
4786
- if (channels === 1) {
4787
- const out2 = new Uint8ClampedArray(png.width * png.height);
4788
- for (let i = 0, p = 0; p < out2.length; i += 4, p++) {
4789
- out2[p] = Math.round((0.299 * png.data[i] + 0.587 * png.data[i + 1] + 0.114 * png.data[i + 2]) * png.data[i + 3] / 255);
4790
- }
4791
- return { width: png.width, height: png.height, data: out2 };
4792
- }
4793
- const out = new Uint8ClampedArray(png.width * png.height * 4);
4794
- for (let p = 0; p < png.width * png.height; p++) {
4795
- out[p * 4] = out[p * 4 + 1] = out[p * 4 + 2] = png.data[p];
4796
- out[p * 4 + 3] = 255;
4722
+ function describeTransform(geometry, reference = "center") {
4723
+ const m = hasWarp(geometry) ? null : matrixFromCorners(geometry.box, geometry.corners);
4724
+ const ref = referencePoint(geometry.box, reference);
4725
+ const bounds = transformedBox(geometry);
4726
+ if (!m) {
4727
+ const h = rectToQuad(geometry.box, geometry.corners);
4728
+ const at2 = (h && applyHomography(h, ref)) ?? ref;
4729
+ return { affine: false, scaleX: 100, scaleY: 100, rotate: 0, skewX: 0, x: at2.x, y: at2.y, bounds, corners: geometry.corners };
4797
4730
  }
4798
- return { width: png.width, height: png.height, data: out };
4731
+ const [a, b, c, d] = m;
4732
+ const sxAbs = Math.hypot(a, b);
4733
+ const rot = Math.atan2(b, a) * 180 / Math.PI;
4734
+ const shear = sxAbs ? (a * c + b * d) / sxAbs : 0;
4735
+ const sy = sxAbs ? (a * d - b * c) / sxAbs : 0;
4736
+ const skew = sxAbs ? Math.atan(shear / sxAbs) * 180 / Math.PI : 0;
4737
+ const at = applyMatrix(m, ref);
4738
+ const clean = (v) => Math.abs(v) < 1e-9 ? 0 : v;
4739
+ return {
4740
+ affine: true,
4741
+ scaleX: clean(sxAbs * 100),
4742
+ scaleY: clean(sy * 100),
4743
+ rotate: clean(rot === -180 ? 180 : rot),
4744
+ skewX: clean(skew),
4745
+ x: at.x,
4746
+ y: at.y,
4747
+ bounds,
4748
+ corners: geometry.corners
4749
+ };
4799
4750
  }
4800
- var num = (v, lo, hi, fallback) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : fallback;
4801
- function readSurface(ref, x, y, channels, defaultValue, ctx, what) {
4802
- if (typeof ref !== "string" || !ref) return emptySurface(channels, defaultValue);
4803
- const url = ctx.assets[ref];
4804
- if (typeof url !== "string") {
4805
- ctx.problems.push(`${what}: asset "${ref}" is missing`);
4806
- return emptySurface(channels, defaultValue);
4807
- }
4808
- try {
4809
- const px2 = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);
4810
- return surfaceFromPixels(px2, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);
4811
- } catch (err) {
4812
- ctx.problems.push(`${what}: asset "${ref}" could not be decoded (${err.message})`);
4813
- return emptySurface(channels, defaultValue);
4814
- }
4751
+ function applyParams(geometry, params) {
4752
+ const ref = params.reference && typeof params.reference === "object" ? params.reference : anchorPoint(geometry, params.reference ?? "center");
4753
+ const m = matrixFromParams({ x: ref.x, y: ref.y, width: 0, height: 0 }, { ...params, reference: ref });
4754
+ const c = geometry.corners;
4755
+ return [applyMatrix(m, c[0]), applyMatrix(m, c[1]), applyMatrix(m, c[2]), applyMatrix(m, c[3])];
4815
4756
  }
4816
- function readMask(raw, ctx, what) {
4817
- if (!raw || typeof raw !== "object") return void 0;
4818
- const r = raw;
4819
- const defaultValue = r.defaultValue === 255 ? 255 : 0;
4820
- const mask = {
4821
- surface: r.source ? emptySurface(1, 255) : readSurface(r.asset, r.x, r.y, 1, defaultValue, ctx, `${what} mask`),
4822
- defaultValue: r.source ? 255 : defaultValue
4823
- };
4824
- if (typeof r.density === "number" && r.density !== 1) mask.density = num(r.density, 0, 1, 1);
4825
- if (typeof r.feather === "number" && r.feather > 0) mask.feather = num(r.feather, 0, 1e3, 0);
4826
- if (r.enabled === false) mask.enabled = false;
4827
- if (r.linked === false) mask.linked = false;
4828
- if (r.source && typeof r.source === "object") {
4829
- const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...r.source };
4830
- mask.source = { ...s, depthRange: [...s.depthRange] };
4831
- }
4832
- return mask;
4757
+ function flipCorners(geometry, axis) {
4758
+ const c = anchorPoint(geometry, "center");
4759
+ const m = axis === "horizontal" ? [-1, 0, 0, 1, 2 * c.x, 0] : [1, 0, 0, -1, 0, 2 * c.y];
4760
+ const q2 = geometry.corners;
4761
+ return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
4833
4762
  }
4834
- function readFilters(raw) {
4835
- if (!Array.isArray(raw)) return void 0;
4836
- const out = [];
4837
- for (const f of raw) {
4838
- if (!f || typeof f !== "object") continue;
4839
- const r = f;
4840
- const id = typeof r.id === "string" && r.id ? r.id : newLayerId("flt");
4841
- const enabled = r.enabled !== false;
4842
- if (r.kind === "blur") out.push({ id, kind: "blur", enabled, radius: num(r.radius, 0, 50, 0) });
4843
- else if (r.kind === "develop") {
4844
- out.push({
4845
- id,
4846
- kind: "develop",
4847
- enabled,
4848
- brightness: num(r.brightness, -100, 100, 0),
4849
- contrast: num(r.contrast, -100, 100, 0),
4850
- saturation: num(r.saturation, -100, 100, 0),
4851
- exposure: num(r.exposure, -100, 100, 0),
4852
- temperature: num(r.temperature, -100, 100, 0),
4853
- tint: num(r.tint, -100, 100, 0),
4854
- process: normalizeProcessVersion(r.process)
4855
- });
4856
- }
4857
- }
4858
- return out.length ? out : void 0;
4763
+ function boxPointOf(geometry, p) {
4764
+ const m = matrixFromCorners(geometry.box, geometry.corners);
4765
+ if (m) return applyMatrix(invertMatrix(m), p);
4766
+ return null;
4859
4767
  }
4860
- function deserializeLayer(raw, ctx) {
4861
- if (!raw || typeof raw !== "object") return null;
4862
- const r = raw;
4863
- const id = typeof r.id === "string" && r.id ? r.id : newLayerId();
4864
- const base = {
4865
- id,
4866
- name: typeof r.name === "string" ? r.name : "",
4867
- visible: r.visible !== false,
4868
- opacity: num(r.opacity, 0, 1, 1),
4869
- blend: normalizeBlendMode(r.blend)
4870
- };
4871
- if (r.clipped === true) base.clipped = true;
4872
- const mask = readMask(r.mask, ctx, `Layer "${base.name || id}"`);
4873
- if (mask) base.mask = mask;
4874
- if (r.locks && typeof r.locks === "object") {
4875
- const locks = {};
4876
- for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
4877
- if (Object.keys(locks).length) base.locks = locks;
4878
- }
4879
- const effects = deserializeEffects(r.effects);
4880
- if (effects && r.type !== "adjustment") base.effects = effects;
4881
- switch (r.type) {
4882
- case "pixel": {
4883
- const layer = {
4884
- ...base,
4885
- type: "pixel",
4886
- surface: r.source === "document" ? emptySurface(4) : readSurface(r.asset, r.x, r.y, 4, 0, ctx, `Layer "${base.name || id}"`),
4887
- offset: { x: Math.round(num(r.offset?.x, -1e7, 1e7, 0)), y: Math.round(num(r.offset?.y, -1e7, 1e7, 0)) }
4888
- };
4889
- if (r.source === "document") layer.source = "document";
4890
- if (typeof r.fillOpacity === "number" && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);
4891
- const filters = readFilters(r.filters);
4892
- if (filters) layer.filters = filters;
4893
- return layer;
4894
- }
4895
- case "adjustment": {
4896
- const norm = normalizeAdjustmentLayer({ id, kind: r.kind, params: r.params });
4897
- if (!norm) {
4898
- ctx.problems.push(`Layer "${base.name || id}": unknown adjustment kind "${r.kind}"`);
4899
- return null;
4900
- }
4901
- return { ...base, type: "adjustment", kind: norm.kind, params: norm.params };
4902
- }
4903
- case "group": {
4904
- const children = Array.isArray(r.children) ? r.children.map((c) => deserializeLayer(c, ctx)).filter(Boolean) : [];
4905
- const blend = r.blend === "pass-through" ? "pass-through" : isBlendMode(r.blend) ? r.blend : "pass-through";
4906
- const group = { ...base, type: "group", blend, children };
4907
- if (r.collapsed === true) group.collapsed = true;
4908
- return group;
4909
- }
4910
- case "annotation": {
4911
- const a = r.annotation;
4912
- if (!a || typeof a !== "object" || typeof a.type !== "string") {
4913
- ctx.problems.push(`Layer "${base.name || id}": annotation is missing`);
4914
- return null;
4915
- }
4916
- return { ...base, type: "annotation", annotation: JSON.parse(JSON.stringify({ ...a, id: typeof a.id === "string" ? a.id : id })) };
4917
- }
4918
- default:
4919
- ctx.problems.push(`Layer "${base.name || id}": unknown layer type "${r.type}"`);
4920
- return null;
4921
- }
4768
+ function presetWarp(box, preset) {
4769
+ return warpPresetMesh(preset, box);
4922
4770
  }
4923
- function deserializeLayers(raw, ctx) {
4924
- if (!Array.isArray(raw)) return [];
4925
- const seen = /* @__PURE__ */ new Set();
4926
- const dedupe = (list2) => list2.map((l) => {
4927
- let next = l;
4928
- if (seen.has(l.id)) next = { ...l, id: newLayerId() };
4929
- seen.add(next.id);
4930
- if (next.type === "group") next = { ...next, children: dedupe(next.children) };
4931
- return next;
4932
- });
4933
- return dedupe(raw.map((l) => deserializeLayer(l, ctx)).filter(Boolean));
4771
+ function isValidWarp(mesh) {
4772
+ return isValidWarpMesh(mesh);
4773
+ }
4774
+ function geometryHomography(geometry) {
4775
+ const m = matrixFromCorners(geometry.box, geometry.corners);
4776
+ return m ? homographyFromAffine(m) : transformHomography(geometry);
4934
4777
  }
4935
4778
 
4936
4779
  // src/image-editor/layers/ops.ts
@@ -4991,6 +4834,10 @@ function withNewIds(layer) {
4991
4834
  const next = { ...layer, id: newLayerId(layer.type === "adjustment" ? "adj" : layer.type) };
4992
4835
  if (next.type === "group") next.children = next.children.map(withNewIds);
4993
4836
  if (next.type === "annotation") next.annotation = { ...next.annotation, id: next.id };
4837
+ if (next.type === "smart" && next.psd?.placed) {
4838
+ next.psd = { ...next.psd };
4839
+ delete next.psd.placed;
4840
+ }
4994
4841
  return next;
4995
4842
  }
4996
4843
  function duplicateLayer(layers, id, name) {
@@ -5016,7 +4863,7 @@ function setLayerProperties(layers, id, props) {
5016
4863
  next.opacity = props.opacity;
5017
4864
  }
5018
4865
  if (props.fillOpacity !== void 0) {
5019
- if (layer.type !== "pixel") fail(`fillOpacity applies to pixel layers; "${id}" is a ${layer.type} layer.`);
4866
+ if (layer.type !== "pixel" && layer.type !== "smart") fail(`fillOpacity applies to pixel layers and smart objects; "${id}" is a ${layer.type} layer.`);
5020
4867
  if (!(typeof props.fillOpacity === "number" && props.fillOpacity >= 0 && props.fillOpacity <= 1)) fail("fillOpacity must be a number from 0 to 1.");
5021
4868
  const p = next;
5022
4869
  if (props.fillOpacity === 1) delete p.fillOpacity;
@@ -5165,89 +5012,1198 @@ function removeLayerFilter(layers, id, filterId) {
5165
5012
  else delete next.filters;
5166
5013
  return mapLayer(layers, id, () => next);
5167
5014
  }
5168
- function requireEffectLayer(layers, id, action) {
5169
- const layer = requireLayer(layers, id).layer;
5170
- if (layer.type === "adjustment") fail(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
5171
- requireUnlocked(layer, "all", action);
5172
- return layer;
5015
+ function requireEffectLayer(layers, id, action) {
5016
+ const layer = requireLayer(layers, id).layer;
5017
+ if (layer.type === "adjustment") fail(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
5018
+ requireUnlocked(layer, "all", action);
5019
+ return layer;
5020
+ }
5021
+ function layerEffectsOf(layer) {
5022
+ return layer.effects ?? { enabled: true, items: [] };
5023
+ }
5024
+ function guard(fn) {
5025
+ try {
5026
+ return fn();
5027
+ } catch (e) {
5028
+ if (e instanceof LayerEffectError) fail(e.message);
5029
+ throw e;
5030
+ }
5031
+ }
5032
+ function setLayerEffects(layers, id, effects) {
5033
+ const layer = requireEffectLayer(layers, id, "change its effects");
5034
+ const next = { ...layer };
5035
+ if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) {
5036
+ guard(() => validateLayerEffects(effects));
5037
+ next.effects = effects;
5038
+ } else delete next.effects;
5039
+ return mapLayer(layers, id, () => next);
5040
+ }
5041
+ function addLayerEffect(layers, id, kind, params = {}, index) {
5042
+ if (!isLayerEffectKind(kind)) fail(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
5043
+ const layer = requireEffectLayer(layers, id, "add effects");
5044
+ const current = layerEffectsOf(layer);
5045
+ const ids = new Set(current.items.map((e) => e.id));
5046
+ let effectId = newEffectId();
5047
+ while (ids.has(effectId)) effectId = newEffectId();
5048
+ const { id: _ignored, kind: _kind, ...rest } = params;
5049
+ const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
5050
+ const items = [...current.items];
5051
+ const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
5052
+ items.splice(at, 0, effect);
5053
+ return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
5054
+ }
5055
+ function requireEffect(layer, effectId) {
5056
+ const items = layer.effects?.items ?? [];
5057
+ const i = items.findIndex((e) => e.id === effectId);
5058
+ if (i === -1) fail(`Layer "${layer.name || layer.id}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
5059
+ return i;
5060
+ }
5061
+ function updateLayerEffect(layers, id, effectId, changes) {
5062
+ const layer = requireEffectLayer(layers, id, "change its effects");
5063
+ const i = requireEffect(layer, effectId);
5064
+ const current = layer.effects;
5065
+ const items = [...current.items];
5066
+ items[i] = guard(() => mergeLayerEffect(items[i], changes));
5067
+ return setLayerEffects(layers, id, { ...current, items });
5068
+ }
5069
+ function removeLayerEffect(layers, id, effectId) {
5070
+ const layer = requireEffectLayer(layers, id, "change its effects");
5071
+ requireEffect(layer, effectId);
5072
+ const current = layer.effects;
5073
+ return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
5074
+ }
5075
+ function setLayerEffectsEnabled(layers, id, enabled, scale) {
5076
+ const layer = requireEffectLayer(layers, id, "change its effects");
5077
+ if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
5078
+ const current = layerEffectsOf(layer);
5079
+ const next = { ...current, enabled };
5080
+ if (scale !== void 0) {
5081
+ if (scale === 1) delete next.scale;
5082
+ else next.scale = scale;
5083
+ }
5084
+ return setLayerEffects(layers, id, next);
5085
+ }
5086
+ function parseBlend(value, allowPassThrough = false) {
5087
+ if (value === void 0) return void 0;
5088
+ if (value === "pass-through" && allowPassThrough) return value;
5089
+ if (!isBlendMode(value)) fail(`Unknown blend mode "${String(value)}".`);
5090
+ return value;
5091
+ }
5092
+ function unionRect(a, b) {
5093
+ if (!a) return b;
5094
+ if (!b) return a;
5095
+ const x = Math.min(a.x, b.x);
5096
+ const y = Math.min(a.y, b.y);
5097
+ return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
5098
+ }
5099
+
5100
+ // src/image-editor/layers/smart.ts
5101
+ var fail2 = (message) => {
5102
+ throw new LayerOperationError(message);
5103
+ };
5104
+ var SMART_RENDER_MARGIN = MAX_EFFECT_REACH;
5105
+ function newContentsId() {
5106
+ return newLayerId("so");
5107
+ }
5108
+ function contentsBox(contents) {
5109
+ return { x: 0, y: 0, width: contents.width, height: contents.height };
5110
+ }
5111
+ function placementGeometry(layer) {
5112
+ return { box: contentsBox(layer.contents), corners: layer.placement.corners, warp: layer.placement.warp ?? null };
5113
+ }
5114
+ function placementAt(contents, x, y) {
5115
+ return { corners: boxQuad({ x, y, width: contents.width, height: contents.height }) };
5116
+ }
5117
+ function isContentsDecoded(contents) {
5118
+ return contents.kind === "raster" ? Boolean(contents.raster) : Boolean(contents.document);
5119
+ }
5120
+ function requireDecoded(layer, action) {
5121
+ if (isContentsDecoded(layer.contents)) return;
5122
+ const what = layer.contents.file?.type ? `a ${layer.contents.file.type.toUpperCase()} file` : "a file";
5123
+ fail2(
5124
+ `The contents of smart object "${layer.name || layer.id}" are ${what} that could not be decoded here, so it cannot ${action}. Run prepareSmartObjects() in a browser first, or rasterize it.`
5125
+ );
5126
+ }
5127
+ function requireSmartObject(layers, id) {
5128
+ const layer = requireLayer(layers, id).layer;
5129
+ if (layer.type !== "smart") fail2(`Layer "${layer.name || id}" is a ${layer.type} layer, not a smart object; convert it with convert_to_smart_object.`);
5130
+ return layer;
5131
+ }
5132
+ var contentsPixels = /* @__PURE__ */ new WeakMap();
5133
+ function contentsSurface(contents) {
5134
+ let s = contentsPixels.get(contents);
5135
+ if (s) return s;
5136
+ if (contents.kind === "raster") {
5137
+ s = contents.raster ?? emptySurface(4);
5138
+ } else {
5139
+ const doc = contents.document;
5140
+ if (!doc) return emptySurface(4);
5141
+ const W = contents.width;
5142
+ const H = contents.height;
5143
+ const data = new LayerCompositor().composite(doc.layers, { x: 0, y: 0, width: W, height: H }, {
5144
+ width: W,
5145
+ height: H,
5146
+ globalLight: doc.globalLight,
5147
+ pattern: (id) => doc.patterns?.find((p) => p.id === id) ?? null
5148
+ });
5149
+ s = surfaceFromPixels({ width: W, height: H, data }, 4, 0, 0);
5150
+ }
5151
+ contentsPixels.set(contents, s);
5152
+ return s;
5153
+ }
5154
+ function rasterContents(pixels, name, id = newContentsId()) {
5155
+ if (!(pixels.width > 0 && pixels.height > 0) || pixels.data.length < pixels.width * pixels.height * 4) fail2("Contents pixels must be RGBA with width \xD7 height \xD7 4 bytes.");
5156
+ return { id, name, width: pixels.width, height: pixels.height, kind: "raster", raster: surfaceFromPixels(pixels, 4, 0, 0) };
5157
+ }
5158
+ function documentContents(document2, width, height, name, id = newContentsId()) {
5159
+ if (!(Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0)) fail2("Contents width and height must be positive whole numbers.");
5160
+ return { id, name, width, height, kind: "document", document: document2 };
5161
+ }
5162
+ var placedCache = /* @__PURE__ */ new WeakMap();
5163
+ var filteredCache = /* @__PURE__ */ new WeakMap();
5164
+ function placementKey(p, canvas) {
5165
+ return JSON.stringify([p.corners, p.warp ?? null, p.interpolation ?? "bicubic", canvas.width, canvas.height]);
5166
+ }
5167
+ function integerMove(layer) {
5168
+ const g = placementGeometry(layer);
5169
+ if (hasWarp(g)) return null;
5170
+ const q2 = boxQuad(g.box);
5171
+ const dx = g.corners[0].x - q2[0].x;
5172
+ const dy = g.corners[0].y - q2[0].y;
5173
+ if (Math.abs(dx - Math.round(dx)) > 1e-9 || Math.abs(dy - Math.round(dy)) > 1e-9) return null;
5174
+ const same = g.corners.every((c, i) => Math.abs(c.x - q2[i].x - dx) < 1e-9 && Math.abs(c.y - q2[i].y - dy) < 1e-9);
5175
+ return same ? { x: Math.round(dx), y: Math.round(dy) } : null;
5176
+ }
5177
+ function placeContents(layer, canvas, options = {}) {
5178
+ const { contents, placement } = layer;
5179
+ const preview = Boolean(options.step && options.step > 1) || Boolean(options.clip);
5180
+ const key = placementKey(placement, canvas);
5181
+ if (!preview) {
5182
+ const hit = placedCache.get(contents)?.get(key);
5183
+ if (hit) return hit;
5184
+ }
5185
+ const src = contentsSurface(contents);
5186
+ const m = SMART_RENDER_MARGIN;
5187
+ const area = { x: -m, y: -m, width: canvas.width + 2 * m, height: canvas.height + 2 * m };
5188
+ let clip = area;
5189
+ if (options.clip) {
5190
+ const c = options.clip;
5191
+ const x0 = Math.max(area.x, Math.floor(c.x));
5192
+ const y0 = Math.max(area.y, Math.floor(c.y));
5193
+ const x1 = Math.min(area.x + area.width, Math.ceil(c.x + c.width));
5194
+ const y1 = Math.min(area.y + area.height, Math.ceil(c.y + c.height));
5195
+ clip = { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
5196
+ }
5197
+ let out;
5198
+ const move = integerMove(layer);
5199
+ if (move && !options.clip) {
5200
+ out = translateSurface(src, move.x, move.y);
5201
+ } else {
5202
+ const g = placementGeometry(layer);
5203
+ const interpolation = preview ? placement.interpolation === "nearest" ? "nearest" : "bilinear" : placement.interpolation ?? "bicubic";
5204
+ const step = options.step && options.step > 1 ? options.step : 1;
5205
+ let source = src;
5206
+ let k = 1;
5207
+ if (step > 1) {
5208
+ k = step;
5209
+ let r = options.reduced?.get(k);
5210
+ if (!r) {
5211
+ r = reduceSurface(src, k);
5212
+ options.reduced?.set(k, r);
5213
+ }
5214
+ source = r;
5215
+ }
5216
+ const box = { x: 0, y: 0, width: Math.ceil(contents.width / k), height: Math.ceil(contents.height / k) };
5217
+ try {
5218
+ if (hasWarp(g)) {
5219
+ const f = transformMap(g);
5220
+ if (!f) fail2("The placement folds the contents over itself or through the horizon; keep the corners a convex quad.");
5221
+ out = warpSurface(source, (p) => f({ x: p.x * k, y: p.y * k }), { interpolation, source: box, clip, step: step > 1 ? step : void 0 });
5222
+ } else {
5223
+ const h = transformHomography(g);
5224
+ if (!h) fail2("The placement folds the contents over itself or through the horizon; keep the corners a convex quad.");
5225
+ const hk = multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], h);
5226
+ out = transformSurface(source, hk, { interpolation, source: box, clip, step: step > 1 ? step : void 0 });
5227
+ }
5228
+ } catch (e) {
5229
+ if (e instanceof TransformTooLargeError) fail2(e.message);
5230
+ throw e;
5231
+ }
5232
+ }
5233
+ if (!preview) {
5234
+ let byKey = placedCache.get(contents);
5235
+ if (!byKey) {
5236
+ byKey = /* @__PURE__ */ new Map();
5237
+ placedCache.set(contents, byKey);
5238
+ }
5239
+ byKey.set(key, out);
5240
+ while (byKey.size > 4) byKey.delete(byKey.keys().next().value);
5241
+ }
5242
+ return out;
5243
+ }
5244
+ function activeSmartFilters(stack) {
5245
+ if (!stack?.enabled) return [];
5246
+ return stack.filters.filter((f) => f.kind !== "psd" && isActiveFilter(f) && f.opacity > 0);
5247
+ }
5248
+ function filterMaskCoverage(mask, canvas) {
5249
+ if (!mask || mask.enabled === false || mask.source) return null;
5250
+ const W = canvas.width;
5251
+ const H = canvas.height;
5252
+ const bytes = readRegion(mask.surface, { x: 0, y: 0, width: W, height: H });
5253
+ let v = new Float32Array(W * H);
5254
+ let all = true;
5255
+ for (let i = 0; i < v.length; i++) {
5256
+ v[i] = bytes[i] / 255;
5257
+ if (bytes[i] !== 255) all = false;
5258
+ }
5259
+ const feather = mask.feather ?? 0;
5260
+ if (all && !(feather > 0)) return null;
5261
+ if (feather > 0) v = gaussianBlur(v, W, H, feather, "clamp");
5262
+ const density = mask.density ?? 1;
5263
+ if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);
5264
+ return v;
5265
+ }
5266
+ function applySmartFilters(placed, stack, canvas) {
5267
+ const active = activeSmartFilters(stack);
5268
+ if (!active.length || !(canvas.width > 0 && canvas.height > 0)) return placed;
5269
+ const W = canvas.width;
5270
+ const H = canvas.height;
5271
+ const key = JSON.stringify([W, H, active.map((f) => ({ ...f, id: void 0 }))]);
5272
+ let entry = filteredCache.get(placed);
5273
+ if (!entry || entry.key !== key) {
5274
+ const base = readRegion(placed, { x: 0, y: 0, width: W, height: H });
5275
+ let acc = new Uint8ClampedArray(base);
5276
+ for (const f of active) {
5277
+ const out = new Uint8ClampedArray(acc);
5278
+ runLayerFilters(out, W, H, [f]);
5279
+ if (f.blend === "normal") {
5280
+ if (f.opacity >= 1) acc = out;
5281
+ else lerpInto(acc, out, f.opacity, null);
5282
+ } else {
5283
+ blendTile(acc, out, f.blend, f.opacity, null);
5284
+ }
5285
+ }
5286
+ entry = { key, base, filtered: acc };
5287
+ filteredCache.set(placed, entry);
5288
+ }
5289
+ const cov = filterMaskCoverage(stack.mask, canvas);
5290
+ let result = entry.filtered;
5291
+ if (cov) {
5292
+ result = new Uint8ClampedArray(entry.base);
5293
+ lerpInto(result, entry.filtered, 1, cov);
5294
+ }
5295
+ return surfaceFromPixels({ width: W, height: H, data: result }, 4, 0, 0);
5296
+ }
5297
+ function lerpInto(dst, src, k, coverage) {
5298
+ for (let i = 0, p = 0; i < dst.length; i += 4, p++) {
5299
+ const t = coverage ? k * coverage[p] : k;
5300
+ if (t <= 0) continue;
5301
+ if (t >= 1) {
5302
+ dst[i] = src[i];
5303
+ dst[i + 1] = src[i + 1];
5304
+ dst[i + 2] = src[i + 2];
5305
+ dst[i + 3] = src[i + 3];
5306
+ continue;
5307
+ }
5308
+ for (let c = 0; c < 4; c++) dst[i + c] = Math.round(dst[i + c] + (src[i + c] - dst[i + c]) * t);
5309
+ }
5310
+ }
5311
+ function renderSmartObject(layer, canvas, options = {}) {
5312
+ const placed = placeContents(layer, canvas, options);
5313
+ return options.skipFilters ? placed : applySmartFilters(placed, layer.smartFilters, canvas);
5314
+ }
5315
+ function rerender(layer, canvas, action = "change") {
5316
+ requireDecoded(layer, action);
5317
+ return { ...layer, surface: renderSmartObject(layer, canvas) };
5318
+ }
5319
+ function normalizePlacement(raw, contents) {
5320
+ const c = raw?.corners;
5321
+ if (!Array.isArray(c) || c.length !== 4 || !c.every((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y))) {
5322
+ fail2("corners must be four finite points [top-left, top-right, bottom-right, bottom-left] in document pixels.");
5323
+ }
5324
+ const out = { corners: c.map((p) => ({ x: p.x, y: p.y })) };
5325
+ if (raw.warp) {
5326
+ if (!isValidWarpMesh(raw.warp)) fail2("warp must be a mesh with us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points in contents pixels.");
5327
+ out.warp = { us: [...raw.warp.us], vs: [...raw.warp.vs], points: raw.warp.points.map((p) => ({ x: p.x, y: p.y })) };
5328
+ }
5329
+ if (raw.interpolation !== void 0) {
5330
+ if (!isInterpolation(raw.interpolation)) fail2(`Unknown interpolation "${String(raw.interpolation)}"; use nearest, bilinear or bicubic.`);
5331
+ if (raw.interpolation !== "bicubic") out.interpolation = raw.interpolation;
5332
+ }
5333
+ const g = { box: { x: 0, y: 0, width: contents.width, height: contents.height }, corners: out.corners, warp: out.warp ?? null };
5334
+ if (!transformHomography(g)) fail2("These corners fold the contents over themselves or through the horizon; keep the quad convex.");
5335
+ if (out.warp && !hasWarp(g)) delete out.warp;
5336
+ return out;
5337
+ }
5338
+ function placementHomography(layer) {
5339
+ const g = placementGeometry(layer);
5340
+ return hasWarp(g) ? null : transformHomography(g);
5341
+ }
5342
+ function followMask(mask, before, after) {
5343
+ if (!mask || mask.linked === false || mask.source) return mask;
5344
+ const h0 = placementHomography(before);
5345
+ const h1 = placementHomography(after);
5346
+ if (!h0 || !h1) return mask;
5347
+ const inv = invertHomography(h0);
5348
+ if (!inv) return mask;
5349
+ const delta = multiplyHomographies(inv, h1);
5350
+ const surface = transformSurface(mask.surface, delta, { interpolation: "bilinear" });
5351
+ return { ...mask, surface };
5352
+ }
5353
+ function setSmartPlacement(layers, id, placement, canvas) {
5354
+ const layer = requireSmartObject(layers, id);
5355
+ requireUnlocked(layer, "position", "transform it");
5356
+ requireDecoded(layer, "be transformed");
5357
+ const clean = normalizePlacement(placement, layer.contents);
5358
+ let next = { ...layer, placement: clean };
5359
+ let masksMoved = false;
5360
+ if (layer.mask) {
5361
+ const m = followMask(layer.mask, layer, next);
5362
+ masksMoved ||= m !== layer.mask;
5363
+ next.mask = m;
5364
+ }
5365
+ if (next.smartFilters?.mask) {
5366
+ const m = followMask(next.smartFilters.mask, layer, next);
5367
+ masksMoved ||= m !== next.smartFilters.mask;
5368
+ next.smartFilters = { ...next.smartFilters, mask: m };
5369
+ }
5370
+ next = rerender(next, canvas, "be transformed");
5371
+ return { layers: mapLayer(layers, id, () => next), layer: next, masksMoved };
5372
+ }
5373
+ function offsetSmartObject(layers, id, dx, dy, canvas) {
5374
+ const layer = requireSmartObject(layers, id);
5375
+ requireUnlocked(layer, "position", "move it");
5376
+ const mx = Math.round(dx);
5377
+ const my = Math.round(dy);
5378
+ if (!mx && !my) return layers;
5379
+ const corners = layer.placement.corners.map((p) => ({ x: p.x + mx, y: p.y + my }));
5380
+ let next = { ...layer, placement: { ...layer.placement, corners } };
5381
+ const moveMask = (m2) => m2 && m2.linked !== false && !m2.source ? { ...m2, surface: translateSurface(m2.surface, mx, my) } : m2;
5382
+ if (layer.mask) next.mask = moveMask(layer.mask);
5383
+ if (layer.smartFilters?.mask) next.smartFilters = { ...layer.smartFilters, mask: moveMask(layer.smartFilters.mask) };
5384
+ const spatial = activeSmartFilters(layer.smartFilters).some((f) => f.kind === "blur");
5385
+ const b = contentBounds(layer.surface);
5386
+ const m = SMART_RENDER_MARGIN;
5387
+ const cut = b && (b.x <= -m || b.y <= -m || b.x + b.width >= canvas.width + m || b.y + b.height >= canvas.height + m);
5388
+ if (!spatial && !cut && !(next.smartFilters?.mask && activeSmartFilters(next.smartFilters).length && next.smartFilters.mask.linked === false)) {
5389
+ next = { ...next, surface: translateSurface(layer.surface, mx, my) };
5390
+ } else {
5391
+ next = rerender(next, canvas, "move");
5392
+ }
5393
+ return mapLayer(layers, id, () => next);
5394
+ }
5395
+ function translateLayer(layer, dx, dy) {
5396
+ if (!dx && !dy) return layer;
5397
+ const moveMask = (m) => {
5398
+ if (!m) return m;
5399
+ if (m.source) fail2(`Layer "${layer.name || layer.id}" has a depth mask, which cannot move into a smart object; bake or remove it first.`);
5400
+ return { ...m, surface: translateSurface(m.surface, dx, dy) };
5401
+ };
5402
+ switch (layer.type) {
5403
+ case "pixel":
5404
+ return { ...layer, offset: { x: layer.offset.x + dx, y: layer.offset.y + dy } };
5405
+ case "smart": {
5406
+ const next = {
5407
+ ...layer,
5408
+ placement: { ...layer.placement, corners: layer.placement.corners.map((p) => ({ x: p.x + dx, y: p.y + dy })) },
5409
+ surface: translateSurface(layer.surface, dx, dy),
5410
+ mask: moveMask(layer.mask)
5411
+ };
5412
+ if (!next.mask) delete next.mask;
5413
+ if (layer.smartFilters?.mask) next.smartFilters = { ...layer.smartFilters, mask: moveMask(layer.smartFilters.mask) };
5414
+ return next;
5415
+ }
5416
+ case "group": {
5417
+ const next = { ...layer, children: layer.children.map((c) => translateLayer(c, dx, dy)), mask: moveMask(layer.mask) };
5418
+ if (!next.mask) delete next.mask;
5419
+ return next;
5420
+ }
5421
+ case "adjustment": {
5422
+ const next = { ...layer, mask: moveMask(layer.mask) };
5423
+ if (!next.mask) delete next.mask;
5424
+ return next;
5425
+ }
5426
+ case "annotation":
5427
+ return fail2(`Annotation layer "${layer.name || layer.id}" cannot go into a smart object; rasterize it first.`);
5428
+ }
5429
+ }
5430
+ function layerExtent(layer, surfaceOf, canvas) {
5431
+ const full = { x: 0, y: 0, width: canvas.width, height: canvas.height };
5432
+ let b = null;
5433
+ if (layer.type === "pixel" || layer.type === "smart") {
5434
+ const s = layer.type === "pixel" ? surfaceOf(layer) : layer.surface;
5435
+ if (s.defaultValue !== 0) b = full;
5436
+ else {
5437
+ const c = contentBounds(s);
5438
+ b = c ? { x: c.x + layer.offset.x, y: c.y + layer.offset.y, width: c.width, height: c.height } : null;
5439
+ }
5440
+ } else if (layer.type === "group") {
5441
+ for (const c of layer.children) b = union(b, layerExtent(c, surfaceOf, canvas));
5442
+ } else {
5443
+ b = full;
5444
+ }
5445
+ const m = effectsMargin(layer.effects);
5446
+ if (b && m > 0) b = { x: b.x - Math.ceil(m), y: b.y - Math.ceil(m), width: b.width + 2 * Math.ceil(m), height: b.height + 2 * Math.ceil(m) };
5447
+ return b;
5448
+ }
5449
+ function union(a, b) {
5450
+ if (!a) return b;
5451
+ if (!b) return a;
5452
+ const x = Math.min(a.x, b.x);
5453
+ const y = Math.min(a.y, b.y);
5454
+ return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
5455
+ }
5456
+ function asSmartFilters(filters) {
5457
+ if (!filters?.length) return void 0;
5458
+ return { enabled: true, filters: filters.map((f) => ({ ...f, blend: "normal", opacity: 1 })) };
5459
+ }
5460
+ function convertToSmartObject(layers, ids, canvas, options) {
5461
+ if (!ids.length) fail2("Give at least one layer id to convert.");
5462
+ const locs = [...new Set(ids)].map((id) => requireLayer(layers, id));
5463
+ const parent = locs[0].parent?.id ?? null;
5464
+ if (locs.some((l) => (l.parent?.id ?? null) !== parent)) fail2("Layers to convert must share one parent; move them next to each other first.");
5465
+ for (const l of locs) requireUnlocked(l.layer, "all", "convert it");
5466
+ const ordered = locs.sort((a, b) => a.index - b.index);
5467
+ const top = ordered[ordered.length - 1];
5468
+ let smart;
5469
+ const single = ordered.length === 1 ? ordered[0].layer : null;
5470
+ if (single && single.type === "pixel") {
5471
+ const surface = options.surfaceOf(single);
5472
+ if (surface.defaultValue !== 0) fail2(`Layer "${single.name || single.id}" has no transparent area to crop to; it cannot be converted.`);
5473
+ const b = contentBounds(surface);
5474
+ if (!b) fail2(`Layer "${single.name || single.id}" has no pixels to convert; paint something first.`);
5475
+ const contents = {
5476
+ id: newContentsId(),
5477
+ name: `${single.name || "Layer"}.png`,
5478
+ width: b.width,
5479
+ height: b.height,
5480
+ kind: "raster",
5481
+ raster: translateSurface(surface, -b.x, -b.y)
5482
+ };
5483
+ smart = {
5484
+ type: "smart",
5485
+ id: single.id,
5486
+ name: single.name,
5487
+ visible: single.visible,
5488
+ opacity: single.opacity,
5489
+ blend: single.blend,
5490
+ contents,
5491
+ placement: placementAt(contents, b.x + single.offset.x, b.y + single.offset.y),
5492
+ surface: emptySurface(4),
5493
+ offset: { x: 0, y: 0 }
5494
+ };
5495
+ if (single.clipped) smart.clipped = true;
5496
+ if (single.fillOpacity !== void 0) smart.fillOpacity = single.fillOpacity;
5497
+ if (single.effects) smart.effects = single.effects;
5498
+ if (single.mask) {
5499
+ smart.mask = single.mask.source ? single.mask : { ...single.mask, surface: translateSurface(single.mask.surface, single.offset.x, single.offset.y) };
5500
+ }
5501
+ const filters = asSmartFilters(single.filters);
5502
+ if (filters) smart.smartFilters = filters;
5503
+ } else {
5504
+ let b = null;
5505
+ for (const l of ordered) b = union(b, layerExtent(l.layer, options.surfaceOf, canvas));
5506
+ if (!b || !(b.width > 0 && b.height > 0)) fail2("The layers have no pixels to convert; paint something first.");
5507
+ const nested = ordered.map((l) => {
5508
+ let layer = l.layer;
5509
+ if (layer.type === "pixel" && layer.source === "document") {
5510
+ const resolved = { ...layer, surface: options.surfaceOf(layer) };
5511
+ delete resolved.source;
5512
+ layer = resolved;
5513
+ }
5514
+ if (flattenLayers([layer]).some((x) => x.type === "pixel" && x.source === "document")) fail2(`Layer "${layer.name || layer.id}" holds the document image inside a group; convert the Background on its own.`);
5515
+ return translateLayer(layer, -b.x, -b.y);
5516
+ });
5517
+ if (nested[0].clipped) {
5518
+ const first = { ...nested[0] };
5519
+ delete first.clipped;
5520
+ nested[0] = first;
5521
+ }
5522
+ const name = options.name?.trim() || top.layer.name || "Smart Object";
5523
+ const contents = documentContents({ layers: nested }, b.width, b.height, `${name}.psb`);
5524
+ smart = {
5525
+ type: "smart",
5526
+ id: ordered.length === 1 ? ordered[0].layer.id : newLayerId("smart"),
5527
+ name,
5528
+ visible: true,
5529
+ opacity: 1,
5530
+ blend: "normal",
5531
+ contents,
5532
+ placement: placementAt(contents, b.x, b.y),
5533
+ surface: emptySurface(4),
5534
+ offset: { x: 0, y: 0 }
5535
+ };
5536
+ }
5537
+ if (options.name?.trim()) smart.name = options.name.trim();
5538
+ smart = { ...smart, surface: renderSmartObject(smart, canvas) };
5539
+ const below = ordered.filter((l) => l.index < top.index).length;
5540
+ let out = layers;
5541
+ for (const l of ordered) out = mapLayer(out, l.layer.id, () => null);
5542
+ return { layers: insertLayer(out, smart, parent, top.index - below), layer: smart };
5543
+ }
5544
+ function rasterizeSmartObject(layers, id) {
5545
+ const layer = requireSmartObject(layers, id);
5546
+ requireUnlocked(layer, "all", "rasterize it");
5547
+ const out = {
5548
+ type: "pixel",
5549
+ id: layer.id,
5550
+ name: layer.name,
5551
+ visible: layer.visible,
5552
+ opacity: layer.opacity,
5553
+ blend: layer.blend,
5554
+ surface: layer.surface,
5555
+ offset: { x: 0, y: 0 }
5556
+ };
5557
+ if (layer.clipped) out.clipped = true;
5558
+ if (layer.mask) out.mask = layer.mask;
5559
+ if (layer.locks) out.locks = layer.locks;
5560
+ if (layer.effects) out.effects = layer.effects;
5561
+ if (layer.fillOpacity !== void 0) out.fillOpacity = layer.fillOpacity;
5562
+ return { layers: mapLayer(layers, id, () => out), layer: out };
5563
+ }
5564
+ function copySmartObject(layers, id, linked, name) {
5565
+ const loc = requireLayer(layers, id);
5566
+ const layer = requireSmartObject(layers, id);
5567
+ const copy = { ...layer, id: newLayerId("smart"), name: name?.trim() || `${layer.name || "Smart Object"} copy` };
5568
+ delete copy.locks;
5569
+ delete copy.psd;
5570
+ if (!linked) copy.contents = { ...layer.contents, id: newContentsId() };
5571
+ if (copy.smartFilters) copy.smartFilters = { ...copy.smartFilters, filters: copy.smartFilters.filters.map((f) => ({ ...f, id: newLayerId("flt") })) };
5572
+ return { layers: insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1), layer: copy };
5573
+ }
5574
+ function instancesOf(layers, contentsId) {
5575
+ return flattenLayers(layers).filter((l) => l.type === "smart" && l.contents.id === contentsId).map((l) => l.id);
5576
+ }
5577
+ function replaceContents(layers, contentsId, contents, canvas) {
5578
+ const ids = instancesOf(layers, contentsId);
5579
+ if (!ids.length) fail2(`No smart object uses contents "${contentsId}".`);
5580
+ for (const id of ids) requireUnlocked(findLayer(layers, id).layer, "all", "replace its contents");
5581
+ const next = { ...contents, id: contentsId };
5582
+ let out = layers;
5583
+ for (const id of ids) {
5584
+ out = mapLayer(out, id, (l) => {
5585
+ const layer = l;
5586
+ return rerender({ ...layer, contents: next, placement: refitPlacement(layer, next) }, canvas, "take the new contents");
5587
+ });
5588
+ }
5589
+ return { layers: out, ids };
5590
+ }
5591
+ function refitPlacement(layer, contents) {
5592
+ const W0 = layer.contents.width;
5593
+ const H0 = layer.contents.height;
5594
+ const W1 = contents.width;
5595
+ const H1 = contents.height;
5596
+ if (W0 === W1 && H0 === H1) return layer.placement;
5597
+ const g = placementGeometry(layer);
5598
+ const h = transformHomography(g);
5599
+ if (!h) return placementAt(contents, 0, 0);
5600
+ const shift = [1, 0, (W0 - W1) / 2, 0, 1, (H0 - H1) / 2, 0, 0, 1];
5601
+ const hn = multiplyHomographies(shift, h);
5602
+ const pts = boxQuad({ x: 0, y: 0, width: W1, height: H1 }).map((p) => {
5603
+ const w = hn[6] * p.x + hn[7] * p.y + hn[8];
5604
+ return { x: (hn[0] * p.x + hn[1] * p.y + hn[2]) / w, y: (hn[3] * p.x + hn[4] * p.y + hn[5]) / w };
5605
+ });
5606
+ const out = { corners: pts };
5607
+ if (layer.placement.interpolation) out.interpolation = layer.placement.interpolation;
5608
+ if (layer.placement.warp) out.warp = mapWarpMeshPoints(layer.placement.warp, (p) => ({ x: p.x * W1 / W0, y: p.y * H1 / H0 }));
5609
+ return out;
5610
+ }
5611
+ function normalizeSmartFilter(raw, id = newLayerId("flt")) {
5612
+ if (raw.kind === "psd") fail2("Photoshop filters can only come from a PSD; add develop or blur.");
5613
+ const base = normalizeFilter(raw, id);
5614
+ const blend = raw.blend ?? "normal";
5615
+ if (!isBlendMode(blend) || blend === "pigment") fail2(`Unknown blend mode "${String(blend)}" for a smart filter.`);
5616
+ const opacity = raw.opacity ?? 1;
5617
+ if (!(typeof opacity === "number" && opacity >= 0 && opacity <= 1)) fail2("opacity must be a number from 0 to 1.");
5618
+ return { ...base, blend, opacity };
5619
+ }
5620
+ function withStack(layer, stack, canvas) {
5621
+ const next = { ...layer };
5622
+ if (stack && (stack.filters.length || stack.mask || !stack.enabled)) next.smartFilters = stack;
5623
+ else delete next.smartFilters;
5624
+ return rerender(next, canvas, "change its smart filters");
5625
+ }
5626
+ function stackOf(layer) {
5627
+ return layer.smartFilters ?? { enabled: true, filters: [] };
5628
+ }
5629
+ function requireFilter(layer, filterId) {
5630
+ const list2 = layer.smartFilters?.filters ?? [];
5631
+ const i = list2.findIndex((f) => f.id === filterId);
5632
+ if (i === -1) fail2(`Smart object "${layer.name || layer.id}" has no smart filter "${filterId}". Filters: ${list2.map((f) => `${f.id} (${f.kind})`).join(", ") || "none"}; call get_smart_object.`);
5633
+ return i;
5634
+ }
5635
+ function editableSmart(layers, id) {
5636
+ const layer = requireSmartObject(layers, id);
5637
+ requireUnlocked(layer, "all", "change its smart filters");
5638
+ return layer;
5639
+ }
5640
+ function addSmartFilter(layers, id, filter, canvas, index) {
5641
+ const layer = editableSmart(layers, id);
5642
+ const stack = stackOf(layer);
5643
+ const filters = [...stack.filters];
5644
+ const at = index === void 0 ? filters.length : Math.max(0, Math.min(filters.length, Math.floor(index)));
5645
+ filters.splice(at, 0, filter);
5646
+ const next = withStack(layer, { ...stack, filters }, canvas);
5647
+ return { layers: mapLayer(layers, id, () => next), layer: next, filter };
5648
+ }
5649
+ function updateSmartFilter(layers, id, filterId, changes, canvas) {
5650
+ const layer = editableSmart(layers, id);
5651
+ const i = requireFilter(layer, filterId);
5652
+ const current = layer.smartFilters.filters[i];
5653
+ if ("kind" in changes && changes.kind !== current.kind) fail2("A smart filter kind cannot change; remove it and add another.");
5654
+ let merged;
5655
+ if (current.kind === "psd") {
5656
+ const extra = Object.keys(changes).filter((k) => !["enabled", "blend", "opacity", "kind"].includes(k));
5657
+ if (extra.length) fail2(`"${current.name}" is a Photoshop filter we keep but do not model; only enabled, blend and opacity change. Unknown: ${extra.join(", ")}.`);
5658
+ const blend = changes.blend ?? current.blend;
5659
+ if (!isBlendMode(blend) || blend === "pigment") fail2(`Unknown blend mode "${String(blend)}" for a smart filter.`);
5660
+ const opacity = changes.opacity ?? current.opacity;
5661
+ if (!(typeof opacity === "number" && opacity >= 0 && opacity <= 1)) fail2("opacity must be a number from 0 to 1.");
5662
+ merged = { ...current, enabled: changes.enabled === void 0 ? current.enabled : Boolean(changes.enabled), blend, opacity };
5663
+ } else {
5664
+ merged = normalizeSmartFilter({ ...current, ...changes, kind: current.kind }, current.id);
5665
+ }
5666
+ const filters = [...layer.smartFilters.filters];
5667
+ filters[i] = merged;
5668
+ const next = withStack(layer, { ...layer.smartFilters, filters }, canvas);
5669
+ return { layers: mapLayer(layers, id, () => next), layer: next, filter: merged };
5670
+ }
5671
+ function removeSmartFilter(layers, id, filterId, canvas) {
5672
+ const layer = editableSmart(layers, id);
5673
+ requireFilter(layer, filterId);
5674
+ const stack = layer.smartFilters;
5675
+ const filters = stack.filters.filter((f) => f.id !== filterId);
5676
+ const next = withStack(layer, filters.length ? { ...stack, filters } : stack.enabled ? void 0 : { enabled: false, filters }, canvas);
5677
+ return { layers: mapLayer(layers, id, () => next), layer: next };
5678
+ }
5679
+ function moveSmartFilter(layers, id, filterId, index, canvas) {
5680
+ const layer = editableSmart(layers, id);
5681
+ const i = requireFilter(layer, filterId);
5682
+ const list2 = layer.smartFilters.filters;
5683
+ if (!Number.isInteger(index) || index < 0 || index >= list2.length) fail2(`index must be a whole number from 0 to ${list2.length - 1}.`);
5684
+ const filters = [...list2];
5685
+ const [f] = filters.splice(i, 1);
5686
+ filters.splice(index, 0, f);
5687
+ const next = withStack(layer, { ...layer.smartFilters, filters }, canvas);
5688
+ return { layers: mapLayer(layers, id, () => next), layer: next };
5689
+ }
5690
+ function setSmartFiltersEnabled(layers, id, enabled, canvas) {
5691
+ const layer = editableSmart(layers, id);
5692
+ const next = withStack(layer, { ...stackOf(layer), enabled: Boolean(enabled) }, canvas);
5693
+ return { layers: mapLayer(layers, id, () => next), layer: next };
5694
+ }
5695
+ function setSmartFilterMask(layers, id, mask, canvas) {
5696
+ const layer = editableSmart(layers, id);
5697
+ if (mask?.source) fail2("A smart filter mask is painted, not generated; use a raster mask.");
5698
+ const stack = { ...stackOf(layer) };
5699
+ if (mask) stack.mask = mask;
5700
+ else delete stack.mask;
5701
+ const next = withStack(layer, stack, canvas);
5702
+ return { layers: mapLayer(layers, id, () => next), layer: next };
5703
+ }
5704
+ function withFilterMaskSurface(layer, surface, canvas) {
5705
+ const stack = stackOf(layer);
5706
+ if (!stack.mask) fail2(`Smart object "${layer.name || layer.id}" has no filter mask; add one with set_smart_filter_mask.`);
5707
+ return withStack(layer, { ...stack, mask: { ...stack.mask, surface } }, canvas);
5708
+ }
5709
+ function placementBounds(layer) {
5710
+ return transformedBox(placementGeometry(layer));
5711
+ }
5712
+
5713
+ // src/image-editor/layers/serialize.ts
5714
+ var LAYERED_SCHEMA_VERSION = 3;
5715
+ function serializePatterns(patterns) {
5716
+ return patterns.map((p) => ({ id: p.id, name: p.name, png: pngDataUrl(p.width, p.height, p.data) }));
5717
+ }
5718
+ function deserializePatterns(raw, problems = []) {
5719
+ if (!Array.isArray(raw)) return [];
5720
+ const out = [];
5721
+ for (const p of raw) {
5722
+ if (!p || typeof p !== "object" || typeof p.id !== "string" || typeof p.png !== "string") continue;
5723
+ try {
5724
+ const png = decodePngDataUrl(p.png, 4);
5725
+ out.push({ id: p.id, name: typeof p.name === "string" ? p.name : p.id, width: png.width, height: png.height, data: png.data });
5726
+ } catch (e) {
5727
+ problems.push(`Pattern "${p.name ?? p.id}" could not be decoded (${e instanceof Error ? e.message : String(e)}).`);
5728
+ }
5729
+ }
5730
+ return out;
5731
+ }
5732
+ var encodedCache = /* @__PURE__ */ new WeakMap();
5733
+ function encodeSurface(surface) {
5734
+ if (encodedCache.has(surface)) return encodedCache.get(surface);
5735
+ const b = contentBounds(surface);
5736
+ let out = null;
5737
+ if (b) {
5738
+ const data = readRegion(surface, b);
5739
+ out = { url: pngDataUrl(b.width, b.height, data, surface.channels), x: b.x, y: b.y };
5740
+ }
5741
+ encodedCache.set(surface, out);
5742
+ return out;
5743
+ }
5744
+ function baseFields(layer, ctx) {
5745
+ const out = {
5746
+ id: layer.id,
5747
+ name: layer.name,
5748
+ visible: layer.visible,
5749
+ opacity: layer.opacity,
5750
+ blend: layer.blend
5751
+ };
5752
+ if (layer.clipped) out.clipped = true;
5753
+ if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
5754
+ if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
5755
+ if (layer.effects) out.effects = serializeEffects(layer.effects);
5756
+ return out;
5757
+ }
5758
+ function serializeEffects(effects) {
5759
+ const out = { enabled: effects.enabled, items: JSON.parse(JSON.stringify(effects.items)) };
5760
+ if (effects.scale !== void 0 && effects.scale !== 1) out.scale = effects.scale;
5761
+ if (effects.psd) out.psd = { key: effects.psd.key, data: bytesToBase64(effects.psd.data) };
5762
+ return out;
5763
+ }
5764
+ function deserializeEffects(raw) {
5765
+ if (!raw || typeof raw !== "object") return void 0;
5766
+ const r = raw;
5767
+ let psd;
5768
+ const p = r.psd;
5769
+ if (p && typeof p.key === "string" && typeof p.data === "string") {
5770
+ try {
5771
+ psd = { key: p.key, data: base64ToBytes(p.data) };
5772
+ } catch {
5773
+ psd = void 0;
5774
+ }
5775
+ }
5776
+ return normalizeLayerEffects({ ...r, psd });
5777
+ }
5778
+ function serializeMask(mask, hint, ctx) {
5779
+ const out = { defaultValue: mask.defaultValue };
5780
+ if (!mask.source) {
5781
+ const enc = encodeSurface(mask.surface);
5782
+ if (enc) {
5783
+ out.asset = ctx.addAsset(enc.url, hint);
5784
+ out.x = enc.x;
5785
+ out.y = enc.y;
5786
+ }
5787
+ } else {
5788
+ out.source = { ...mask.source, depthRange: [...mask.source.depthRange] };
5789
+ }
5790
+ if (mask.density !== void 0 && mask.density !== 1) out.density = mask.density;
5791
+ if (mask.feather) out.feather = mask.feather;
5792
+ if (mask.enabled === false) out.enabled = false;
5793
+ if (mask.linked === false) out.linked = false;
5794
+ return out;
5795
+ }
5796
+ function serializeLayer(layer, ctx) {
5797
+ const base = baseFields(layer, ctx);
5798
+ switch (layer.type) {
5799
+ case "pixel": {
5800
+ const out = { ...base, type: "pixel", offset: { ...layer.offset } };
5801
+ if (layer.source === "document") out.source = "document";
5802
+ else {
5803
+ const enc = encodeSurface(layer.surface);
5804
+ if (enc) {
5805
+ out.asset = ctx.addAsset(enc.url, layer.id);
5806
+ out.x = enc.x;
5807
+ out.y = enc.y;
5808
+ }
5809
+ }
5810
+ if (layer.fillOpacity !== void 0 && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;
5811
+ if (layer.filters?.length) out.filters = JSON.parse(JSON.stringify(layer.filters));
5812
+ return out;
5813
+ }
5814
+ case "adjustment":
5815
+ return { ...base, type: "adjustment", kind: layer.kind, params: JSON.parse(JSON.stringify(layer.params)) };
5816
+ case "group": {
5817
+ const out = { ...base, type: "group", children: layer.children.map((c) => serializeLayer(c, ctx)) };
5818
+ if (layer.collapsed) out.collapsed = true;
5819
+ return out;
5820
+ }
5821
+ case "annotation":
5822
+ return { ...base, type: "annotation", annotation: JSON.parse(JSON.stringify(layer.annotation)) };
5823
+ case "smart":
5824
+ return serializeSmartObject(layer, base, ctx);
5825
+ }
5826
+ }
5827
+ function serializeSmartFilters(stack, layerId, ctx) {
5828
+ const out = {
5829
+ enabled: stack.enabled,
5830
+ filters: stack.filters.map((f) => f.kind === "psd" ? { ...f, data: bytesToBase64(f.data) } : JSON.parse(JSON.stringify(f)))
5831
+ };
5832
+ if (stack.mask) out.mask = serializeMask(stack.mask, `${layerId}-filter-mask`, ctx);
5833
+ return out;
5834
+ }
5835
+ function serializeSmartObject(layer, base, ctx) {
5836
+ if (!ctx.addContents) throw new Error("Smart objects need a contents table to be written.");
5837
+ ctx.addContents(layer.contents);
5838
+ const p = layer.placement;
5839
+ const out = {
5840
+ ...base,
5841
+ type: "smart",
5842
+ contents: layer.contents.id,
5843
+ placement: { corners: p.corners.map((c) => ({ x: c.x, y: c.y })) }
5844
+ };
5845
+ if (p.warp) out.placement.warp = JSON.parse(JSON.stringify(p.warp));
5846
+ if (p.interpolation && p.interpolation !== "bicubic") out.placement.interpolation = p.interpolation;
5847
+ if (layer.smartFilters) out.smartFilters = serializeSmartFilters(layer.smartFilters, layer.id, ctx);
5848
+ const enc = encodeSurface(layer.surface);
5849
+ if (enc) {
5850
+ out.asset = ctx.addAsset(enc.url, layer.id);
5851
+ out.x = enc.x;
5852
+ out.y = enc.y;
5853
+ }
5854
+ if (layer.fillOpacity !== void 0 && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;
5855
+ if (layer.psd) {
5856
+ out.psd = { key: layer.psd.key, data: bytesToBase64(layer.psd.data) };
5857
+ if (layer.psd.placed) out.psd.placed = layer.psd.placed;
5858
+ }
5859
+ return out;
5860
+ }
5861
+ function serializeContents(c, ctx) {
5862
+ const out = { id: c.id, name: c.name, width: c.width, height: c.height, kind: c.kind };
5863
+ if (c.kind === "raster" && c.raster) {
5864
+ const enc = encodeSurface(c.raster);
5865
+ if (enc) {
5866
+ if (enc.x !== 0 || enc.y !== 0) {
5867
+ const data = readRegion(c.raster, { x: 0, y: 0, width: c.width, height: c.height });
5868
+ out.asset = ctx.addAsset(pngDataUrl(c.width, c.height, data, 4), `contents-${c.id}`);
5869
+ } else out.asset = ctx.addAsset(enc.url, `contents-${c.id}`);
5870
+ }
5871
+ }
5872
+ if (c.kind === "document" && c.document) {
5873
+ out.layers = c.document.layers.map((l) => serializeLayer(l, ctx));
5874
+ if (c.document.globalLight) out.globalLight = { ...c.document.globalLight };
5875
+ if (c.document.patterns?.length) out.patterns = serializePatterns(c.document.patterns);
5876
+ }
5877
+ if (c.file) out.file = { type: c.file.type, data: bytesToBase64(c.file.bytes) };
5878
+ if (c.linked) out.linked = { path: c.linked.path };
5879
+ return out;
5880
+ }
5881
+ function serializeLayers(layers) {
5882
+ const assets = {};
5883
+ const contents = /* @__PURE__ */ new Map();
5884
+ const ctx = {
5885
+ addAsset(url, hint) {
5886
+ let id = hint;
5887
+ for (let n = 2; id in assets && assets[id] !== url; n++) id = `${hint}-${n}`;
5888
+ assets[id] = url;
5889
+ return id;
5890
+ },
5891
+ addContents(c) {
5892
+ if (contents.has(c.id)) return;
5893
+ contents.set(c.id, null);
5894
+ contents.set(c.id, serializeContents(c, ctx));
5895
+ }
5896
+ };
5897
+ const out = layers.map((l) => serializeLayer(l, ctx));
5898
+ const list2 = [...contents.values()].filter(Boolean);
5899
+ return list2.length ? { layers: out, assets, smartContents: list2 } : { layers: out, assets };
5900
+ }
5901
+ var externalRefs = /* @__PURE__ */ new WeakMap();
5902
+ async function serializeLayersExternal(layers, store) {
5903
+ const inline = serializeLayers(layers);
5904
+ let refs = externalRefs.get(store);
5905
+ if (!refs) {
5906
+ refs = /* @__PURE__ */ new Map();
5907
+ externalRefs.set(store, refs);
5908
+ }
5909
+ const assets = {};
5910
+ for (const [id, url] of Object.entries(inline.assets)) {
5911
+ let ref = refs.get(url);
5912
+ if (!ref) {
5913
+ const parsed = parseDataUrl(url);
5914
+ const png = decodePngHeader(parsed.bytes);
5915
+ ref = await store.put(new Blob([parsed.bytes], { type: "image/png" }), { id, width: png.width, height: png.height });
5916
+ if (typeof ref !== "string" || !ref) throw new Error(`The asset store returned no reference for "${id}".`);
5917
+ refs.set(url, ref);
5918
+ }
5919
+ assets[id] = ref;
5920
+ }
5921
+ return inline.smartContents ? { layers: inline.layers, assets, smartContents: inline.smartContents } : { layers: inline.layers, assets };
5922
+ }
5923
+ function decodePngHeader(bytes) {
5924
+ const u32 = (o) => (bytes[o] << 24 | bytes[o + 1] << 16 | bytes[o + 2] << 8 | bytes[o + 3]) >>> 0;
5925
+ return { width: u32(16), height: u32(20) };
5926
+ }
5927
+ async function resolveExternalAssets(assets, store, problems) {
5928
+ const out = {};
5929
+ for (const [id, ref] of Object.entries(assets)) {
5930
+ if (ref.startsWith("data:")) {
5931
+ out[id] = ref;
5932
+ continue;
5933
+ }
5934
+ if (!store) {
5935
+ problems.push(`Asset "${id}" is external (${ref}) and no asset store was given.`);
5936
+ continue;
5937
+ }
5938
+ try {
5939
+ const blob = await store.get(ref);
5940
+ const bytes = new Uint8Array(await blob.arrayBuffer());
5941
+ out[id] = `data:image/png;base64,${bytesToBase64(bytes)}`;
5942
+ } catch (err) {
5943
+ problems.push(`Asset "${id}" (${ref}) could not be fetched: ${err.message}`);
5944
+ }
5945
+ }
5946
+ return out;
5947
+ }
5948
+ function decodePngDataUrl(url, channels) {
5949
+ const parsed = parseDataUrl(url);
5950
+ if (!parsed) throw new Error("Asset is not a data URL.");
5951
+ const png = decodePng(parsed.bytes, { keepGray: channels === 1 });
5952
+ if (png.channels === channels) return png;
5953
+ if (channels === 1) {
5954
+ const out2 = new Uint8ClampedArray(png.width * png.height);
5955
+ for (let i = 0, p = 0; p < out2.length; i += 4, p++) {
5956
+ out2[p] = Math.round((0.299 * png.data[i] + 0.587 * png.data[i + 1] + 0.114 * png.data[i + 2]) * png.data[i + 3] / 255);
5957
+ }
5958
+ return { width: png.width, height: png.height, data: out2 };
5959
+ }
5960
+ const out = new Uint8ClampedArray(png.width * png.height * 4);
5961
+ for (let p = 0; p < png.width * png.height; p++) {
5962
+ out[p * 4] = out[p * 4 + 1] = out[p * 4 + 2] = png.data[p];
5963
+ out[p * 4 + 3] = 255;
5964
+ }
5965
+ return { width: png.width, height: png.height, data: out };
5966
+ }
5967
+ var num = (v, lo, hi, fallback) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : fallback;
5968
+ function readSurface(ref, x, y, channels, defaultValue, ctx, what) {
5969
+ if (typeof ref !== "string" || !ref) return emptySurface(channels, defaultValue);
5970
+ const url = ctx.assets[ref];
5971
+ if (typeof url !== "string") {
5972
+ ctx.problems.push(`${what}: asset "${ref}" is missing`);
5973
+ return emptySurface(channels, defaultValue);
5974
+ }
5975
+ try {
5976
+ const px2 = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);
5977
+ return surfaceFromPixels(px2, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);
5978
+ } catch (err) {
5979
+ ctx.problems.push(`${what}: asset "${ref}" could not be decoded (${err.message})`);
5980
+ return emptySurface(channels, defaultValue);
5981
+ }
5982
+ }
5983
+ function readMask(raw, ctx, what) {
5984
+ if (!raw || typeof raw !== "object") return void 0;
5985
+ const r = raw;
5986
+ const defaultValue = r.defaultValue === 255 ? 255 : 0;
5987
+ const mask = {
5988
+ surface: r.source ? emptySurface(1, 255) : readSurface(r.asset, r.x, r.y, 1, defaultValue, ctx, `${what} mask`),
5989
+ defaultValue: r.source ? 255 : defaultValue
5990
+ };
5991
+ if (typeof r.density === "number" && r.density !== 1) mask.density = num(r.density, 0, 1, 1);
5992
+ if (typeof r.feather === "number" && r.feather > 0) mask.feather = num(r.feather, 0, 1e3, 0);
5993
+ if (r.enabled === false) mask.enabled = false;
5994
+ if (r.linked === false) mask.linked = false;
5995
+ if (r.source && typeof r.source === "object") {
5996
+ const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...r.source };
5997
+ mask.source = { ...s, depthRange: [...s.depthRange] };
5998
+ }
5999
+ return mask;
6000
+ }
6001
+ function readFilters(raw) {
6002
+ if (!Array.isArray(raw)) return void 0;
6003
+ const out = [];
6004
+ for (const f of raw) {
6005
+ if (!f || typeof f !== "object") continue;
6006
+ const r = f;
6007
+ const id = typeof r.id === "string" && r.id ? r.id : newLayerId("flt");
6008
+ const enabled = r.enabled !== false;
6009
+ if (r.kind === "blur") out.push({ id, kind: "blur", enabled, radius: num(r.radius, 0, 50, 0) });
6010
+ else if (r.kind === "develop") {
6011
+ out.push({
6012
+ id,
6013
+ kind: "develop",
6014
+ enabled,
6015
+ brightness: num(r.brightness, -100, 100, 0),
6016
+ contrast: num(r.contrast, -100, 100, 0),
6017
+ saturation: num(r.saturation, -100, 100, 0),
6018
+ exposure: num(r.exposure, -100, 100, 0),
6019
+ temperature: num(r.temperature, -100, 100, 0),
6020
+ tint: num(r.tint, -100, 100, 0),
6021
+ process: normalizeProcessVersion(r.process)
6022
+ });
6023
+ }
6024
+ }
6025
+ return out.length ? out : void 0;
6026
+ }
6027
+ function deserializeLayer(raw, ctx) {
6028
+ if (!raw || typeof raw !== "object") return null;
6029
+ const r = raw;
6030
+ const id = typeof r.id === "string" && r.id ? r.id : newLayerId();
6031
+ const base = {
6032
+ id,
6033
+ name: typeof r.name === "string" ? r.name : "",
6034
+ visible: r.visible !== false,
6035
+ opacity: num(r.opacity, 0, 1, 1),
6036
+ blend: normalizeBlendMode(r.blend)
6037
+ };
6038
+ if (r.clipped === true) base.clipped = true;
6039
+ const mask = readMask(r.mask, ctx, `Layer "${base.name || id}"`);
6040
+ if (mask) base.mask = mask;
6041
+ if (r.locks && typeof r.locks === "object") {
6042
+ const locks = {};
6043
+ for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
6044
+ if (Object.keys(locks).length) base.locks = locks;
6045
+ }
6046
+ const effects = deserializeEffects(r.effects);
6047
+ if (effects && r.type !== "adjustment") base.effects = effects;
6048
+ switch (r.type) {
6049
+ case "pixel": {
6050
+ const layer = {
6051
+ ...base,
6052
+ type: "pixel",
6053
+ surface: r.source === "document" ? emptySurface(4) : readSurface(r.asset, r.x, r.y, 4, 0, ctx, `Layer "${base.name || id}"`),
6054
+ offset: { x: Math.round(num(r.offset?.x, -1e7, 1e7, 0)), y: Math.round(num(r.offset?.y, -1e7, 1e7, 0)) }
6055
+ };
6056
+ if (r.source === "document") layer.source = "document";
6057
+ if (typeof r.fillOpacity === "number" && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);
6058
+ const filters = readFilters(r.filters);
6059
+ if (filters) layer.filters = filters;
6060
+ return layer;
6061
+ }
6062
+ case "adjustment": {
6063
+ const norm = normalizeAdjustmentLayer({ id, kind: r.kind, params: r.params });
6064
+ if (!norm) {
6065
+ ctx.problems.push(`Layer "${base.name || id}": unknown adjustment kind "${r.kind}"`);
6066
+ return null;
6067
+ }
6068
+ return { ...base, type: "adjustment", kind: norm.kind, params: norm.params };
6069
+ }
6070
+ case "group": {
6071
+ const children = Array.isArray(r.children) ? r.children.map((c) => deserializeLayer(c, ctx)).filter(Boolean) : [];
6072
+ const blend = r.blend === "pass-through" ? "pass-through" : isBlendMode(r.blend) ? r.blend : "pass-through";
6073
+ const group = { ...base, type: "group", blend, children };
6074
+ if (r.collapsed === true) group.collapsed = true;
6075
+ return group;
6076
+ }
6077
+ case "annotation": {
6078
+ const a = r.annotation;
6079
+ if (!a || typeof a !== "object" || typeof a.type !== "string") {
6080
+ ctx.problems.push(`Layer "${base.name || id}": annotation is missing`);
6081
+ return null;
6082
+ }
6083
+ return { ...base, type: "annotation", annotation: JSON.parse(JSON.stringify({ ...a, id: typeof a.id === "string" ? a.id : id })) };
6084
+ }
6085
+ case "smart":
6086
+ return readSmartObject(r, base, ctx);
6087
+ default:
6088
+ ctx.problems.push(`Layer "${base.name || id}": unknown layer type "${r.type}"`);
6089
+ return null;
6090
+ }
6091
+ }
6092
+ function readContents(id, ctx) {
6093
+ const memo = ctx.contentsMemo ??= /* @__PURE__ */ new Map();
6094
+ if (memo.has(id)) {
6095
+ const c2 = memo.get(id);
6096
+ if (c2 === null) ctx.problems.push(`Smart object contents "${id}" contain themselves; the inner instance was dropped`);
6097
+ return c2 ?? null;
6098
+ }
6099
+ const raw = (ctx.smartContents ?? []).find((c2) => c2 && typeof c2 === "object" && c2.id === id);
6100
+ if (!raw) {
6101
+ ctx.problems.push(`Smart object contents "${id}" are missing`);
6102
+ return null;
6103
+ }
6104
+ const width = Math.round(num(raw.width, 0, 3e5, 0));
6105
+ const height = Math.round(num(raw.height, 0, 3e5, 0));
6106
+ if (!(width > 0 && height > 0)) {
6107
+ ctx.problems.push(`Smart object contents "${id}" have no size`);
6108
+ return null;
6109
+ }
6110
+ memo.set(id, null);
6111
+ const c = { id, name: typeof raw.name === "string" ? raw.name : "Contents", width, height, kind: raw.kind === "document" ? "document" : "raster" };
6112
+ if (raw.file && typeof raw.file.type === "string" && typeof raw.file.data === "string") {
6113
+ try {
6114
+ c.file = { type: raw.file.type, bytes: base64ToBytes(raw.file.data) };
6115
+ } catch {
6116
+ ctx.problems.push(`Smart object contents "${c.name}": the embedded file could not be read`);
6117
+ }
6118
+ }
6119
+ if (raw.linked && typeof raw.linked.path === "string") c.linked = { path: raw.linked.path };
6120
+ if (c.kind === "raster") {
6121
+ if (typeof raw.asset === "string") c.raster = readSurface(raw.asset, 0, 0, 4, 0, ctx, `Smart object contents "${c.name}"`);
6122
+ else if (!c.file) c.raster = emptySurface(4);
6123
+ } else {
6124
+ const outer = ctx.canvas;
6125
+ ctx.canvas = { width, height };
6126
+ const layers = deserializeLayers(raw.layers, ctx);
6127
+ ctx.canvas = outer;
6128
+ c.document = { layers };
6129
+ if (raw.globalLight && typeof raw.globalLight === "object") c.document.globalLight = { angle: num(raw.globalLight.angle, -360, 360, 120), altitude: num(raw.globalLight.altitude, 0, 90, 30) };
6130
+ const patterns = deserializePatterns(raw.patterns, ctx.problems);
6131
+ if (patterns.length) c.document.patterns = patterns;
6132
+ }
6133
+ memo.set(id, c);
6134
+ return c;
5173
6135
  }
5174
- function layerEffectsOf(layer) {
5175
- return layer.effects ?? { enabled: true, items: [] };
6136
+ function readSmartFilters(raw, ctx, what) {
6137
+ if (!raw || typeof raw !== "object") return void 0;
6138
+ const r = raw;
6139
+ const filters = [];
6140
+ const list2 = Array.isArray(r.filters) ? r.filters : [];
6141
+ const plain3 = readFilters(list2.filter((f) => f && f.kind !== "psd")) ?? [];
6142
+ let k = 0;
6143
+ for (const f of list2) {
6144
+ if (!f || typeof f !== "object") continue;
6145
+ const blend = isBlendMode(f.blend) && f.blend !== "pigment" ? f.blend : "normal";
6146
+ const opacity = num(f.opacity, 0, 1, 1);
6147
+ if (f.kind === "psd") {
6148
+ try {
6149
+ filters.push({ id: typeof f.id === "string" ? f.id : newLayerId("flt"), kind: "psd", enabled: f.enabled !== false, name: typeof f.name === "string" ? f.name : "Filter", data: base64ToBytes(String(f.data ?? "")), blend, opacity });
6150
+ } catch {
6151
+ ctx.problems.push(`${what}: a Photoshop smart filter could not be read`);
6152
+ }
6153
+ } else if (f.kind === "develop" || f.kind === "blur") {
6154
+ const base = plain3[k++];
6155
+ if (base) filters.push({ ...base, blend, opacity });
6156
+ }
6157
+ }
6158
+ const stack = { enabled: r.enabled !== false, filters };
6159
+ const mask = readMask(r.mask, ctx, `${what} filter`);
6160
+ if (mask) stack.mask = mask;
6161
+ return stack;
5176
6162
  }
5177
- function guard(fn) {
6163
+ function readSmartObject(r, base, ctx) {
6164
+ const what = `Layer "${base.name || base.id}"`;
6165
+ const contents = typeof r.contents === "string" ? readContents(r.contents, ctx) : null;
6166
+ if (!contents) {
6167
+ ctx.problems.push(`${what}: its smart object contents are missing; the layer was dropped`);
6168
+ return null;
6169
+ }
6170
+ const p = r.placement ?? {};
6171
+ let placement;
5178
6172
  try {
5179
- return fn();
6173
+ const raw = { corners: p.corners };
6174
+ if (p.warp && isValidWarpMesh(p.warp)) raw.warp = p.warp;
6175
+ if (isInterpolation(p.interpolation)) raw.interpolation = p.interpolation;
6176
+ placement = normalizePlacement(raw, contents);
5180
6177
  } catch (e) {
5181
- if (e instanceof LayerEffectError) fail(e.message);
5182
- throw e;
5183
- }
5184
- }
5185
- function setLayerEffects(layers, id, effects) {
5186
- const layer = requireEffectLayer(layers, id, "change its effects");
5187
- const next = { ...layer };
5188
- if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) {
5189
- guard(() => validateLayerEffects(effects));
5190
- next.effects = effects;
5191
- } else delete next.effects;
5192
- return mapLayer(layers, id, () => next);
5193
- }
5194
- function addLayerEffect(layers, id, kind, params = {}, index) {
5195
- if (!isLayerEffectKind(kind)) fail(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
5196
- const layer = requireEffectLayer(layers, id, "add effects");
5197
- const current = layerEffectsOf(layer);
5198
- const ids = new Set(current.items.map((e) => e.id));
5199
- let effectId = newEffectId();
5200
- while (ids.has(effectId)) effectId = newEffectId();
5201
- const { id: _ignored, kind: _kind, ...rest } = params;
5202
- const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
5203
- const items = [...current.items];
5204
- const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
5205
- items.splice(at, 0, effect);
5206
- return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
5207
- }
5208
- function requireEffect(layer, effectId) {
5209
- const items = layer.effects?.items ?? [];
5210
- const i = items.findIndex((e) => e.id === effectId);
5211
- if (i === -1) fail(`Layer "${layer.name || layer.id}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
5212
- return i;
5213
- }
5214
- function updateLayerEffect(layers, id, effectId, changes) {
5215
- const layer = requireEffectLayer(layers, id, "change its effects");
5216
- const i = requireEffect(layer, effectId);
5217
- const current = layer.effects;
5218
- const items = [...current.items];
5219
- items[i] = guard(() => mergeLayerEffect(items[i], changes));
5220
- return setLayerEffects(layers, id, { ...current, items });
5221
- }
5222
- function removeLayerEffect(layers, id, effectId) {
5223
- const layer = requireEffectLayer(layers, id, "change its effects");
5224
- requireEffect(layer, effectId);
5225
- const current = layer.effects;
5226
- return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
5227
- }
5228
- function setLayerEffectsEnabled(layers, id, enabled, scale) {
5229
- const layer = requireEffectLayer(layers, id, "change its effects");
5230
- if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
5231
- const current = layerEffectsOf(layer);
5232
- const next = { ...current, enabled };
5233
- if (scale !== void 0) {
5234
- if (scale === 1) delete next.scale;
5235
- else next.scale = scale;
6178
+ ctx.problems.push(`${what}: its placement is invalid (${e instanceof Error ? e.message : String(e)}); it was placed at the top left`);
6179
+ placement = { corners: [{ x: 0, y: 0 }, { x: contents.width, y: 0 }, { x: contents.width, y: contents.height }, { x: 0, y: contents.height }] };
6180
+ }
6181
+ const layer = { ...base, type: "smart", contents, placement, surface: emptySurface(4), offset: { x: 0, y: 0 } };
6182
+ const filters = readSmartFilters(r.smartFilters, ctx, what);
6183
+ if (filters) layer.smartFilters = filters;
6184
+ if (typeof r.fillOpacity === "number" && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);
6185
+ if (r.psd && typeof r.psd.key === "string" && typeof r.psd.data === "string") {
6186
+ try {
6187
+ layer.psd = { key: r.psd.key, data: base64ToBytes(r.psd.data) };
6188
+ if (typeof r.psd.placed === "string") layer.psd.placed = r.psd.placed;
6189
+ } catch {
6190
+ }
5236
6191
  }
5237
- return setLayerEffects(layers, id, next);
5238
- }
5239
- function parseBlend(value, allowPassThrough = false) {
5240
- if (value === void 0) return void 0;
5241
- if (value === "pass-through" && allowPassThrough) return value;
5242
- if (!isBlendMode(value)) fail(`Unknown blend mode "${String(value)}".`);
5243
- return value;
6192
+ if (typeof r.asset === "string") layer.surface = readSurface(r.asset, r.x, r.y, 4, 0, ctx, what);
6193
+ else if (contents.raster || contents.document) layer.surface = renderSmartObject(layer, ctx.canvas ?? { width: 1, height: 1 });
6194
+ return layer;
5244
6195
  }
5245
- function unionRect(a, b) {
5246
- if (!a) return b;
5247
- if (!b) return a;
5248
- const x = Math.min(a.x, b.x);
5249
- const y = Math.min(a.y, b.y);
5250
- return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
6196
+ function deserializeLayers(raw, ctx) {
6197
+ if (!Array.isArray(raw)) return [];
6198
+ const seen = /* @__PURE__ */ new Set();
6199
+ const dedupe = (list2) => list2.map((l) => {
6200
+ let next = l;
6201
+ if (seen.has(l.id)) next = { ...l, id: newLayerId() };
6202
+ seen.add(next.id);
6203
+ if (next.type === "group") next = { ...next, children: dedupe(next.children) };
6204
+ return next;
6205
+ });
6206
+ return dedupe(raw.map((l) => deserializeLayer(l, ctx)).filter(Boolean));
5251
6207
  }
5252
6208
 
5253
6209
  // src/image-editor/psd/binary.ts
@@ -8165,202 +9121,44 @@ function exportPsd(input) {
8165
9121
  { id: 1057, name: "", data: version.result() }
8166
9122
  ];
8167
9123
  if (input.globalLight) {
8168
- const i32 = (v) => {
8169
- const w = new PsdWriter();
8170
- w.i32(Math.round(v));
8171
- return w.result();
8172
- };
8173
- resources.push({ id: 1037, name: "", data: i32(input.globalLight.angle) }, { id: 1049, name: "", data: i32(input.globalLight.altitude) });
8174
- }
8175
- const globalBlocks = [];
8176
- if (patternIds2.size) {
8177
- const byId = new Map((input.patterns ?? []).map((p) => [p.id, p]));
8178
- const embed = [];
8179
- for (const id of patternIds2) {
8180
- const p = byId.get(id) ?? builtinPattern(id);
8181
- if (p) embed.push(p);
8182
- else note(`Pattern "${id}" is not in the document and was not embedded`, id);
8183
- }
8184
- if (embed.length) globalBlocks.push({ key: "Patt", data: writePatternBlock(embed) });
8185
- }
8186
- const bytes = writePsd(
8187
- {
8188
- header: { psb, channels: planes.length, width, height, depth: 8, colorMode: 3 },
8189
- colorModeData: new Uint8Array(0),
8190
- resources,
8191
- layers: records,
8192
- mergedAlpha: transparent && records.length > 0,
8193
- globalBlocks,
8194
- imageCompression: 1,
8195
- imageData: encodeRle(merged, { planes: planes.length, width, height, depth: 8, psb })
8196
- },
8197
- encodeLegacyName
8198
- );
8199
- const warnings = [...notes].map(([msg, names]) => `${msg} (${names.map((n2) => `"${n2}"`).join(", ")}).`);
8200
- return { bytes, psb, records: records.length, warnings };
8201
- }
8202
-
8203
- // src/image-editor/transform.ts
8204
- var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
8205
- var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
8206
- var ANCHOR_FRACTION = {
8207
- "top-left": [0, 0],
8208
- top: [0.5, 0],
8209
- "top-right": [1, 0],
8210
- left: [0, 0.5],
8211
- center: [0.5, 0.5],
8212
- right: [1, 0.5],
8213
- "bottom-left": [0, 1],
8214
- bottom: [0.5, 1],
8215
- "bottom-right": [1, 1]
8216
- };
8217
- function isTransformAnchor(value) {
8218
- return typeof value === "string" && value in ANCHOR_FRACTION;
8219
- }
8220
- function identityTransform(box) {
8221
- return { box: { ...box }, corners: boxQuad(box) };
8222
- }
8223
- function cornersFromMatrix(box, m) {
8224
- const q2 = boxQuad(box);
8225
- return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
8226
- }
8227
- function matrixFromCorners(box, corners, eps = 1e-6) {
8228
- const [tl, tr, br, bl] = corners;
8229
- const size = 1 + Math.max(box.width, box.height);
8230
- if (Math.abs(tl.x + br.x - tr.x - bl.x) > eps * size || Math.abs(tl.y + br.y - tr.y - bl.y) > eps * size) return null;
8231
- const h = rectToQuad(box, corners);
8232
- return h ? affineFromHomography(h, 1e-9) : null;
8233
- }
8234
- function transformHomography(geometry) {
8235
- const h = rectToQuad(geometry.box, geometry.corners);
8236
- if (!h) return null;
8237
- return homographyKeepsQuad(h, boxQuad(geometry.box)) ? h : null;
8238
- }
8239
- function hasWarp(geometry) {
8240
- const w = geometry.warp;
8241
- if (!w) return false;
8242
- const id = identityWarpMesh(geometry.box, w.us.length - 1, w.vs.length - 1);
8243
- if (id.points.length !== w.points.length || id.us.some((u, i) => Math.abs(u - w.us[i]) > 1e-12) || id.vs.some((v, i) => Math.abs(v - w.vs[i]) > 1e-12)) return true;
8244
- return w.points.some((p, i) => Math.abs(p.x - id.points[i].x) > 1e-6 || Math.abs(p.y - id.points[i].y) > 1e-6);
8245
- }
8246
- function isIdentityTransform(geometry) {
8247
- if (hasWarp(geometry)) return false;
8248
- const q2 = boxQuad(geometry.box);
8249
- return geometry.corners.every((c, i) => Math.abs(c.x - q2[i].x) < 1e-6 && Math.abs(c.y - q2[i].y) < 1e-6);
8250
- }
8251
- function transformMap(geometry) {
8252
- const h = transformHomography(geometry);
8253
- if (!h) return null;
8254
- const { box } = geometry;
8255
- const warp = hasWarp(geometry) ? geometry.warp : null;
8256
- if (!warp) return (p) => applyHomography(h, p);
8257
- return (p) => applyHomography(h, evalWarpMesh(warp, (p.x - box.x) / box.width, (p.y - box.y) / box.height));
8258
- }
8259
- function transformedPoint(geometry, fx, fy) {
8260
- const f = transformMap(geometry);
8261
- const { box } = geometry;
8262
- return f ? f({ x: box.x + fx * box.width, y: box.y + fy * box.height }) : null;
8263
- }
8264
- function anchorPoint(geometry, anchor) {
8265
- const [fx, fy] = ANCHOR_FRACTION[anchor];
8266
- const h = rectToQuad(geometry.box, geometry.corners);
8267
- const p = { x: geometry.box.x + fx * geometry.box.width, y: geometry.box.y + fy * geometry.box.height };
8268
- return (h && applyHomography(h, p)) ?? p;
8269
- }
8270
- function transformedBox(geometry) {
8271
- const f = transformMap(geometry);
8272
- if (!f) return null;
8273
- const { box } = geometry;
8274
- const pts = [];
8275
- const n = hasWarp(geometry) ? 16 : 1;
8276
- for (let i = 0; i <= n; i++) {
8277
- const t = i / n;
8278
- for (const [fx, fy] of [[t, 0], [1, t], [1 - t, 1], [0, 1 - t]]) {
8279
- const p = f({ x: box.x + fx * box.width, y: box.y + fy * box.height });
8280
- if (!p) return null;
8281
- pts.push(p);
8282
- }
9124
+ const i32 = (v) => {
9125
+ const w = new PsdWriter();
9126
+ w.i32(Math.round(v));
9127
+ return w.result();
9128
+ };
9129
+ resources.push({ id: 1037, name: "", data: i32(input.globalLight.angle) }, { id: 1049, name: "", data: i32(input.globalLight.altitude) });
8283
9130
  }
8284
- const xs = pts.map((p) => p.x);
8285
- const ys = pts.map((p) => p.y);
8286
- const x = Math.min(...xs);
8287
- const y = Math.min(...ys);
8288
- return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
8289
- }
8290
- function referencePoint(box, reference) {
8291
- if (reference && typeof reference === "object") return { x: reference.x, y: reference.y };
8292
- const [fx, fy] = ANCHOR_FRACTION[reference ?? "center"];
8293
- return { x: box.x + fx * box.width, y: box.y + fy * box.height };
8294
- }
8295
- function matrixFromParams(box, params) {
8296
- const sx = (params.scaleX ?? 100) / 100;
8297
- const sy = (params.scaleY ?? 100) / 100;
8298
- const tx = Math.tan((params.skewX ?? 0) * Math.PI / 180);
8299
- const ty = Math.tan((params.skewY ?? 0) * Math.PI / 180);
8300
- const ref = referencePoint(box, params.reference);
8301
- const linear = multiplyMatrices(multiplyMatrices([1, ty, tx, 1, 0, 0], [sx, 0, 0, sy, 0, 0]), rotateMatrix(params.rotate ?? 0));
8302
- return multiplyMatrices(matrixAbout(linear, ref), translateMatrix(params.dx ?? 0, params.dy ?? 0));
8303
- }
8304
- function describeTransform(geometry, reference = "center") {
8305
- const m = hasWarp(geometry) ? null : matrixFromCorners(geometry.box, geometry.corners);
8306
- const ref = referencePoint(geometry.box, reference);
8307
- const bounds = transformedBox(geometry);
8308
- if (!m) {
8309
- const h = rectToQuad(geometry.box, geometry.corners);
8310
- const at2 = (h && applyHomography(h, ref)) ?? ref;
8311
- return { affine: false, scaleX: 100, scaleY: 100, rotate: 0, skewX: 0, x: at2.x, y: at2.y, bounds, corners: geometry.corners };
9131
+ const globalBlocks = [];
9132
+ if (patternIds2.size) {
9133
+ const byId = new Map((input.patterns ?? []).map((p) => [p.id, p]));
9134
+ const embed = [];
9135
+ for (const id of patternIds2) {
9136
+ const p = byId.get(id) ?? builtinPattern(id);
9137
+ if (p) embed.push(p);
9138
+ else note(`Pattern "${id}" is not in the document and was not embedded`, id);
9139
+ }
9140
+ if (embed.length) globalBlocks.push({ key: "Patt", data: writePatternBlock(embed) });
8312
9141
  }
8313
- const [a, b, c, d] = m;
8314
- const sxAbs = Math.hypot(a, b);
8315
- const rot = Math.atan2(b, a) * 180 / Math.PI;
8316
- const shear = sxAbs ? (a * c + b * d) / sxAbs : 0;
8317
- const sy = sxAbs ? (a * d - b * c) / sxAbs : 0;
8318
- const skew = sxAbs ? Math.atan(shear / sxAbs) * 180 / Math.PI : 0;
8319
- const at = applyMatrix(m, ref);
8320
- const clean = (v) => Math.abs(v) < 1e-9 ? 0 : v;
8321
- return {
8322
- affine: true,
8323
- scaleX: clean(sxAbs * 100),
8324
- scaleY: clean(sy * 100),
8325
- rotate: clean(rot === -180 ? 180 : rot),
8326
- skewX: clean(skew),
8327
- x: at.x,
8328
- y: at.y,
8329
- bounds,
8330
- corners: geometry.corners
8331
- };
8332
- }
8333
- function applyParams(geometry, params) {
8334
- const ref = params.reference && typeof params.reference === "object" ? params.reference : anchorPoint(geometry, params.reference ?? "center");
8335
- const m = matrixFromParams({ x: ref.x, y: ref.y, width: 0, height: 0 }, { ...params, reference: ref });
8336
- const c = geometry.corners;
8337
- return [applyMatrix(m, c[0]), applyMatrix(m, c[1]), applyMatrix(m, c[2]), applyMatrix(m, c[3])];
8338
- }
8339
- function flipCorners(geometry, axis) {
8340
- const c = anchorPoint(geometry, "center");
8341
- const m = axis === "horizontal" ? [-1, 0, 0, 1, 2 * c.x, 0] : [1, 0, 0, -1, 0, 2 * c.y];
8342
- const q2 = geometry.corners;
8343
- return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
8344
- }
8345
- function boxPointOf(geometry, p) {
8346
- const m = matrixFromCorners(geometry.box, geometry.corners);
8347
- if (m) return applyMatrix(invertMatrix(m), p);
8348
- return null;
8349
- }
8350
- function presetWarp(box, preset) {
8351
- return warpPresetMesh(preset, box);
8352
- }
8353
- function isValidWarp(mesh) {
8354
- return isValidWarpMesh(mesh);
8355
- }
8356
- function geometryHomography(geometry) {
8357
- const m = matrixFromCorners(geometry.box, geometry.corners);
8358
- return m ? homographyFromAffine(m) : transformHomography(geometry);
9142
+ const bytes = writePsd(
9143
+ {
9144
+ header: { psb, channels: planes.length, width, height, depth: 8, colorMode: 3 },
9145
+ colorModeData: new Uint8Array(0),
9146
+ resources,
9147
+ layers: records,
9148
+ mergedAlpha: transparent && records.length > 0,
9149
+ globalBlocks,
9150
+ imageCompression: 1,
9151
+ imageData: encodeRle(merged, { planes: planes.length, width, height, depth: 8, psb })
9152
+ },
9153
+ encodeLegacyName
9154
+ );
9155
+ const warnings = [...notes].map(([msg, names]) => `${msg} (${names.map((n2) => `"${n2}"`).join(", ")}).`);
9156
+ return { bytes, psb, records: records.length, warnings };
8359
9157
  }
8360
9158
 
8361
9159
  // src/image-editor/free-transform.ts
8362
9160
  var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
8363
- var fail2 = (message) => {
9161
+ var fail3 = (message) => {
8364
9162
  throw new LayerOperationError(message);
8365
9163
  };
8366
9164
  var T = (x, y) => [1, 0, x, 0, 1, y, 0, 0, 1];
@@ -8419,17 +9217,20 @@ function liftMask(mask, sel, o) {
8419
9217
  var FreeTransformSession = class {
8420
9218
  constructor(host, init = {}) {
8421
9219
  this.host = host;
8422
- if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail2(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
9220
+ if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail3(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
8423
9221
  this.interp = init.interpolation ?? "bicubic";
8424
9222
  const sel = host.selection();
8425
9223
  const selected = Boolean(sel?.bounds);
8426
9224
  let content = init.content ?? "auto";
8427
9225
  if (!["auto", "layer", "selection", "selection-copy", "selection-outline"].includes(content)) {
8428
- fail2(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
9226
+ fail3(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
8429
9227
  }
8430
- if (content === "auto") content = selected ? "selection" : "layer";
9228
+ const targetId = init.layerId ?? host.activeLayerId();
9229
+ const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
9230
+ const smartTarget = target?.type === "smart" && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
9231
+ if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
8431
9232
  this.content = content;
8432
- if (content !== "layer" && !selected) fail2(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
9233
+ if (content !== "layer" && !selected) fail3(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
8433
9234
  const layers = host.layers();
8434
9235
  this.startLayers = layers;
8435
9236
  this.startSelection = sel;
@@ -8444,25 +9245,39 @@ var FreeTransformSession = class {
8444
9245
  box = { ...sel.bounds };
8445
9246
  } else {
8446
9247
  const id = init.layerId ?? host.activeLayerId();
8447
- if (!id) fail2("No layer given and none is active; pass layerId (see list_layers).");
9248
+ if (!id) fail3("No layer given and none is active; pass layerId (see list_layers).");
8448
9249
  const layer = requireLayer(layers, id).layer;
8449
9250
  this.layerId = layer.id;
8450
9251
  const mask = layer.mask && !layer.mask.source ? layer.mask : void 0;
8451
9252
  const channel = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : "pixels");
8452
- if (channel !== "pixels" && channel !== "mask") fail2(`Unknown channel "${String(channel)}"; use pixels or mask.`);
9253
+ if (channel !== "pixels" && channel !== "mask") fail3(`Unknown channel "${String(channel)}"; use pixels or mask.`);
8453
9254
  const linked = mask?.linked !== false;
8454
9255
  const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
8455
- if (channel === "mask" && !mask) fail2(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.`);
8456
- if (!maskAlone && layer.type !== "pixel") fail2(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel layers, layer masks (channel mask) and selections.`);
8457
- if (!layer.visible && content !== "selection-copy") fail2(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
9256
+ if (channel === "mask" && !mask) fail3(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.`);
9257
+ if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart") fail3(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel layers, smart objects, layer masks (channel mask) and selections.`);
9258
+ if (!layer.visible && content !== "selection-copy") fail3(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
8458
9259
  if (content !== "selection-copy") {
8459
9260
  requireUnlocked(layer, "position", "transform it");
8460
9261
  if (!maskAlone) requireUnlocked(layer, "pixels", "transform it");
8461
9262
  }
8462
9263
  const o = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
8463
9264
  const pixels = layer.type === "pixel" ? host.pixelsOf(layer) : null;
9265
+ if (layer.type === "smart" && !maskAlone) {
9266
+ if (content !== "layer") fail3(`Layer "${layer.name || id}" is a smart object, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
9267
+ if (!isContentsDecoded(layer.contents)) fail3(`The contents of "${layer.name || id}" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);
9268
+ this.smart = layer;
9269
+ this.smartObject = true;
9270
+ this.startGeometry = placementGeometry(layer);
9271
+ if (init.interpolation === void 0 && layer.placement.interpolation) this.interp = layer.placement.interpolation;
9272
+ this.startInterp = this.interp;
9273
+ this.box = contentsBox(layer.contents);
9274
+ this.moves = mask && linked ? ["pixels", "mask"] : ["pixels"];
9275
+ this.geometryValue = this.startGeometry;
9276
+ this.shown = this.startLayers;
9277
+ return;
9278
+ }
8464
9279
  if (maskAlone) {
8465
- if (content === "selection-copy") fail2("Layer via Copy copies pixels; transform a mask with content layer or selection.");
9280
+ if (content === "selection-copy") fail3("Layer via Copy copies pixels; transform a mask with content layer or selection.");
8466
9281
  if (content === "selection") {
8467
9282
  const { lifted, rest } = liftMask(mask.surface, sel, o);
8468
9283
  this.parts.push({ what: "mask", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
@@ -8517,11 +9332,13 @@ var FreeTransformSession = class {
8517
9332
  moves.push("selection");
8518
9333
  }
8519
9334
  }
8520
- if (!box && moves[0] === "mask") fail2("The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).");
8521
- if (!box) fail2(content === "layer" ? "The layer has no pixels to transform." : "The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).");
9335
+ if (!box && moves[0] === "mask") fail3("The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).");
9336
+ if (!box) fail3(content === "layer" ? "The layer has no pixels to transform." : "The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).");
8522
9337
  this.box = box;
8523
9338
  this.moves = moves;
8524
- this.geometryValue = identityTransform(box);
9339
+ this.startGeometry = identityTransform(box);
9340
+ this.startInterp = this.interp;
9341
+ this.geometryValue = this.startGeometry;
8525
9342
  this.shown = this.startLayers;
8526
9343
  if (this.copyId) host.show(this.startLayers, sel, this.copyId);
8527
9344
  this.shown = host.layers();
@@ -8536,7 +9353,19 @@ var FreeTransformSession = class {
8536
9353
  box;
8537
9354
  /** What moves. */
8538
9355
  moves;
9356
+ /**
9357
+ * The geometry the transform starts from: the identity over `box`, or for a smart object its
9358
+ * placement (`box` is then the contents' rectangle in contents pixels, and the transform edits
9359
+ * the placement without resampling anything twice).
9360
+ */
9361
+ startGeometry;
9362
+ /** True when the layer is a smart object (the transform sets its placement). */
9363
+ smartObject = false;
8539
9364
  geometryValue;
9365
+ smart = null;
9366
+ smartReduced = /* @__PURE__ */ new Map();
9367
+ startInterp;
9368
+ smartMasksMoved = false;
8540
9369
  interp;
8541
9370
  parts = [];
8542
9371
  before;
@@ -8557,10 +9386,10 @@ var FreeTransformSession = class {
8557
9386
  return !this.closed;
8558
9387
  }
8559
9388
  check() {
8560
- if (this.closed) fail2("This transform has already been committed or cancelled; begin a new one.");
9389
+ if (this.closed) fail3("This transform has already been committed or cancelled; begin a new one.");
8561
9390
  if (this.host.layers() !== this.shown) {
8562
9391
  this.closed = true;
8563
- fail2("The document changed while the transform was open, so it was abandoned; begin a new one.");
9392
+ fail3("The document changed while the transform was open, so it was abandoned; begin a new one.");
8564
9393
  }
8565
9394
  }
8566
9395
  /**
@@ -8570,7 +9399,7 @@ var FreeTransformSession = class {
8570
9399
  update(changes, preview = {}) {
8571
9400
  this.check();
8572
9401
  if (changes.interpolation !== void 0) {
8573
- if (!isInterpolation(changes.interpolation)) fail2(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
9402
+ if (!isInterpolation(changes.interpolation)) fail3(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
8574
9403
  this.interp = changes.interpolation;
8575
9404
  }
8576
9405
  const next = {
@@ -8578,14 +9407,15 @@ var FreeTransformSession = class {
8578
9407
  corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })),
8579
9408
  warp: changes.warp === void 0 ? this.geometryValue.warp ?? null : changes.warp
8580
9409
  };
8581
- if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail2("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
8582
- if (!transformMap(next)) fail2("These corners fold the frame over itself or through the horizon; keep the quad convex.");
9410
+ if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail3("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
9411
+ if (!transformMap(next)) fail3("These corners fold the frame over itself or through the horizon; keep the quad convex.");
8583
9412
  this.geometryValue = next;
8584
9413
  if (preview !== false) this.render(preview);
8585
9414
  return next;
8586
9415
  }
8587
9416
  /** Resamples every part for the current geometry and shows it (or returns it, for commit). */
8588
9417
  resample(final, preview = {}) {
9418
+ if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };
8589
9419
  const g = this.geometryValue;
8590
9420
  const identity = isIdentityTransform(g);
8591
9421
  const warp = hasWarp(g);
@@ -8625,7 +9455,7 @@ var FreeTransformSession = class {
8625
9455
  moved = transformSurface(src, hl, opts);
8626
9456
  }
8627
9457
  } catch (e) {
8628
- if (e instanceof TransformTooLargeError) fail2(e.message);
9458
+ if (e instanceof TransformTooLargeError) fail3(e.message);
8629
9459
  throw e;
8630
9460
  }
8631
9461
  }
@@ -8647,6 +9477,35 @@ var FreeTransformSession = class {
8647
9477
  }
8648
9478
  return { layers, selection };
8649
9479
  }
9480
+ /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */
9481
+ resampleSmart(final, preview) {
9482
+ const layer = this.smart;
9483
+ const g = this.geometryValue;
9484
+ const placement = { corners: g.corners, warp: g.warp ?? null, interpolation: this.interp };
9485
+ const canvas = this.host.canvas();
9486
+ try {
9487
+ if (final) {
9488
+ const res = setSmartPlacement(this.startLayers, layer.id, placement, canvas);
9489
+ this.smartMasksMoved = res.masksMoved;
9490
+ return res.layers;
9491
+ }
9492
+ const step = previewStep(preview.scale);
9493
+ const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });
9494
+ return mapLayer(this.startLayers, layer.id, (l) => ({ ...l, placement, surface }));
9495
+ } catch (e) {
9496
+ if (e instanceof TransformTooLargeError) fail3(e.message);
9497
+ throw e;
9498
+ }
9499
+ }
9500
+ /** Whether the geometry (or the interpolation of a smart object) differs from where it started. */
9501
+ isChanged() {
9502
+ const g = this.geometryValue;
9503
+ if (!this.smart) return !isIdentityTransform(g);
9504
+ const a = this.startGeometry;
9505
+ const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);
9506
+ const warp = (w, box) => w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : "";
9507
+ return !same || warp(g.warp, g.box) !== warp(a.warp, a.box) || this.interp !== this.startInterp;
9508
+ }
8650
9509
  render(preview) {
8651
9510
  const { layers, selection } = this.resample(false, preview);
8652
9511
  this.host.show(layers, selection, this.copyId ?? this.startActive);
@@ -8655,8 +9514,7 @@ var FreeTransformSession = class {
8655
9514
  /** Finishes the transform with full-quality resampling, as one undo step. */
8656
9515
  commit() {
8657
9516
  this.check();
8658
- const g = this.geometryValue;
8659
- const changed = !isIdentityTransform(g) || Boolean(this.copyId);
9517
+ const changed = this.isChanged() || Boolean(this.copyId);
8660
9518
  if (!changed) {
8661
9519
  this.closed = true;
8662
9520
  this.host.restore(this.before);
@@ -8679,12 +9537,13 @@ var FreeTransformSession = class {
8679
9537
  const id = this.copyId ?? this.layerId;
8680
9538
  const layer = id ? findLayer(layers, id)?.layer : void 0;
8681
9539
  if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
9540
+ else if (this.moves.includes("pixels") && layer?.type === "smart") bounds = contentBounds(layer.surface);
8682
9541
  else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
8683
9542
  else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
8684
9543
  return {
8685
9544
  layerId: this.copyId ?? this.layerId,
8686
9545
  content: this.content,
8687
- moved: [...this.moves],
9546
+ moved: this.smart ? this.moves.filter((m) => m !== "mask" || this.smartMasksMoved) : [...this.moves],
8688
9547
  box: { ...this.box },
8689
9548
  corners: this.geometryValue.corners,
8690
9549
  bounds,
@@ -8694,6 +9553,39 @@ var FreeTransformSession = class {
8694
9553
  };
8695
9554
 
8696
9555
  // src/image-editor/controller.ts
9556
+ function prepareNestedForPsd(layers) {
9557
+ const out = [];
9558
+ for (const l of layers) {
9559
+ if (l.type === "annotation") continue;
9560
+ let next = l;
9561
+ if (l.type === "group") next = { ...l, children: prepareNestedForPsd(l.children) };
9562
+ else if (l.type === "smart") next = rasterizeSmartObject([l], l.id).layer;
9563
+ else if (l.type === "pixel" && l.filters?.length) {
9564
+ const p = { ...l };
9565
+ if (l.filters.some(isActiveFilter)) {
9566
+ const b = l.surface.bounds;
9567
+ if (b) {
9568
+ const r = { x: b.x + l.offset.x - 64, y: b.y + l.offset.y - 64, width: b.width + 128, height: b.height + 128 };
9569
+ const plain3 = { ...l, visible: true, opacity: 1, blend: "normal" };
9570
+ delete plain3.mask;
9571
+ delete plain3.clipped;
9572
+ delete plain3.effects;
9573
+ delete plain3.fillOpacity;
9574
+ const data = new LayerCompositor().composite([plain3], r, { width: r.x + r.width, height: r.y + r.height });
9575
+ p.surface = surfaceFromPixels({ width: r.width, height: r.height, data }, 4, r.x - l.offset.x, r.y - l.offset.y);
9576
+ }
9577
+ }
9578
+ delete p.filters;
9579
+ next = p;
9580
+ }
9581
+ if (next.mask?.source) {
9582
+ next = { ...next };
9583
+ delete next.mask;
9584
+ }
9585
+ out.push(next);
9586
+ }
9587
+ return out;
9588
+ }
8697
9589
  var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
8698
9590
  function paintCoverageInto(base, coverage, apply, rects) {
8699
9591
  const tiles = new Map(base.tiles);
@@ -8994,6 +9886,7 @@ var ImageEditorController = class {
8994
9886
  * for undoable source replacement.
8995
9887
  */
8996
9888
  async loadImage(src) {
9889
+ this.abandonContents();
8997
9890
  if (typeof src === "string") this.state.sourceUrl = src;
8998
9891
  const source = await this.resolveSource(src);
8999
9892
  const prev = this.state.imageDimensions;
@@ -9283,7 +10176,7 @@ var ImageEditorController = class {
9283
10176
  setActiveLayer(id, target = "pixels") {
9284
10177
  if (id !== null && !this.getLayer(id)) return false;
9285
10178
  const layer = id ? this.getLayer(id) : void 0;
9286
- const t = target === "mask" && layer?.mask ? "mask" : "pixels";
10179
+ const t = target === "mask" && layer?.mask ? "mask" : target === "filter-mask" && layer?.type === "smart" && layer.smartFilters?.mask ? "filter-mask" : "pixels";
9287
10180
  if (this.state.activeLayerId === id && this.state.activeTarget === t) return true;
9288
10181
  this.state.activeLayerId = id;
9289
10182
  this.state.activeTarget = t;
@@ -9296,7 +10189,7 @@ var ImageEditorController = class {
9296
10189
  */
9297
10190
  getLayerPixels(id) {
9298
10191
  const layer = this.getLayer(id);
9299
- if (!layer || layer.type !== "pixel") return null;
10192
+ if (!layer || layer.type !== "pixel" && layer.type !== "smart") return null;
9300
10193
  const surface = this.surfaceOf(layer);
9301
10194
  const b = contentBounds(surface);
9302
10195
  if (!b) return null;
@@ -9309,7 +10202,7 @@ var ImageEditorController = class {
9309
10202
  */
9310
10203
  getLayerBounds(id) {
9311
10204
  const layer = this.getLayer(id);
9312
- if (!layer || layer.type !== "pixel") return null;
10205
+ if (!layer || layer.type !== "pixel" && layer.type !== "smart") return null;
9313
10206
  const surface = this.surfaceOf(layer);
9314
10207
  let b = this.boundsCache.get(surface);
9315
10208
  if (b === void 0) {
@@ -9358,9 +10251,9 @@ var ImageEditorController = class {
9358
10251
  }
9359
10252
  return String(id);
9360
10253
  }
9361
- /** The pixels a pixel layer composites (the decoded source for the Background). */
10254
+ /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */
9362
10255
  surfaceOf(layer) {
9363
- if (layer.source !== "document") return layer.surface;
10256
+ if (layer.type === "smart" || layer.source !== "document") return layer.surface;
9364
10257
  return this.sourceSurface() ?? layer.surface;
9365
10258
  }
9366
10259
  /**
@@ -9604,6 +10497,15 @@ var ImageEditorController = class {
9604
10497
  */
9605
10498
  offsetLayer(id, move, commitToHistory = true) {
9606
10499
  const layer = requireLayer(this.state.layers, id).layer;
10500
+ if (layer.type === "smart") {
10501
+ const tl = layer.placement.corners[0];
10502
+ const dx = move.x !== void 0 ? move.x - tl.x : move.dx ?? 0;
10503
+ const dy = move.y !== void 0 ? move.y - tl.y : move.dy ?? 0;
10504
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError("x and y must be numbers.");
10505
+ this.commitLayers(offsetSmartObject(this.state.layers, id, dx, dy, this.canvasSize), void 0, commitToHistory);
10506
+ const moved = findLayer(this.state.layers, id).layer;
10507
+ return { offset: { x: Math.round(moved.placement.corners[0].x), y: Math.round(moved.placement.corners[0].y) }, bounds: this.getLayerBounds(id) };
10508
+ }
9607
10509
  const current = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
9608
10510
  const x = move.x ?? current.x + (move.dx ?? 0);
9609
10511
  const y = move.y ?? current.y + (move.dy ?? 0);
@@ -9810,78 +10712,476 @@ var ImageEditorController = class {
9810
10712
  const [a, b, c, d] = spec.matrix;
9811
10713
  if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError("matrix is singular (it collapses the layer to a line); its determinant a\xB7d \u2212 b\xB7c must not be 0.");
9812
10714
  }
9813
- const session = this.beginFreeTransform(spec);
9814
- try {
9815
- const g = identityTransform(session.box);
9816
- const corners = spec.matrix ? cornersFromMatrix(session.box, spec.matrix) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;
9817
- let warp = null;
9818
- if (spec.warp) {
9819
- if ("style" in spec.warp) {
9820
- if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
9821
- warp = presetWarp(session.box, spec.warp);
9822
- } else {
9823
- if (!isValidWarp(spec.warp)) throw new LayerOperationError("mesh must have us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points, row by row.");
9824
- warp = spec.warp;
9825
- }
10715
+ const session = this.beginFreeTransform(spec);
10716
+ try {
10717
+ const g = session.startGeometry;
10718
+ const corners = spec.matrix ? g.corners.map((p) => applyMatrix(spec.matrix, p)) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;
10719
+ let warp = spec.warp === void 0 ? g.warp ?? null : null;
10720
+ if (spec.warp) {
10721
+ if ("style" in spec.warp) {
10722
+ if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
10723
+ warp = presetWarp(session.box, spec.warp);
10724
+ } else {
10725
+ if (!isValidWarp(spec.warp)) throw new LayerOperationError("mesh must have us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points, row by row.");
10726
+ warp = spec.warp;
10727
+ }
10728
+ }
10729
+ session.update({ corners, warp }, false);
10730
+ return session.commit();
10731
+ } catch (e) {
10732
+ session.cancel();
10733
+ throw e;
10734
+ }
10735
+ }
10736
+ /** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */
10737
+ flipLayer(axis, init = {}) {
10738
+ if (axis !== "horizontal" && axis !== "vertical") throw new LayerOperationError(`axis must be horizontal or vertical, got "${String(axis)}".`);
10739
+ const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
10740
+ try {
10741
+ session.update({ corners: flipCorners(session.startGeometry, axis) }, false);
10742
+ return session.commit();
10743
+ } catch (e) {
10744
+ session.cancel();
10745
+ throw e;
10746
+ }
10747
+ }
10748
+ /**
10749
+ * Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's
10750
+ * centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).
10751
+ */
10752
+ rotateLayer90(turn, init = {}) {
10753
+ if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
10754
+ const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
10755
+ try {
10756
+ const b = session.smartObject ? transformedBox(session.startGeometry) ?? session.box : session.box;
10757
+ const cx = b.x + b.width / 2;
10758
+ const cy = b.y + b.height / 2;
10759
+ const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
10760
+ const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);
10761
+ const m = turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];
10762
+ session.update({ corners: session.startGeometry.corners.map((p) => applyMatrix(m, p)) }, false);
10763
+ return session.commit();
10764
+ } catch (e) {
10765
+ session.cancel();
10766
+ throw e;
10767
+ }
10768
+ }
10769
+ /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */
10770
+ addLayerFilter(id, filter) {
10771
+ const f = normalizeFilter(filter);
10772
+ const next = addLayerFilter(this.state.layers, id, f);
10773
+ this.commitLayers(next);
10774
+ return findLayer(next, id).layer.filters ?? [];
10775
+ }
10776
+ /** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */
10777
+ updateLayerFilter(id, filterId, changes, commitToHistory = true) {
10778
+ const next = updateLayerFilter(this.state.layers, id, filterId, changes);
10779
+ this.commitLayers(next, void 0, commitToHistory);
10780
+ return findLayer(next, id).layer.filters ?? [];
10781
+ }
10782
+ /** Removes one filter (one undo step). Returns the remaining filters. */
10783
+ removeLayerFilter(id, filterId) {
10784
+ const next = removeLayerFilter(this.state.layers, id, filterId);
10785
+ this.commitLayers(next);
10786
+ return findLayer(next, id).layer.filters ?? [];
10787
+ }
10788
+ // --- Smart objects ---
10789
+ get canvasSize() {
10790
+ return this.naturalSize;
10791
+ }
10792
+ /**
10793
+ * Layer › Smart Objects › Convert to Smart Object (one undo step): sibling layers (default the
10794
+ * active one) become one smart object at the topmost one's position. A single pixel layer keeps
10795
+ * its id and properties and its filters become smart filters (it renders exactly as before);
10796
+ * anything else nests as a layer document. Returns the smart object's id.
10797
+ */
10798
+ convertToSmartObject(ids, options = {}) {
10799
+ const list2 = ids?.length ? ids : this.state.activeLayerId ? [this.state.activeLayerId] : [];
10800
+ if (!list2.length) throw new LayerOperationError("No layers given and none is active; pass layerIds (see list_layers).");
10801
+ const res = convertToSmartObject(this.state.layers, list2, this.canvasSize, { surfaceOf: (l) => this.surfaceOf(l), name: options.name });
10802
+ this.commitLayers(res.layers, () => {
10803
+ this.state.activeLayerId = res.layer.id;
10804
+ this.state.activeTarget = "pixels";
10805
+ });
10806
+ return res.layer.id;
10807
+ }
10808
+ /** Layer › Smart Objects › Rasterize (one undo step): the smart object becomes a pixel layer holding what it renders. */
10809
+ rasterizeSmartObject(id) {
10810
+ const res = rasterizeSmartObject(this.state.layers, id);
10811
+ this.commitLayers(res.layers, () => {
10812
+ if (this.state.activeLayerId === id && this.state.activeTarget === "filter-mask") this.state.activeTarget = "pixels";
10813
+ });
10814
+ return id;
10815
+ }
10816
+ /** A smart object's contents, placement and smart filters as plain data (see `SmartObjectInfo`). */
10817
+ getSmartObject(id) {
10818
+ const layer = requireSmartObject(this.state.layers, id);
10819
+ const c = layer.contents;
10820
+ const g = placementGeometry(layer);
10821
+ const readout = describeTransform(g);
10822
+ const stack = layer.smartFilters;
10823
+ return {
10824
+ layerId: layer.id,
10825
+ contents: {
10826
+ id: c.id,
10827
+ name: c.name,
10828
+ kind: c.kind,
10829
+ width: c.width,
10830
+ height: c.height,
10831
+ decoded: isContentsDecoded(c),
10832
+ fileType: c.file?.type ?? null,
10833
+ linkedPath: c.linked?.path ?? null,
10834
+ instances: instancesOf(this.state.layers, c.id),
10835
+ layerCount: c.document ? flattenLayers(c.document.layers).length : 0
10836
+ },
10837
+ placement: {
10838
+ corners: g.corners.map((p) => ({ x: p.x, y: p.y })),
10839
+ warp: layer.placement.warp ?? null,
10840
+ interpolation: layer.placement.interpolation ?? "bicubic",
10841
+ affine: readout.affine,
10842
+ scaleX: readout.scaleX,
10843
+ scaleY: readout.scaleY,
10844
+ rotate: readout.rotate,
10845
+ skewX: readout.skewX,
10846
+ center: anchorPoint(g, "center"),
10847
+ bounds: transformedBox(g)
10848
+ },
10849
+ smartFilters: {
10850
+ enabled: stack?.enabled ?? true,
10851
+ filters: (stack?.filters ?? []).map((f) => f.kind === "psd" ? { id: f.id, kind: "psd", name: f.name, enabled: f.enabled, blend: f.blend, opacity: f.opacity } : { ...f }),
10852
+ mask: stack?.mask ? { enabled: stack.mask.enabled !== false, linked: stack.mask.linked !== false, density: stack.mask.density ?? 1, feather: stack.mask.feather ?? 0, bounds: contentBounds(stack.mask.surface), defaultValue: stack.mask.defaultValue } : null
10853
+ },
10854
+ bounds: this.getLayerBounds(id)
10855
+ };
10856
+ }
10857
+ /**
10858
+ * Sets a smart object's placement (one undo step, or none with `commitToHistory = false` during a
10859
+ * drag): where the contents' corners land, an optional warp in contents pixels and the
10860
+ * interpolation. The contents render again from the source; a linked mask follows a warp-free
10861
+ * change. Returns the new placement summary.
10862
+ */
10863
+ setSmartObjectPlacement(id, placement, commitToHistory = true) {
10864
+ const res = setSmartPlacement(this.state.layers, id, placement, this.canvasSize);
10865
+ this.commitLayers(res.layers, void 0, commitToHistory);
10866
+ return { ...this.getSmartObject(id).placement, masksMoved: res.masksMoved };
10867
+ }
10868
+ /**
10869
+ * Sets a smart object's transform absolutely (one undo step): exactly one of `corners` (where the
10870
+ * contents' corners land), `matrix` (contents pixels → document) or the numbers (`scaleX`,
10871
+ * `scaleY` in percent of the contents' own size, `rotate`, `skewX`, `skewY`, and `x`, `y` where
10872
+ * the contents' centre goes, default where it is now); plus, optionally, a warp (a preset, a mesh
10873
+ * in contents pixels, or `null` to remove it) and the interpolation. Unlike `transformLayer`,
10874
+ * nothing is relative to the current transform.
10875
+ */
10876
+ setSmartObjectTransform(id, spec) {
10877
+ const layer = requireSmartObject(this.state.layers, id);
10878
+ const g = placementGeometry(layer);
10879
+ const numeric = ["scaleX", "scaleY", "rotate", "skewX", "skewY", "x", "y"].filter((k) => spec[k] !== void 0);
10880
+ const ways = [spec.corners !== void 0, spec.matrix !== void 0, numeric.length > 0].filter(Boolean).length;
10881
+ if (ways > 1) throw new LayerOperationError("Give the transform only one way: corners, matrix, or the numbers (scaleX, scaleY, rotate, skewX, skewY, x, y).");
10882
+ let corners = g.corners;
10883
+ if (spec.corners) corners = spec.corners;
10884
+ else if (spec.matrix) {
10885
+ if (!(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) throw new LayerOperationError("matrix must be six finite numbers [a, b, c, d, e, f] (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f).");
10886
+ const [a, b, c, d] = spec.matrix;
10887
+ if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError("matrix is singular (it collapses the contents to a line); a\xB7d \u2212 b\xB7c must not be 0.");
10888
+ corners = cornersFromMatrix(g.box, spec.matrix);
10889
+ } else if (numeric.length) {
10890
+ if (spec.scaleX === 0 || spec.scaleY === 0) throw new LayerOperationError("scaleX and scaleY must not be 0 (that collapses the contents to a line).");
10891
+ for (const k of numeric) if (!Number.isFinite(spec[k])) throw new LayerOperationError(`${k} must be a finite number.`);
10892
+ const now = describeTransform(g);
10893
+ const centre = anchorPoint(g, "center");
10894
+ const params = {
10895
+ scaleX: spec.scaleX ?? (now.affine ? now.scaleX : 100),
10896
+ scaleY: spec.scaleY ?? (now.affine ? now.scaleY : 100),
10897
+ rotate: spec.rotate ?? (now.affine ? now.rotate : 0),
10898
+ skewX: spec.skewX ?? (now.affine ? now.skewX : 0),
10899
+ skewY: spec.skewY ?? 0
10900
+ };
10901
+ const m = matrixFromParams(g.box, { ...params, reference: "center" });
10902
+ const cx = g.box.width / 2;
10903
+ const cy = g.box.height / 2;
10904
+ const to = { x: spec.x ?? centre.x, y: spec.y ?? centre.y };
10905
+ corners = cornersFromMatrix(g.box, multiplyMatrices(m, translateMatrix(to.x - cx, to.y - cy)));
10906
+ }
10907
+ let warp = layer.placement.warp ?? null;
10908
+ if (spec.warp === null) warp = null;
10909
+ else if (spec.warp) {
10910
+ if ("style" in spec.warp) {
10911
+ if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
10912
+ warp = presetWarp(g.box, spec.warp);
10913
+ } else {
10914
+ if (!isValidWarp(spec.warp)) throw new LayerOperationError("mesh must have us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points in contents pixels, row by row.");
10915
+ warp = spec.warp;
10916
+ }
10917
+ }
10918
+ const next = { corners, warp, interpolation: spec.interpolation ?? layer.placement.interpolation };
10919
+ return this.setSmartObjectPlacement(id, next);
10920
+ }
10921
+ /**
10922
+ * Layer › Smart Objects › Replace Contents (one undo step): new contents for the smart object and
10923
+ * every instance linked to it, from RGBA pixels, PSD bytes (a layer document) or contents. Each
10924
+ * instance keeps its scale and rotation about the contents' centre. Returns the contents id and
10925
+ * the layers that changed.
10926
+ */
10927
+ replaceSmartObjectContents(id, source) {
10928
+ const layer = requireSmartObject(this.state.layers, id);
10929
+ let contents;
10930
+ if ("contents" in source) contents = source.contents;
10931
+ else if ("pixels" in source) contents = rasterContents(source.pixels, source.name ?? layer.contents.name.replace(/\.\w+$/, ".png"));
10932
+ else {
10933
+ const psd = importPsd(source.psd, { newId: () => newLayerId() });
10934
+ contents = documentContents({ layers: psd.layers, globalLight: psd.globalLight, patterns: psd.patterns.length ? psd.patterns : void 0 }, psd.width, psd.height, source.name ?? `${layer.name || "Contents"}.psb`);
10935
+ contents.file = { bytes: source.psd.slice(), type: psd.info.psb ? "psb" : "psd" };
10936
+ }
10937
+ const res = replaceContents(this.state.layers, layer.contents.id, contents, this.canvasSize);
10938
+ this.commitLayers(res.layers);
10939
+ return { contentsId: layer.contents.id, width: contents.width, height: contents.height, layerIds: res.ids };
10940
+ }
10941
+ /** `replaceSmartObjectContents` from an image (PNG data URL or base64 decodes anywhere; other formats need a browser). */
10942
+ async replaceSmartObjectContentsFromImage(id, image, name) {
10943
+ const pixels = await this.decodeImage(image);
10944
+ return this.replaceSmartObjectContents(id, { pixels, name });
10945
+ }
10946
+ /**
10947
+ * Layer › Smart Objects › Export Contents: the file the contents came from while unedited, else a
10948
+ * PNG (raster contents) or a PSD / PSB of the nested document. Returns the file name, MIME type
10949
+ * and bytes.
10950
+ */
10951
+ exportSmartObjectContents(id) {
10952
+ const layer = requireSmartObject(this.state.layers, id);
10953
+ const c = layer.contents;
10954
+ const base = c.name.replace(/\.\w+$/, "") || "Contents";
10955
+ if (c.file) {
10956
+ const mime = { psd: "image/vnd.adobe.photoshop", psb: "image/vnd.adobe.photoshop", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", tif: "image/tiff", tiff: "image/tiff", gif: "image/gif" };
10957
+ const ext = c.file.type.toLowerCase();
10958
+ return { name: /\.\w+$/.test(c.name) ? c.name : `${base}.${ext}`, mimeType: mime[ext] ?? "application/octet-stream", bytes: c.file.bytes };
10959
+ }
10960
+ if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of "${layer.name || id}" are not available here.`);
10961
+ if (c.kind === "raster") {
10962
+ const data = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });
10963
+ return { name: `${base}.png`, mimeType: "image/png", bytes: encodePng(c.width, c.height, data) };
10964
+ }
10965
+ const doc = c.document;
10966
+ const merged = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });
10967
+ const prepared = prepareNestedForPsd(doc.layers);
10968
+ const res = exportPsd({ width: c.width, height: c.height, layers: prepared, merged, patterns: doc.patterns, globalLight: doc.globalLight });
10969
+ return { name: `${base}.${res.psb ? "psb" : "psd"}`, mimeType: "image/vnd.adobe.photoshop", bytes: res.bytes };
10970
+ }
10971
+ /**
10972
+ * Layer › Smart Objects › New Smart Object via Copy (`linked: false`: independent contents) or a
10973
+ * linked duplicate (`linked: true`: edits to the contents show in both), directly above, as one
10974
+ * undo step. Returns the copy's id.
10975
+ */
10976
+ copySmartObject(id, linked = false, name) {
10977
+ const res = copySmartObject(this.state.layers, id, linked, name);
10978
+ this.commitLayers(res.layers, () => {
10979
+ this.state.activeLayerId = res.layer.id;
10980
+ this.state.activeTarget = "pixels";
10981
+ });
10982
+ return res.layer.id;
10983
+ }
10984
+ /** Adds a smart filter (`develop` or `blur`, with `blend` and `opacity`) at `index` (default the top), as one undo step. */
10985
+ addSmartFilter(id, filter, index) {
10986
+ const res = addSmartFilter(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);
10987
+ this.commitLayers(res.layers);
10988
+ return res.filter;
10989
+ }
10990
+ /** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */
10991
+ updateSmartFilter(id, filterId, changes, commitToHistory = true) {
10992
+ const res = updateSmartFilter(this.state.layers, id, filterId, changes, this.canvasSize);
10993
+ this.commitLayers(res.layers, void 0, commitToHistory);
10994
+ return res.filter;
10995
+ }
10996
+ /** Removes one smart filter (one undo step). Returns the remaining filters. */
10997
+ removeSmartFilter(id, filterId) {
10998
+ const res = removeSmartFilter(this.state.layers, id, filterId, this.canvasSize);
10999
+ this.commitLayers(res.layers);
11000
+ return res.layer.smartFilters?.filters ?? [];
11001
+ }
11002
+ /** Moves one smart filter to `index` (0 = bottom, applied first), as one undo step. Returns the filters. */
11003
+ moveSmartFilter(id, filterId, index) {
11004
+ const res = moveSmartFilter(this.state.layers, id, filterId, index, this.canvasSize);
11005
+ this.commitLayers(res.layers);
11006
+ return res.layer.smartFilters?.filters ?? [];
11007
+ }
11008
+ /** Turns a smart object's whole filter stack on or off (one undo step). */
11009
+ setSmartFiltersEnabled(id, enabled) {
11010
+ const res = setSmartFiltersEnabled(this.state.layers, id, enabled, this.canvasSize);
11011
+ this.commitLayers(res.layers);
11012
+ return res.layer.smartFilters?.enabled ?? true;
11013
+ }
11014
+ /**
11015
+ * Sets the smart filter mask (one undo step): `reveal-all`, `hide-all`, `selection` (the
11016
+ * selection's coverage), or `none` to remove it; `props` sets density, feather, enabled and linked.
11017
+ * Painting it targets channel `filter-mask`. Returns the mask, or `null` when removed.
11018
+ */
11019
+ setSmartFilterMask(id, from, props = {}, commitToHistory = true) {
11020
+ const layer = requireSmartObject(this.state.layers, id);
11021
+ let mask;
11022
+ if (from === "none") mask = null;
11023
+ else if (from === "keep") {
11024
+ if (!layer.smartFilters?.mask) throw new LayerOperationError(`Smart object "${layer.name || id}" has no filter mask; add one with from reveal-all, hide-all or selection.`);
11025
+ mask = withMaskProps(layer.smartFilters.mask, props);
11026
+ } else if (from === "selection") {
11027
+ const sel = this.state.selection;
11028
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, or use reveal-all / hide-all.");
11029
+ mask = this.maskFromSelection(sel, props);
11030
+ } else {
11031
+ const v = from === "reveal-all" ? 255 : 0;
11032
+ mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);
11033
+ }
11034
+ const res = setSmartFilterMask(this.state.layers, id, mask, this.canvasSize);
11035
+ this.commitLayers(res.layers, () => {
11036
+ if (this.state.activeLayerId !== id) return;
11037
+ if (mask && from !== "keep") this.state.activeTarget = "filter-mask";
11038
+ else if (!mask && this.state.activeTarget === "filter-mask") this.state.activeTarget = "pixels";
11039
+ }, commitToHistory);
11040
+ return res.layer.smartFilters?.mask ?? null;
11041
+ }
11042
+ /**
11043
+ * Decodes smart object contents that came as files the synchronous decoders cannot read (a JPEG
11044
+ * or TIFF embedded in a PSD), through the browser's image decoder. No undo step: the pixels on
11045
+ * screen do not change. Returns the contents ids still undecoded.
11046
+ */
11047
+ async prepareSmartObjects() {
11048
+ const pending = /* @__PURE__ */ new Map();
11049
+ for (const l of flattenLayers(this.state.layers)) if (l.type === "smart" && !isContentsDecoded(l.contents) && l.contents.file) pending.set(l.contents.id, l.contents);
11050
+ const left = [];
11051
+ for (const [cid, c] of pending) {
11052
+ try {
11053
+ const url = `data:${c.file.type === "png" ? "image/png" : c.file.type === "gif" ? "image/gif" : c.file.type === "tif" || c.file.type === "tiff" ? "image/tiff" : "image/jpeg"};base64,${bytesToBase64(c.file.bytes)}`;
11054
+ const px2 = await this.decodeImage(url);
11055
+ const decoded2 = { ...c, kind: "raster", raster: rasterContents(px2, c.name, cid).raster, width: px2.width, height: px2.height };
11056
+ const swap = (layers) => {
11057
+ let out = layers;
11058
+ for (const l of flattenLayers(layers)) if (l.type === "smart" && l.contents.id === cid) out = mapLayer(out, l.id, (x) => ({ ...x, contents: decoded2 }));
11059
+ return out;
11060
+ };
11061
+ this.state.layers = swap(this.state.layers);
11062
+ for (const e of [...this.undoStack, ...this.redoStack]) e.layers = swap(e.layers);
11063
+ } catch {
11064
+ left.push(cid);
9826
11065
  }
9827
- session.update({ corners, warp }, false);
9828
- return session.commit();
9829
- } catch (e) {
9830
- session.cancel();
9831
- throw e;
9832
11066
  }
11067
+ if (pending.size) this.notify();
11068
+ return left;
9833
11069
  }
9834
- /** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */
9835
- flipLayer(axis, init = {}) {
9836
- if (axis !== "horizontal" && axis !== "vertical") throw new LayerOperationError(`axis must be horizontal or vertical, got "${String(axis)}".`);
9837
- const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
9838
- try {
9839
- session.update({ corners: flipCorners(identityTransform(session.box), axis) }, false);
9840
- return session.commit();
9841
- } catch (e) {
9842
- session.cancel();
9843
- throw e;
9844
- }
11070
+ // --- Smart object contents (Edit Contents) ---
11071
+ contentsStack = [];
11072
+ /** The smart objects whose contents are open (outermost first); empty when the document itself is shown. */
11073
+ getEditingContents() {
11074
+ return this.contentsStack.map((e) => ({ layerId: e.layerId, name: e.name }));
9845
11075
  }
9846
11076
  /**
9847
- * Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's
9848
- * centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).
11077
+ * Layer › Smart Objects › Edit Contents: shows the smart object's contents as a document of their
11078
+ * own (a layer document, or raster contents as one pixel layer), with their own undo history.
11079
+ * Every editor operation and tool then works on the contents until `closeSmartObjectContents`.
11080
+ * Contents can nest. Returns the contents' size and layer ids.
9849
11081
  */
9850
- rotateLayer90(turn, init = {}) {
9851
- if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
9852
- const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
9853
- try {
9854
- const b = session.box;
9855
- const cx = b.x + b.width / 2;
9856
- const cy = b.y + b.height / 2;
9857
- const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
9858
- const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);
9859
- const m = turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];
9860
- session.update({ corners: cornersFromMatrix(b, m) }, false);
9861
- return session.commit();
9862
- } catch (e) {
9863
- session.cancel();
9864
- throw e;
9865
- }
11082
+ editSmartObjectContents(id) {
11083
+ if (this.getFreeTransform()) throw new LayerOperationError("A Free Transform is open; commit or cancel it first.");
11084
+ const layer = requireSmartObject(this.state.layers, id);
11085
+ const c = layer.contents;
11086
+ if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of "${layer.name || id}" are a ${c.file?.type ?? "file"} that could not be decoded here; run prepareSmartObjects() in a browser first.`);
11087
+ const saved = {
11088
+ state: { ...this.state },
11089
+ undo: this.undoStack,
11090
+ redo: this.redoStack,
11091
+ initial: this.initialSnapshot,
11092
+ element: this.imageElement,
11093
+ samplePixels: this.samplePixels,
11094
+ samplePixelsOverride: this.samplePixelsOverride,
11095
+ psdResolution: this.psdResolution
11096
+ };
11097
+ const layers = c.kind === "document" ? c.document.layers : [{ type: "pixel", id: newLayerId(), name: "Layer 0", visible: true, opacity: 1, blend: "normal", surface: c.raster, offset: { x: 0, y: 0 } }];
11098
+ this.contentsStack.push({ layerId: layer.id, contentsId: c.id, name: layer.name || c.name, saved });
11099
+ const s = this.state;
11100
+ this.state = {
11101
+ ...s,
11102
+ sourceUrl: null,
11103
+ imageDimensions: { width: c.width, height: c.height },
11104
+ crop: null,
11105
+ transform: { ...DEFAULT_TRANSFORM_VALUES },
11106
+ layers,
11107
+ activeLayerId: layers.length ? layers[layers.length - 1].id : null,
11108
+ activeTarget: "pixels",
11109
+ selection: null,
11110
+ selectedAnnotationId: null,
11111
+ outputOpacity: 1,
11112
+ zoom: 1,
11113
+ pan: { x: 0, y: 0 },
11114
+ editingContents: this.getEditingContents()
11115
+ };
11116
+ delete this.state.legacyOutputBlur;
11117
+ if (c.document?.globalLight) this.state.globalLight = c.document.globalLight;
11118
+ else delete this.state.globalLight;
11119
+ if (c.document?.patterns?.length) this.state.patterns = c.document.patterns;
11120
+ else delete this.state.patterns;
11121
+ this.imageElement = null;
11122
+ this.samplePixels = null;
11123
+ this.samplePixelsOverride = false;
11124
+ this.undoStack = [];
11125
+ this.redoStack = [];
11126
+ this.viewKey = [];
11127
+ this.initialSnapshot = this.captureHistoryEntry();
11128
+ this.notify();
11129
+ return { layerId: layer.id, depth: this.contentsStack.length, width: c.width, height: c.height, layerIds: flattenLayers(layers).map((l) => l.id) };
9866
11130
  }
9867
- /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */
9868
- addLayerFilter(id, filter) {
9869
- const f = normalizeFilter(filter);
9870
- const next = addLayerFilter(this.state.layers, id, f);
9871
- this.commitLayers(next);
9872
- return findLayer(next, id).layer.filters ?? [];
11131
+ /**
11132
+ * Closes the innermost open contents: with `save` (default) the edited layers become the smart
11133
+ * object's contents, and it and every linked instance render again (one undo step in the
11134
+ * document); without, the edits are dropped. Returns the smart object and the layers that changed.
11135
+ */
11136
+ closeSmartObjectContents(save = true) {
11137
+ const top = this.contentsStack.pop();
11138
+ if (!top) throw new LayerOperationError("No smart object contents are open; open them with edit_smart_object_contents.");
11139
+ if (this.openTransform?.isOpen) this.openTransform.cancel();
11140
+ this.openTransform = null;
11141
+ const edited = this.state;
11142
+ const changed = save && this.undoStack.length > 0;
11143
+ let next = null;
11144
+ if (changed) {
11145
+ const layer = findLayer(top.saved.state.layers, top.layerId)?.layer;
11146
+ const old = layer?.contents;
11147
+ const { width, height } = edited.imageDimensions;
11148
+ const only = edited.layers.length === 1 ? edited.layers[0] : null;
11149
+ const plain3 = only?.type === "pixel" && only.visible && only.opacity === 1 && only.blend === "normal" && !only.mask && !only.effects && !only.filters?.length && !only.clipped && (only.fillOpacity ?? 1) === 1 && only.source !== "document";
11150
+ if (old?.kind === "raster" && plain3) {
11151
+ const px2 = readRegion(only.surface, { x: -only.offset.x, y: -only.offset.y, width, height });
11152
+ next = rasterContents({ width, height, data: px2 }, old.name, old.id);
11153
+ } else {
11154
+ const doc = { layers: edited.layers, globalLight: edited.globalLight, patterns: edited.patterns };
11155
+ next = documentContents(doc, width, height, old?.kind === "document" ? old.name : `${(old?.name ?? "Contents").replace(/\.\w+$/, "")}.psb`, top.contentsId);
11156
+ }
11157
+ }
11158
+ const saved = top.saved;
11159
+ this.state = saved.state;
11160
+ this.undoStack = saved.undo;
11161
+ this.redoStack = saved.redo;
11162
+ this.initialSnapshot = saved.initial;
11163
+ this.imageElement = saved.element;
11164
+ this.samplePixels = saved.samplePixels;
11165
+ this.samplePixelsOverride = saved.samplePixelsOverride;
11166
+ this.psdResolution = saved.psdResolution;
11167
+ this.viewKey = [];
11168
+ if (this.contentsStack.length) this.state.editingContents = this.getEditingContents();
11169
+ else delete this.state.editingContents;
11170
+ if (!next) {
11171
+ this.notify();
11172
+ return { layerId: top.layerId, changed: false, layerIds: [] };
11173
+ }
11174
+ const res = replaceContents(this.state.layers, top.contentsId, next, this.canvasSize);
11175
+ this.commitLayers(res.layers);
11176
+ return { layerId: top.layerId, changed: true, layerIds: res.ids };
9873
11177
  }
9874
- /** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */
9875
- updateLayerFilter(id, filterId, changes, commitToHistory = true) {
9876
- const next = updateLayerFilter(this.state.layers, id, filterId, changes);
9877
- this.commitLayers(next, void 0, commitToHistory);
9878
- return findLayer(next, id).layer.filters ?? [];
11178
+ /** The document's own layers (not the contents open for editing). */
11179
+ rootLayers() {
11180
+ return this.contentsStack[0]?.saved.state.layers ?? this.state.layers;
9879
11181
  }
9880
- /** Removes one filter (one undo step). Returns the remaining filters. */
9881
- removeLayerFilter(id, filterId) {
9882
- const next = removeLayerFilter(this.state.layers, id, filterId);
9883
- this.commitLayers(next);
9884
- return findLayer(next, id).layer.filters ?? [];
11182
+ /** Drops every open contents edit (loading another document). */
11183
+ abandonContents() {
11184
+ while (this.contentsStack.length) this.closeSmartObjectContents(false);
9885
11185
  }
9886
11186
  // --- Layer effects ---
9887
11187
  /** A layer's effects (an empty, enabled list when it has none). */
@@ -10073,7 +11373,7 @@ var ImageEditorController = class {
10073
11373
  const k = Math.min(1, side / Math.max(width, height));
10074
11374
  const tw = Math.max(1, Math.round(width * k));
10075
11375
  const th = Math.max(1, Math.round(height * k));
10076
- const surface = options.mask ? layer.mask && !layer.mask.source ? layer.mask.surface : null : layer.type === "pixel" ? this.surfaceOf(layer) : null;
11376
+ const surface = options.mask ? layer.mask && !layer.mask.source ? layer.mask.surface : null : layer.type === "pixel" || layer.type === "smart" ? this.surfaceOf(layer) : null;
10077
11377
  if (!surface) return null;
10078
11378
  const ox = layer.type === "pixel" ? layer.offset.x : 0;
10079
11379
  const oy = layer.type === "pixel" ? layer.offset.y : 0;
@@ -10160,19 +11460,27 @@ var ImageEditorController = class {
10160
11460
  if (!layer.mask) throw new LayerOperationError(`Layer "${layer.name || id}" has no mask to ${action}; add one with add_layer_mask, or target pixels.`);
10161
11461
  if (layer.mask.source) throw new LayerOperationError(`Layer "${layer.name || id}" has a depth mask, which is generated, not painted.`);
10162
11462
  requireUnlocked(layer, "all", action);
11463
+ } else if (ch === "filter-mask") {
11464
+ if (layer.type !== "smart" || !layer.smartFilters?.mask) {
11465
+ throw new LayerOperationError(`Layer "${layer.name || id}" has no smart filter mask to ${action}; add one with set_smart_filter_mask, or target pixels or mask.`);
11466
+ }
11467
+ if (!isContentsDecoded(layer.contents)) throw new LayerOperationError(`The contents of "${layer.name || id}" are not decoded here, so its filter mask cannot be painted; run prepareSmartObjects() first.`);
11468
+ requireUnlocked(layer, "all", action);
10163
11469
  } else {
11470
+ 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.`);
10164
11471
  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).`);
10165
11472
  requireUnlocked(layer, "pixels", action);
10166
11473
  }
10167
11474
  if (!layer.visible) throw new LayerOperationError(`Layer "${layer.name || id}" is hidden; show it before you ${action}.`);
10168
11475
  const offset = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
10169
- const surface = ch === "mask" ? layer.mask.surface : this.surfaceOf(layer);
11476
+ const surface = ch === "mask" ? layer.mask.surface : ch === "filter-mask" ? layer.smartFilters.mask.surface : this.surfaceOf(layer);
10170
11477
  const lockTransparency = ch === "pixels" && Boolean(layer.locks?.transparency);
10171
11478
  return { id, layer, channel: ch, offset, surface, lockTransparency };
10172
11479
  }
10173
11480
  /** The layer with its pixels (or mask) replaced by `surface`. */
10174
11481
  withSurface(layer, channel, surface) {
10175
11482
  if (channel === "mask") return { ...layer, mask: { ...layer.mask, surface } };
11483
+ if (channel === "filter-mask") return withFilterMaskSurface(layer, surface, this.canvasSize);
10176
11484
  const next = { ...layer, surface };
10177
11485
  delete next.source;
10178
11486
  return next;
@@ -10187,7 +11495,7 @@ var ImageEditorController = class {
10187
11495
  const tool = init.tool ?? "brush";
10188
11496
  if (tool !== "brush" && tool !== "eraser" && tool !== "mixer") throw new LayerOperationError(`Unknown paint tool "${String(tool)}"; use brush, eraser or mixer.`);
10189
11497
  const target = this.paintTarget(init.layerId, init.channel, tool === "eraser" ? "erase" : "paint");
10190
- if (tool === "mixer" && target.channel === "mask") {
11498
+ if (tool === "mixer" && target.channel !== "pixels") {
10191
11499
  throw new LayerOperationError("The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.");
10192
11500
  }
10193
11501
  const brush = normalizeBrush({ ...this.state.paint.brush, ...init.brush ?? {} });
@@ -10199,10 +11507,10 @@ var ImageEditorController = class {
10199
11507
  };
10200
11508
  const apply = {
10201
11509
  mode: tool === "eraser" ? "erase" : "paint",
10202
- color: target.channel === "mask" ? toMaskGrey(color) : color,
10203
- background: target.channel === "mask" ? toMaskGrey(background) : background,
11510
+ color: target.channel !== "pixels" ? toMaskGrey(color) : color,
11511
+ background: target.channel !== "pixels" ? toMaskGrey(background) : background,
10204
11512
  opacity: brush.opacity,
10205
- blend: target.channel === "mask" ? "normal" : brush.blend,
11513
+ blend: target.channel !== "pixels" ? "normal" : brush.blend,
10206
11514
  lockTransparency: target.lockTransparency,
10207
11515
  selection: this.state.selection?.bounds ? this.state.selection : null,
10208
11516
  offset: target.offset
@@ -10230,7 +11538,7 @@ var ImageEditorController = class {
10230
11538
  /** @internal Shows a stroke's live surface (no history). */
10231
11539
  _strokeUpdate(layerId, channel, surface, rects) {
10232
11540
  this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
10233
- this.notify(rects);
11541
+ this.notify(channel === "filter-mask" ? void 0 : rects);
10234
11542
  }
10235
11543
  /** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */
10236
11544
  _strokeCommit(before, layerId, channel, surface) {
@@ -10302,10 +11610,10 @@ var ImageEditorController = class {
10302
11610
  };
10303
11611
  const apply = {
10304
11612
  mode,
10305
- color: target.channel === "mask" ? grey(color) : color,
10306
- background: target.channel === "mask" ? grey(background) : background,
11613
+ color: target.channel !== "pixels" ? grey(color) : color,
11614
+ background: target.channel !== "pixels" ? grey(background) : background,
10307
11615
  opacity,
10308
- blend: target.channel === "mask" ? "normal" : blend,
11616
+ blend: target.channel !== "pixels" ? "normal" : blend,
10309
11617
  lockTransparency: target.lockTransparency,
10310
11618
  selection: null,
10311
11619
  offset: target.offset
@@ -10691,7 +11999,7 @@ var ImageEditorController = class {
10691
11999
  const sources = {
10692
12000
  width,
10693
12001
  height,
10694
- surfaceOf: (l) => l.source === "document" && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l),
12002
+ surfaceOf: (l) => l.type === "pixel" && l.source === "document" && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l),
10695
12003
  version: pixels ? "pixels" : this.sourceVersion()
10696
12004
  };
10697
12005
  const stack = withoutAnnotations(this.state.layers);
@@ -11181,8 +12489,10 @@ var ImageEditorController = class {
11181
12489
  if (layer.mask && (layer.offset.x || layer.offset.y)) {
11182
12490
  next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };
11183
12491
  }
12492
+ } else if (layer.type === "smart") {
12493
+ return this.rasterizeSmartObject(id);
11184
12494
  } else {
11185
- throw new LayerOperationError(`Only annotation layers and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
12495
+ 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.`);
11186
12496
  }
11187
12497
  this.commitLayers(mapLayer(this.state.layers, id, () => next));
11188
12498
  return id;
@@ -11198,6 +12508,7 @@ var ImageEditorController = class {
11198
12508
  */
11199
12509
  loadPsd(bytes) {
11200
12510
  const result = importPsd(bytes, { newId: () => newLayerId() });
12511
+ this.abandonContents();
11201
12512
  this.recordHistory();
11202
12513
  const s = this.state;
11203
12514
  this.imageElement = null;
@@ -11306,6 +12617,13 @@ var ImageEditorController = class {
11306
12617
  if (mask2) out2.mask = mask2;
11307
12618
  return out2;
11308
12619
  }
12620
+ if (layer.type === "smart") {
12621
+ add2("Smart objects were written as pixel layers (their rendered pixels)", layer.name);
12622
+ const px2 = rasterizeSmartObject([layer], layer.id).layer;
12623
+ const mask2 = bakeMask(px2);
12624
+ if (mask2) px2.mask = mask2;
12625
+ return px2;
12626
+ }
11309
12627
  let out = { ...layer };
11310
12628
  if (layer.source === "document") {
11311
12629
  out.surface = this.surfaceOf(layer);
@@ -11391,10 +12709,27 @@ var ImageEditorController = class {
11391
12709
  this.notify();
11392
12710
  }
11393
12711
  toJSON(options) {
11394
- const out = this.serializeState();
11395
- if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
12712
+ const root = this.contentsStack[0]?.saved;
12713
+ const current = this.state;
12714
+ const element = this.imageElement;
12715
+ if (root) {
12716
+ this.state = root.state;
12717
+ this.imageElement = root.element;
12718
+ this.viewKey = [];
12719
+ }
12720
+ let out;
12721
+ try {
12722
+ out = this.serializeState();
12723
+ if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
12724
+ } finally {
12725
+ if (root) {
12726
+ this.state = current;
12727
+ this.imageElement = element;
12728
+ this.viewKey = [];
12729
+ }
12730
+ }
11396
12731
  if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;
11397
- return serializeLayersExternal(this.state.layers, options.store).then(({ layers, assets }) => ({ ...out, layers, assets }));
12732
+ return serializeLayersExternal(this.rootLayers(), options.store).then(({ layers, assets, smartContents }) => smartContents ? { ...out, layers, assets, smartContents } : { ...out, layers, assets });
11398
12733
  }
11399
12734
  /**
11400
12735
  * Loads a stored state whose v3 assets may live in an asset store (references other than data
@@ -11413,6 +12748,7 @@ var ImageEditorController = class {
11413
12748
  }
11414
12749
  /** Loads a stored state of any version (one undo step). */
11415
12750
  loadJSON(data) {
12751
+ this.abandonContents();
11416
12752
  this.recordHistory();
11417
12753
  this.applyStored(data);
11418
12754
  let selection = null;
@@ -11435,7 +12771,8 @@ var ImageEditorController = class {
11435
12771
  this.lastLoadProblems = [];
11436
12772
  if (isLayered(data)) {
11437
12773
  const problems = [];
11438
- s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems });
12774
+ const canvas = typeof data.width === "number" && data.width > 0 && typeof data.height === "number" && data.height > 0 ? { width: data.width, height: data.height } : this.naturalSize;
12775
+ s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems, smartContents: Array.isArray(data.smartContents) ? data.smartContents : [], canvas });
11439
12776
  this.lastLoadProblems = problems;
11440
12777
  s.crop = data.crop ? { ...data.crop } : null;
11441
12778
  s.transform = { ...data.transform };
@@ -11487,7 +12824,7 @@ var ImageEditorController = class {
11487
12824
  annotations: JSON.parse(JSON.stringify(view.annotations))
11488
12825
  };
11489
12826
  }
11490
- const { layers, assets } = serializeLayers(this.state.layers);
12827
+ const { layers, assets, smartContents } = serializeLayers(this.state.layers);
11491
12828
  const out = {
11492
12829
  version: LAYERED_SCHEMA_VERSION,
11493
12830
  width,
@@ -11502,6 +12839,7 @@ var ImageEditorController = class {
11502
12839
  if (this.state.legacyOutputBlur !== void 0) out.legacyOutputBlur = this.state.legacyOutputBlur;
11503
12840
  if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };
11504
12841
  if (this.state.patterns?.length) out.patterns = serializePatterns(this.state.patterns);
12842
+ if (smartContents) out.smartContents = smartContents;
11505
12843
  return out;
11506
12844
  }
11507
12845
  };
@@ -11636,6 +12974,12 @@ function collectStateTiles(layers, into) {
11636
12974
  for (const l of flattenLayers(layers)) {
11637
12975
  if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
11638
12976
  if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);
12977
+ if (l.type === "smart") {
12978
+ collectTiles(l.surface, into);
12979
+ if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);
12980
+ if (l.contents.raster) collectTiles(l.contents.raster, into);
12981
+ if (l.contents.document) collectStateTiles(l.contents.document.layers, into);
12982
+ }
11639
12983
  }
11640
12984
  }
11641
12985
  function pixelsOnly(layers) {
@@ -11868,7 +13212,7 @@ function loadImageEditorDocument(input) {
11868
13212
  if (state.selection === void 0) delete state.selection;
11869
13213
  return { state, report: result.report, unknown: result.unknown };
11870
13214
  }
11871
- var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation"]);
13215
+ var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart"]);
11872
13216
  function cleanLayers(value, dropped) {
11873
13217
  const out = [];
11874
13218
  for (const raw of value) {
@@ -11934,6 +13278,13 @@ function loadLayeredDocument(doc) {
11934
13278
  return { width: mask.width, height: mask.height, rle: v.rle };
11935
13279
  },
11936
13280
  defaults: void 0
13281
+ },
13282
+ smartContents: {
13283
+ parse: (value) => {
13284
+ if (!Array.isArray(value)) throw new Error("smartContents must be an array");
13285
+ return value.filter((c) => isObject(c) && typeof c.id === "string").map((c) => JSON.parse(JSON.stringify(c)));
13286
+ },
13287
+ defaults: void 0
11937
13288
  }
11938
13289
  },
11939
13290
  { currentVersion: 3, steps: {} }
@@ -11947,6 +13298,7 @@ function loadLayeredDocument(doc) {
11947
13298
  const state = { version: 3, ...result.value };
11948
13299
  if (state.selection === void 0) delete state.selection;
11949
13300
  if (state.legacyOutputBlur === void 0) delete state.legacyOutputBlur;
13301
+ if (state.smartContents === void 0) delete state.smartContents;
11950
13302
  return { state, report: result.report, unknown: result.unknown };
11951
13303
  }
11952
13304
 
@@ -12766,6 +14118,13 @@ function describeLayer(layer, activeId, boundsOf) {
12766
14118
  out.collapsed = Boolean(layer.collapsed);
12767
14119
  out.children = layer.children.map((c) => describeLayer(c, activeId, boundsOf));
12768
14120
  break;
14121
+ case "smart":
14122
+ out.bounds = boundsOf(layer);
14123
+ out.fillOpacity = round3(layer.fillOpacity ?? 1);
14124
+ out.contents = { id: layer.contents.id, name: layer.contents.name, kind: layer.contents.kind, width: layer.contents.width, height: layer.contents.height };
14125
+ out.smartFilters = (layer.smartFilters?.filters ?? []).map((f) => ({ id: f.id, kind: f.kind, enabled: f.enabled }));
14126
+ out.filterMask = maskSummary(layer.smartFilters?.mask);
14127
+ break;
12769
14128
  }
12770
14129
  return out;
12771
14130
  }
@@ -13735,7 +15094,11 @@ var WRITE4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: fals
13735
15094
  var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
13736
15095
  var TARGET = {
13737
15096
  layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
13738
- channel: { type: "string", enum: ["pixels", "mask"], description: "pixels (default for a pixel layer) or the layer's mask: black hides, white reveals." }
15097
+ channel: {
15098
+ type: "string",
15099
+ enum: ["pixels", "mask", "filter-mask"],
15100
+ description: "pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals."
15101
+ }
13739
15102
  };
13740
15103
  var BRUSH = {
13741
15104
  type: "object",
@@ -14072,7 +15435,7 @@ function createImageTransformTools(controller) {
14072
15435
  const getFrame = {
14073
15436
  name: "get_transform_frame",
14074
15437
  title: "Get the Free Transform frame",
14075
- description: "Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.",
15438
+ description: "Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. For a smart object (smartObject true) the frame is the contents\u2019 rectangle in contents pixels and the corners are where they land now. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.",
14076
15439
  inputSchema: {
14077
15440
  type: "object",
14078
15441
  properties: {
@@ -14094,9 +15457,10 @@ function createImageTransformTools(controller) {
14094
15457
  content: args.content === "selection-copy" ? "selection-copy" : s.content,
14095
15458
  moved: [...s.moves],
14096
15459
  frame: { ...s.box },
14097
- corners: identityTransform(s.box).corners.map(roundPoint),
14098
- center: roundPoint({ x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 })
15460
+ corners: s.startGeometry.corners.map(roundPoint),
15461
+ center: roundPoint(anchorPoint(s.startGeometry, "center"))
14099
15462
  };
15463
+ if (s.smartObject) out.smartObject = true;
14100
15464
  s.cancel();
14101
15465
  if (args.meshColumns !== void 0) {
14102
15466
  const cols = args.meshColumns;
@@ -14225,6 +15589,366 @@ function createImageTransformTools(controller) {
14225
15589
  return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];
14226
15590
  }
14227
15591
 
15592
+ // src/image-editor/smart-tools.ts
15593
+ var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
15594
+ var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
15595
+ var WRITE_IDEMPOTENT4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
15596
+ var DESTRUCTIVE5 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
15597
+ var round5 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
15598
+ var roundPoint2 = (p) => ({ x: round5(p.x), y: round5(p.y) });
15599
+ var LAYER_ID4 = { type: "string", description: "Smart object layer id from list_layers (type smart)." };
15600
+ var FILTER_ID = { type: "string", description: "Smart filter id from get_smart_object or add_smart_filter." };
15601
+ var POINT2 = {
15602
+ type: "object",
15603
+ properties: { x: { type: "number", description: "Document pixels from the left." }, y: { type: "number", description: "Document pixels from the top." } },
15604
+ required: ["x", "y"],
15605
+ additionalProperties: false
15606
+ };
15607
+ var FILTER_BLENDS = BLEND_MODES.map((m) => m.id).filter((m) => m !== "pigment");
15608
+ var FILTER_SETTINGS = {
15609
+ enabled: { type: "boolean", description: "false keeps the filter but skips it (the eye in the Layers panel; default true)." },
15610
+ blend: { type: "string", enum: FILTER_BLENDS, description: "How the filter\u2019s result blends with the result below it (Blending Options; default normal)." },
15611
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Strength of the filter\u2019s result over the result below it, 0..1 (default 1)." },
15612
+ radius: { type: "number", minimum: 0, maximum: 50, description: "blur: box radius in px (default 2)." },
15613
+ brightness: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 (default 0)." },
15614
+ contrast: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 (default 0)." },
15615
+ saturation: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 (default 0)." },
15616
+ exposure: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 (default 0)." },
15617
+ temperature: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 blue to yellow (default 0)." },
15618
+ tint: { type: "number", minimum: -100, maximum: 100, description: "develop: -100..100 green to magenta (default 0)." }
15619
+ };
15620
+ function settingsFrom(args) {
15621
+ const out = {};
15622
+ for (const k of Object.keys(FILTER_SETTINGS)) if (args[k] !== void 0) out[k] = args[k];
15623
+ return out;
15624
+ }
15625
+ function imageArg(image) {
15626
+ if (typeof image !== "string" || !image.trim()) throw new LayerOperationError("image is required: a PNG as a data URL or base64 (other formats need a browser).");
15627
+ return image.trim();
15628
+ }
15629
+ function createImageSmartObjectTools(controller) {
15630
+ const smartId = (id) => {
15631
+ if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a smart object id from list_layers).");
15632
+ requireSmartObject(controller.getLayers(), id);
15633
+ return id;
15634
+ };
15635
+ const info = (id) => {
15636
+ const i = controller.getSmartObject(id);
15637
+ return {
15638
+ ...i,
15639
+ placement: { ...i.placement, corners: i.placement.corners.map(roundPoint2), center: roundPoint2(i.placement.center), scaleX: round5(i.placement.scaleX), scaleY: round5(i.placement.scaleY), rotate: round5(i.placement.rotate), skewX: round5(i.placement.skewX) }
15640
+ };
15641
+ };
15642
+ const layerSummary = (id) => {
15643
+ const s = controller.getState();
15644
+ const loc = findLayer(s.layers, id);
15645
+ return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;
15646
+ };
15647
+ const getSmart = {
15648
+ name: "get_smart_object",
15649
+ title: "Read a smart object",
15650
+ description: "Returns a smart object\u2019s contents (id shared by linked instances, name, kind raster or document, pixel size, the file type it came from, the instances placed from it), its placement (corners in document pixels, warp mesh in contents pixels, interpolation, and scale / rotate / skew in percent and degrees of the contents\u2019 own size when affine), its smart filters bottom to top (ids, kind, settings, enabled, blend, opacity; kind psd marks a Photoshop filter kept but not drawn) with the filter mask, and the rendered bounds. Changes nothing.",
15651
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
15652
+ annotations: { title: "Read a smart object", ...READ7 },
15653
+ handler: ({ layerId }) => info(smartId(layerId))
15654
+ };
15655
+ const convert = {
15656
+ name: "convert_to_smart_object",
15657
+ title: "Convert to Smart Object",
15658
+ description: "Layer \u203A Smart Objects \u203A Convert to Smart Object: sibling layers become one smart object at the topmost one\u2019s position, so later transforms and filters are non-destructive. One pixel layer keeps its id, name, opacity, blend, fill, mask and effects, and its filters become smart filters (it looks exactly the same); several layers, a group or an adjustment become a nested layer document (edit it with edit_smart_object_contents). One undo step. Returns the new layer and its contents.",
15659
+ inputSchema: {
15660
+ type: "object",
15661
+ properties: {
15662
+ layerIds: { type: "array", items: { type: "string" }, minItems: 1, description: "Sibling layer ids from list_layers (default the active layer)." },
15663
+ name: { type: "string", description: "Name of the smart object (default the top layer\u2019s name)." }
15664
+ },
15665
+ additionalProperties: false
15666
+ },
15667
+ annotations: { title: "Convert to Smart Object", ...WRITE6 },
15668
+ handler: ({ layerIds, name }) => {
15669
+ for (const id2 of layerIds ?? []) requireLayer(controller.getLayers(), id2);
15670
+ const id = controller.convertToSmartObject(layerIds, { name });
15671
+ return { layer: layerSummary(id), smartObject: info(id) };
15672
+ }
15673
+ };
15674
+ const rasterize = {
15675
+ name: "rasterize_smart_object",
15676
+ title: "Rasterize a smart object",
15677
+ description: "Layer \u203A Smart Objects \u203A Rasterize: the smart object becomes a pixel layer holding what it shows now (smart filters baked; same id, mask, effects, opacity, blend and fill). The contents, placement and filters are gone, so later transforms resample the pixels. One undo step. Returns the layer.",
15678
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
15679
+ annotations: { title: "Rasterize a smart object", ...DESTRUCTIVE5 },
15680
+ handler: ({ layerId }) => ({ layer: layerSummary(controller.rasterizeSmartObject(smartId(layerId))) })
15681
+ };
15682
+ const setTransform = {
15683
+ name: "set_smart_object_transform",
15684
+ title: "Set a smart object\u2019s transform",
15685
+ description: "Sets where a smart object\u2019s contents land, absolutely (not relative to the current transform), and renders it again from the contents, so nothing degrades however often it is transformed. Give at most one of: corners (Distort / Perspective), matrix (contents pixels \u2192 document), or the numbers scaleX / scaleY (percent of the contents\u2019 own size), rotate, skewX, skewY and x, y (where the contents\u2019 centre goes; unset numbers keep their current values). warp sets a preset or a mesh in contents pixels (null removes it); with nothing else given the corners stay. A linked mask follows a warp-free change. For a relative nudge use transform_layer. One undo step. Returns the placement and bounds.",
15686
+ inputSchema: {
15687
+ type: "object",
15688
+ properties: {
15689
+ layerId: LAYER_ID4,
15690
+ corners: { type: "array", items: POINT2, minItems: 4, maxItems: 4, description: "Where the contents\u2019 corners land: [top-left, top-right, bottom-right, bottom-left] in document pixels. The quad must stay convex." },
15691
+ matrix: { type: "array", items: { type: "number" }, minItems: 6, maxItems: 6, description: "Affine map from contents pixels to document pixels [a, b, c, d, e, f] (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f)." },
15692
+ scaleX: { type: "number", description: "Width in percent of the contents\u2019 own width (negative flips)." },
15693
+ scaleY: { type: "number", description: "Height in percent of the contents\u2019 own height (negative flips)." },
15694
+ rotate: { type: "number", description: "Rotation in degrees, clockwise." },
15695
+ skewX: { type: "number", minimum: -89, maximum: 89, description: "Horizontal skew in degrees." },
15696
+ skewY: { type: "number", minimum: -89, maximum: 89, description: "Vertical skew in degrees." },
15697
+ x: { type: "number", description: "Document x of the contents\u2019 centre (default where it is now)." },
15698
+ y: { type: "number", description: "Document y of the contents\u2019 centre (default where it is now)." },
15699
+ warp: {
15700
+ description: "A warp over the contents: a preset {style, bend, horizontal, vertical, orientation}, a mesh {columns, rows, points} in contents pixels, or null to remove the warp. Omit to keep the current warp.",
15701
+ anyOf: [
15702
+ { type: "null" },
15703
+ {
15704
+ type: "object",
15705
+ properties: {
15706
+ style: { type: "string", enum: WARP_STYLES, description: "Preset (Photoshop\u2019s menu)." },
15707
+ bend: { type: "number", minimum: -100, maximum: 100, description: "Bend in percent (default 50)." },
15708
+ horizontal: { type: "number", minimum: -100, maximum: 100, description: "Horizontal distortion in percent (default 0)." },
15709
+ vertical: { type: "number", minimum: -100, maximum: 100, description: "Vertical distortion in percent (default 0)." },
15710
+ orientation: { type: "string", enum: ["horizontal", "vertical"], description: "Bend axis (default horizontal)." }
15711
+ },
15712
+ required: ["style"],
15713
+ additionalProperties: false
15714
+ },
15715
+ {
15716
+ type: "object",
15717
+ properties: {
15718
+ columns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns." },
15719
+ rows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows." },
15720
+ points: { type: "array", items: POINT2, minItems: 16, description: "The (3\xB7columns + 1) \xD7 (3\xB7rows + 1) control points row by row, in contents pixels (0..width \xD7 0..height is no warp)." }
15721
+ },
15722
+ required: ["columns", "rows", "points"],
15723
+ additionalProperties: false
15724
+ }
15725
+ ]
15726
+ },
15727
+ interpolation: { type: "string", enum: INTERPOLATIONS, description: "Resampling: nearest, bilinear or bicubic (default: as before, bicubic for new smart objects)." }
15728
+ },
15729
+ required: ["layerId"],
15730
+ additionalProperties: false
15731
+ },
15732
+ annotations: { title: "Set a smart object\u2019s transform", ...WRITE_IDEMPOTENT4 },
15733
+ handler: (args) => {
15734
+ const id = smartId(args.layerId);
15735
+ let warp;
15736
+ const w = args.warp;
15737
+ if (w === null) warp = null;
15738
+ else if (w && "style" in w) warp = { style: w.style, bend: w.bend ?? 50, horizontal: w.horizontal, vertical: w.vertical, orientation: w.orientation };
15739
+ else if (w) {
15740
+ const m = w;
15741
+ const want = (3 * m.columns + 1) * (3 * m.rows + 1);
15742
+ if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} \xD7 ${m.rows} mesh needs (3\xB7${m.columns} + 1) \xD7 (3\xB7${m.rows} + 1) = ${want} points, got ${m.points.length}.`);
15743
+ const knots = (n) => Array.from({ length: n + 1 }, (_, i) => i / n);
15744
+ warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };
15745
+ }
15746
+ const keys = ["corners", "matrix", "scaleX", "scaleY", "rotate", "skewX", "skewY", "x", "y", "warp", "interpolation"];
15747
+ if (!keys.some((k) => args[k] !== void 0)) throw new LayerOperationError(`Give something to set: ${keys.join(", ")}.`);
15748
+ const res = controller.setSmartObjectTransform(id, {
15749
+ corners: args.corners,
15750
+ matrix: args.matrix,
15751
+ scaleX: args.scaleX,
15752
+ scaleY: args.scaleY,
15753
+ rotate: args.rotate,
15754
+ skewX: args.skewX,
15755
+ skewY: args.skewY,
15756
+ x: args.x,
15757
+ y: args.y,
15758
+ warp,
15759
+ interpolation: args.interpolation
15760
+ });
15761
+ return { layerId: id, placement: info(id).placement, masksMoved: res.masksMoved, bounds: controller.getLayerBounds(id) };
15762
+ }
15763
+ };
15764
+ const replace = {
15765
+ name: "replace_smart_object_contents",
15766
+ title: "Replace a smart object\u2019s contents",
15767
+ description: "Layer \u203A Smart Objects \u203A Replace Contents: new raster contents from an image for the smart object and every linked instance (same contents id). Each instance keeps its scale and rotation about the contents\u2019 centre and its smart filters, and renders again. One undo step. Returns the contents id, the new size and the layers that changed.",
15768
+ inputSchema: {
15769
+ type: "object",
15770
+ properties: {
15771
+ layerId: LAYER_ID4,
15772
+ image: { type: "string", description: "The new contents: a PNG as a data URL or base64 (other formats and URLs need a browser)." },
15773
+ name: { type: "string", description: "File name of the contents (default the old name with .png)." }
15774
+ },
15775
+ required: ["layerId", "image"],
15776
+ additionalProperties: false
15777
+ },
15778
+ annotations: { title: "Replace a smart object\u2019s contents", ...DESTRUCTIVE5 },
15779
+ handler: async ({ layerId, image, name }) => controller.replaceSmartObjectContentsFromImage(smartId(layerId), imageArg(image), name)
15780
+ };
15781
+ const exportContents = {
15782
+ name: "export_smart_object_contents",
15783
+ title: "Export a smart object\u2019s contents",
15784
+ description: "Layer \u203A Smart Objects \u203A Export Contents: the contents as a file, base64: the file they came from while unedited (a PSB, PNG, JPEG\u2026), else a PNG for raster contents or a PSD / PSB of a nested document. Returns name, mimeType, byteLength and base64. Changes nothing.",
15785
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
15786
+ annotations: { title: "Export a smart object\u2019s contents", ...READ7 },
15787
+ handler: ({ layerId }) => {
15788
+ const f = controller.exportSmartObjectContents(smartId(layerId));
15789
+ return { name: f.name, mimeType: f.mimeType, byteLength: f.bytes.length, base64: bytesToBase64(f.bytes) };
15790
+ }
15791
+ };
15792
+ const copy = {
15793
+ name: "copy_smart_object",
15794
+ title: "Copy a smart object",
15795
+ description: "Duplicates a smart object directly above it. linked true (Duplicate Layer) shares the contents, so editing or replacing them changes both; linked false (Layer \u203A Smart Objects \u203A New Smart Object via Copy) gives the copy its own contents. Placement, filters, mask and effects are copied either way. One undo step. Returns the copy.",
15796
+ inputSchema: {
15797
+ type: "object",
15798
+ properties: {
15799
+ layerId: LAYER_ID4,
15800
+ linked: { type: "boolean", description: "true shares the contents with the original; false (default) makes independent contents." },
15801
+ name: { type: "string", description: 'Name of the copy (default "<name> copy").' }
15802
+ },
15803
+ required: ["layerId"],
15804
+ additionalProperties: false
15805
+ },
15806
+ annotations: { title: "Copy a smart object", ...WRITE6 },
15807
+ handler: ({ layerId, linked, name }) => {
15808
+ const id = controller.copySmartObject(smartId(layerId), linked === true, name);
15809
+ return { layer: layerSummary(id), contentsId: controller.getSmartObject(id).contents.id };
15810
+ }
15811
+ };
15812
+ const editContents = {
15813
+ name: "edit_smart_object_contents",
15814
+ title: "Edit a smart object\u2019s contents",
15815
+ description: 'Layer \u203A Smart Objects \u203A Edit Contents: opens the contents as a document of their own (a layer document, or raster contents as one pixel layer "Layer 0") with their own undo history. Every other tool (list_layers, paint, adjustments\u2026) then works inside the contents until close_smart_object_contents saves them back. Contents can nest. Returns the contents\u2019 size and layers.',
15816
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
15817
+ annotations: { title: "Edit a smart object\u2019s contents", ...WRITE6 },
15818
+ handler: ({ layerId }) => {
15819
+ const res = controller.editSmartObjectContents(smartId(layerId));
15820
+ const s = controller.getState();
15821
+ return { ...res, editing: controller.getEditingContents(), layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))) };
15822
+ }
15823
+ };
15824
+ const closeContents = {
15825
+ name: "close_smart_object_contents",
15826
+ title: "Close a smart object\u2019s contents",
15827
+ description: "Closes the contents opened with edit_smart_object_contents. save true (default) writes the edits back: the smart object and every linked instance render again, as one undo step in the document; save false drops them. Returns the smart object, whether it changed, the layers that changed and what is still open.",
15828
+ inputSchema: {
15829
+ type: "object",
15830
+ properties: { save: { type: "boolean", description: "true (default) saves the edits into the smart object; false discards them." } },
15831
+ additionalProperties: false
15832
+ },
15833
+ annotations: { title: "Close a smart object\u2019s contents", ...WRITE6 },
15834
+ handler: ({ save }) => ({ ...controller.closeSmartObjectContents(save !== false), editing: controller.getEditingContents() })
15835
+ };
15836
+ const addFilter = {
15837
+ name: "add_smart_filter",
15838
+ title: "Add a smart filter",
15839
+ description: "Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. One undo step. Returns the filter (with its id) and the stack.",
15840
+ inputSchema: {
15841
+ type: "object",
15842
+ properties: {
15843
+ layerId: LAYER_ID4,
15844
+ kind: { type: "string", enum: ["develop", "blur"], description: "Filter kind." },
15845
+ index: { type: "integer", minimum: 0, description: "Position in the stack, 0 = bottom, applied first (default the top)." },
15846
+ ...FILTER_SETTINGS
15847
+ },
15848
+ required: ["layerId", "kind"],
15849
+ additionalProperties: false
15850
+ },
15851
+ annotations: { title: "Add a smart filter", ...WRITE6 },
15852
+ handler: (args) => {
15853
+ const id = smartId(args.layerId);
15854
+ const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index);
15855
+ return { layerId: id, filter, smartFilters: info(id).smartFilters };
15856
+ }
15857
+ };
15858
+ const updateFilter = {
15859
+ name: "update_smart_filter",
15860
+ title: "Change a smart filter",
15861
+ description: "Changes one smart filter: its settings, enabled, blend or opacity (only the given values; the kind cannot change). A Photoshop filter kept from a PSD (kind psd) only takes enabled, blend and opacity. One undo step. Returns the filter and the stack.",
15862
+ inputSchema: {
15863
+ type: "object",
15864
+ properties: { layerId: LAYER_ID4, filterId: FILTER_ID, ...FILTER_SETTINGS },
15865
+ required: ["layerId", "filterId"],
15866
+ additionalProperties: false
15867
+ },
15868
+ annotations: { title: "Change a smart filter", ...WRITE_IDEMPOTENT4 },
15869
+ handler: (args) => {
15870
+ const id = smartId(args.layerId);
15871
+ const changes = settingsFrom(args);
15872
+ if (!Object.keys(changes).length) throw new LayerOperationError("Give at least one value to change.");
15873
+ const filter = controller.updateSmartFilter(id, args.filterId, changes);
15874
+ return { layerId: id, filter, smartFilters: info(id).smartFilters };
15875
+ }
15876
+ };
15877
+ const removeFilter = {
15878
+ name: "remove_smart_filter",
15879
+ title: "Remove a smart filter",
15880
+ description: "Removes one smart filter (the filter mask goes with the last one). One undo step. Returns the remaining stack.",
15881
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID4, filterId: FILTER_ID }, required: ["layerId", "filterId"], additionalProperties: false },
15882
+ annotations: { title: "Remove a smart filter", ...DESTRUCTIVE5 },
15883
+ handler: ({ layerId, filterId }) => {
15884
+ const id = smartId(layerId);
15885
+ controller.removeSmartFilter(id, filterId);
15886
+ return { layerId: id, smartFilters: info(id).smartFilters };
15887
+ }
15888
+ };
15889
+ const moveFilter = {
15890
+ name: "move_smart_filter",
15891
+ title: "Reorder a smart filter",
15892
+ description: "Moves one smart filter to a position in the stack (0 = bottom, applied first), as dragging it in the Layers panel. One undo step. Returns the stack in its new order.",
15893
+ inputSchema: {
15894
+ type: "object",
15895
+ properties: { layerId: LAYER_ID4, filterId: FILTER_ID, index: { type: "integer", minimum: 0, description: "New position, 0 = bottom (applied first)." } },
15896
+ required: ["layerId", "filterId", "index"],
15897
+ additionalProperties: false
15898
+ },
15899
+ annotations: { title: "Reorder a smart filter", ...WRITE_IDEMPOTENT4 },
15900
+ handler: ({ layerId, filterId, index }) => {
15901
+ const id = smartId(layerId);
15902
+ controller.moveSmartFilter(id, filterId, index);
15903
+ return { layerId: id, smartFilters: info(id).smartFilters };
15904
+ }
15905
+ };
15906
+ const setEnabled = {
15907
+ name: "set_smart_filters_enabled",
15908
+ title: "Turn smart filters on or off",
15909
+ description: "Layer \u203A Smart Filter \u203A Disable / Enable Smart Filters: switches the whole stack; each filter keeps its own enabled flag. One undo step. Returns the stack.",
15910
+ inputSchema: {
15911
+ type: "object",
15912
+ properties: { layerId: LAYER_ID4, enabled: { type: "boolean", description: "false hides every smart filter; true shows them again." } },
15913
+ required: ["layerId", "enabled"],
15914
+ additionalProperties: false
15915
+ },
15916
+ annotations: { title: "Turn smart filters on or off", ...WRITE_IDEMPOTENT4 },
15917
+ handler: ({ layerId, enabled }) => {
15918
+ const id = smartId(layerId);
15919
+ controller.setSmartFiltersEnabled(id, enabled === true);
15920
+ return { layerId: id, smartFilters: info(id).smartFilters };
15921
+ }
15922
+ };
15923
+ const setMask = {
15924
+ name: "set_smart_filter_mask",
15925
+ title: "Set the smart filter mask",
15926
+ description: "Sets the one filter mask of a smart object\u2019s smart filters, which says where the filtered result shows over the unfiltered contents: from reveal-all (white), hide-all (black), selection (the selection\u2019s coverage) or none (remove it); keep changes only its properties. Paint it with paint_stroke channel filter-mask. One undo step. Returns the mask summary.",
15927
+ inputSchema: {
15928
+ type: "object",
15929
+ properties: {
15930
+ layerId: LAYER_ID4,
15931
+ from: { type: "string", enum: ["reveal-all", "hide-all", "selection", "none", "keep"], description: "What the mask starts from; none removes it; keep only changes the properties below." },
15932
+ density: { type: "number", minimum: 0, maximum: 1, description: "How much hidden areas hide, 0..1 (default 1)." },
15933
+ feather: { type: "number", minimum: 0, maximum: 1e3, description: "Gaussian softening in px (default 0)." },
15934
+ enabled: { type: "boolean", description: "false keeps the mask but ignores it." },
15935
+ linked: { type: "boolean", description: "false keeps the mask in place when the smart object is transformed or moved (default linked)." }
15936
+ },
15937
+ required: ["layerId", "from"],
15938
+ additionalProperties: false
15939
+ },
15940
+ annotations: { title: "Set the smart filter mask", ...WRITE6 },
15941
+ handler: (args) => {
15942
+ const id = smartId(args.layerId);
15943
+ const props = {};
15944
+ for (const k of ["density", "feather", "enabled", "linked"]) if (args[k] !== void 0) props[k] = args[k];
15945
+ const mask = controller.setSmartFilterMask(id, args.from, props);
15946
+ return { layerId: id, filterMask: maskSummary(mask ?? void 0) };
15947
+ }
15948
+ };
15949
+ return [getSmart, convert, rasterize, setTransform, replace, exportContents, copy, editContents, closeContents, addFilter, updateFilter, removeFilter, moveFilter, setEnabled, setMask];
15950
+ }
15951
+
14228
15952
  // src/image-editor/tools.ts
14229
15953
  var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
14230
15954
  function adjustmentsSummary(adjustments) {
@@ -14463,6 +16187,7 @@ function createImageEditorToolProvider(controller, options = {}) {
14463
16187
  ...createImageEditTools(controller),
14464
16188
  ...createImageLayerTools(controller),
14465
16189
  ...createImageTransformTools(controller),
16190
+ ...createImageSmartObjectTools(controller),
14466
16191
  ...createImageEffectTools(controller),
14467
16192
  ...createImagePaintTools(controller),
14468
16193
  ...createPigmentTools(),
@@ -14801,6 +16526,8 @@ function layerBadge(layer) {
14801
16526
  return "grp";
14802
16527
  case "annotation":
14803
16528
  return layer.annotation.type === "text" ? "T" : "ann";
16529
+ case "smart":
16530
+ return "so";
14804
16531
  }
14805
16532
  }
14806
16533
  function dropZone(fraction, isGroup) {
@@ -14840,7 +16567,11 @@ function layerMenu(controller) {
14840
16567
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
14841
16568
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
14842
16569
  { action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
14843
- { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
16570
+ { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
16571
+ { action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
16572
+ { action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
16573
+ { action: "smart-via-copy", label: "New Smart Object via Copy", enabled: active?.type === "smart" },
16574
+ { action: "add-filter-mask", label: "Add Smart Filter Mask", enabled: active?.type === "smart" && !active.smartFilters?.mask },
14844
16575
  { action: "toggle-clip", label: active?.clipped ? "Release Clipping Mask" : "Create Clipping Mask", enabled: Boolean(loc && loc.index > 0) }
14845
16576
  ];
14846
16577
  }
@@ -14901,6 +16632,18 @@ async function runLayerMenuAction(controller, action) {
14901
16632
  case "rasterize":
14902
16633
  if (id) await controller.rasterizeLayer(id);
14903
16634
  break;
16635
+ case "convert-smart":
16636
+ if (id) controller.convertToSmartObject([id]);
16637
+ break;
16638
+ case "edit-contents":
16639
+ if (id) controller.editSmartObjectContents(id);
16640
+ break;
16641
+ case "smart-via-copy":
16642
+ if (id) controller.copySmartObject(id, false);
16643
+ break;
16644
+ case "add-filter-mask":
16645
+ if (id) controller.setSmartFilterMask(id, "reveal-all");
16646
+ break;
14904
16647
  case "toggle-clip":
14905
16648
  if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });
14906
16649
  break;
@@ -14910,6 +16653,17 @@ async function runLayerMenuAction(controller, action) {
14910
16653
  return err instanceof Error ? err.message : String(err);
14911
16654
  }
14912
16655
  }
16656
+ function smartFilterRows(layer) {
16657
+ if (layer.type !== "smart") return [];
16658
+ const list2 = layer.smartFilters?.filters ?? [];
16659
+ const rows = list2.map((f, index) => ({
16660
+ filter: f,
16661
+ index,
16662
+ drawn: f.kind !== "psd",
16663
+ label: f.kind === "psd" ? f.name.replace(/&/g, "").replace(/\.\.\.$/, "") : f.kind === "blur" ? `Blur (${f.radius} px)` : "Develop"
16664
+ }));
16665
+ return rows.reverse();
16666
+ }
14913
16667
 
14914
16668
  // src/image-editor/layers/effects-ui.ts
14915
16669
  function layerEffectsPanel(layer) {
@@ -15267,7 +17021,7 @@ var FreeTransformTool = class {
15267
17021
  this.corners = s.geometry.corners;
15268
17022
  this.refBox = { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 };
15269
17023
  this.modeValue = "scale";
15270
- this.mesh = null;
17024
+ this.mesh = s.geometry.warp ?? null;
15271
17025
  this.warpSettings = { style: "custom", bend: 50, horizontal: 0, vertical: 0, orientation: "horizontal", columns: 1, rows: 1 };
15272
17026
  this.typed = {};
15273
17027
  this.error = null;
@@ -15770,6 +17524,7 @@ export {
15770
17524
  SELECTION_MODE_LABELS,
15771
17525
  SELECTION_MODIFY_OPS,
15772
17526
  SELECTION_TOOLS,
17527
+ SMART_RENDER_MARGIN,
15773
17528
  SUPPORTED_ADJUSTMENT_KEYS,
15774
17529
  SelectionGesture,
15775
17530
  StrokeCoverage,
@@ -15790,8 +17545,10 @@ export {
15790
17545
  XYZ_D50_TO_SRGB,
15791
17546
  ZlibError,
15792
17547
  activeAdjustmentLayers,
17548
+ activeSmartFilters,
15793
17549
  addLayerEffect,
15794
17550
  addLayerFilter,
17551
+ addSmartFilter,
15795
17552
  addToHistogram,
15796
17553
  adjustmentKindLabel,
15797
17554
  adjustmentLayersKey,
@@ -15814,6 +17571,7 @@ export {
15814
17571
  applyProcess2Adjustments,
15815
17572
  applyRegionBlur,
15816
17573
  applyRegionMosaic,
17574
+ applySmartFilters,
15817
17575
  applyStrokeToTile,
15818
17576
  backgroundLayer,
15819
17577
  base64ToBytes,
@@ -15853,9 +17611,13 @@ export {
15853
17611
  computeHistogram,
15854
17612
  computeSelectionBounds,
15855
17613
  contentBounds,
17614
+ contentsBox,
17615
+ contentsSurface,
15856
17616
  contractCoverage,
15857
17617
  controlPosition,
15858
17618
  controlTrackBackground,
17619
+ convertToSmartObject,
17620
+ copySmartObject,
15859
17621
  cornersFromMatrix,
15860
17622
  coverageTileCoord,
15861
17623
  coverageToByte,
@@ -15869,6 +17631,7 @@ export {
15869
17631
  createImagePaintTools,
15870
17632
  createImagePsdTools,
15871
17633
  createImageSelectionTools,
17634
+ createImageSmartObjectTools,
15872
17635
  createImageTransformTools,
15873
17636
  createPigment,
15874
17637
  createPigmentTools,
@@ -15906,6 +17669,7 @@ export {
15906
17669
  developFromAdjustments,
15907
17670
  developTarget,
15908
17671
  distanceTransform,
17672
+ documentContents,
15909
17673
  downloadPsd,
15910
17674
  dragWarpMeshPoint,
15911
17675
  dropPlacement,
@@ -15932,6 +17696,7 @@ export {
15932
17696
  exportPsd,
15933
17697
  featherCoverage,
15934
17698
  fillRect,
17699
+ filterMaskCoverage,
15935
17700
  findAdjustmentControl,
15936
17701
  findLayer,
15937
17702
  findLayerControl,
@@ -15956,6 +17721,7 @@ export {
15956
17721
  handlePaintShortcut,
15957
17722
  hasActiveEffects,
15958
17723
  hasResampledLegacyMask,
17724
+ hasSurface,
15959
17725
  hasWarp,
15960
17726
  hashMaskContent,
15961
17727
  hashNoise,
@@ -15982,12 +17748,14 @@ export {
15982
17748
  inflateRaw,
15983
17749
  insertCurvePoint,
15984
17750
  insertLayer,
17751
+ instancesOf,
15985
17752
  invertHomography,
15986
17753
  invertMatrix3,
15987
17754
  invertSelection,
15988
17755
  isActiveFilter,
15989
17756
  isAdjustmentKind,
15990
17757
  isBlendMode,
17758
+ isContentsDecoded,
15991
17759
  isDefaultDepthMask,
15992
17760
  isIdentityTransform,
15993
17761
  isInterpolation,
@@ -16049,9 +17817,11 @@ export {
16049
17817
  modifySelection,
16050
17818
  moveCurvePoint,
16051
17819
  moveLayer,
17820
+ moveSmartFilter,
16052
17821
  mulMatrix3,
16053
17822
  needsLegacyBase,
16054
17823
  neutralHueRange,
17824
+ newContentsId,
16055
17825
  newEffectId,
16056
17826
  newLayerId,
16057
17827
  normalizeAdjustmentLayer,
@@ -16063,11 +17833,14 @@ export {
16063
17833
  normalizeLayerEffects,
16064
17834
  normalizeLayerMask,
16065
17835
  normalizeMixer,
17836
+ normalizePlacement,
16066
17837
  normalizeProcessVersion,
16067
17838
  normalizeRect,
16068
17839
  normalizeRefineEdgeParams,
17840
+ normalizeSmartFilter,
16069
17841
  normalizeStoredAdjustments,
16070
17842
  offsetLayer,
17843
+ offsetSmartObject,
16071
17844
  openPsdFile,
16072
17845
  packRgba,
16073
17846
  parseAdjustmentBlock,
@@ -16083,7 +17856,11 @@ export {
16083
17856
  photoFilterMatrix,
16084
17857
  pixelLayerBounds,
16085
17858
  pixelLayers,
17859
+ placeContents,
16086
17860
  placeLayer,
17861
+ placementAt,
17862
+ placementBounds,
17863
+ placementGeometry,
16087
17864
  pngDataUrl,
16088
17865
  pointerPressure,
16089
17866
  polygonCoverage,
@@ -16102,8 +17879,10 @@ export {
16102
17879
  pullWarpMesh,
16103
17880
  quickMaskPixels,
16104
17881
  rangeLightness,
17882
+ rasterContents,
16105
17883
  rasterMaskFromLegacy,
16106
17884
  rasterizeDab,
17885
+ rasterizeSmartObject,
16107
17886
  readAdjustmentControls,
16108
17887
  readPixel,
16109
17888
  readPsd,
@@ -16121,10 +17900,15 @@ export {
16121
17900
  removeLayer,
16122
17901
  removeLayerEffect,
16123
17902
  removeLayerFilter,
17903
+ removeSmartFilter,
16124
17904
  renderLayerEffects,
16125
17905
  renderLayeredBase,
17906
+ renderSmartObject,
17907
+ replaceContents,
16126
17908
  requireLayer,
17909
+ requireSmartObject,
16127
17910
  requireUnlocked,
17911
+ rerender,
16128
17912
  resizePixels,
16129
17913
  resizeSelection,
16130
17914
  resizeWarpMesh,
@@ -16134,6 +17918,7 @@ export {
16134
17918
  rgbToHsl,
16135
17919
  rgbToReflectance,
16136
17920
  rgbUnitToHex,
17921
+ runLayerFilters,
16137
17922
  runLayerMenuAction,
16138
17923
  sampleLut,
16139
17924
  sampleToneLut,
@@ -16146,6 +17931,7 @@ export {
16146
17931
  selectionToCoverage,
16147
17932
  selectionToPixels,
16148
17933
  selectionValueAt,
17934
+ serializeContents,
16149
17935
  serializeEffects,
16150
17936
  serializeLayer,
16151
17937
  serializeLayers,
@@ -16159,7 +17945,12 @@ export {
16159
17945
  setLayerProperties,
16160
17946
  setLum,
16161
17947
  setPigmentColor,
17948
+ setSmartFilterMask,
17949
+ setSmartFiltersEnabled,
17950
+ setSmartPlacement,
16162
17951
  shiftCoverage,
17952
+ smartFilterRows,
17953
+ smartObjectLayers,
16163
17954
  smoothCoverage,
16164
17955
  smoothPoints,
16165
17956
  solidSurface,
@@ -16197,6 +17988,7 @@ export {
16197
17988
  updateLayerEffect,
16198
17989
  updateLayerFilter,
16199
17990
  updateLayerMaskProps,
17991
+ updateSmartFilter,
16200
17992
  uvToXy,
16201
17993
  validateEffectGradient,
16202
17994
  validateLayerEffect,
@@ -16213,6 +18005,7 @@ export {
16213
18005
  wbMatrix,
16214
18006
  whiteBalanceFromSample,
16215
18007
  whiteBalanceMatrix,
18008
+ withFilterMaskSurface,
16216
18009
  withNewIds,
16217
18010
  writeAdjustmentBlock,
16218
18011
  writeLfx2,