@pixerate/editor 0.188.0 → 0.190.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-IGMNJM2H.js → chunk-7ZWLDVFH.js} +979 -753
- package/dist/{chunk-JYV4R62M.cjs → chunk-T4U4O6DS.cjs} +1013 -787
- package/dist/image-editor/controller.d.cts +68 -1
- package/dist/image-editor/controller.d.ts +68 -1
- package/dist/image-editor/free-transform.d.cts +3 -6
- package/dist/image-editor/free-transform.d.ts +3 -6
- package/dist/image-editor/index.cjs +1867 -1413
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +869 -415
- package/dist/image-editor/layer-tools.d.cts +1 -1
- package/dist/image-editor/layer-tools.d.ts +1 -1
- package/dist/image-editor/layers/compositor.d.cts +3 -0
- package/dist/image-editor/layers/compositor.d.ts +3 -0
- package/dist/image-editor/layers/model.d.cts +23 -0
- package/dist/image-editor/layers/model.d.ts +23 -0
- package/dist/image-editor/layers/paint-ui.d.cts +2 -1
- package/dist/image-editor/layers/paint-ui.d.ts +2 -1
- package/dist/image-editor/layers/panel.d.cts +14 -3
- package/dist/image-editor/layers/panel.d.ts +14 -3
- package/dist/image-editor/layers/serialize.d.cts +11 -0
- package/dist/image-editor/layers/serialize.d.ts +11 -0
- package/dist/image-editor/layers/shape.d.cts +5 -0
- package/dist/image-editor/layers/shape.d.ts +5 -0
- package/dist/image-editor/layers/vector-mask.d.cts +51 -0
- package/dist/image-editor/layers/vector-mask.d.ts +51 -0
- package/dist/image-editor/psd/import.d.cts +0 -7
- package/dist/image-editor/psd/import.d.ts +0 -7
- package/dist/image-editor/renderer.d.cts +6 -0
- package/dist/image-editor/renderer.d.ts +6 -0
- package/dist/image-editor/selection/gesture.d.cts +10 -0
- package/dist/image-editor/selection/gesture.d.ts +10 -0
- package/dist/image-editor/selection/layer-mask.d.cts +6 -0
- package/dist/image-editor/selection/layer-mask.d.ts +6 -0
- package/dist/image-editor/types.d.cts +8 -0
- package/dist/image-editor/types.d.ts +8 -0
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +7 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +10 -3
|
@@ -1421,6 +1421,13 @@ function modifySelection(mask, op, radius) {
|
|
|
1421
1421
|
}
|
|
1422
1422
|
|
|
1423
1423
|
// src/image-editor/selection/layer-mask.ts
|
|
1424
|
+
function layerMaskSelection(layer, width, height) {
|
|
1425
|
+
if (!layer.mask || layer.mask.source) return null;
|
|
1426
|
+
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
1427
|
+
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
1428
|
+
const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });
|
|
1429
|
+
return createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));
|
|
1430
|
+
}
|
|
1424
1431
|
var MAX_SIDE2 = 1 << 15;
|
|
1425
1432
|
function normalizeLayerMask(raw) {
|
|
1426
1433
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -2582,192 +2589,6 @@ function removeAlpha(imageData) {
|
|
|
2582
2589
|
}
|
|
2583
2590
|
}
|
|
2584
2591
|
|
|
2585
|
-
// src/text/types.ts
|
|
2586
|
-
var TEXT_ALIGNS = ["left", "center", "right", "justify"];
|
|
2587
|
-
|
|
2588
|
-
// src/image-editor/layers/model.ts
|
|
2589
|
-
var MAX_EFFECT_ANIMATION_MS = 6e4;
|
|
2590
|
-
var SHAPE_OPERATIONS = ["combine", "subtract", "intersect", "exclude"];
|
|
2591
|
-
var LIVE_SHAPE_KINDS = ["rect", "ellipse", "polygon", "star", "line"];
|
|
2592
|
-
var idCounter = 0;
|
|
2593
|
-
function newLayerId(prefix = "layer") {
|
|
2594
|
-
return `${prefix}_${Date.now().toString(36)}_${(++idCounter).toString(36)}`;
|
|
2595
|
-
}
|
|
2596
|
-
function pixelLayers(layers) {
|
|
2597
|
-
return flattenLayers(layers).filter((l) => l.type === "pixel");
|
|
2598
|
-
}
|
|
2599
|
-
function developFilter(layer) {
|
|
2600
|
-
return layer.filters?.find((f) => f.kind === "develop");
|
|
2601
|
-
}
|
|
2602
|
-
function blurFilter(layer) {
|
|
2603
|
-
return layer.filters?.find((f) => f.kind === "blur");
|
|
2604
|
-
}
|
|
2605
|
-
function isActiveFilter(f) {
|
|
2606
|
-
if (!f.enabled) return false;
|
|
2607
|
-
if (f.kind === "blur") return Math.min(Math.round(f.radius), 40) >= 1;
|
|
2608
|
-
return Boolean(f.brightness || f.contrast || f.saturation || f.exposure || f.temperature || f.tint);
|
|
2609
|
-
}
|
|
2610
|
-
function isSpatialFilter(f) {
|
|
2611
|
-
return f.kind === "blur";
|
|
2612
|
-
}
|
|
2613
|
-
function pixelLayerBounds(layer, surface = layer.surface) {
|
|
2614
|
-
const b = surface.bounds;
|
|
2615
|
-
if (!b) return null;
|
|
2616
|
-
return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height };
|
|
2617
|
-
}
|
|
2618
|
-
function layerTypeLabel(type) {
|
|
2619
|
-
return type === "pixel" ? "Pixel layer" : type === "adjustment" ? "Adjustment layer" : type === "group" ? "Group" : type === "smart" ? "Smart object" : type === "shape" ? "Shape layer" : type === "type" ? "Type layer" : "Annotation";
|
|
2620
|
-
}
|
|
2621
|
-
function hasSurface(layer) {
|
|
2622
|
-
return layer.type === "pixel" || layer.type === "smart" || layer.type === "shape" || layer.type === "type";
|
|
2623
|
-
}
|
|
2624
|
-
function isRenderedLayer(layer) {
|
|
2625
|
-
return layer.type === "smart" || layer.type === "shape" || layer.type === "type";
|
|
2626
|
-
}
|
|
2627
|
-
function shapeLayers(layers) {
|
|
2628
|
-
return flattenLayers(layers).filter((l) => l.type === "shape");
|
|
2629
|
-
}
|
|
2630
|
-
function smartObjectLayers(layers) {
|
|
2631
|
-
return flattenLayers(layers).filter((l) => l.type === "smart");
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
// src/image-editor/layers/compositor.ts
|
|
2635
|
-
var legacyViewCache = /* @__PURE__ */ new WeakMap();
|
|
2636
|
-
function adjustmentNodeAsLayer(node) {
|
|
2637
|
-
let view = legacyViewCache.get(node);
|
|
2638
|
-
if (!view) {
|
|
2639
|
-
view = {
|
|
2640
|
-
id: node.id,
|
|
2641
|
-
kind: node.kind,
|
|
2642
|
-
enabled: node.visible,
|
|
2643
|
-
opacity: node.opacity,
|
|
2644
|
-
blend: node.blend,
|
|
2645
|
-
params: node.params
|
|
2646
|
-
};
|
|
2647
|
-
legacyViewCache.set(node, view);
|
|
2648
|
-
}
|
|
2649
|
-
return view;
|
|
2650
|
-
}
|
|
2651
|
-
function developAdjustments(f) {
|
|
2652
|
-
return {
|
|
2653
|
-
brightness: f.brightness,
|
|
2654
|
-
contrast: f.contrast,
|
|
2655
|
-
saturation: f.saturation,
|
|
2656
|
-
exposure: f.exposure,
|
|
2657
|
-
temperature: f.temperature,
|
|
2658
|
-
tint: f.tint ?? 0,
|
|
2659
|
-
blur: 0,
|
|
2660
|
-
opacity: 1,
|
|
2661
|
-
process: f.process
|
|
2662
|
-
};
|
|
2663
|
-
}
|
|
2664
|
-
function applyPixelFilters(data, filters) {
|
|
2665
|
-
for (const f of filters) {
|
|
2666
|
-
if (!isActiveFilter(f) || f.kind !== "develop") continue;
|
|
2667
|
-
const adj = developAdjustments(f);
|
|
2668
|
-
if (normalizeProcessVersion(adj.process) === LATEST_PROCESS_VERSION) applyProcess2Adjustments({ data }, adj);
|
|
2669
|
-
else applyLegacyAdjustments({ data }, adj);
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
function depthMaskAlpha(depth, options) {
|
|
2673
|
-
const { mode, depthRange, softness, invert } = options;
|
|
2674
|
-
let v = invert ? 255 - depth : depth;
|
|
2675
|
-
const soft = Math.max(1, softness || 10);
|
|
2676
|
-
const [lo, hi] = depthRange;
|
|
2677
|
-
if (mode === "threshold") {
|
|
2678
|
-
if (v >= hi + soft) return 0;
|
|
2679
|
-
if (v > hi) return Math.round((hi + soft - v) / soft * 255);
|
|
2680
|
-
if (v >= lo) return 255;
|
|
2681
|
-
if (v < lo - soft) return 0;
|
|
2682
|
-
return Math.round((v - (lo - soft)) / soft * 255);
|
|
2683
|
-
}
|
|
2684
|
-
v += lo;
|
|
2685
|
-
return v < 0 ? 0 : v > 255 ? 255 : v;
|
|
2686
|
-
}
|
|
2687
|
-
function runLayerFilters(data, width, height, filters) {
|
|
2688
|
-
for (const f of filters) {
|
|
2689
|
-
if (!isActiveFilter(f)) continue;
|
|
2690
|
-
if (f.kind === "develop") applyPixelFilters(data, [f]);
|
|
2691
|
-
else if (f.kind === "blur") applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
function runLayerFiltersFloat(data, width, height, filters, linear) {
|
|
2695
|
-
for (const f of filters) {
|
|
2696
|
-
if (!isActiveFilter(f)) continue;
|
|
2697
|
-
if (f.kind === "blur") applyBoxBlurFloat({ width, height, data }, Math.min(Math.round(f.radius), 40));
|
|
2698
|
-
else if (f.kind === "develop") {
|
|
2699
|
-
const adj = developAdjustments(f);
|
|
2700
|
-
if (normalizeProcessVersion(adj.process) === LATEST_PROCESS_VERSION) applyProcess2Float(data, adj, linear);
|
|
2701
|
-
else applyLegacyFloat(data, adj, linear);
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
function filterReach(filters) {
|
|
2706
|
-
let r = 0;
|
|
2707
|
-
for (const f of filters) if (isActiveFilter(f) && f.kind === "blur") r += Math.min(Math.round(f.radius), 40);
|
|
2708
|
-
return r;
|
|
2709
|
-
}
|
|
2710
|
-
var filterSpecCache = /* @__PURE__ */ new WeakMap();
|
|
2711
|
-
function pixelFilterSpec(layer) {
|
|
2712
|
-
const list2 = layer.filters;
|
|
2713
|
-
if (!list2?.length) return null;
|
|
2714
|
-
if (filterSpecCache.has(list2)) return filterSpecCache.get(list2);
|
|
2715
|
-
const filters = list2.filter(isActiveFilter);
|
|
2716
|
-
let spec = null;
|
|
2717
|
-
if (filters.length) {
|
|
2718
|
-
const spatial = filters.some(isSpatialFilter);
|
|
2719
|
-
spec = {
|
|
2720
|
-
key: JSON.stringify(filters),
|
|
2721
|
-
spatial,
|
|
2722
|
-
apply: (data, width, height) => runLayerFilters(data, width, height, filters)
|
|
2723
|
-
};
|
|
2724
|
-
}
|
|
2725
|
-
filterSpecCache.set(list2, spec);
|
|
2726
|
-
return spec;
|
|
2727
|
-
}
|
|
2728
|
-
var PHOTO_PROFILE = {
|
|
2729
|
-
isFilterLayer: (l) => l.type === "adjustment",
|
|
2730
|
-
applyFilterRun: (layers, acc, _r, masks) => {
|
|
2731
|
-
const views = layers.map(adjustmentNodeAsLayer);
|
|
2732
|
-
applyAdjustmentLayers({ data: acc }, views, masks);
|
|
2733
|
-
},
|
|
2734
|
-
contentTile: (l, r, sources) => l.type === "annotation" ? sources.rasterizeAnnotation?.(l.annotation, r) ?? null : null,
|
|
2735
|
-
/** Annotation layers draw with their annotation's blend mode unless the layer sets one. */
|
|
2736
|
-
contentBlend: (l) => l.type === "annotation" && l.blend === "normal" && l.annotation.blend ? l.annotation.blend : l.blend,
|
|
2737
|
-
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,
|
|
2738
|
-
pixelFilters: (l) => l.type === "pixel" ? pixelFilterSpec(l) : null,
|
|
2739
|
-
/**
|
|
2740
|
-
* A smart object draws its rendered pixels (contents → placement → smart filters) like a pixel
|
|
2741
|
-
* layer, and a shape layer the pixels rendered from its paths.
|
|
2742
|
-
*/
|
|
2743
|
-
isPixelLike: (l) => l.type === "smart" || l.type === "shape" || l.type === "type",
|
|
2744
|
-
maskSourcePlane: (source, W, H, sources) => {
|
|
2745
|
-
const opts = source;
|
|
2746
|
-
const depth = sources.depthMap?.(opts) ?? null;
|
|
2747
|
-
if (!depth || depth.width !== W || depth.height !== H) return null;
|
|
2748
|
-
const v = new Float32Array(W * H);
|
|
2749
|
-
for (let i = 0; i < v.length; i++) v[i] = depthMaskAlpha(depth.data[i * 4], opts) / 255;
|
|
2750
|
-
return v;
|
|
2751
|
-
}
|
|
2752
|
-
};
|
|
2753
|
-
var LayerCompositor = class extends RasterCompositor {
|
|
2754
|
-
constructor(options = {}) {
|
|
2755
|
-
super(PHOTO_PROFILE, options);
|
|
2756
|
-
}
|
|
2757
|
-
};
|
|
2758
|
-
function compositeDeep(layers, rect, sources, depth) {
|
|
2759
|
-
const deep = new DeepCompositor(new LayerCompositor(), PHOTO_PROFILE, {
|
|
2760
|
-
applyFilterRunDeep: (run, acc, _r, masks) => applyAdjustmentLayersFloat(acc, run.map(adjustmentNodeAsLayer), masks),
|
|
2761
|
-
pixelFiltersDeep: (l) => {
|
|
2762
|
-
if (l.type !== "pixel") return null;
|
|
2763
|
-
const filters = (l.filters ?? []).filter(isActiveFilter);
|
|
2764
|
-
if (!filters.length) return null;
|
|
2765
|
-
return { reach: filterReach(filters), apply: (data, width, height, linear) => runLayerFiltersFloat(data, width, height, filters, linear) };
|
|
2766
|
-
}
|
|
2767
|
-
}, depth);
|
|
2768
|
-
return deep.composite(layers, rect, sources);
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
2592
|
// src/image-editor/layers/vector-raster.ts
|
|
2772
2593
|
var FLATNESS = 0.01;
|
|
2773
2594
|
function flattenPath(path, tolerance = FLATNESS) {
|
|
@@ -3101,439 +2922,143 @@ function combineCoverage(a, b, op, n) {
|
|
|
3101
2922
|
return out;
|
|
3102
2923
|
}
|
|
3103
2924
|
|
|
3104
|
-
// src/
|
|
3105
|
-
var
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
if (err instanceof LayerOperationError && err.userMessage) return err.userMessage;
|
|
3115
|
-
return err instanceof Error ? err.message : String(err);
|
|
3116
|
-
}
|
|
3117
|
-
function isPaintableLayer(layer) {
|
|
3118
|
-
return layer.type === "pixel" && layer.visible && !isLayerLocked(layer, "pixels");
|
|
2925
|
+
// src/text/types.ts
|
|
2926
|
+
var TEXT_ALIGNS = ["left", "center", "right", "justify"];
|
|
2927
|
+
|
|
2928
|
+
// src/image-editor/layers/model.ts
|
|
2929
|
+
var MAX_EFFECT_ANIMATION_MS = 6e4;
|
|
2930
|
+
var SHAPE_OPERATIONS = ["combine", "subtract", "intersect", "exclude"];
|
|
2931
|
+
var LIVE_SHAPE_KINDS = ["rect", "ellipse", "polygon", "star", "line"];
|
|
2932
|
+
var idCounter = 0;
|
|
2933
|
+
function newLayerId(prefix = "layer") {
|
|
2934
|
+
return `${prefix}_${Date.now().toString(36)}_${(++idCounter).toString(36)}`;
|
|
3119
2935
|
}
|
|
3120
|
-
function
|
|
3121
|
-
|
|
3122
|
-
for (let i = pixels.length - 1; i >= 0; i--) if (isPaintableLayer(pixels[i])) return pixels[i].id;
|
|
3123
|
-
return pixels[pixels.length - 1]?.id ?? null;
|
|
2936
|
+
function pixelLayers(layers) {
|
|
2937
|
+
return flattenLayers(layers).filter((l) => l.type === "pixel");
|
|
3124
2938
|
}
|
|
3125
|
-
function
|
|
3126
|
-
|
|
3127
|
-
const at = before.findIndex((l) => l.id === removedId);
|
|
3128
|
-
for (let i = at - 1; i >= 0; i--) {
|
|
3129
|
-
const loc = findLayer(next, before[i].id);
|
|
3130
|
-
if (loc && isPaintableLayer(loc.layer)) return loc.layer.id;
|
|
3131
|
-
}
|
|
3132
|
-
return defaultActiveLayerId(next);
|
|
2939
|
+
function developFilter(layer) {
|
|
2940
|
+
return layer.filters?.find((f) => f.kind === "develop");
|
|
3133
2941
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
};
|
|
3137
|
-
function requireLayer(layers, id) {
|
|
3138
|
-
const loc = findLayer(layers, id);
|
|
3139
|
-
if (!loc) {
|
|
3140
|
-
const ids = flattenLayers(layers).map((l) => l.id);
|
|
3141
|
-
fail(`Unknown layer "${id}". ${ids.length ? `Layers: ${ids.join(", ")}.` : "The document has no layers."} Call list_layers for ids.`);
|
|
3142
|
-
}
|
|
3143
|
-
return loc;
|
|
2942
|
+
function blurFilter(layer) {
|
|
2943
|
+
return layer.filters?.find((f) => f.kind === "blur");
|
|
3144
2944
|
}
|
|
3145
|
-
function
|
|
3146
|
-
if (
|
|
3147
|
-
if (
|
|
2945
|
+
function isActiveFilter(f) {
|
|
2946
|
+
if (!f.enabled) return false;
|
|
2947
|
+
if (f.kind === "blur") return Math.min(Math.round(f.radius), 40) >= 1;
|
|
2948
|
+
return Boolean(f.brightness || f.contrast || f.saturation || f.exposure || f.temperature || f.tint);
|
|
3148
2949
|
}
|
|
3149
|
-
function
|
|
3150
|
-
|
|
3151
|
-
if (given > 1) fail("Give only one of aboveId, belowId and parentId.");
|
|
3152
|
-
if (placement.aboveId || placement.belowId) {
|
|
3153
|
-
const ref = requireLayer(layers, placement.aboveId ?? placement.belowId);
|
|
3154
|
-
return insertLayer(layers, layer, ref.parent?.id ?? null, ref.index + (placement.aboveId ? 1 : 0));
|
|
3155
|
-
}
|
|
3156
|
-
if (placement.parentId) {
|
|
3157
|
-
const ref = requireLayer(layers, placement.parentId);
|
|
3158
|
-
if (ref.layer.type !== "group") fail(`Layer "${placement.parentId}" is not a group; parentId must be a group id.`);
|
|
3159
|
-
return insertLayer(layers, layer, ref.layer.id, placement.index);
|
|
3160
|
-
}
|
|
3161
|
-
return insertLayer(layers, layer, null, placement.index ?? defaultIndex);
|
|
2950
|
+
function isSpatialFilter(f) {
|
|
2951
|
+
return f.kind === "blur";
|
|
3162
2952
|
}
|
|
3163
|
-
function
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
return
|
|
3167
|
-
}
|
|
3168
|
-
function moveLayer(layers, id, placement) {
|
|
3169
|
-
const loc = requireLayer(layers, id);
|
|
3170
|
-
requireUnlocked(loc.layer, "all", "move it");
|
|
3171
|
-
const target = placement.aboveId ?? placement.belowId ?? placement.parentId;
|
|
3172
|
-
if (target === id) fail("A layer cannot be placed relative to itself.");
|
|
3173
|
-
if (target && loc.layer.type === "group" && flattenLayers(loc.layer.children).some((l) => l.id === target)) {
|
|
3174
|
-
fail(`Group "${id}" cannot be moved into its own child "${target}".`);
|
|
3175
|
-
}
|
|
3176
|
-
if (target) requireLayer(layers, target);
|
|
3177
|
-
if (!target && placement.index === void 0) fail("Give aboveId, belowId, parentId or index.");
|
|
3178
|
-
const without = mapLayer(layers, id, () => null);
|
|
3179
|
-
return placeLayer(without, loc.layer, placement);
|
|
2953
|
+
function pixelLayerBounds(layer, surface = layer.surface) {
|
|
2954
|
+
const b = surface.bounds;
|
|
2955
|
+
if (!b) return null;
|
|
2956
|
+
return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height };
|
|
3180
2957
|
}
|
|
3181
|
-
function
|
|
3182
|
-
|
|
3183
|
-
if (next.type === "group") next.children = next.children.map(withNewIds);
|
|
3184
|
-
if (next.type === "annotation") next.annotation = { ...next.annotation, id: next.id };
|
|
3185
|
-
if (next.type === "shape" || next.type === "type") delete next.annotation;
|
|
3186
|
-
if (next.type === "smart" && next.psd?.placed) {
|
|
3187
|
-
next.psd = { ...next.psd };
|
|
3188
|
-
delete next.psd.placed;
|
|
3189
|
-
}
|
|
3190
|
-
return next;
|
|
2958
|
+
function layerTypeLabel(type) {
|
|
2959
|
+
return type === "pixel" ? "Pixel layer" : type === "adjustment" ? "Adjustment layer" : type === "group" ? "Group" : type === "smart" ? "Smart object" : type === "shape" ? "Shape layer" : type === "type" ? "Type layer" : "Annotation";
|
|
3191
2960
|
}
|
|
3192
|
-
function
|
|
3193
|
-
|
|
3194
|
-
const copy = withNewIds(loc.layer);
|
|
3195
|
-
copy.name = name ?? `${loc.layer.name || "Layer"} copy`;
|
|
3196
|
-
delete copy.locks;
|
|
3197
|
-
return { layers: insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1), id: copy.id };
|
|
2961
|
+
function hasSurface(layer) {
|
|
2962
|
+
return layer.type === "pixel" || layer.type === "smart" || layer.type === "shape" || layer.type === "type";
|
|
3198
2963
|
}
|
|
3199
|
-
function
|
|
3200
|
-
|
|
3201
|
-
const layer = loc.layer;
|
|
3202
|
-
const onlyUnlocking = Object.keys(props).every((k) => k === "locks" || k === "visible" || k === "collapsed");
|
|
3203
|
-
if (!onlyUnlocking) requireUnlocked(layer, "all", "change its properties");
|
|
3204
|
-
const next = { ...layer };
|
|
3205
|
-
if (props.name !== void 0) {
|
|
3206
|
-
if (typeof props.name !== "string" || !props.name.trim()) fail("name must be a non-empty string.");
|
|
3207
|
-
next.name = props.name.trim();
|
|
3208
|
-
}
|
|
3209
|
-
if (props.visible !== void 0) next.visible = Boolean(props.visible);
|
|
3210
|
-
if (props.opacity !== void 0) {
|
|
3211
|
-
if (!(typeof props.opacity === "number" && props.opacity >= 0 && props.opacity <= 1)) fail("opacity must be a number from 0 to 1.");
|
|
3212
|
-
next.opacity = props.opacity;
|
|
3213
|
-
}
|
|
3214
|
-
if (props.fillOpacity !== void 0) {
|
|
3215
|
-
if (layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape" && layer.type !== "type") fail(`fillOpacity applies to pixel, shape, type and smart object layers; "${id}" is a ${layer.type} layer.`);
|
|
3216
|
-
if (!(typeof props.fillOpacity === "number" && props.fillOpacity >= 0 && props.fillOpacity <= 1)) fail("fillOpacity must be a number from 0 to 1.");
|
|
3217
|
-
const p = next;
|
|
3218
|
-
if (props.fillOpacity === 1) delete p.fillOpacity;
|
|
3219
|
-
else p.fillOpacity = props.fillOpacity;
|
|
3220
|
-
}
|
|
3221
|
-
if (props.blend !== void 0) {
|
|
3222
|
-
if (props.blend === "pass-through") {
|
|
3223
|
-
if (layer.type !== "group") fail("pass-through is only for groups.");
|
|
3224
|
-
} else if (!isBlendMode(props.blend)) fail(`Unknown blend mode "${props.blend}".`);
|
|
3225
|
-
next.blend = props.blend;
|
|
3226
|
-
}
|
|
3227
|
-
if (props.clipped !== void 0) {
|
|
3228
|
-
if (props.clipped && loc.index === 0) fail("The bottom layer of a group or the stack has nothing below to clip to.");
|
|
3229
|
-
if (props.clipped) next.clipped = true;
|
|
3230
|
-
else delete next.clipped;
|
|
3231
|
-
}
|
|
3232
|
-
if (props.locks !== void 0) {
|
|
3233
|
-
const locks = {};
|
|
3234
|
-
for (const k of ["pixels", "transparency", "position", "all"]) if (props.locks[k]) locks[k] = true;
|
|
3235
|
-
if (Object.keys(locks).length) next.locks = locks;
|
|
3236
|
-
else delete next.locks;
|
|
3237
|
-
}
|
|
3238
|
-
if (props.collapsed !== void 0) {
|
|
3239
|
-
if (layer.type !== "group") fail("collapsed applies to groups.");
|
|
3240
|
-
const g = next;
|
|
3241
|
-
if (props.collapsed) g.collapsed = true;
|
|
3242
|
-
else delete g.collapsed;
|
|
3243
|
-
}
|
|
3244
|
-
return mapLayer(layers, id, () => next);
|
|
2964
|
+
function isRenderedLayer(layer) {
|
|
2965
|
+
return layer.type === "smart" || layer.type === "shape" || layer.type === "type";
|
|
3245
2966
|
}
|
|
3246
|
-
function
|
|
3247
|
-
|
|
3248
|
-
const locs = ids.map((id) => requireLayer(layers, id));
|
|
3249
|
-
const parent = locs[0].parent?.id ?? null;
|
|
3250
|
-
if (locs.some((l) => (l.parent?.id ?? null) !== parent)) fail("Layers to group must share one parent; move them next to each other first.");
|
|
3251
|
-
for (const l of locs) requireUnlocked(l.layer, "all", "group it");
|
|
3252
|
-
const ordered = [...new Set(locs)].sort((a, b) => a.index - b.index);
|
|
3253
|
-
const top = ordered[ordered.length - 1].index;
|
|
3254
|
-
const below = ordered.filter((l) => l.index < top).length;
|
|
3255
|
-
const group = {
|
|
3256
|
-
type: "group",
|
|
3257
|
-
id: newLayerId("group"),
|
|
3258
|
-
name,
|
|
3259
|
-
visible: true,
|
|
3260
|
-
opacity: 1,
|
|
3261
|
-
blend: "pass-through",
|
|
3262
|
-
children: ordered.map((l) => {
|
|
3263
|
-
const c = { ...l.layer };
|
|
3264
|
-
return c;
|
|
3265
|
-
})
|
|
3266
|
-
};
|
|
3267
|
-
if (group.children[0]?.clipped) {
|
|
3268
|
-
const c = { ...group.children[0] };
|
|
3269
|
-
delete c.clipped;
|
|
3270
|
-
group.children[0] = c;
|
|
3271
|
-
}
|
|
3272
|
-
let out = layers;
|
|
3273
|
-
for (const l of ordered) out = mapLayer(out, l.layer.id, () => null);
|
|
3274
|
-
return { layers: insertLayer(out, group, parent, top - below), id: group.id };
|
|
2967
|
+
function shapeLayers(layers) {
|
|
2968
|
+
return flattenLayers(layers).filter((l) => l.type === "shape");
|
|
3275
2969
|
}
|
|
3276
|
-
function
|
|
3277
|
-
|
|
3278
|
-
if (loc.layer.type !== "group") fail(`Layer "${id}" is not a group.`);
|
|
3279
|
-
requireUnlocked(loc.layer, "all", "ungroup it");
|
|
3280
|
-
const group = loc.layer;
|
|
3281
|
-
let out = mapLayer(layers, id, () => null);
|
|
3282
|
-
group.children.forEach((c, i) => {
|
|
3283
|
-
out = insertLayer(out, c, loc.parent?.id ?? null, loc.index + i);
|
|
3284
|
-
});
|
|
3285
|
-
return { layers: out, ids: group.children.map((c) => c.id) };
|
|
2970
|
+
function smartObjectLayers(layers) {
|
|
2971
|
+
return flattenLayers(layers).filter((l) => l.type === "smart");
|
|
3286
2972
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
2973
|
+
|
|
2974
|
+
// src/image-editor/layers/shape.ts
|
|
2975
|
+
var SHAPE_RENDER_MARGIN = MAX_EFFECT_REACH;
|
|
2976
|
+
var DEFAULT_MITER_LIMIT = 100;
|
|
2977
|
+
var fail = (message) => {
|
|
2978
|
+
throw new LayerOperationError(message);
|
|
2979
|
+
};
|
|
2980
|
+
var orientation = (anchors) => {
|
|
2981
|
+
let a = 0;
|
|
2982
|
+
for (let i = 0; i < anchors.length; i++) {
|
|
2983
|
+
const p = anchors[i];
|
|
2984
|
+
const q2 = anchors[(i + 1) % anchors.length];
|
|
2985
|
+
a += p.x * q2.y - q2.x * p.y;
|
|
3297
2986
|
}
|
|
3298
|
-
return
|
|
2987
|
+
return a;
|
|
2988
|
+
};
|
|
2989
|
+
function polygon(points) {
|
|
2990
|
+
const anchors = orientation(points) < 0 ? [...points].reverse() : points;
|
|
2991
|
+
return { closed: true, anchors: anchors.map((p) => ({ x: p.x, y: p.y })) };
|
|
3299
2992
|
}
|
|
3300
|
-
function
|
|
3301
|
-
const
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
});
|
|
2993
|
+
function arrowheadPolygon(head, tip, dir) {
|
|
2994
|
+
const len = Math.max(0, head.length);
|
|
2995
|
+
const hw = Math.max(0, head.width) / 2;
|
|
2996
|
+
if (!(len > 0) || !(hw > 0)) return null;
|
|
2997
|
+
const n = { x: -dir.y, y: dir.x };
|
|
2998
|
+
const base = { x: tip.x - dir.x * len, y: tip.y - dir.y * len };
|
|
2999
|
+
const c = Math.max(-0.5, Math.min(0.5, head.concavity ?? 0));
|
|
3000
|
+
const pts = [tip, { x: base.x + n.x * hw, y: base.y + n.y * hw }];
|
|
3001
|
+
if (c) pts.push({ x: base.x + dir.x * len * c, y: base.y + dir.y * len * c });
|
|
3002
|
+
pts.push({ x: base.x - n.x * hw, y: base.y - n.y * hw });
|
|
3003
|
+
return polygon(pts);
|
|
3309
3004
|
}
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
const
|
|
3313
|
-
if (
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
}
|
|
3318
|
-
if (raw.kind !== "develop") fail(`Unknown filter kind "${raw.kind}". Use develop or blur.`);
|
|
3319
|
-
const out = { id, kind: "develop", enabled, brightness: 0, contrast: 0, saturation: 0, exposure: 0, temperature: 0, tint: 0, process: 2 };
|
|
3320
|
-
for (const k of DEVELOP_FIELDS) {
|
|
3321
|
-
const v = raw[k];
|
|
3322
|
-
if (v === void 0) continue;
|
|
3323
|
-
if (!(typeof v === "number" && v >= -100 && v <= 100)) fail(`${k} must be a number from -100 to 100.`);
|
|
3324
|
-
out[k] = v;
|
|
3325
|
-
}
|
|
3326
|
-
const p = raw.process;
|
|
3327
|
-
if (p !== void 0) {
|
|
3328
|
-
if (p !== 1 && p !== 2) fail("process must be 1 or 2.");
|
|
3329
|
-
out.process = p;
|
|
3330
|
-
}
|
|
3005
|
+
function mapAnchor(a, f) {
|
|
3006
|
+
const p = f(a);
|
|
3007
|
+
const out = { x: p.x, y: p.y };
|
|
3008
|
+
if (a.in) out.in = f(a.in);
|
|
3009
|
+
if (a.out) out.out = f(a.out);
|
|
3010
|
+
if (a.type) out.type = a.type;
|
|
3011
|
+
if (a.corner) out.corner = { ...a.corner };
|
|
3331
3012
|
return out;
|
|
3332
3013
|
}
|
|
3333
|
-
function
|
|
3334
|
-
|
|
3335
|
-
if (loc.layer.type !== "pixel") fail(`Filters belong to pixel layers; "${id}" is a ${loc.layer.type} layer.`);
|
|
3336
|
-
requireUnlocked(loc.layer, "all", "change its filters");
|
|
3337
|
-
return loc.layer;
|
|
3014
|
+
function mapSubpaths(subpaths, f) {
|
|
3015
|
+
return expandVectorCorners(subpaths).map((sp) => ({ closed: sp.closed, anchors: sp.anchors.map((a) => mapAnchor(a, f)) }));
|
|
3338
3016
|
}
|
|
3339
|
-
function
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3017
|
+
function liveShapeSubpaths(live) {
|
|
3018
|
+
let out;
|
|
3019
|
+
switch (live.kind) {
|
|
3020
|
+
case "rect": {
|
|
3021
|
+
const x = Math.min(live.x, live.x + live.width);
|
|
3022
|
+
const y = Math.min(live.y, live.y + live.height);
|
|
3023
|
+
const sp = rectSubpath({ x, y, width: Math.abs(live.width), height: Math.abs(live.height) });
|
|
3024
|
+
const r = live.radii ?? [0, 0, 0, 0];
|
|
3025
|
+
out = [{ closed: true, anchors: sp.anchors.map((a, i) => r[i] > 0 ? setAnchorCorner(a, r[i]) : a) }];
|
|
3026
|
+
break;
|
|
3027
|
+
}
|
|
3028
|
+
case "ellipse":
|
|
3029
|
+
out = [ellipseSubpath(normBox(live))];
|
|
3030
|
+
break;
|
|
3031
|
+
case "polygon":
|
|
3032
|
+
out = [polygonSubpath(normBox(live), live.sides)];
|
|
3033
|
+
break;
|
|
3034
|
+
case "star":
|
|
3035
|
+
out = [starSubpath(normBox(live), live.points, Math.max(0, Math.min(1, live.innerRatio)))];
|
|
3036
|
+
break;
|
|
3037
|
+
case "line": {
|
|
3038
|
+
const a = { x: live.x1, y: live.y1 };
|
|
3039
|
+
const b = { x: live.x2, y: live.y2 };
|
|
3040
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
3041
|
+
out = [];
|
|
3042
|
+
if (len > 0) {
|
|
3043
|
+
const d = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };
|
|
3044
|
+
const hw = Math.max(0, live.weight) / 2;
|
|
3045
|
+
if (hw > 0) {
|
|
3046
|
+
const n = { x: -d.y * hw, y: d.x * hw };
|
|
3047
|
+
out.push(polygon([{ x: a.x + n.x, y: a.y + n.y }, { x: b.x + n.x, y: b.y + n.y }, { x: b.x - n.x, y: b.y - n.y }, { x: a.x - n.x, y: a.y - n.y }]));
|
|
3048
|
+
}
|
|
3049
|
+
const end = live.endArrow ? arrowheadPolygon(live.endArrow, b, d) : null;
|
|
3050
|
+
const start = live.startArrow ? arrowheadPolygon(live.startArrow, a, { x: -d.x, y: -d.y }) : null;
|
|
3051
|
+
if (start) out.push(start);
|
|
3052
|
+
if (end) out.push(end);
|
|
3053
|
+
}
|
|
3054
|
+
break;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
const m = live.transform;
|
|
3058
|
+
return m && !isIdentityMatrix(m) ? mapSubpaths(out, (p) => applyMatrix(m, p)) : out;
|
|
3343
3059
|
}
|
|
3344
|
-
function
|
|
3345
|
-
|
|
3346
|
-
const i = (layer.filters ?? []).findIndex((f) => f.id === filterId);
|
|
3347
|
-
if (i === -1) fail(`Layer "${id}" has no filter "${filterId}". Filters: ${(layer.filters ?? []).map((f) => f.id).join(", ") || "none"}.`);
|
|
3348
|
-
const current = layer.filters[i];
|
|
3349
|
-
if ("kind" in changes && changes.kind !== current.kind) fail("A filter kind cannot change; remove it and add another.");
|
|
3350
|
-
const merged2 = normalizeFilter({ ...current, ...changes, kind: current.kind }, current.id);
|
|
3351
|
-
const filters = [...layer.filters];
|
|
3352
|
-
filters[i] = merged2;
|
|
3353
|
-
return mapLayer(layers, id, () => ({ ...layer, filters }));
|
|
3354
|
-
}
|
|
3355
|
-
function removeLayerFilter(layers, id, filterId) {
|
|
3356
|
-
const layer = requirePixelLayer(layers, id);
|
|
3357
|
-
if (!(layer.filters ?? []).some((f) => f.id === filterId)) fail(`Layer "${id}" has no filter "${filterId}".`);
|
|
3358
|
-
const filters = layer.filters.filter((f) => f.id !== filterId);
|
|
3359
|
-
const next = { ...layer };
|
|
3360
|
-
if (filters.length) next.filters = filters;
|
|
3361
|
-
else delete next.filters;
|
|
3362
|
-
return mapLayer(layers, id, () => next);
|
|
3363
|
-
}
|
|
3364
|
-
function requireEffectLayer(layers, id, action) {
|
|
3365
|
-
const layer = requireLayer(layers, id).layer;
|
|
3366
|
-
if (layer.type === "adjustment") fail(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
|
|
3367
|
-
requireUnlocked(layer, "all", action);
|
|
3368
|
-
return layer;
|
|
3369
|
-
}
|
|
3370
|
-
function layerEffectsOf(layer) {
|
|
3371
|
-
return layer.effects ?? { enabled: true, items: [] };
|
|
3372
|
-
}
|
|
3373
|
-
function guard(fn) {
|
|
3374
|
-
try {
|
|
3375
|
-
return fn();
|
|
3376
|
-
} catch (e) {
|
|
3377
|
-
if (e instanceof LayerEffectError) fail(e.message);
|
|
3378
|
-
throw e;
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
|
-
function setLayerEffects(layers, id, effects) {
|
|
3382
|
-
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
3383
|
-
const next = { ...layer };
|
|
3384
|
-
if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) {
|
|
3385
|
-
guard(() => validateLayerEffects(effects));
|
|
3386
|
-
next.effects = effects;
|
|
3387
|
-
} else delete next.effects;
|
|
3388
|
-
return mapLayer(layers, id, () => next);
|
|
3389
|
-
}
|
|
3390
|
-
function addLayerEffect(layers, id, kind, params = {}, index) {
|
|
3391
|
-
if (!isLayerEffectKind(kind)) fail(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
|
|
3392
|
-
const layer = requireEffectLayer(layers, id, "add effects");
|
|
3393
|
-
const current = layerEffectsOf(layer);
|
|
3394
|
-
const ids = new Set(current.items.map((e) => e.id));
|
|
3395
|
-
let effectId = newEffectId();
|
|
3396
|
-
while (ids.has(effectId)) effectId = newEffectId();
|
|
3397
|
-
const { id: _ignored, kind: _kind, ...rest } = params;
|
|
3398
|
-
const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
|
|
3399
|
-
const items = [...current.items];
|
|
3400
|
-
const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
|
|
3401
|
-
items.splice(at, 0, effect);
|
|
3402
|
-
return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
|
|
3403
|
-
}
|
|
3404
|
-
function requireEffect(layer, effectId) {
|
|
3405
|
-
const items = layer.effects?.items ?? [];
|
|
3406
|
-
const i = items.findIndex((e) => e.id === effectId);
|
|
3407
|
-
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.`);
|
|
3408
|
-
return i;
|
|
3409
|
-
}
|
|
3410
|
-
function updateLayerEffect(layers, id, effectId, changes) {
|
|
3411
|
-
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
3412
|
-
const i = requireEffect(layer, effectId);
|
|
3413
|
-
const current = layer.effects;
|
|
3414
|
-
const items = [...current.items];
|
|
3415
|
-
items[i] = guard(() => mergeLayerEffect(items[i], changes));
|
|
3416
|
-
return setLayerEffects(layers, id, { ...current, items });
|
|
3417
|
-
}
|
|
3418
|
-
function removeLayerEffect(layers, id, effectId) {
|
|
3419
|
-
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
3420
|
-
requireEffect(layer, effectId);
|
|
3421
|
-
const current = layer.effects;
|
|
3422
|
-
return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
|
|
3423
|
-
}
|
|
3424
|
-
function setLayerEffectsEnabled(layers, id, enabled, scale) {
|
|
3425
|
-
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
3426
|
-
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}.`);
|
|
3427
|
-
const current = layerEffectsOf(layer);
|
|
3428
|
-
const next = { ...current, enabled };
|
|
3429
|
-
if (scale !== void 0) {
|
|
3430
|
-
if (scale === 1) delete next.scale;
|
|
3431
|
-
else next.scale = scale;
|
|
3432
|
-
}
|
|
3433
|
-
return setLayerEffects(layers, id, next);
|
|
3434
|
-
}
|
|
3435
|
-
function parseBlend(value, allowPassThrough = false) {
|
|
3436
|
-
if (value === void 0) return void 0;
|
|
3437
|
-
if (value === "pass-through" && allowPassThrough) return value;
|
|
3438
|
-
if (!isBlendMode(value)) fail(`Unknown blend mode "${String(value)}".`);
|
|
3439
|
-
return value;
|
|
3440
|
-
}
|
|
3441
|
-
function unionRect(a, b) {
|
|
3442
|
-
if (!a) return b;
|
|
3443
|
-
if (!b) return a;
|
|
3444
|
-
const x = Math.min(a.x, b.x);
|
|
3445
|
-
const y = Math.min(a.y, b.y);
|
|
3446
|
-
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 };
|
|
3447
|
-
}
|
|
3448
|
-
|
|
3449
|
-
// src/image-editor/layers/shape.ts
|
|
3450
|
-
var SHAPE_RENDER_MARGIN = MAX_EFFECT_REACH;
|
|
3451
|
-
var DEFAULT_MITER_LIMIT = 100;
|
|
3452
|
-
var fail2 = (message) => {
|
|
3453
|
-
throw new LayerOperationError(message);
|
|
3454
|
-
};
|
|
3455
|
-
var orientation = (anchors) => {
|
|
3456
|
-
let a = 0;
|
|
3457
|
-
for (let i = 0; i < anchors.length; i++) {
|
|
3458
|
-
const p = anchors[i];
|
|
3459
|
-
const q2 = anchors[(i + 1) % anchors.length];
|
|
3460
|
-
a += p.x * q2.y - q2.x * p.y;
|
|
3461
|
-
}
|
|
3462
|
-
return a;
|
|
3463
|
-
};
|
|
3464
|
-
function polygon(points) {
|
|
3465
|
-
const anchors = orientation(points) < 0 ? [...points].reverse() : points;
|
|
3466
|
-
return { closed: true, anchors: anchors.map((p) => ({ x: p.x, y: p.y })) };
|
|
3467
|
-
}
|
|
3468
|
-
function arrowheadPolygon(head, tip, dir) {
|
|
3469
|
-
const len = Math.max(0, head.length);
|
|
3470
|
-
const hw = Math.max(0, head.width) / 2;
|
|
3471
|
-
if (!(len > 0) || !(hw > 0)) return null;
|
|
3472
|
-
const n = { x: -dir.y, y: dir.x };
|
|
3473
|
-
const base = { x: tip.x - dir.x * len, y: tip.y - dir.y * len };
|
|
3474
|
-
const c = Math.max(-0.5, Math.min(0.5, head.concavity ?? 0));
|
|
3475
|
-
const pts = [tip, { x: base.x + n.x * hw, y: base.y + n.y * hw }];
|
|
3476
|
-
if (c) pts.push({ x: base.x + dir.x * len * c, y: base.y + dir.y * len * c });
|
|
3477
|
-
pts.push({ x: base.x - n.x * hw, y: base.y - n.y * hw });
|
|
3478
|
-
return polygon(pts);
|
|
3479
|
-
}
|
|
3480
|
-
function mapAnchor(a, f) {
|
|
3481
|
-
const p = f(a);
|
|
3482
|
-
const out = { x: p.x, y: p.y };
|
|
3483
|
-
if (a.in) out.in = f(a.in);
|
|
3484
|
-
if (a.out) out.out = f(a.out);
|
|
3485
|
-
if (a.type) out.type = a.type;
|
|
3486
|
-
if (a.corner) out.corner = { ...a.corner };
|
|
3487
|
-
return out;
|
|
3488
|
-
}
|
|
3489
|
-
function mapSubpaths(subpaths, f) {
|
|
3490
|
-
return expandVectorCorners(subpaths).map((sp) => ({ closed: sp.closed, anchors: sp.anchors.map((a) => mapAnchor(a, f)) }));
|
|
3491
|
-
}
|
|
3492
|
-
function liveShapeSubpaths(live) {
|
|
3493
|
-
let out;
|
|
3494
|
-
switch (live.kind) {
|
|
3495
|
-
case "rect": {
|
|
3496
|
-
const x = Math.min(live.x, live.x + live.width);
|
|
3497
|
-
const y = Math.min(live.y, live.y + live.height);
|
|
3498
|
-
const sp = rectSubpath({ x, y, width: Math.abs(live.width), height: Math.abs(live.height) });
|
|
3499
|
-
const r = live.radii ?? [0, 0, 0, 0];
|
|
3500
|
-
out = [{ closed: true, anchors: sp.anchors.map((a, i) => r[i] > 0 ? setAnchorCorner(a, r[i]) : a) }];
|
|
3501
|
-
break;
|
|
3502
|
-
}
|
|
3503
|
-
case "ellipse":
|
|
3504
|
-
out = [ellipseSubpath(normBox(live))];
|
|
3505
|
-
break;
|
|
3506
|
-
case "polygon":
|
|
3507
|
-
out = [polygonSubpath(normBox(live), live.sides)];
|
|
3508
|
-
break;
|
|
3509
|
-
case "star":
|
|
3510
|
-
out = [starSubpath(normBox(live), live.points, Math.max(0, Math.min(1, live.innerRatio)))];
|
|
3511
|
-
break;
|
|
3512
|
-
case "line": {
|
|
3513
|
-
const a = { x: live.x1, y: live.y1 };
|
|
3514
|
-
const b = { x: live.x2, y: live.y2 };
|
|
3515
|
-
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
3516
|
-
out = [];
|
|
3517
|
-
if (len > 0) {
|
|
3518
|
-
const d = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };
|
|
3519
|
-
const hw = Math.max(0, live.weight) / 2;
|
|
3520
|
-
if (hw > 0) {
|
|
3521
|
-
const n = { x: -d.y * hw, y: d.x * hw };
|
|
3522
|
-
out.push(polygon([{ x: a.x + n.x, y: a.y + n.y }, { x: b.x + n.x, y: b.y + n.y }, { x: b.x - n.x, y: b.y - n.y }, { x: a.x - n.x, y: a.y - n.y }]));
|
|
3523
|
-
}
|
|
3524
|
-
const end = live.endArrow ? arrowheadPolygon(live.endArrow, b, d) : null;
|
|
3525
|
-
const start = live.startArrow ? arrowheadPolygon(live.startArrow, a, { x: -d.x, y: -d.y }) : null;
|
|
3526
|
-
if (start) out.push(start);
|
|
3527
|
-
if (end) out.push(end);
|
|
3528
|
-
}
|
|
3529
|
-
break;
|
|
3530
|
-
}
|
|
3531
|
-
}
|
|
3532
|
-
const m = live.transform;
|
|
3533
|
-
return m && !isIdentityMatrix(m) ? mapSubpaths(out, (p) => applyMatrix(m, p)) : out;
|
|
3534
|
-
}
|
|
3535
|
-
function normBox(b) {
|
|
3536
|
-
return { x: Math.min(b.x, b.x + b.width), y: Math.min(b.y, b.y + b.height), width: Math.abs(b.width), height: Math.abs(b.height) };
|
|
3060
|
+
function normBox(b) {
|
|
3061
|
+
return { x: Math.min(b.x, b.x + b.width), y: Math.min(b.y, b.y + b.height), width: Math.abs(b.width), height: Math.abs(b.height) };
|
|
3537
3062
|
}
|
|
3538
3063
|
function liveComponent(live, op = "combine") {
|
|
3539
3064
|
return { op, subpaths: liveShapeSubpaths(live), live };
|
|
@@ -3660,6 +3185,10 @@ function fillCoverage(geo, rect, rule, inverted = false) {
|
|
|
3660
3185
|
});
|
|
3661
3186
|
return acc;
|
|
3662
3187
|
}
|
|
3188
|
+
function componentsCoverage(components, rect, rule = "nonzero", inverted = false) {
|
|
3189
|
+
const geo = { fills: components.map((c) => ({ op: c.op, flat: flattenPath(componentPath(c)) })) };
|
|
3190
|
+
return fillCoverage(geo, rect, rule, inverted) ?? new Float32Array(rect.width * rect.height);
|
|
3191
|
+
}
|
|
3663
3192
|
function paintContext(env) {
|
|
3664
3193
|
const tiles = /* @__PURE__ */ new Map();
|
|
3665
3194
|
return {
|
|
@@ -3759,29 +3288,29 @@ function rerenderShape(layer, env) {
|
|
|
3759
3288
|
}
|
|
3760
3289
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
3761
3290
|
function requireNum(v, path, min = -Infinity, max = Infinity) {
|
|
3762
|
-
if (!isNum(v))
|
|
3763
|
-
if (v < min || v > max)
|
|
3291
|
+
if (!isNum(v)) fail(`${path} must be a finite number.`);
|
|
3292
|
+
if (v < min || v > max) fail(`${path} must be between ${min} and ${max}, got ${v}.`);
|
|
3764
3293
|
return v;
|
|
3765
3294
|
}
|
|
3766
3295
|
function normalizeShapePaint(v, path = "paint") {
|
|
3767
3296
|
if (typeof v === "string") {
|
|
3768
3297
|
const c = parseCssColor(v);
|
|
3769
|
-
if (!c) return
|
|
3298
|
+
if (!c) return fail(`${path}: "${v}" is not a colour; use #rrggbb (or #rrggbbaa, rgb(), a CSS colour name).`);
|
|
3770
3299
|
return c.alpha < 1 ? { type: "color", color: c.hex, opacity: c.alpha } : { type: "color", color: c.hex };
|
|
3771
3300
|
}
|
|
3772
|
-
if (!v || typeof v !== "object") return
|
|
3301
|
+
if (!v || typeof v !== "object") return fail(`${path} must be a colour string or an object with type color, gradient or pattern.`);
|
|
3773
3302
|
const o = v;
|
|
3774
3303
|
const opacity = o.opacity === void 0 ? void 0 : requireNum(o.opacity, `${path}.opacity`, 0, 1);
|
|
3775
3304
|
const withOpacity = (p) => opacity !== void 0 && opacity !== 1 ? { ...p, opacity } : p;
|
|
3776
3305
|
if (o.type === "color") {
|
|
3777
3306
|
const c = parseCssColor(o.color);
|
|
3778
|
-
if (!c) return
|
|
3307
|
+
if (!c) return fail(`${path}.color must be a colour such as #rrggbb.`);
|
|
3779
3308
|
const op = (opacity ?? 1) * c.alpha;
|
|
3780
3309
|
return op !== 1 ? { type: "color", color: c.hex, opacity: op } : { type: "color", color: c.hex };
|
|
3781
3310
|
}
|
|
3782
3311
|
if (o.type === "gradient") {
|
|
3783
3312
|
const g = o.gradient;
|
|
3784
|
-
if (!g || typeof g !== "object") return
|
|
3313
|
+
if (!g || typeof g !== "object") return fail(`${path}.gradient is required: { stops: [{ position, color }], style, angle, \u2026 }.`);
|
|
3785
3314
|
const gradient = {
|
|
3786
3315
|
stops: Array.isArray(g.stops) ? g.stops.map((s) => ({ position: s.position, color: parseCssColor(s.color)?.hex ?? s.color })) : [],
|
|
3787
3316
|
...g.opacityStops ? { opacityStops: g.opacityStops } : {},
|
|
@@ -3795,14 +3324,14 @@ function normalizeShapePaint(v, path = "paint") {
|
|
|
3795
3324
|
try {
|
|
3796
3325
|
validateEffectGradient(gradient, `${path}.gradient`);
|
|
3797
3326
|
} catch (e) {
|
|
3798
|
-
if (e instanceof LayerEffectError)
|
|
3327
|
+
if (e instanceof LayerEffectError) fail(e.message);
|
|
3799
3328
|
throw e;
|
|
3800
3329
|
}
|
|
3801
|
-
if (gradient.stops.length < 1)
|
|
3330
|
+
if (gradient.stops.length < 1) fail(`${path}.gradient.stops needs at least one { position, color }.`);
|
|
3802
3331
|
return withOpacity({ type: "gradient", gradient });
|
|
3803
3332
|
}
|
|
3804
3333
|
if (o.type === "pattern") {
|
|
3805
|
-
if (typeof o.patternId !== "string" || !o.patternId)
|
|
3334
|
+
if (typeof o.patternId !== "string" || !o.patternId) fail(`${path}.patternId is required (a document or built-in pattern id).`);
|
|
3806
3335
|
const p = { type: "pattern", patternId: o.patternId, scale: o.scale === void 0 ? 1 : requireNum(o.scale, `${path}.scale`, 0.01, 10) };
|
|
3807
3336
|
if (typeof o.name === "string") p.name = o.name;
|
|
3808
3337
|
if (o.angle !== void 0) p.angle = requireNum(o.angle, `${path}.angle`, -360, 360);
|
|
@@ -3813,10 +3342,10 @@ function normalizeShapePaint(v, path = "paint") {
|
|
|
3813
3342
|
}
|
|
3814
3343
|
return withOpacity(p);
|
|
3815
3344
|
}
|
|
3816
|
-
return
|
|
3345
|
+
return fail(`${path}.type must be color, gradient or pattern, got ${JSON.stringify(o.type)}.`);
|
|
3817
3346
|
}
|
|
3818
3347
|
function normalizeShapeStroke(v, path = "stroke", base) {
|
|
3819
|
-
if (!v || typeof v !== "object") return
|
|
3348
|
+
if (!v || typeof v !== "object") return fail(`${path} must be an object { width, paint, align?, cap?, join?, miterLimit?, dashes?, dashOffset? } or null.`);
|
|
3820
3349
|
const o = v;
|
|
3821
3350
|
const width = o.width === void 0 && base ? base.width : requireNum(o.width, `${path}.width`, 0, 1e4);
|
|
3822
3351
|
const paint = o.paint === void 0 && base ? base.paint : o.paint === void 0 && o.color !== void 0 ? normalizeShapePaint(o.color, `${path}.color`) : normalizeShapePaint(o.paint, `${path}.paint`);
|
|
@@ -3824,7 +3353,7 @@ function normalizeShapeStroke(v, path = "stroke", base) {
|
|
|
3824
3353
|
const pick = (k, allowed) => {
|
|
3825
3354
|
const value = o[k] === void 0 ? base?.[k] : o[k];
|
|
3826
3355
|
if (value === void 0) return;
|
|
3827
|
-
if (!allowed.includes(value))
|
|
3356
|
+
if (!allowed.includes(value)) fail(`${path}.${k} must be one of ${allowed.join(", ")}, got ${JSON.stringify(value)}.`);
|
|
3828
3357
|
if (value !== (k === "align" ? "center" : k === "cap" ? "butt" : "miter")) out[k] = value;
|
|
3829
3358
|
};
|
|
3830
3359
|
pick("align", ["inside", "center", "outside"]);
|
|
@@ -3834,9 +3363,9 @@ function normalizeShapeStroke(v, path = "stroke", base) {
|
|
|
3834
3363
|
if (miter !== void 0 && miter !== DEFAULT_MITER_LIMIT) out.miterLimit = miter;
|
|
3835
3364
|
const dashes = o.dashes === void 0 ? base?.dashes : o.dashes;
|
|
3836
3365
|
if (dashes !== void 0 && dashes !== null) {
|
|
3837
|
-
if (!Array.isArray(dashes) || dashes.length > 32 || !dashes.every((d) => isNum(d) && d >= 0 && d <= 1e4))
|
|
3366
|
+
if (!Array.isArray(dashes) || dashes.length > 32 || !dashes.every((d) => isNum(d) && d >= 0 && d <= 1e4)) fail(`${path}.dashes must be at most 32 lengths 0..10000 (multiples of the width).`);
|
|
3838
3367
|
if (dashes.length) {
|
|
3839
|
-
if (dashes.reduce((s, d) => s + d, 0) < 0.01)
|
|
3368
|
+
if (dashes.reduce((s, d) => s + d, 0) < 0.01) fail(`${path}.dashes must add up to at least 0.01 widths.`);
|
|
3840
3369
|
out.dashes = [...dashes];
|
|
3841
3370
|
}
|
|
3842
3371
|
}
|
|
@@ -3854,7 +3383,7 @@ function box(o, path) {
|
|
|
3854
3383
|
}
|
|
3855
3384
|
function arrowhead(v, path) {
|
|
3856
3385
|
if (v === null || v === void 0 || v === false) return null;
|
|
3857
|
-
if (typeof v !== "object") return
|
|
3386
|
+
if (typeof v !== "object") return fail(`${path} must be { width, length, concavity? } in pixels, or null.`);
|
|
3858
3387
|
const o = v;
|
|
3859
3388
|
const head = { width: requireNum(o.width, `${path}.width`, 0, 1e5), length: requireNum(o.length, `${path}.length`, 0, 1e5) };
|
|
3860
3389
|
if (o.concavity !== void 0 && o.concavity !== 0) head.concavity = requireNum(o.concavity, `${path}.concavity`, -0.5, 0.5);
|
|
@@ -3862,13 +3391,13 @@ function arrowhead(v, path) {
|
|
|
3862
3391
|
}
|
|
3863
3392
|
function matrixArg(v, path) {
|
|
3864
3393
|
if (v === void 0 || v === null) return void 0;
|
|
3865
|
-
if (!Array.isArray(v) || v.length !== 6 || !v.every(isNum))
|
|
3394
|
+
if (!Array.isArray(v) || v.length !== 6 || !v.every(isNum)) fail(`${path} must be six numbers [a, b, c, d, e, f].`);
|
|
3866
3395
|
const m = v;
|
|
3867
|
-
if (Math.abs(m[0] * m[3] - m[1] * m[2]) < 1e-12)
|
|
3396
|
+
if (Math.abs(m[0] * m[3] - m[1] * m[2]) < 1e-12) fail(`${path} is singular.`);
|
|
3868
3397
|
return isIdentityMatrix(m) ? void 0 : [...m];
|
|
3869
3398
|
}
|
|
3870
3399
|
function normalizeLiveShape(v, path = "shape") {
|
|
3871
|
-
if (!v || typeof v !== "object") return
|
|
3400
|
+
if (!v || typeof v !== "object") return fail(`${path} must be an object with kind ${LIVE_SHAPE_KINDS.join(", ")}.`);
|
|
3872
3401
|
const o = v;
|
|
3873
3402
|
const transform = matrixArg(o.transform, `${path}.transform`);
|
|
3874
3403
|
const t = transform ? { transform } : {};
|
|
@@ -3879,7 +3408,7 @@ function normalizeLiveShape(v, path = "shape") {
|
|
|
3879
3408
|
if (o.radii !== void 0 || o.radius !== void 0) {
|
|
3880
3409
|
const r = o.radii ?? o.radius;
|
|
3881
3410
|
const list2 = typeof r === "number" ? [r, r, r, r] : r;
|
|
3882
|
-
if (!Array.isArray(list2) || list2.length !== 4 || !list2.every((x) => isNum(x) && x >= 0))
|
|
3411
|
+
if (!Array.isArray(list2) || list2.length !== 4 || !list2.every((x) => isNum(x) && x >= 0)) fail(`${path}.radii must be four radii \u2265 0 [top-left, top-right, bottom-right, bottom-left] (or radius, one number).`);
|
|
3883
3412
|
if (list2.some((x) => x > 0)) radii = [...list2];
|
|
3884
3413
|
}
|
|
3885
3414
|
return { kind: "rect", ...b, ...radii ? { radii } : {}, ...t };
|
|
@@ -3910,16 +3439,16 @@ function normalizeLiveShape(v, path = "shape") {
|
|
|
3910
3439
|
const e = arrowhead(o.endArrow, `${path}.endArrow`);
|
|
3911
3440
|
if (s) line.startArrow = s;
|
|
3912
3441
|
if (e) line.endArrow = e;
|
|
3913
|
-
if (line.x1 === line.x2 && line.y1 === line.y2)
|
|
3442
|
+
if (line.x1 === line.x2 && line.y1 === line.y2) fail(`${path}: the line's two ends are the same point.`);
|
|
3914
3443
|
return line;
|
|
3915
3444
|
}
|
|
3916
3445
|
default:
|
|
3917
|
-
return
|
|
3446
|
+
return fail(`${path}.kind must be one of ${LIVE_SHAPE_KINDS.join(", ")}, got ${JSON.stringify(o.kind)}.`);
|
|
3918
3447
|
}
|
|
3919
3448
|
}
|
|
3920
3449
|
function point(v, path) {
|
|
3921
3450
|
const p = v;
|
|
3922
|
-
if (!p || typeof p !== "object") return
|
|
3451
|
+
if (!p || typeof p !== "object") return fail(`${path} must be { x, y } in document pixels.`);
|
|
3923
3452
|
return { x: requireNum(p.x, `${path}.x`), y: requireNum(p.y, `${path}.y`) };
|
|
3924
3453
|
}
|
|
3925
3454
|
function normalizeSubpaths(v, path = "subpaths") {
|
|
@@ -3928,15 +3457,15 @@ function normalizeSubpaths(v, path = "subpaths") {
|
|
|
3928
3457
|
try {
|
|
3929
3458
|
subs = bezierToSubpaths(v);
|
|
3930
3459
|
} catch {
|
|
3931
|
-
return
|
|
3460
|
+
return fail(`${path}: could not read the SVG path data.`);
|
|
3932
3461
|
}
|
|
3933
|
-
if (!subs.length || !subs.every((s) => s.anchors.every((a) => isNum(a.x) && isNum(a.y))))
|
|
3462
|
+
if (!subs.length || !subs.every((s) => s.anchors.every((a) => isNum(a.x) && isNum(a.y)))) fail(`${path}: the SVG path data holds no drawable subpath.`);
|
|
3934
3463
|
return subs;
|
|
3935
3464
|
}
|
|
3936
|
-
if (!Array.isArray(v) || !v.length) return
|
|
3465
|
+
if (!Array.isArray(v) || !v.length) return fail(`${path} must be a non-empty array of { anchors: [{ x, y, in?, out? }], closed } (or SVG path data).`);
|
|
3937
3466
|
return v.map((s, i) => {
|
|
3938
3467
|
const sp = s;
|
|
3939
|
-
if (!sp || !Array.isArray(sp.anchors) || sp.anchors.length < 1)
|
|
3468
|
+
if (!sp || !Array.isArray(sp.anchors) || sp.anchors.length < 1) fail(`${path}[${i}].anchors must be a non-empty array.`);
|
|
3940
3469
|
const anchors = sp.anchors.map((a, j) => {
|
|
3941
3470
|
const at = `${path}[${i}].anchors[${j}]`;
|
|
3942
3471
|
const p = point(a, at);
|
|
@@ -3956,16 +3485,16 @@ function normalizeSubpaths(v, path = "subpaths") {
|
|
|
3956
3485
|
}
|
|
3957
3486
|
function requireOperation(v, path = "op") {
|
|
3958
3487
|
if (v === void 0) return "combine";
|
|
3959
|
-
if (!SHAPE_OPERATIONS.includes(v))
|
|
3488
|
+
if (!SHAPE_OPERATIONS.includes(v)) fail(`${path} must be one of ${SHAPE_OPERATIONS.join(", ")}, got ${JSON.stringify(v)}.`);
|
|
3960
3489
|
return v;
|
|
3961
3490
|
}
|
|
3962
3491
|
function requireShapeLayer(layers, id) {
|
|
3963
3492
|
const layer = requireLayer(layers, id).layer;
|
|
3964
|
-
if (layer.type !== "shape")
|
|
3493
|
+
if (layer.type !== "shape") fail(`Layer "${layer.name || id}" is a ${layer.type} layer, not a shape layer; list_layers shows each layer's type.`);
|
|
3965
3494
|
return layer;
|
|
3966
3495
|
}
|
|
3967
3496
|
function createShapeLayer(init, env, id = newLayerId()) {
|
|
3968
|
-
if (!init.components.length)
|
|
3497
|
+
if (!init.components.length) fail("A shape needs at least one component.");
|
|
3969
3498
|
const layer = {
|
|
3970
3499
|
type: "shape",
|
|
3971
3500
|
id,
|
|
@@ -4005,7 +3534,7 @@ function updateShapeLayer(layers, id, change, env, action = "edit it") {
|
|
|
4005
3534
|
const patch = change(current);
|
|
4006
3535
|
const next = { ...current, ...patch };
|
|
4007
3536
|
if (!("annotation" in patch)) delete next.annotation;
|
|
4008
|
-
if (!next.components.length)
|
|
3537
|
+
if (!next.components.length) fail("A shape needs at least one component; delete the layer instead.");
|
|
4009
3538
|
const rendered = rerenderShape(next, env);
|
|
4010
3539
|
return { layers: mapLayer(layers, id, () => rendered), layer: rendered };
|
|
4011
3540
|
}
|
|
@@ -4029,7 +3558,7 @@ function mergeShapeComponents(components, fillRule = "nonzero") {
|
|
|
4029
3558
|
if (components.length < 2) return [...components];
|
|
4030
3559
|
const path = shapeOutline(components, fillRule);
|
|
4031
3560
|
const subpaths = bezierToSubpaths(path);
|
|
4032
|
-
if (!subpaths.length)
|
|
3561
|
+
if (!subpaths.length) fail("The components combine to nothing (the operations remove every area), so there is nothing to merge into.");
|
|
4033
3562
|
return [{ op: "combine", subpaths }];
|
|
4034
3563
|
}
|
|
4035
3564
|
function shapeAsPixelLayer(layer) {
|
|
@@ -4099,20 +3628,645 @@ function shapeFromAnnotation(a) {
|
|
|
4099
3628
|
default:
|
|
4100
3629
|
return null;
|
|
4101
3630
|
}
|
|
4102
|
-
}
|
|
4103
|
-
function annotationBlend(a, layerBlend) {
|
|
4104
|
-
if (layerBlend !== "normal") return layerBlend;
|
|
4105
|
-
return a.blend && isBlendMode(a.blend) ? a.blend : "normal";
|
|
4106
|
-
}
|
|
4107
|
-
function annotationOfShape(layer) {
|
|
4108
|
-
const a = layer.annotation;
|
|
4109
|
-
if (!a) return null;
|
|
4110
|
-
const blend = layer.blend;
|
|
4111
|
-
if ((a.blend ?? "normal") === blend) return a;
|
|
4112
|
-
const out = { ...a };
|
|
4113
|
-
if (blend === "normal") delete out.blend;
|
|
4114
|
-
else out.blend = blend;
|
|
4115
|
-
return out;
|
|
3631
|
+
}
|
|
3632
|
+
function annotationBlend(a, layerBlend) {
|
|
3633
|
+
if (layerBlend !== "normal") return layerBlend;
|
|
3634
|
+
return a.blend && isBlendMode(a.blend) ? a.blend : "normal";
|
|
3635
|
+
}
|
|
3636
|
+
function annotationOfShape(layer) {
|
|
3637
|
+
const a = layer.annotation;
|
|
3638
|
+
if (!a) return null;
|
|
3639
|
+
const blend = layer.blend;
|
|
3640
|
+
if ((a.blend ?? "normal") === blend) return a;
|
|
3641
|
+
const out = { ...a };
|
|
3642
|
+
if (blend === "normal") delete out.blend;
|
|
3643
|
+
else out.blend = blend;
|
|
3644
|
+
return out;
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// src/image-editor/layers/vector-mask.ts
|
|
3648
|
+
function isActiveVectorMask(vm) {
|
|
3649
|
+
return Boolean(vm && vm.enabled !== false);
|
|
3650
|
+
}
|
|
3651
|
+
function vectorMaskCoverage(vm, width, height) {
|
|
3652
|
+
let v = componentsCoverage(vm.components, { x: 0, y: 0, width, height }, vm.fillRule ?? "nonzero", vm.inverted === true);
|
|
3653
|
+
if ((vm.feather ?? 0) > 0) v = gaussianBlur(v, width, height, vm.feather, "clamp");
|
|
3654
|
+
const density = vm.density ?? 1;
|
|
3655
|
+
if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);
|
|
3656
|
+
return v;
|
|
3657
|
+
}
|
|
3658
|
+
function outsideValue(vm) {
|
|
3659
|
+
return 1 - (vm.density ?? 1) * (1 - (vm.inverted ? 1 : 0));
|
|
3660
|
+
}
|
|
3661
|
+
function rasterCoverage(mask, ox, oy, width, height) {
|
|
3662
|
+
const bytes = readRegion(mask.surface, { x: -ox, y: -oy, width, height });
|
|
3663
|
+
let v = new Float32Array(width * height);
|
|
3664
|
+
for (let i = 0; i < v.length; i++) v[i] = bytes[i] / 255;
|
|
3665
|
+
if ((mask.feather ?? 0) > 0) v = gaussianBlur(v, width, height, mask.feather, "clamp");
|
|
3666
|
+
const density = mask.density ?? 1;
|
|
3667
|
+
if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);
|
|
3668
|
+
return v;
|
|
3669
|
+
}
|
|
3670
|
+
var layerOffset = (layer) => layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
|
|
3671
|
+
function effectiveLayerMask(layer, width, height) {
|
|
3672
|
+
const vm = layer.vectorMask;
|
|
3673
|
+
const raster = layer.mask;
|
|
3674
|
+
if (!isActiveVectorMask(vm) || raster?.source || width < 1 || height < 1) return raster;
|
|
3675
|
+
const { x: ox, y: oy } = layerOffset(layer);
|
|
3676
|
+
const v = vectorMaskCoverage(vm, width, height);
|
|
3677
|
+
let outside = outsideValue(vm);
|
|
3678
|
+
if (raster && raster.enabled !== false) {
|
|
3679
|
+
const r = rasterCoverage(raster, ox, oy, width, height);
|
|
3680
|
+
for (let i = 0; i < v.length; i++) v[i] *= r[i];
|
|
3681
|
+
outside *= 1 - (raster.density ?? 1) * (1 - raster.defaultValue / 255);
|
|
3682
|
+
}
|
|
3683
|
+
const bytes = new Uint8ClampedArray(v.length);
|
|
3684
|
+
for (let i = 0; i < v.length; i++) bytes[i] = Math.round(v[i] * 255);
|
|
3685
|
+
const defaultValue = outside >= 0.5 ? 255 : 0;
|
|
3686
|
+
return { surface: writeRegion(emptySurface(1, defaultValue), { x: -ox, y: -oy, width, height }, bytes), defaultValue };
|
|
3687
|
+
}
|
|
3688
|
+
var resolved = /* @__PURE__ */ new WeakMap();
|
|
3689
|
+
function hasVectorMask(layers) {
|
|
3690
|
+
for (const l of layers) {
|
|
3691
|
+
if (isActiveVectorMask(l.vectorMask)) return true;
|
|
3692
|
+
if (l.type === "group" && hasVectorMask(l.children)) return true;
|
|
3693
|
+
}
|
|
3694
|
+
return false;
|
|
3695
|
+
}
|
|
3696
|
+
function resolveVectorMasks(layers, width, height) {
|
|
3697
|
+
if (!hasVectorMask(layers)) return layers;
|
|
3698
|
+
return layers.map((l) => resolveLayer(l, width, height));
|
|
3699
|
+
}
|
|
3700
|
+
function resolveLayer(layer, width, height) {
|
|
3701
|
+
let next = layer;
|
|
3702
|
+
if (layer.type === "group" && hasVectorMask(layer.children)) {
|
|
3703
|
+
const key2 = `${width}x${height}`;
|
|
3704
|
+
const hit2 = resolved.get(layer);
|
|
3705
|
+
if (hit2?.key === key2) return hit2.layer;
|
|
3706
|
+
next = { ...layer, children: layer.children.map((c) => resolveLayer(c, width, height)) };
|
|
3707
|
+
if (!isActiveVectorMask(layer.vectorMask)) {
|
|
3708
|
+
resolved.set(layer, { key: key2, layer: next });
|
|
3709
|
+
return next;
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
if (!isActiveVectorMask(layer.vectorMask)) return layer;
|
|
3713
|
+
const key = `${width}x${height}:v`;
|
|
3714
|
+
const hit = resolved.get(layer);
|
|
3715
|
+
if (hit?.key === key) return hit.layer;
|
|
3716
|
+
const mask = effectiveLayerMask(layer, width, height);
|
|
3717
|
+
const out = { ...next, mask };
|
|
3718
|
+
delete out.vectorMask;
|
|
3719
|
+
if (!mask) delete out.mask;
|
|
3720
|
+
resolved.set(layer, { key, layer: out });
|
|
3721
|
+
return out;
|
|
3722
|
+
}
|
|
3723
|
+
function offsetVectorMask(vm, dx, dy) {
|
|
3724
|
+
if (!dx && !dy) return vm;
|
|
3725
|
+
return { ...vm, components: vm.components.map((c) => ({ ...c, subpaths: mapSubpaths(c.subpaths, (p) => ({ x: p.x + dx, y: p.y + dy })), ...c.live ? { live: offsetLive(c.live, dx, dy) } : {} })) };
|
|
3726
|
+
}
|
|
3727
|
+
function offsetLive(live, dx, dy) {
|
|
3728
|
+
if (live.kind === "line") return { ...live, x1: live.x1 + dx, y1: live.y1 + dy, x2: live.x2 + dx, y2: live.y2 + dy };
|
|
3729
|
+
return { ...live, x: live.x + dx, y: live.y + dy };
|
|
3730
|
+
}
|
|
3731
|
+
function transformVectorMask(vm, f, affine) {
|
|
3732
|
+
return { ...vm, components: transformShapeComponents(vm.components, f, affine) };
|
|
3733
|
+
}
|
|
3734
|
+
function followLayerMove(layer, dx, dy) {
|
|
3735
|
+
const vm = layer.vectorMask;
|
|
3736
|
+
if (!vm || vm.linked === false || !dx && !dy) return layer;
|
|
3737
|
+
return { ...layer, vectorMask: offsetVectorMask(vm, dx, dy) };
|
|
3738
|
+
}
|
|
3739
|
+
function vectorMaskBounds(vm) {
|
|
3740
|
+
let x0 = Infinity;
|
|
3741
|
+
let y0 = Infinity;
|
|
3742
|
+
let x1 = -Infinity;
|
|
3743
|
+
let y1 = -Infinity;
|
|
3744
|
+
for (const c of vm.components) {
|
|
3745
|
+
for (const sp of c.subpaths) {
|
|
3746
|
+
for (const a of sp.anchors) {
|
|
3747
|
+
for (const p of [a, a.in, a.out]) {
|
|
3748
|
+
if (!p) continue;
|
|
3749
|
+
x0 = Math.min(x0, p.x);
|
|
3750
|
+
y0 = Math.min(y0, p.y);
|
|
3751
|
+
x1 = Math.max(x1, p.x);
|
|
3752
|
+
y1 = Math.max(y1, p.y);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
return x0 === Infinity ? null : { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
3758
|
+
}
|
|
3759
|
+
var thumbCache = /* @__PURE__ */ new WeakMap();
|
|
3760
|
+
function vectorMaskThumbnail(vm, width, height, maxSide = 64) {
|
|
3761
|
+
const key = `${width}x${height}:${maxSide}`;
|
|
3762
|
+
const hit = thumbCache.get(vm);
|
|
3763
|
+
if (hit?.key === key) return hit.pixels;
|
|
3764
|
+
const k = Math.min(1, maxSide / Math.max(1, width, height));
|
|
3765
|
+
const tw = Math.max(1, Math.round(width * k));
|
|
3766
|
+
const th = Math.max(1, Math.round(height * k));
|
|
3767
|
+
const scaled = transformVectorMask(vm, (p) => ({ x: p.x * k, y: p.y * k }), [k, 0, 0, k, 0, 0]);
|
|
3768
|
+
const cov = componentsCoverage(scaled.components, { x: 0, y: 0, width: tw, height: th }, vm.fillRule ?? "nonzero", vm.inverted === true);
|
|
3769
|
+
const data = new Uint8ClampedArray(tw * th * 4);
|
|
3770
|
+
for (let i = 0; i < cov.length; i++) {
|
|
3771
|
+
const v = Math.round(Math.min(1, Math.max(0, cov[i])) * 255);
|
|
3772
|
+
data[i * 4] = data[i * 4 + 1] = data[i * 4 + 2] = v;
|
|
3773
|
+
data[i * 4 + 3] = 255;
|
|
3774
|
+
}
|
|
3775
|
+
const pixels = { width: tw, height: th, data };
|
|
3776
|
+
thumbCache.set(vm, { key, pixels });
|
|
3777
|
+
return pixels;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
// src/image-editor/layers/ops.ts
|
|
3781
|
+
var LayerOperationError = class extends Error {
|
|
3782
|
+
userMessage;
|
|
3783
|
+
constructor(message, userMessage) {
|
|
3784
|
+
super(message);
|
|
3785
|
+
this.name = "LayerOperationError";
|
|
3786
|
+
if (userMessage) this.userMessage = userMessage;
|
|
3787
|
+
}
|
|
3788
|
+
};
|
|
3789
|
+
function userErrorMessage(err) {
|
|
3790
|
+
if (err instanceof LayerOperationError && err.userMessage) return err.userMessage;
|
|
3791
|
+
return err instanceof Error ? err.message : String(err);
|
|
3792
|
+
}
|
|
3793
|
+
function isPaintableLayer(layer) {
|
|
3794
|
+
return layer.type === "pixel" && layer.visible && !isLayerLocked(layer, "pixels");
|
|
3795
|
+
}
|
|
3796
|
+
function defaultActiveLayerId(layers) {
|
|
3797
|
+
const pixels = flattenLayers(layers).filter((l) => l.type === "pixel");
|
|
3798
|
+
for (let i = pixels.length - 1; i >= 0; i--) if (isPaintableLayer(pixels[i])) return pixels[i].id;
|
|
3799
|
+
return pixels[pixels.length - 1]?.id ?? null;
|
|
3800
|
+
}
|
|
3801
|
+
function activeLayerAfterRemoval(prev, next, removedId) {
|
|
3802
|
+
const before = flattenLayers(prev);
|
|
3803
|
+
const at = before.findIndex((l) => l.id === removedId);
|
|
3804
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
3805
|
+
const loc = findLayer(next, before[i].id);
|
|
3806
|
+
if (loc && isPaintableLayer(loc.layer)) return loc.layer.id;
|
|
3807
|
+
}
|
|
3808
|
+
return defaultActiveLayerId(next);
|
|
3809
|
+
}
|
|
3810
|
+
var fail2 = (message) => {
|
|
3811
|
+
throw new LayerOperationError(message);
|
|
3812
|
+
};
|
|
3813
|
+
function requireLayer(layers, id) {
|
|
3814
|
+
const loc = findLayer(layers, id);
|
|
3815
|
+
if (!loc) {
|
|
3816
|
+
const ids = flattenLayers(layers).map((l) => l.id);
|
|
3817
|
+
fail2(`Unknown layer "${id}". ${ids.length ? `Layers: ${ids.join(", ")}.` : "The document has no layers."} Call list_layers for ids.`);
|
|
3818
|
+
}
|
|
3819
|
+
return loc;
|
|
3820
|
+
}
|
|
3821
|
+
function requireUnlocked(layer, what, action) {
|
|
3822
|
+
if (isLayerLocked(layer, "all")) fail2(`Layer "${layer.name || layer.id}" is locked (all); unlock it before you ${action}.`);
|
|
3823
|
+
if (what !== "all" && isLayerLocked(layer, what)) fail2(`Layer "${layer.name || layer.id}" has its ${what} locked; unlock it before you ${action}.`);
|
|
3824
|
+
}
|
|
3825
|
+
function placeLayer(layers, layer, placement = {}, defaultIndex) {
|
|
3826
|
+
const given = [placement.aboveId, placement.belowId, placement.parentId].filter((v) => v !== void 0).length;
|
|
3827
|
+
if (given > 1) fail2("Give only one of aboveId, belowId and parentId.");
|
|
3828
|
+
if (placement.aboveId || placement.belowId) {
|
|
3829
|
+
const ref = requireLayer(layers, placement.aboveId ?? placement.belowId);
|
|
3830
|
+
return insertLayer(layers, layer, ref.parent?.id ?? null, ref.index + (placement.aboveId ? 1 : 0));
|
|
3831
|
+
}
|
|
3832
|
+
if (placement.parentId) {
|
|
3833
|
+
const ref = requireLayer(layers, placement.parentId);
|
|
3834
|
+
if (ref.layer.type !== "group") fail2(`Layer "${placement.parentId}" is not a group; parentId must be a group id.`);
|
|
3835
|
+
return insertLayer(layers, layer, ref.layer.id, placement.index);
|
|
3836
|
+
}
|
|
3837
|
+
return insertLayer(layers, layer, null, placement.index ?? defaultIndex);
|
|
3838
|
+
}
|
|
3839
|
+
function removeLayer(layers, id) {
|
|
3840
|
+
const loc = requireLayer(layers, id);
|
|
3841
|
+
requireUnlocked(loc.layer, "all", "delete it");
|
|
3842
|
+
return mapLayer(layers, id, () => null);
|
|
3843
|
+
}
|
|
3844
|
+
function moveLayer(layers, id, placement) {
|
|
3845
|
+
const loc = requireLayer(layers, id);
|
|
3846
|
+
requireUnlocked(loc.layer, "all", "move it");
|
|
3847
|
+
const target = placement.aboveId ?? placement.belowId ?? placement.parentId;
|
|
3848
|
+
if (target === id) fail2("A layer cannot be placed relative to itself.");
|
|
3849
|
+
if (target && loc.layer.type === "group" && flattenLayers(loc.layer.children).some((l) => l.id === target)) {
|
|
3850
|
+
fail2(`Group "${id}" cannot be moved into its own child "${target}".`);
|
|
3851
|
+
}
|
|
3852
|
+
if (target) requireLayer(layers, target);
|
|
3853
|
+
if (!target && placement.index === void 0) fail2("Give aboveId, belowId, parentId or index.");
|
|
3854
|
+
const without = mapLayer(layers, id, () => null);
|
|
3855
|
+
return placeLayer(without, loc.layer, placement);
|
|
3856
|
+
}
|
|
3857
|
+
function withNewIds(layer) {
|
|
3858
|
+
const next = { ...layer, id: newLayerId(layer.type === "adjustment" ? "adj" : layer.type) };
|
|
3859
|
+
if (next.type === "group") next.children = next.children.map(withNewIds);
|
|
3860
|
+
if (next.type === "annotation") next.annotation = { ...next.annotation, id: next.id };
|
|
3861
|
+
if (next.type === "shape" || next.type === "type") delete next.annotation;
|
|
3862
|
+
if (next.type === "smart" && next.psd?.placed) {
|
|
3863
|
+
next.psd = { ...next.psd };
|
|
3864
|
+
delete next.psd.placed;
|
|
3865
|
+
}
|
|
3866
|
+
return next;
|
|
3867
|
+
}
|
|
3868
|
+
function duplicateLayer(layers, id, name) {
|
|
3869
|
+
const loc = requireLayer(layers, id);
|
|
3870
|
+
const copy = withNewIds(loc.layer);
|
|
3871
|
+
copy.name = name ?? `${loc.layer.name || "Layer"} copy`;
|
|
3872
|
+
delete copy.locks;
|
|
3873
|
+
return { layers: insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1), id: copy.id };
|
|
3874
|
+
}
|
|
3875
|
+
function setLayerProperties(layers, id, props) {
|
|
3876
|
+
const loc = requireLayer(layers, id);
|
|
3877
|
+
const layer = loc.layer;
|
|
3878
|
+
const onlyUnlocking = Object.keys(props).every((k) => k === "locks" || k === "visible" || k === "collapsed");
|
|
3879
|
+
if (!onlyUnlocking) requireUnlocked(layer, "all", "change its properties");
|
|
3880
|
+
const next = { ...layer };
|
|
3881
|
+
if (props.name !== void 0) {
|
|
3882
|
+
if (typeof props.name !== "string" || !props.name.trim()) fail2("name must be a non-empty string.");
|
|
3883
|
+
next.name = props.name.trim();
|
|
3884
|
+
}
|
|
3885
|
+
if (props.visible !== void 0) next.visible = Boolean(props.visible);
|
|
3886
|
+
if (props.opacity !== void 0) {
|
|
3887
|
+
if (!(typeof props.opacity === "number" && props.opacity >= 0 && props.opacity <= 1)) fail2("opacity must be a number from 0 to 1.");
|
|
3888
|
+
next.opacity = props.opacity;
|
|
3889
|
+
}
|
|
3890
|
+
if (props.fillOpacity !== void 0) {
|
|
3891
|
+
if (layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape" && layer.type !== "type") fail2(`fillOpacity applies to pixel, shape, type and smart object layers; "${id}" is a ${layer.type} layer.`);
|
|
3892
|
+
if (!(typeof props.fillOpacity === "number" && props.fillOpacity >= 0 && props.fillOpacity <= 1)) fail2("fillOpacity must be a number from 0 to 1.");
|
|
3893
|
+
const p = next;
|
|
3894
|
+
if (props.fillOpacity === 1) delete p.fillOpacity;
|
|
3895
|
+
else p.fillOpacity = props.fillOpacity;
|
|
3896
|
+
}
|
|
3897
|
+
if (props.blend !== void 0) {
|
|
3898
|
+
if (props.blend === "pass-through") {
|
|
3899
|
+
if (layer.type !== "group") fail2("pass-through is only for groups.");
|
|
3900
|
+
} else if (!isBlendMode(props.blend)) fail2(`Unknown blend mode "${props.blend}".`);
|
|
3901
|
+
next.blend = props.blend;
|
|
3902
|
+
}
|
|
3903
|
+
if (props.clipped !== void 0) {
|
|
3904
|
+
if (props.clipped && loc.index === 0) fail2("The bottom layer of a group or the stack has nothing below to clip to.");
|
|
3905
|
+
if (props.clipped) next.clipped = true;
|
|
3906
|
+
else delete next.clipped;
|
|
3907
|
+
}
|
|
3908
|
+
if (props.locks !== void 0) {
|
|
3909
|
+
const locks = {};
|
|
3910
|
+
for (const k of ["pixels", "transparency", "position", "all"]) if (props.locks[k]) locks[k] = true;
|
|
3911
|
+
if (Object.keys(locks).length) next.locks = locks;
|
|
3912
|
+
else delete next.locks;
|
|
3913
|
+
}
|
|
3914
|
+
if (props.collapsed !== void 0) {
|
|
3915
|
+
if (layer.type !== "group") fail2("collapsed applies to groups.");
|
|
3916
|
+
const g = next;
|
|
3917
|
+
if (props.collapsed) g.collapsed = true;
|
|
3918
|
+
else delete g.collapsed;
|
|
3919
|
+
}
|
|
3920
|
+
return mapLayer(layers, id, () => next);
|
|
3921
|
+
}
|
|
3922
|
+
function groupLayers(layers, ids, name = "Group") {
|
|
3923
|
+
if (!ids.length) fail2("Give at least one layer id to group.");
|
|
3924
|
+
const locs = ids.map((id) => requireLayer(layers, id));
|
|
3925
|
+
const parent = locs[0].parent?.id ?? null;
|
|
3926
|
+
if (locs.some((l) => (l.parent?.id ?? null) !== parent)) fail2("Layers to group must share one parent; move them next to each other first.");
|
|
3927
|
+
for (const l of locs) requireUnlocked(l.layer, "all", "group it");
|
|
3928
|
+
const ordered = [...new Set(locs)].sort((a, b) => a.index - b.index);
|
|
3929
|
+
const top = ordered[ordered.length - 1].index;
|
|
3930
|
+
const below = ordered.filter((l) => l.index < top).length;
|
|
3931
|
+
const group = {
|
|
3932
|
+
type: "group",
|
|
3933
|
+
id: newLayerId("group"),
|
|
3934
|
+
name,
|
|
3935
|
+
visible: true,
|
|
3936
|
+
opacity: 1,
|
|
3937
|
+
blend: "pass-through",
|
|
3938
|
+
children: ordered.map((l) => {
|
|
3939
|
+
const c = { ...l.layer };
|
|
3940
|
+
return c;
|
|
3941
|
+
})
|
|
3942
|
+
};
|
|
3943
|
+
if (group.children[0]?.clipped) {
|
|
3944
|
+
const c = { ...group.children[0] };
|
|
3945
|
+
delete c.clipped;
|
|
3946
|
+
group.children[0] = c;
|
|
3947
|
+
}
|
|
3948
|
+
let out = layers;
|
|
3949
|
+
for (const l of ordered) out = mapLayer(out, l.layer.id, () => null);
|
|
3950
|
+
return { layers: insertLayer(out, group, parent, top - below), id: group.id };
|
|
3951
|
+
}
|
|
3952
|
+
function ungroupLayer(layers, id) {
|
|
3953
|
+
const loc = requireLayer(layers, id);
|
|
3954
|
+
if (loc.layer.type !== "group") fail2(`Layer "${id}" is not a group.`);
|
|
3955
|
+
requireUnlocked(loc.layer, "all", "ungroup it");
|
|
3956
|
+
const group = loc.layer;
|
|
3957
|
+
let out = mapLayer(layers, id, () => null);
|
|
3958
|
+
group.children.forEach((c, i) => {
|
|
3959
|
+
out = insertLayer(out, c, loc.parent?.id ?? null, loc.index + i);
|
|
3960
|
+
});
|
|
3961
|
+
return { layers: out, ids: group.children.map((c) => c.id) };
|
|
3962
|
+
}
|
|
3963
|
+
function offsetLayer(layers, id, offset) {
|
|
3964
|
+
const loc = requireLayer(layers, id);
|
|
3965
|
+
if (loc.layer.type !== "pixel") fail2(`Only pixel layers move; "${id}" is a ${loc.layer.type} layer.`);
|
|
3966
|
+
requireUnlocked(loc.layer, "position", "move it");
|
|
3967
|
+
if (!Number.isFinite(offset.x) || !Number.isFinite(offset.y)) fail2("x and y must be numbers.");
|
|
3968
|
+
const layer = loc.layer;
|
|
3969
|
+
const next = { ...layer, offset: { x: Math.round(offset.x), y: Math.round(offset.y) } };
|
|
3970
|
+
const mask = layer.mask;
|
|
3971
|
+
if (mask && mask.linked === false && !mask.source) {
|
|
3972
|
+
next.mask = { ...mask, surface: translateSurface(mask.surface, layer.offset.x - next.offset.x, layer.offset.y - next.offset.y) };
|
|
3973
|
+
}
|
|
3974
|
+
return mapLayer(layers, id, () => followLayerMove(next, next.offset.x - layer.offset.x, next.offset.y - layer.offset.y));
|
|
3975
|
+
}
|
|
3976
|
+
function setLayerMask(layers, id, mask) {
|
|
3977
|
+
const loc = requireLayer(layers, id);
|
|
3978
|
+
requireUnlocked(loc.layer, "all", "change its mask");
|
|
3979
|
+
return mapLayer(layers, id, (l) => {
|
|
3980
|
+
const next = { ...l };
|
|
3981
|
+
if (mask) next.mask = mask;
|
|
3982
|
+
else delete next.mask;
|
|
3983
|
+
return next;
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
var DEVELOP_FIELDS = ["brightness", "contrast", "saturation", "exposure", "temperature", "tint"];
|
|
3987
|
+
function normalizeFilter(raw, id = newLayerId("flt")) {
|
|
3988
|
+
const enabled = raw.enabled !== false;
|
|
3989
|
+
if (raw.kind === "blur") {
|
|
3990
|
+
const r = raw.radius;
|
|
3991
|
+
if (r !== void 0 && !(typeof r === "number" && r >= 0 && r <= 50)) fail2("blur radius must be a number from 0 to 50 px.");
|
|
3992
|
+
return { id, kind: "blur", enabled, radius: typeof r === "number" ? r : 2 };
|
|
3993
|
+
}
|
|
3994
|
+
if (raw.kind !== "develop") fail2(`Unknown filter kind "${raw.kind}". Use develop or blur.`);
|
|
3995
|
+
const out = { id, kind: "develop", enabled, brightness: 0, contrast: 0, saturation: 0, exposure: 0, temperature: 0, tint: 0, process: 2 };
|
|
3996
|
+
for (const k of DEVELOP_FIELDS) {
|
|
3997
|
+
const v = raw[k];
|
|
3998
|
+
if (v === void 0) continue;
|
|
3999
|
+
if (!(typeof v === "number" && v >= -100 && v <= 100)) fail2(`${k} must be a number from -100 to 100.`);
|
|
4000
|
+
out[k] = v;
|
|
4001
|
+
}
|
|
4002
|
+
const p = raw.process;
|
|
4003
|
+
if (p !== void 0) {
|
|
4004
|
+
if (p !== 1 && p !== 2) fail2("process must be 1 or 2.");
|
|
4005
|
+
out.process = p;
|
|
4006
|
+
}
|
|
4007
|
+
return out;
|
|
4008
|
+
}
|
|
4009
|
+
function requirePixelLayer(layers, id) {
|
|
4010
|
+
const loc = requireLayer(layers, id);
|
|
4011
|
+
if (loc.layer.type !== "pixel") fail2(`Filters belong to pixel layers; "${id}" is a ${loc.layer.type} layer.`);
|
|
4012
|
+
requireUnlocked(loc.layer, "all", "change its filters");
|
|
4013
|
+
return loc.layer;
|
|
4014
|
+
}
|
|
4015
|
+
function addLayerFilter(layers, id, filter) {
|
|
4016
|
+
const layer = requirePixelLayer(layers, id);
|
|
4017
|
+
if (layer.filters?.some((f) => f.kind === filter.kind)) fail2(`Layer "${id}" already has a ${filter.kind} filter; change it with update_layer_filter.`);
|
|
4018
|
+
return mapLayer(layers, id, () => ({ ...layer, filters: [...layer.filters ?? [], filter] }));
|
|
4019
|
+
}
|
|
4020
|
+
function updateLayerFilter(layers, id, filterId, changes) {
|
|
4021
|
+
const layer = requirePixelLayer(layers, id);
|
|
4022
|
+
const i = (layer.filters ?? []).findIndex((f) => f.id === filterId);
|
|
4023
|
+
if (i === -1) fail2(`Layer "${id}" has no filter "${filterId}". Filters: ${(layer.filters ?? []).map((f) => f.id).join(", ") || "none"}.`);
|
|
4024
|
+
const current = layer.filters[i];
|
|
4025
|
+
if ("kind" in changes && changes.kind !== current.kind) fail2("A filter kind cannot change; remove it and add another.");
|
|
4026
|
+
const merged2 = normalizeFilter({ ...current, ...changes, kind: current.kind }, current.id);
|
|
4027
|
+
const filters = [...layer.filters];
|
|
4028
|
+
filters[i] = merged2;
|
|
4029
|
+
return mapLayer(layers, id, () => ({ ...layer, filters }));
|
|
4030
|
+
}
|
|
4031
|
+
function removeLayerFilter(layers, id, filterId) {
|
|
4032
|
+
const layer = requirePixelLayer(layers, id);
|
|
4033
|
+
if (!(layer.filters ?? []).some((f) => f.id === filterId)) fail2(`Layer "${id}" has no filter "${filterId}".`);
|
|
4034
|
+
const filters = layer.filters.filter((f) => f.id !== filterId);
|
|
4035
|
+
const next = { ...layer };
|
|
4036
|
+
if (filters.length) next.filters = filters;
|
|
4037
|
+
else delete next.filters;
|
|
4038
|
+
return mapLayer(layers, id, () => next);
|
|
4039
|
+
}
|
|
4040
|
+
function requireEffectLayer(layers, id, action) {
|
|
4041
|
+
const layer = requireLayer(layers, id).layer;
|
|
4042
|
+
if (layer.type === "adjustment") fail2(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
|
|
4043
|
+
requireUnlocked(layer, "all", action);
|
|
4044
|
+
return layer;
|
|
4045
|
+
}
|
|
4046
|
+
function layerEffectsOf(layer) {
|
|
4047
|
+
return layer.effects ?? { enabled: true, items: [] };
|
|
4048
|
+
}
|
|
4049
|
+
function guard(fn) {
|
|
4050
|
+
try {
|
|
4051
|
+
return fn();
|
|
4052
|
+
} catch (e) {
|
|
4053
|
+
if (e instanceof LayerEffectError) fail2(e.message);
|
|
4054
|
+
throw e;
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
function setLayerEffects(layers, id, effects) {
|
|
4058
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
4059
|
+
const next = { ...layer };
|
|
4060
|
+
if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) {
|
|
4061
|
+
guard(() => validateLayerEffects(effects));
|
|
4062
|
+
next.effects = effects;
|
|
4063
|
+
} else delete next.effects;
|
|
4064
|
+
return mapLayer(layers, id, () => next);
|
|
4065
|
+
}
|
|
4066
|
+
function addLayerEffect(layers, id, kind, params = {}, index) {
|
|
4067
|
+
if (!isLayerEffectKind(kind)) fail2(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
|
|
4068
|
+
const layer = requireEffectLayer(layers, id, "add effects");
|
|
4069
|
+
const current = layerEffectsOf(layer);
|
|
4070
|
+
const ids = new Set(current.items.map((e) => e.id));
|
|
4071
|
+
let effectId = newEffectId();
|
|
4072
|
+
while (ids.has(effectId)) effectId = newEffectId();
|
|
4073
|
+
const { id: _ignored, kind: _kind, ...rest } = params;
|
|
4074
|
+
const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
|
|
4075
|
+
const items = [...current.items];
|
|
4076
|
+
const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
|
|
4077
|
+
items.splice(at, 0, effect);
|
|
4078
|
+
return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
|
|
4079
|
+
}
|
|
4080
|
+
function requireEffect(layer, effectId) {
|
|
4081
|
+
const items = layer.effects?.items ?? [];
|
|
4082
|
+
const i = items.findIndex((e) => e.id === effectId);
|
|
4083
|
+
if (i === -1) fail2(`Layer "${layer.name || layer.id}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
|
|
4084
|
+
return i;
|
|
4085
|
+
}
|
|
4086
|
+
function updateLayerEffect(layers, id, effectId, changes) {
|
|
4087
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
4088
|
+
const i = requireEffect(layer, effectId);
|
|
4089
|
+
const current = layer.effects;
|
|
4090
|
+
const items = [...current.items];
|
|
4091
|
+
items[i] = guard(() => mergeLayerEffect(items[i], changes));
|
|
4092
|
+
return setLayerEffects(layers, id, { ...current, items });
|
|
4093
|
+
}
|
|
4094
|
+
function removeLayerEffect(layers, id, effectId) {
|
|
4095
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
4096
|
+
requireEffect(layer, effectId);
|
|
4097
|
+
const current = layer.effects;
|
|
4098
|
+
return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
|
|
4099
|
+
}
|
|
4100
|
+
function setLayerEffectsEnabled(layers, id, enabled, scale) {
|
|
4101
|
+
const layer = requireEffectLayer(layers, id, "change its effects");
|
|
4102
|
+
if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail2(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
|
|
4103
|
+
const current = layerEffectsOf(layer);
|
|
4104
|
+
const next = { ...current, enabled };
|
|
4105
|
+
if (scale !== void 0) {
|
|
4106
|
+
if (scale === 1) delete next.scale;
|
|
4107
|
+
else next.scale = scale;
|
|
4108
|
+
}
|
|
4109
|
+
return setLayerEffects(layers, id, next);
|
|
4110
|
+
}
|
|
4111
|
+
function parseBlend(value, allowPassThrough = false) {
|
|
4112
|
+
if (value === void 0) return void 0;
|
|
4113
|
+
if (value === "pass-through" && allowPassThrough) return value;
|
|
4114
|
+
if (!isBlendMode(value)) fail2(`Unknown blend mode "${String(value)}".`);
|
|
4115
|
+
return value;
|
|
4116
|
+
}
|
|
4117
|
+
function unionRect(a, b) {
|
|
4118
|
+
if (!a) return b;
|
|
4119
|
+
if (!b) return a;
|
|
4120
|
+
const x = Math.min(a.x, b.x);
|
|
4121
|
+
const y = Math.min(a.y, b.y);
|
|
4122
|
+
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 };
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
// src/image-editor/layers/compositor.ts
|
|
4126
|
+
var legacyViewCache = /* @__PURE__ */ new WeakMap();
|
|
4127
|
+
function adjustmentNodeAsLayer(node) {
|
|
4128
|
+
let view = legacyViewCache.get(node);
|
|
4129
|
+
if (!view) {
|
|
4130
|
+
view = {
|
|
4131
|
+
id: node.id,
|
|
4132
|
+
kind: node.kind,
|
|
4133
|
+
enabled: node.visible,
|
|
4134
|
+
opacity: node.opacity,
|
|
4135
|
+
blend: node.blend,
|
|
4136
|
+
params: node.params
|
|
4137
|
+
};
|
|
4138
|
+
legacyViewCache.set(node, view);
|
|
4139
|
+
}
|
|
4140
|
+
return view;
|
|
4141
|
+
}
|
|
4142
|
+
function developAdjustments(f) {
|
|
4143
|
+
return {
|
|
4144
|
+
brightness: f.brightness,
|
|
4145
|
+
contrast: f.contrast,
|
|
4146
|
+
saturation: f.saturation,
|
|
4147
|
+
exposure: f.exposure,
|
|
4148
|
+
temperature: f.temperature,
|
|
4149
|
+
tint: f.tint ?? 0,
|
|
4150
|
+
blur: 0,
|
|
4151
|
+
opacity: 1,
|
|
4152
|
+
process: f.process
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
function applyPixelFilters(data, filters) {
|
|
4156
|
+
for (const f of filters) {
|
|
4157
|
+
if (!isActiveFilter(f) || f.kind !== "develop") continue;
|
|
4158
|
+
const adj = developAdjustments(f);
|
|
4159
|
+
if (normalizeProcessVersion(adj.process) === LATEST_PROCESS_VERSION) applyProcess2Adjustments({ data }, adj);
|
|
4160
|
+
else applyLegacyAdjustments({ data }, adj);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
function depthMaskAlpha(depth, options) {
|
|
4164
|
+
const { mode, depthRange, softness, invert } = options;
|
|
4165
|
+
let v = invert ? 255 - depth : depth;
|
|
4166
|
+
const soft = Math.max(1, softness || 10);
|
|
4167
|
+
const [lo, hi] = depthRange;
|
|
4168
|
+
if (mode === "threshold") {
|
|
4169
|
+
if (v >= hi + soft) return 0;
|
|
4170
|
+
if (v > hi) return Math.round((hi + soft - v) / soft * 255);
|
|
4171
|
+
if (v >= lo) return 255;
|
|
4172
|
+
if (v < lo - soft) return 0;
|
|
4173
|
+
return Math.round((v - (lo - soft)) / soft * 255);
|
|
4174
|
+
}
|
|
4175
|
+
v += lo;
|
|
4176
|
+
return v < 0 ? 0 : v > 255 ? 255 : v;
|
|
4177
|
+
}
|
|
4178
|
+
function runLayerFilters(data, width, height, filters) {
|
|
4179
|
+
for (const f of filters) {
|
|
4180
|
+
if (!isActiveFilter(f)) continue;
|
|
4181
|
+
if (f.kind === "develop") applyPixelFilters(data, [f]);
|
|
4182
|
+
else if (f.kind === "blur") applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
function runLayerFiltersFloat(data, width, height, filters, linear) {
|
|
4186
|
+
for (const f of filters) {
|
|
4187
|
+
if (!isActiveFilter(f)) continue;
|
|
4188
|
+
if (f.kind === "blur") applyBoxBlurFloat({ width, height, data }, Math.min(Math.round(f.radius), 40));
|
|
4189
|
+
else if (f.kind === "develop") {
|
|
4190
|
+
const adj = developAdjustments(f);
|
|
4191
|
+
if (normalizeProcessVersion(adj.process) === LATEST_PROCESS_VERSION) applyProcess2Float(data, adj, linear);
|
|
4192
|
+
else applyLegacyFloat(data, adj, linear);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
function filterReach(filters) {
|
|
4197
|
+
let r = 0;
|
|
4198
|
+
for (const f of filters) if (isActiveFilter(f) && f.kind === "blur") r += Math.min(Math.round(f.radius), 40);
|
|
4199
|
+
return r;
|
|
4200
|
+
}
|
|
4201
|
+
var filterSpecCache = /* @__PURE__ */ new WeakMap();
|
|
4202
|
+
function pixelFilterSpec(layer) {
|
|
4203
|
+
const list2 = layer.filters;
|
|
4204
|
+
if (!list2?.length) return null;
|
|
4205
|
+
if (filterSpecCache.has(list2)) return filterSpecCache.get(list2);
|
|
4206
|
+
const filters = list2.filter(isActiveFilter);
|
|
4207
|
+
let spec = null;
|
|
4208
|
+
if (filters.length) {
|
|
4209
|
+
const spatial = filters.some(isSpatialFilter);
|
|
4210
|
+
spec = {
|
|
4211
|
+
key: JSON.stringify(filters),
|
|
4212
|
+
spatial,
|
|
4213
|
+
apply: (data, width, height) => runLayerFilters(data, width, height, filters)
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
filterSpecCache.set(list2, spec);
|
|
4217
|
+
return spec;
|
|
4218
|
+
}
|
|
4219
|
+
var PHOTO_PROFILE = {
|
|
4220
|
+
isFilterLayer: (l) => l.type === "adjustment",
|
|
4221
|
+
applyFilterRun: (layers, acc, _r, masks) => {
|
|
4222
|
+
const views = layers.map(adjustmentNodeAsLayer);
|
|
4223
|
+
applyAdjustmentLayers({ data: acc }, views, masks);
|
|
4224
|
+
},
|
|
4225
|
+
contentTile: (l, r, sources) => l.type === "annotation" ? sources.rasterizeAnnotation?.(l.annotation, r) ?? null : null,
|
|
4226
|
+
/** Annotation layers draw with their annotation's blend mode unless the layer sets one. */
|
|
4227
|
+
contentBlend: (l) => l.type === "annotation" && l.blend === "normal" && l.annotation.blend ? l.annotation.blend : l.blend,
|
|
4228
|
+
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,
|
|
4229
|
+
pixelFilters: (l) => l.type === "pixel" ? pixelFilterSpec(l) : null,
|
|
4230
|
+
/**
|
|
4231
|
+
* A smart object draws its rendered pixels (contents → placement → smart filters) like a pixel
|
|
4232
|
+
* layer, and a shape layer the pixels rendered from its paths.
|
|
4233
|
+
*/
|
|
4234
|
+
isPixelLike: (l) => l.type === "smart" || l.type === "shape" || l.type === "type",
|
|
4235
|
+
maskSourcePlane: (source, W, H, sources) => {
|
|
4236
|
+
const opts = source;
|
|
4237
|
+
const depth = sources.depthMap?.(opts) ?? null;
|
|
4238
|
+
if (!depth || depth.width !== W || depth.height !== H) return null;
|
|
4239
|
+
const v = new Float32Array(W * H);
|
|
4240
|
+
for (let i = 0; i < v.length; i++) v[i] = depthMaskAlpha(depth.data[i * 4], opts) / 255;
|
|
4241
|
+
return v;
|
|
4242
|
+
}
|
|
4243
|
+
};
|
|
4244
|
+
var LayerCompositor = class extends RasterCompositor {
|
|
4245
|
+
constructor(options = {}) {
|
|
4246
|
+
super(PHOTO_PROFILE, options);
|
|
4247
|
+
}
|
|
4248
|
+
// Vector masks reach the engine as part of each layer's raster mask (see `resolveVectorMasks`).
|
|
4249
|
+
composite(layers, rect, sources, options) {
|
|
4250
|
+
return super.composite(resolveVectorMasks(layers, sources.width, sources.height), rect, sources, options);
|
|
4251
|
+
}
|
|
4252
|
+
compositeTiles(layers, rect, sources, onTile, options) {
|
|
4253
|
+
super.compositeTiles(resolveVectorMasks(layers, sources.width, sources.height), rect, sources, onTile, options);
|
|
4254
|
+
}
|
|
4255
|
+
tile(layers, tx, ty, sources) {
|
|
4256
|
+
return super.tile(resolveVectorMasks(layers, sources.width, sources.height), tx, ty, sources);
|
|
4257
|
+
}
|
|
4258
|
+
};
|
|
4259
|
+
function compositeDeep(layers, rect, sources, depth) {
|
|
4260
|
+
const deep = new DeepCompositor(new LayerCompositor(), PHOTO_PROFILE, {
|
|
4261
|
+
applyFilterRunDeep: (run, acc, _r, masks) => applyAdjustmentLayersFloat(acc, run.map(adjustmentNodeAsLayer), masks),
|
|
4262
|
+
pixelFiltersDeep: (l) => {
|
|
4263
|
+
if (l.type !== "pixel") return null;
|
|
4264
|
+
const filters = (l.filters ?? []).filter(isActiveFilter);
|
|
4265
|
+
if (!filters.length) return null;
|
|
4266
|
+
return { reach: filterReach(filters), apply: (data, width, height, linear) => runLayerFiltersFloat(data, width, height, filters, linear) };
|
|
4267
|
+
}
|
|
4268
|
+
}, depth);
|
|
4269
|
+
return deep.composite(resolveVectorMasks(layers, sources.width, sources.height), rect, sources);
|
|
4116
4270
|
}
|
|
4117
4271
|
|
|
4118
4272
|
// src/image-editor/layers/text.ts
|
|
@@ -5240,6 +5394,7 @@ function offsetSmartObject(layers, id, dx, dy, canvas) {
|
|
|
5240
5394
|
const moveMask = (m2) => m2 && m2.linked !== false && !m2.source ? { ...m2, surface: translateSurface(m2.surface, mx, my) } : m2;
|
|
5241
5395
|
if (layer.mask) next.mask = moveMask(layer.mask);
|
|
5242
5396
|
if (layer.smartFilters?.mask) next.smartFilters = { ...layer.smartFilters, mask: moveMask(layer.smartFilters.mask) };
|
|
5397
|
+
next = followLayerMove(next, mx, my);
|
|
5243
5398
|
const spatial = activeSmartFilters(layer.smartFilters).some((f) => f.kind === "blur") || activeEffectSmartFilters(layer.smartFilters).length > 0;
|
|
5244
5399
|
const b = contentBounds(layer.surface);
|
|
5245
5400
|
const m = SMART_RENDER_MARGIN;
|
|
@@ -5382,9 +5537,9 @@ function convertToSmartObject(layers, ids, canvas, options) {
|
|
|
5382
5537
|
const nested = ordered.map((l) => {
|
|
5383
5538
|
let layer = l.layer;
|
|
5384
5539
|
if (layer.type === "pixel" && layer.source === "document") {
|
|
5385
|
-
const
|
|
5386
|
-
delete
|
|
5387
|
-
layer =
|
|
5540
|
+
const resolved2 = { ...layer, surface: options.surfaceOf(layer) };
|
|
5541
|
+
delete resolved2.source;
|
|
5542
|
+
layer = resolved2;
|
|
5388
5543
|
}
|
|
5389
5544
|
if (flattenLayers([layer]).some((x) => x.type === "pixel" && x.source === "document")) fail4(`Layer "${layer.name || layer.id}" holds the document image inside a group; convert the Background on its own.`);
|
|
5390
5545
|
return translateLayer(layer, -b.x, -b.y);
|
|
@@ -5676,6 +5831,7 @@ function baseFields(layer, ctx) {
|
|
|
5676
5831
|
};
|
|
5677
5832
|
if (layer.clipped) out.clipped = true;
|
|
5678
5833
|
if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
|
|
5834
|
+
if (layer.vectorMask) out.vectorMask = serializeVectorMask(layer.vectorMask);
|
|
5679
5835
|
if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
|
|
5680
5836
|
if (layer.effects) out.effects = serializeEffects(layer.effects);
|
|
5681
5837
|
return out;
|
|
@@ -5700,6 +5856,36 @@ function deserializeEffects(raw) {
|
|
|
5700
5856
|
}
|
|
5701
5857
|
return normalizeLayerEffects({ ...r, psd });
|
|
5702
5858
|
}
|
|
5859
|
+
function serializeVectorMask(vm) {
|
|
5860
|
+
const out = { components: JSON.parse(JSON.stringify(vm.components)) };
|
|
5861
|
+
if (vm.fillRule === "evenodd") out.fillRule = "evenodd";
|
|
5862
|
+
if (vm.inverted) out.inverted = true;
|
|
5863
|
+
if (vm.enabled === false) out.enabled = false;
|
|
5864
|
+
if (vm.linked === false) out.linked = false;
|
|
5865
|
+
if (vm.density !== void 0 && vm.density !== 1) out.density = vm.density;
|
|
5866
|
+
if (vm.feather) out.feather = vm.feather;
|
|
5867
|
+
return out;
|
|
5868
|
+
}
|
|
5869
|
+
function readVectorMask(r) {
|
|
5870
|
+
if (!Array.isArray(r.components)) throw new Error("its components are missing");
|
|
5871
|
+
const components = r.components.map((raw, i) => {
|
|
5872
|
+
const c = raw;
|
|
5873
|
+
const op = requireOperation(c?.op, `components[${i}].op`);
|
|
5874
|
+
if (c?.live) {
|
|
5875
|
+
const live = normalizeLiveShape(c.live, `components[${i}].live`);
|
|
5876
|
+
return { op, subpaths: liveShapeSubpaths(live), live };
|
|
5877
|
+
}
|
|
5878
|
+
return { op, subpaths: Array.isArray(c?.subpaths) && !c.subpaths.length ? [] : normalizeSubpaths(c?.subpaths, `components[${i}].subpaths`) };
|
|
5879
|
+
});
|
|
5880
|
+
const vm = { components };
|
|
5881
|
+
if (r.fillRule === "evenodd") vm.fillRule = "evenodd";
|
|
5882
|
+
if (r.inverted === true) vm.inverted = true;
|
|
5883
|
+
if (r.enabled === false) vm.enabled = false;
|
|
5884
|
+
if (r.linked === false) vm.linked = false;
|
|
5885
|
+
if (typeof r.density === "number" && r.density !== 1) vm.density = num2(r.density, 0, 1, 1);
|
|
5886
|
+
if (typeof r.feather === "number" && r.feather > 0) vm.feather = num2(r.feather, 0, 1e3, 0);
|
|
5887
|
+
return vm;
|
|
5888
|
+
}
|
|
5703
5889
|
function serializeMask(mask, hint, ctx) {
|
|
5704
5890
|
const out = { defaultValue: mask.defaultValue };
|
|
5705
5891
|
if (!mask.source) {
|
|
@@ -6107,6 +6293,13 @@ function deserializeLayer(raw, ctx) {
|
|
|
6107
6293
|
if (r.clipped === true) base.clipped = true;
|
|
6108
6294
|
const mask = readMask(r.mask, ctx, `Layer "${base.name || id}"`);
|
|
6109
6295
|
if (mask) base.mask = mask;
|
|
6296
|
+
if (r.vectorMask && typeof r.vectorMask === "object" && r.type !== "shape") {
|
|
6297
|
+
try {
|
|
6298
|
+
base.vectorMask = readVectorMask(r.vectorMask);
|
|
6299
|
+
} catch (err) {
|
|
6300
|
+
ctx.problems.push(`The vector mask of layer "${base.name || id}" could not be read (${err instanceof Error ? err.message : String(err)}); it was left out.`);
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6110
6303
|
if (r.locks && typeof r.locks === "object") {
|
|
6111
6304
|
const locks = {};
|
|
6112
6305
|
for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
|
|
@@ -7330,97 +7523,6 @@ function writeAdjustmentBlock(kind, params) {
|
|
|
7330
7523
|
throw new Error(`Unknown adjustment kind "${kind}".`);
|
|
7331
7524
|
}
|
|
7332
7525
|
|
|
7333
|
-
// src/image-editor/psd/channels.ts
|
|
7334
|
-
var hexToRgb2 = (hex) => {
|
|
7335
|
-
const v = parseInt(/^#?([0-9a-f]{6})$/i.exec(hex)?.[1] ?? "ff0000", 16);
|
|
7336
|
-
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
7337
|
-
};
|
|
7338
|
-
var rgbToHex = (r, g, b) => `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
7339
|
-
function alphaChannelResources(channels) {
|
|
7340
|
-
if (!channels.length) return [];
|
|
7341
|
-
const names = new PsdWriter();
|
|
7342
|
-
for (const c of channels) names.pascal(encodeLegacyName(c.name), 1);
|
|
7343
|
-
const unicode = new PsdWriter();
|
|
7344
|
-
for (const c of channels) unicode.unicode(c.name);
|
|
7345
|
-
const display = new PsdWriter();
|
|
7346
|
-
display.u32(1);
|
|
7347
|
-
for (const c of channels) {
|
|
7348
|
-
const [r, g, b] = hexToRgb2(c.color);
|
|
7349
|
-
display.u16(0);
|
|
7350
|
-
for (const v of [r, g, b, 0]) display.u16(v * 257);
|
|
7351
|
-
display.u16(Math.round(Math.min(1, Math.max(0, c.opacity)) * 100));
|
|
7352
|
-
display.u8(c.indicates === "selected" ? 0 : 1);
|
|
7353
|
-
display.u8(0);
|
|
7354
|
-
}
|
|
7355
|
-
return [
|
|
7356
|
-
{ id: 1006, name: "", data: names.result() },
|
|
7357
|
-
{ id: 1045, name: "", data: unicode.result() },
|
|
7358
|
-
{ id: 1077, name: "", data: display.result() }
|
|
7359
|
-
];
|
|
7360
|
-
}
|
|
7361
|
-
function planeTo8(plane, n, depth) {
|
|
7362
|
-
if (depth === 8) return plane.slice(0, n);
|
|
7363
|
-
const out = new Uint8Array(n);
|
|
7364
|
-
const view = new DataView(plane.buffer, plane.byteOffset, plane.byteLength);
|
|
7365
|
-
for (let i = 0; i < n; i++) {
|
|
7366
|
-
if (depth === 16) out[i] = plane[2 * i];
|
|
7367
|
-
else out[i] = Math.round(Math.min(1, Math.max(0, view.getFloat32(4 * i, false))) * 255);
|
|
7368
|
-
}
|
|
7369
|
-
return out;
|
|
7370
|
-
}
|
|
7371
|
-
function readAlphaChannels(planes, header, first, resource2, warn) {
|
|
7372
|
-
const count = header.channels - first;
|
|
7373
|
-
if (count <= 0 || header.depth === 1) return [];
|
|
7374
|
-
const size = rowBytes(header.width, header.depth) * header.height;
|
|
7375
|
-
const n = header.width * header.height;
|
|
7376
|
-
const names = [];
|
|
7377
|
-
const unicode = resource2(1045);
|
|
7378
|
-
if (unicode) {
|
|
7379
|
-
const r = new PsdReader(unicode);
|
|
7380
|
-
while (r.remaining >= 4 && names.length < count) names.push(r.unicode().replace(/\0+$/, ""));
|
|
7381
|
-
} else {
|
|
7382
|
-
const legacy = resource2(1006);
|
|
7383
|
-
if (legacy) {
|
|
7384
|
-
const r = new PsdReader(legacy);
|
|
7385
|
-
while (r.remaining >= 1 && names.length < count) names.push(new TextDecoder("latin1").decode(r.pascal(1)));
|
|
7386
|
-
}
|
|
7387
|
-
}
|
|
7388
|
-
const display = [];
|
|
7389
|
-
const info = resource2(1077);
|
|
7390
|
-
if (info && info.length >= 4) {
|
|
7391
|
-
const r = new PsdReader(info);
|
|
7392
|
-
r.u32();
|
|
7393
|
-
while (r.remaining >= 14 && display.length < count) {
|
|
7394
|
-
r.u16();
|
|
7395
|
-
const [cr, cg, cb] = [r.u16(), r.u16(), r.u16()];
|
|
7396
|
-
r.u16();
|
|
7397
|
-
const opacity = r.u16();
|
|
7398
|
-
const kind = r.u8();
|
|
7399
|
-
r.u8();
|
|
7400
|
-
display.push({ color: rgbToHex(cr >> 8, cg >> 8, cb >> 8), opacity: Math.min(100, opacity) / 100, kind });
|
|
7401
|
-
}
|
|
7402
|
-
}
|
|
7403
|
-
const out = [];
|
|
7404
|
-
for (let i = 0; i < count; i++) {
|
|
7405
|
-
const start = (first + i) * size;
|
|
7406
|
-
if (start + size > planes.length) break;
|
|
7407
|
-
const d = display[i];
|
|
7408
|
-
const name = names[i] || `Alpha ${i + 1}`;
|
|
7409
|
-
if (d?.kind === 2) {
|
|
7410
|
-
warn(`Spot colour channel "${name}" is not supported and was left out.`);
|
|
7411
|
-
continue;
|
|
7412
|
-
}
|
|
7413
|
-
out.push({
|
|
7414
|
-
name,
|
|
7415
|
-
data: planeTo8(planes.subarray(start, start + size), n, header.depth),
|
|
7416
|
-
color: d?.color ?? "#ff0000",
|
|
7417
|
-
opacity: d?.opacity ?? 0.5,
|
|
7418
|
-
indicates: d?.kind === 0 ? "selected" : "masked"
|
|
7419
|
-
});
|
|
7420
|
-
}
|
|
7421
|
-
return out;
|
|
7422
|
-
}
|
|
7423
|
-
|
|
7424
7526
|
// src/image-editor/psd/paths.ts
|
|
7425
7527
|
var RECORD = 26;
|
|
7426
7528
|
var fixed = (v) => v / 16777216;
|
|
@@ -7480,6 +7582,8 @@ function writePathRecords(path) {
|
|
|
7480
7582
|
return w.result();
|
|
7481
7583
|
}
|
|
7482
7584
|
var VMSK_INVERT = 1;
|
|
7585
|
+
var VMSK_NOT_LINKED = 2;
|
|
7586
|
+
var VMSK_DISABLED = 4;
|
|
7483
7587
|
function readVectorBlock(data) {
|
|
7484
7588
|
if (data.length < 8) return null;
|
|
7485
7589
|
const r = new PsdReader(data);
|
|
@@ -7550,6 +7654,97 @@ function vectorBlockForComponents(components, inverted, width, height, flags = 0
|
|
|
7550
7654
|
return writeVectorBlock({ subpaths, initialFill: subpaths.length === 0 && inverted }, flags & ~VMSK_INVERT | (inverted && subpaths.length ? VMSK_INVERT : 0));
|
|
7551
7655
|
}
|
|
7552
7656
|
|
|
7657
|
+
// src/image-editor/psd/channels.ts
|
|
7658
|
+
var hexToRgb2 = (hex) => {
|
|
7659
|
+
const v = parseInt(/^#?([0-9a-f]{6})$/i.exec(hex)?.[1] ?? "ff0000", 16);
|
|
7660
|
+
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
7661
|
+
};
|
|
7662
|
+
var rgbToHex = (r, g, b) => `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
7663
|
+
function alphaChannelResources(channels) {
|
|
7664
|
+
if (!channels.length) return [];
|
|
7665
|
+
const names = new PsdWriter();
|
|
7666
|
+
for (const c of channels) names.pascal(encodeLegacyName(c.name), 1);
|
|
7667
|
+
const unicode = new PsdWriter();
|
|
7668
|
+
for (const c of channels) unicode.unicode(c.name);
|
|
7669
|
+
const display = new PsdWriter();
|
|
7670
|
+
display.u32(1);
|
|
7671
|
+
for (const c of channels) {
|
|
7672
|
+
const [r, g, b] = hexToRgb2(c.color);
|
|
7673
|
+
display.u16(0);
|
|
7674
|
+
for (const v of [r, g, b, 0]) display.u16(v * 257);
|
|
7675
|
+
display.u16(Math.round(Math.min(1, Math.max(0, c.opacity)) * 100));
|
|
7676
|
+
display.u8(c.indicates === "selected" ? 0 : 1);
|
|
7677
|
+
display.u8(0);
|
|
7678
|
+
}
|
|
7679
|
+
return [
|
|
7680
|
+
{ id: 1006, name: "", data: names.result() },
|
|
7681
|
+
{ id: 1045, name: "", data: unicode.result() },
|
|
7682
|
+
{ id: 1077, name: "", data: display.result() }
|
|
7683
|
+
];
|
|
7684
|
+
}
|
|
7685
|
+
function planeTo8(plane, n, depth) {
|
|
7686
|
+
if (depth === 8) return plane.slice(0, n);
|
|
7687
|
+
const out = new Uint8Array(n);
|
|
7688
|
+
const view = new DataView(plane.buffer, plane.byteOffset, plane.byteLength);
|
|
7689
|
+
for (let i = 0; i < n; i++) {
|
|
7690
|
+
if (depth === 16) out[i] = plane[2 * i];
|
|
7691
|
+
else out[i] = Math.round(Math.min(1, Math.max(0, view.getFloat32(4 * i, false))) * 255);
|
|
7692
|
+
}
|
|
7693
|
+
return out;
|
|
7694
|
+
}
|
|
7695
|
+
function readAlphaChannels(planes, header, first, resource2, warn) {
|
|
7696
|
+
const count = header.channels - first;
|
|
7697
|
+
if (count <= 0 || header.depth === 1) return [];
|
|
7698
|
+
const size = rowBytes(header.width, header.depth) * header.height;
|
|
7699
|
+
const n = header.width * header.height;
|
|
7700
|
+
const names = [];
|
|
7701
|
+
const unicode = resource2(1045);
|
|
7702
|
+
if (unicode) {
|
|
7703
|
+
const r = new PsdReader(unicode);
|
|
7704
|
+
while (r.remaining >= 4 && names.length < count) names.push(r.unicode().replace(/\0+$/, ""));
|
|
7705
|
+
} else {
|
|
7706
|
+
const legacy = resource2(1006);
|
|
7707
|
+
if (legacy) {
|
|
7708
|
+
const r = new PsdReader(legacy);
|
|
7709
|
+
while (r.remaining >= 1 && names.length < count) names.push(new TextDecoder("latin1").decode(r.pascal(1)));
|
|
7710
|
+
}
|
|
7711
|
+
}
|
|
7712
|
+
const display = [];
|
|
7713
|
+
const info = resource2(1077);
|
|
7714
|
+
if (info && info.length >= 4) {
|
|
7715
|
+
const r = new PsdReader(info);
|
|
7716
|
+
r.u32();
|
|
7717
|
+
while (r.remaining >= 14 && display.length < count) {
|
|
7718
|
+
r.u16();
|
|
7719
|
+
const [cr, cg, cb] = [r.u16(), r.u16(), r.u16()];
|
|
7720
|
+
r.u16();
|
|
7721
|
+
const opacity = r.u16();
|
|
7722
|
+
const kind = r.u8();
|
|
7723
|
+
r.u8();
|
|
7724
|
+
display.push({ color: rgbToHex(cr >> 8, cg >> 8, cb >> 8), opacity: Math.min(100, opacity) / 100, kind });
|
|
7725
|
+
}
|
|
7726
|
+
}
|
|
7727
|
+
const out = [];
|
|
7728
|
+
for (let i = 0; i < count; i++) {
|
|
7729
|
+
const start = (first + i) * size;
|
|
7730
|
+
if (start + size > planes.length) break;
|
|
7731
|
+
const d = display[i];
|
|
7732
|
+
const name = names[i] || `Alpha ${i + 1}`;
|
|
7733
|
+
if (d?.kind === 2) {
|
|
7734
|
+
warn(`Spot colour channel "${name}" is not supported and was left out.`);
|
|
7735
|
+
continue;
|
|
7736
|
+
}
|
|
7737
|
+
out.push({
|
|
7738
|
+
name,
|
|
7739
|
+
data: planeTo8(planes.subarray(start, start + size), n, header.depth),
|
|
7740
|
+
color: d?.color ?? "#ff0000",
|
|
7741
|
+
opacity: d?.opacity ?? 0.5,
|
|
7742
|
+
indicates: d?.kind === 0 ? "selected" : "masked"
|
|
7743
|
+
});
|
|
7744
|
+
}
|
|
7745
|
+
return out;
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7553
7748
|
// src/image-editor/psd/shapes.ts
|
|
7554
7749
|
var SHAPE_PSD_KEYS = ["vsms", "vmsk", "vscg", "vstk", "vogk", "SoCo", "GdFl", "PtFl"];
|
|
7555
7750
|
var unit = (u, value) => ({ type: "UntF", unit: u, value });
|
|
@@ -10181,9 +10376,22 @@ function baseProps(ctx, rec, name, notes) {
|
|
|
10181
10376
|
ctx.warn.add("Artboards are not supported; they came in as plain groups, without their background colour and clipping", name);
|
|
10182
10377
|
notes.push("artboard imported as a group");
|
|
10183
10378
|
}
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10379
|
+
const vectorBlock = psdKind(rec) === "shape" ? void 0 : rec.blocks.find((b) => b.key === "vsms") ?? rec.blocks.find((b) => b.key === "vmsk");
|
|
10380
|
+
if (vectorBlock) {
|
|
10381
|
+
const read2 = componentsFromVectorBlock(vectorBlock.data, ctx.file.header.width, ctx.file.header.height);
|
|
10382
|
+
if (read2) {
|
|
10383
|
+
const vm = { components: read2.components };
|
|
10384
|
+
if (read2.inverted) vm.inverted = true;
|
|
10385
|
+
if (read2.flags & VMSK_NOT_LINKED) vm.linked = false;
|
|
10386
|
+
if (read2.flags & VMSK_DISABLED) vm.enabled = false;
|
|
10387
|
+
const m = rec.mask;
|
|
10388
|
+
if (m?.vectorDensity !== void 0 && m.vectorDensity < 255) vm.density = percent(m.vectorDensity);
|
|
10389
|
+
if (m?.vectorFeather !== void 0 && m.vectorFeather > 0) vm.feather = Math.min(1e3, m.vectorFeather);
|
|
10390
|
+
props.vectorMask = vm;
|
|
10391
|
+
} else {
|
|
10392
|
+
ctx.warn.add("A vector mask could not be read and was ignored", name);
|
|
10393
|
+
notes.push("vector mask ignored");
|
|
10394
|
+
}
|
|
10187
10395
|
}
|
|
10188
10396
|
return props;
|
|
10189
10397
|
}
|
|
@@ -11532,6 +11740,13 @@ function exportPsd(input) {
|
|
|
11532
11740
|
if (fx) blocks.push(fx);
|
|
11533
11741
|
const locks = lockBits(layer);
|
|
11534
11742
|
if (locks) blocks.push({ key: "lspf", data: u32Bytes(locks >>> 0) });
|
|
11743
|
+
const vm = layer.type !== "shape" ? layer.vectorMask : void 0;
|
|
11744
|
+
if (vm) {
|
|
11745
|
+
const flags = (vm.linked === false ? VMSK_NOT_LINKED : 0) | (vm.enabled === false ? VMSK_DISABLED : 0);
|
|
11746
|
+
blocks.push({ key: "vmsk", data: vectorBlockForComponents(vm.components, vm.inverted === true, width, height, flags) });
|
|
11747
|
+
if ((vm.density ?? 1) !== 1 || (vm.feather ?? 0) > 0) note("Vector mask density and feather are not written; the paths are", layer.name);
|
|
11748
|
+
if (vm.fillRule === "evenodd") note("Vector masks are written with the nonzero fill rule (Photoshop has no even-odd paths)", layer.name);
|
|
11749
|
+
}
|
|
11535
11750
|
return {
|
|
11536
11751
|
opacity: Math.round(Math.min(1, Math.max(0, layer.opacity)) * 255),
|
|
11537
11752
|
clipping: layer.clipped ? 1 : 0,
|
|
@@ -11870,6 +12085,7 @@ export {
|
|
|
11870
12085
|
shiftCoverage,
|
|
11871
12086
|
SELECTION_MODIFY_OPS,
|
|
11872
12087
|
modifySelection,
|
|
12088
|
+
layerMaskSelection,
|
|
11873
12089
|
normalizeLayerMask,
|
|
11874
12090
|
layerMaskFromSelection,
|
|
11875
12091
|
updateLayerMaskProps,
|
|
@@ -11926,6 +12142,8 @@ export {
|
|
|
11926
12142
|
applyBoxBlur,
|
|
11927
12143
|
applyDepthMask,
|
|
11928
12144
|
removeAlpha,
|
|
12145
|
+
flattenPath,
|
|
12146
|
+
rasterizeCoverage,
|
|
11929
12147
|
TEXT_ALIGNS,
|
|
11930
12148
|
MAX_EFFECT_ANIMATION_MS,
|
|
11931
12149
|
SHAPE_OPERATIONS,
|
|
@@ -11942,17 +12160,6 @@ export {
|
|
|
11942
12160
|
isRenderedLayer,
|
|
11943
12161
|
shapeLayers,
|
|
11944
12162
|
smartObjectLayers,
|
|
11945
|
-
adjustmentNodeAsLayer,
|
|
11946
|
-
depthMaskAlpha,
|
|
11947
|
-
runLayerFilters,
|
|
11948
|
-
runLayerFiltersFloat,
|
|
11949
|
-
LayerCompositor,
|
|
11950
|
-
compositeDeep,
|
|
11951
|
-
DEFAULT_ADJUSTMENTS_VALUES,
|
|
11952
|
-
DEFAULT_TRANSFORM_VALUES,
|
|
11953
|
-
DEFAULT_DEPTH_MASK_VALUES,
|
|
11954
|
-
flattenPath,
|
|
11955
|
-
rasterizeCoverage,
|
|
11956
12163
|
LayerOperationError,
|
|
11957
12164
|
userErrorMessage,
|
|
11958
12165
|
isPaintableLayer,
|
|
@@ -11990,6 +12197,7 @@ export {
|
|
|
11990
12197
|
componentPath,
|
|
11991
12198
|
shapeOutline,
|
|
11992
12199
|
shapeGeometryBounds,
|
|
12200
|
+
componentsCoverage,
|
|
11993
12201
|
shapeRenderClip,
|
|
11994
12202
|
renderShapeRect,
|
|
11995
12203
|
renderShape,
|
|
@@ -12010,6 +12218,24 @@ export {
|
|
|
12010
12218
|
shapeFromAnnotation,
|
|
12011
12219
|
annotationBlend,
|
|
12012
12220
|
annotationOfShape,
|
|
12221
|
+
isActiveVectorMask,
|
|
12222
|
+
vectorMaskCoverage,
|
|
12223
|
+
effectiveLayerMask,
|
|
12224
|
+
resolveVectorMasks,
|
|
12225
|
+
offsetVectorMask,
|
|
12226
|
+
transformVectorMask,
|
|
12227
|
+
followLayerMove,
|
|
12228
|
+
vectorMaskBounds,
|
|
12229
|
+
vectorMaskThumbnail,
|
|
12230
|
+
adjustmentNodeAsLayer,
|
|
12231
|
+
depthMaskAlpha,
|
|
12232
|
+
runLayerFilters,
|
|
12233
|
+
runLayerFiltersFloat,
|
|
12234
|
+
LayerCompositor,
|
|
12235
|
+
compositeDeep,
|
|
12236
|
+
DEFAULT_ADJUSTMENTS_VALUES,
|
|
12237
|
+
DEFAULT_TRANSFORM_VALUES,
|
|
12238
|
+
DEFAULT_DEPTH_MASK_VALUES,
|
|
12013
12239
|
TEXT_RENDER_MARGIN,
|
|
12014
12240
|
textMatrix,
|
|
12015
12241
|
textOutlines,
|