@pixerate/editor 0.95.0 → 0.97.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.
@@ -191,6 +191,7 @@ GridCraft. Apache POI is © The Apache Software Foundation, licensed under the A
191
191
  - `crates/io/src/psd_export.rs` and `crates/psd/src/builder.rs` (layer records with RLE channels, group divider and folder records, adjustment blocks, user masks with parameters, `luni` / `lyid` / `iOpa` / `lspf`, the resolution and version-info resources, PSB above 30 000 px) → `packages/core/src/image-editor/psd/export.ts`; 8-bit RGB only, with the merged image from our compositor
192
192
  - `crates/doc/src/effects.rs` (the layer-style model: shadows, glows, strokes, satin, bevel & emboss with its Contour and Texture elements, colour / gradient / pattern overlays, the shared enabled / blend / opacity, Photoshop's defaults, the global light) → `packages/core/src/raster/effects/model.ts`; colours are `#rrggbb`, effects carry ids, and validation, merging and Scale Effects are our own
193
193
  - `crates/compose/src/effects.rs` (render order and the per-kind maps: `margin`, `maps_are_local`, the Felzenszwalb–Huttenlocher `dt1` / `edt_nearest`, the 5 × 5 `chamfer_from`, `dist_outside` / `dist_inside` with coverage-placed edges, `local_coverage`, `dilate`, `spread_split`, the fractional `box_geom` / `box_line` / `tent_line` blur, `contour_lut` with corners, `ranged_lut`, `satin_lut`, `hash_noise` / `noise`, `shadow_map`, `glow_map`, `satin_map`, `bevel_geom`, `bevel_chisel_h`, `bevel_height`, `bevel_maps`, `gradient_units`, `gradient_t`, `paint_glow`, `stroke_widths`, the stroke frame and the outside-stroke shares, `knock_out_exterior`, `premerges_layer` and `composite_with_effects`), `crates/compose/src/fill_layout.rs` (`gradient_layout`) and `crates/compose/src/pattern.rs` (`Tile::sample`, `Placement`) → `packages/core/src/raster/effects/render.ts` and `patterns.ts`; single-threaded, without vector-shape outlines, text gamma, Transparency Shapes Layer or Blend Interior Effects as Group, with chiselled bevel edges measured to the coverage-gradient edge line instead of photocraft's sharp-edge refinement, and the maps built once per layer region and cropped per tile by our compositor; the built-in patterns are our own; test cases re-expressed → `packages/core/tests/raster-effects.test.ts`
194
+ - `crates/io/src/effects_map.rs` (`parse_lfx2` / `write_lfx2`: the per-kind descriptor keys, the single and `…Multi` instance keys, `present` / `enab`, blend ids as four-character codes or long string ids, contours with corner points, `parse_lrfx` for legacy effects), `crates/io/src/blocks.rs` (`color_from_desc` / `color_to_desc`, `gradient_style`, the `Grdn` colour and transparency stops, `pattern_ref`, `pattern_placement`), `crates/io/src/psd_import.rs` / `psd_export.rs` (`lmfx` over `lfx2` / `lfxs`, the block kept and written back while the effects are unchanged, odd-length blocks padded) and `crates/psd/src/patterns.rs` (`Patt` / `Pat2` / `Pat3` blocks: patterns, virtual memory array lists, written channels, padded channel rectangles) → `packages/core/src/image-editor/psd/effects.ts` and `patterns.ts`; gradient midpoints become extra stops and smoothness is drawn linearly (photocraft bakes Photoshop's interpolation), and patterns are written as 8-bit RGB; test cases re-expressed → `packages/core/tests/image-psd-effects.test.ts`
194
195
 
195
196
  ## soundcraft
196
197
 
@@ -10,6 +10,7 @@
10
10
 
11
11
 
12
12
 
13
+
13
14
  var _chunkCRMAXIUVcjs = require('./chunk-CRMAXIUV.cjs');
14
15
 
15
16
  // src/raster/blur.ts
@@ -372,6 +373,7 @@ function oneOf(r, key, path, values) {
372
373
  }
373
374
  function blend(r, key, path) {
374
375
  if (!_chunkCRMAXIUVcjs.isBlendMode.call(void 0, r[key])) fail(`${path}.${key}`, `must be a blend mode such as normal, multiply or screen (got ${JSON.stringify(r[key])}).`);
376
+ if (r[key] === "pigment") fail(`${path}.${key}`, "pigment is a layer blend mode only; effects cannot use it.");
375
377
  }
376
378
  function contour(v, path) {
377
379
  if (v === void 0) return;
@@ -1078,11 +1080,18 @@ function boxLine(src, x0, dst, bw) {
1078
1080
  }
1079
1081
  }
1080
1082
  var BLUR_EPS = 1e-7;
1083
+ var BLUR_FIX = 2 ** 32;
1081
1084
  function tentLine(src, dst, bw, mid) {
1082
1085
  const [r] = boxGeom(bw);
1083
- boxLine(src, -r, mid.subarray(0, src.length + 2 * r), bw);
1084
- boxLine(mid.subarray(0, src.length + 2 * r), r, dst, bw);
1085
- for (let i = 0; i < dst.length; i++) if (Math.abs(dst[i]) < BLUR_EPS) dst[i] = 0;
1086
+ const m = mid.subarray(0, src.length + 2 * r);
1087
+ for (let i = 0; i < src.length; i++) src[i] = Math.round(src[i] * BLUR_FIX);
1088
+ boxLine(src, -r, m, bw);
1089
+ for (let i = 0; i < m.length; i++) m[i] = Math.round(m[i]);
1090
+ boxLine(m, r, dst, bw);
1091
+ for (let i = 0; i < dst.length; i++) {
1092
+ const v = dst[i] / BLUR_FIX;
1093
+ dst[i] = Math.abs(v) < BLUR_EPS ? 0 : v;
1094
+ }
1086
1095
  }
1087
1096
  function tent(m, w) {
1088
1097
  if (!Number.isFinite(w) || m.v.length === 0) return;
@@ -1384,25 +1393,41 @@ function effectsAreLocal(items) {
1384
1393
  }
1385
1394
  });
1386
1395
  }
1396
+ function shapeBounds(shape, rect) {
1397
+ const { width: w, height: h } = rect;
1398
+ const rowHit = (y) => {
1399
+ for (let x = 0, i = y * w; x < w; x++, i++) if (shape[i] > INSIDE_EPS) return true;
1400
+ return false;
1401
+ };
1402
+ let y0 = 0;
1403
+ while (y0 < h && !rowHit(y0)) y0++;
1404
+ if (y0 === h) return null;
1405
+ let y1 = h - 1;
1406
+ while (!rowHit(y1)) y1--;
1407
+ let x0 = w;
1408
+ let x1 = -1;
1409
+ for (let y = y0; y <= y1; y++) {
1410
+ const o = y * w;
1411
+ for (let x = 0; x < x0; x++) {
1412
+ if (shape[o + x] > INSIDE_EPS) {
1413
+ x0 = x;
1414
+ break;
1415
+ }
1416
+ }
1417
+ for (let x = w - 1; x > x1; x--) {
1418
+ if (shape[o + x] > INSIDE_EPS) {
1419
+ x1 = x;
1420
+ break;
1421
+ }
1422
+ }
1423
+ }
1424
+ return { x: rect.x + x0, y: rect.y + y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };
1425
+ }
1387
1426
  function buildEffectMaps(items, shape, rect, ctx) {
1388
1427
  const w = rect.width;
1389
1428
  const h = rect.height;
1390
1429
  const s = { w, h, v: shape };
1391
- let x0 = Infinity;
1392
- let y0 = Infinity;
1393
- let x1 = -Infinity;
1394
- let y1 = -Infinity;
1395
- for (let y = 0; y < h; y++) {
1396
- for (let x = 0; x < w; x++) {
1397
- if (shape[y * w + x] > INSIDE_EPS) {
1398
- if (x < x0) x0 = x;
1399
- if (x > x1) x1 = x;
1400
- if (y < y0) y0 = y;
1401
- if (y > y1) y1 = y;
1402
- }
1403
- }
1404
- }
1405
- const bounds = x0 === Infinity ? null : { x: rect.x + x0, y: rect.y + y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };
1430
+ const bounds = shapeBounds(shape, rect);
1406
1431
  const anchor = bounds ? { x: bounds.x, y: bounds.y } : { x: rect.x, y: rect.y };
1407
1432
  const per = [];
1408
1433
  const bevelPaint = [];
@@ -1450,8 +1475,84 @@ function buildEffectMaps(items, shape, rect, ctx) {
1450
1475
  for (const d of [din, dout, doutExact]) if (d) bytes += d.byteLength;
1451
1476
  return { rect, items, bounds, shape, per, bevelPaint, din, dout, doutExact, frames, bytes };
1452
1477
  }
1478
+ var rectArea = (r) => r.width * r.height;
1479
+ function clipRect(a, b) {
1480
+ const x0 = Math.max(a.x, b.x);
1481
+ const y0 = Math.max(a.y, b.y);
1482
+ const x1 = Math.min(a.x + a.width, b.x + b.width);
1483
+ const y1 = Math.min(a.y + a.height, b.y + b.height);
1484
+ return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : null;
1485
+ }
1486
+ var growRect = (r, m) => ({ x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m });
1487
+ function unionRect(a, b) {
1488
+ const x0 = Math.min(a.x, b.x);
1489
+ const y0 = Math.min(a.y, b.y);
1490
+ return { x: x0, y: y0, width: Math.max(a.x + a.width, b.x + b.width) - x0, height: Math.max(a.y + a.height, b.y + b.height) - y0 };
1491
+ }
1492
+ function rectMinus(r, cut) {
1493
+ const i = clipRect(r, cut);
1494
+ if (!i) return [r];
1495
+ const parts = [
1496
+ { x: r.x, y: r.y, width: r.width, height: i.y - r.y },
1497
+ { x: r.x, y: i.y + i.height, width: r.width, height: r.y + r.height - (i.y + i.height) },
1498
+ { x: r.x, y: i.y, width: i.x - r.x, height: i.height },
1499
+ { x: i.x + i.width, y: i.y, width: r.x + r.width - (i.x + i.width), height: i.height }
1500
+ ];
1501
+ return parts.filter((p) => p.width > 0 && p.height > 0);
1502
+ }
1503
+ function pasteMap(dst, to, src, from, a) {
1504
+ for (let y = a.y; y < a.y + a.height; y++) {
1505
+ const s = (y - from.y) * from.width + (a.x - from.x);
1506
+ dst.set(src.subarray(s, s + a.width), (y - to.y) * to.width + (a.x - to.x));
1507
+ }
1508
+ }
1509
+ function patchEffectMaps(prev, items, rect, changed, margin, shapeOver, ctx) {
1510
+ if (!effectsAreLocal(items) || rectArea(rect) <= 0) return null;
1511
+ let areas = [];
1512
+ for (const c of changed) {
1513
+ const a = clipRect(growRect(c, margin), rect);
1514
+ if (a) areas.push(a);
1515
+ }
1516
+ areas.push(...rectMinus(rect, prev.rect));
1517
+ for (let merged = true; merged; ) {
1518
+ merged = false;
1519
+ for (let i = 0; i < areas.length && !merged; i++) {
1520
+ for (let j = i + 1; j < areas.length; j++) {
1521
+ if (clipRect(growRect(areas[i], 2 * margin), areas[j])) {
1522
+ areas[i] = unionRect(areas[i], areas[j]);
1523
+ areas.splice(j, 1);
1524
+ merged = true;
1525
+ break;
1526
+ }
1527
+ }
1528
+ }
1529
+ }
1530
+ const windows = areas.map((a) => clipRect(growRect(a, margin), rect));
1531
+ if (windows.reduce((n, w) => n + rectArea(w), 0) > rectArea(rect) / 2) return null;
1532
+ const same = rect.x === prev.rect.x && rect.y === prev.rect.y && rect.width === prev.rect.width && rect.height === prev.rect.height;
1533
+ const take = (v) => same ? v : crop(v, prev.rect, rect, 0);
1534
+ const shape = take(prev.shape);
1535
+ const per = prev.per.map((maps) => maps.map(take));
1536
+ const din = prev.din && take(prev.din);
1537
+ const dout = prev.dout && take(prev.dout);
1538
+ const doutExact = prev.doutExact && take(prev.doutExact);
1539
+ areas.forEach((a, k) => {
1540
+ const w = windows[k];
1541
+ const part = buildEffectMaps(items, shapeOver(w), w, ctx);
1542
+ pasteMap(shape, rect, part.shape, w, a);
1543
+ per.forEach((maps, i) => maps.forEach((m, j) => pasteMap(m, rect, part.per[i][j], w, a)));
1544
+ if (din) pasteMap(din, rect, part.din, w, a);
1545
+ if (dout) pasteMap(dout, rect, part.dout, w, a);
1546
+ if (doutExact) pasteMap(doutExact, rect, part.doutExact, w, a);
1547
+ });
1548
+ let bytes = shape.byteLength;
1549
+ for (const maps of per) for (const m of maps) bytes += m.byteLength;
1550
+ for (const d of [din, dout, doutExact]) if (d) bytes += d.byteLength;
1551
+ return { rect, items, bounds: shapeBounds(shape, rect), shape, per, bevelPaint: prev.bevelPaint, din, dout, doutExact, frames: /* @__PURE__ */ new Map(), bytes };
1552
+ }
1453
1553
  var rgbScratchB = [0, 0, 0];
1454
1554
  var rgbScratchS = [0, 0, 0];
1555
+ var rgbScratchM = [0, 0, 0];
1455
1556
  function compositeAt(dst, i, mode, separable, sr, sg, sb, sa) {
1456
1557
  if (!(sa > 0)) return;
1457
1558
  const ab = dst[i + 3];
@@ -1463,6 +1564,20 @@ function compositeAt(dst, i, mode, separable, sr, sg, sb, sa) {
1463
1564
  const b0 = dst[i];
1464
1565
  const b1 = dst[i + 1];
1465
1566
  const b2 = dst[i + 2];
1567
+ if (mode === "pigment") {
1568
+ rgbScratchB[0] = b0;
1569
+ rgbScratchB[1] = b1;
1570
+ rgbScratchB[2] = b2;
1571
+ rgbScratchS[0] = sr;
1572
+ rgbScratchS[1] = sg;
1573
+ rgbScratchS[2] = sb;
1574
+ _chunkCRMAXIUVcjs.mixPigmentRgb.call(void 0, rgbScratchB, rgbScratchS, sa / ao, rgbScratchM);
1575
+ dst[i] = rgbScratchM[0];
1576
+ dst[i + 1] = rgbScratchM[1];
1577
+ dst[i + 2] = rgbScratchM[2];
1578
+ dst[i + 3] = ao;
1579
+ return;
1580
+ }
1466
1581
  let m0;
1467
1582
  let m1;
1468
1583
  let m2;
@@ -2042,7 +2157,7 @@ var ZERO_TILE = new Uint8ClampedArray(TILE_BYTES);
2042
2157
  var isGroup = (l) => l.type === "group";
2043
2158
  var isPixel = (l) => l.type === "pixel";
2044
2159
  var RasterCompositor = (_class = class {
2045
- constructor(profile = {}, options = {}) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);_class.prototype.__init16.call(this);_class.prototype.__init17.call(this);_class.prototype.__init18.call(this);_class.prototype.__init19.call(this);
2160
+ constructor(profile = {}, options = {}) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);_class.prototype.__init16.call(this);_class.prototype.__init17.call(this);_class.prototype.__init18.call(this);_class.prototype.__init19.call(this);_class.prototype.__init20.call(this);_class.prototype.__init21.call(this);
2046
2161
  this.profile = profile;
2047
2162
  this.maxCacheBytes = _nullishCoalesce(options.maxCacheBytes, () => ( 256 * 1024 * 1024));
2048
2163
  }
@@ -2067,8 +2182,11 @@ var RasterCompositor = (_class = class {
2067
2182
  __init15() {this.patternTiles = /* @__PURE__ */ new WeakMap()}
2068
2183
  __init16() {this.reachCache = /* @__PURE__ */ new WeakMap()}
2069
2184
  __init17() {this.fxContext = null}
2070
- __init18() {this.lastStats = { tiles: 0, cached: 0, stepsRun: 0 }}
2185
+ __init18() {this.lastStats = { tiles: 0, cached: 0, stepsRun: 0, effectsBuilt: 0, effectsPatched: 0 }}
2071
2186
  __init19() {this.hint = -1}
2187
+ /** The `dirty` rectangles of the current call, as passed. */
2188
+ __init20() {this.callDirty = null}
2189
+ __init21() {this.callStats = this.lastStats}
2072
2190
  /** Drops every cached tile and plane. */
2073
2191
  clear() {
2074
2192
  this.tiles.clear();
@@ -2116,7 +2234,9 @@ var RasterCompositor = (_class = class {
2116
2234
  const steps = this.buildSteps(layers);
2117
2235
  this.hint = options.checkpointLayerId ? this.stepIndexOf(steps, options.checkpointLayerId) : -1;
2118
2236
  this.stamp++;
2119
- const stats = { tiles: 0, cached: 0, stepsRun: 0 };
2237
+ this.callDirty = _nullishCoalesce(options.dirty, () => ( null));
2238
+ const stats = { tiles: 0, cached: 0, stepsRun: 0, effectsBuilt: 0, effectsPatched: 0 };
2239
+ this.callStats = stats;
2120
2240
  const grow = options.dirty ? this.effectsReach(layers) : 0;
2121
2241
  const dirty = grow > 0 ? options.dirty.map((d) => inflateRect(d, grow)) : options.dirty;
2122
2242
  for (const { tx, ty } of _chunkCRMAXIUVcjs.tilesInRect.call(void 0, rect)) {
@@ -2132,8 +2252,10 @@ var RasterCompositor = (_class = class {
2132
2252
  tile(layers, tx, ty, sources) {
2133
2253
  this.setSources(sources);
2134
2254
  this.stamp++;
2135
- const stats = { tiles: 1, cached: 0, stepsRun: 0 };
2255
+ const stats = { tiles: 1, cached: 0, stepsRun: 0, effectsBuilt: 0, effectsPatched: 0 };
2256
+ this.callStats = stats;
2136
2257
  this.hint = -1;
2258
+ this.callDirty = null;
2137
2259
  const data = this.compositeTile(layers, this.buildSteps(layers), tx, ty, stats);
2138
2260
  this.lastStats = stats;
2139
2261
  this.evict();
@@ -2203,7 +2325,8 @@ var RasterCompositor = (_class = class {
2203
2325
  let first = 0;
2204
2326
  if (entry) {
2205
2327
  while (first < sigs.length && first < entry.sigs.length && sameSig(entry.sigs[first], sigs[first])) first++;
2206
- if (first === sigs.length && first === entry.sigs.length) {
2328
+ if (first === sigs.length && first === entry.sigs.length || this.changedOnlyNearby(steps, sigs, entry, dirty)) {
2329
+ entry.sigs = sigs;
2207
2330
  entry.stamp = this.stamp;
2208
2331
  stats.cached++;
2209
2332
  return entry.result;
@@ -2268,6 +2391,20 @@ var RasterCompositor = (_class = class {
2268
2391
  this.cacheBytes += this.entryBytes(next) - prevBytes;
2269
2392
  return result;
2270
2393
  }
2394
+ /**
2395
+ * Whether a tile with no part in this call's (grown) `dirty` rectangles changed only in what local
2396
+ * effects read beyond it: every changed step is a pixel layer with local effects whose changes stay
2397
+ * inside `dirty` (and so are farther than its margin), which leaves the tile's pixels as they were.
2398
+ */
2399
+ changedOnlyNearby(steps, sigs, entry, dirty) {
2400
+ if (this.callDirty === null || dirty || entry.stamp !== this.stamp - 1 || sigs.length !== entry.sigs.length) return false;
2401
+ return steps.every((step, i) => {
2402
+ if (sameSig(entry.sigs[i], sigs[i])) return true;
2403
+ if (step.kind !== "content" || entry.sigs[i].key !== sigs[i].key) return false;
2404
+ const { layer, clipped } = step;
2405
+ return this.hasEffects(layer) && effectsAreLocal(activeEffectItems(layer.effects)) && this.dirtyCoversShape(layer) && clipped.every((c) => this.dirtyCoversShape(c));
2406
+ });
2407
+ }
2271
2408
  entryBytes(e) {
2272
2409
  let n = e.result === ZERO_TILE ? 0 : TILE_BYTES;
2273
2410
  for (const cp of e.checkpoints.values()) n += cp.data.byteLength;
@@ -2398,14 +2535,20 @@ var RasterCompositor = (_class = class {
2398
2535
  if (!b) return { x: 0, y: 0, width: 0, height: 0 };
2399
2536
  return intersectRect({ x: b.x + dx - m, y: b.y + dy - m, width: b.width + 2 * m, height: b.height + 2 * m }, canvas);
2400
2537
  }
2538
+ /** The signature of a layer's shape (its surface tiles and mask refs) per tile of `region`. */
2539
+ shapeSigs(layer, region) {
2540
+ const sigs = /* @__PURE__ */ new Map();
2541
+ if (region.width <= 0 || region.height <= 0) return sigs;
2542
+ const steps = isGroup(layer) ? this.buildSteps(_nullishCoalesce(layer.children, () => ( []))) : null;
2543
+ for (const t of _chunkCRMAXIUVcjs.tilesInRect.call(void 0, region)) {
2544
+ sigs.set(_chunkCRMAXIUVcjs.tileKey.call(void 0, t.tx, t.ty), steps ? this.plainGroupSig(layer, steps, t.tx, t.ty) : this.plainContentSig(layer, t.tx, t.ty));
2545
+ }
2546
+ return sigs;
2547
+ }
2401
2548
  /** The surface tiles (and mask refs) a layer's shape reads over `region`. */
2402
2549
  shapeRefs(layer, region) {
2403
2550
  const refs = [];
2404
- if (region.width <= 0 || region.height <= 0) return refs;
2405
- for (const t of _chunkCRMAXIUVcjs.tilesInRect.call(void 0, region)) {
2406
- const sig = isGroup(layer) ? this.plainGroupSig(layer, this.buildSteps(_nullishCoalesce(layer.children, () => ( []))), t.tx, t.ty) : this.plainContentSig(layer, t.tx, t.ty);
2407
- for (const r of sig.refs) refs.push(r);
2408
- }
2551
+ for (const sig of this.shapeSigs(layer, region).values()) for (const r of sig.refs) refs.push(r);
2409
2552
  return refs;
2410
2553
  }
2411
2554
  lightKey() {
@@ -2431,18 +2574,28 @@ var RasterCompositor = (_class = class {
2431
2574
  const memo = this.effectMemo.get(layer);
2432
2575
  if (memo && memo.stamp === this.stamp) return memo.maps;
2433
2576
  const region = this.effectRegion(layer);
2434
- const key = `${this.propsKey(layer)}${this.lightKey()}|${region.x},${region.y},${region.width},${region.height}`;
2435
- const refs = this.shapeRefs(layer, region);
2577
+ const key = this.propsKey(layer) + this.lightKey();
2578
+ const tiles = this.shapeSigs(layer, region);
2436
2579
  const hit = this.effectCache.get(layer.id);
2437
- let maps;
2438
- if (hit && hit.key === key && sameSig({ key, refs: hit.refs }, { key, refs })) {
2439
- this.effectCache.delete(layer.id);
2440
- this.effectCache.set(layer.id, hit);
2441
- maps = hit.maps;
2580
+ this.effectCache.delete(layer.id);
2581
+ let entry;
2582
+ if (hit && hit.key === key && sameRect(hit.region, region) && [...tiles].every(([k, sig]) => sameSig(hit.tiles.get(k), sig))) {
2583
+ entry = hit;
2442
2584
  } else {
2443
- maps = buildEffectMaps(activeEffectItems(layer.effects), this.layerShape(layer, region), region, this.effectContext());
2444
- this.effectCache.delete(layer.id);
2445
- this.effectCache.set(layer.id, { key, refs, maps });
2585
+ const items = activeEffectItems(layer.effects);
2586
+ const ctx = this.effectContext();
2587
+ let maps = null;
2588
+ if (hit && hit.key === key && items.every((e) => effectReach(e) <= MAX_EFFECT_REACH)) {
2589
+ const changed = this.shapeChanges(layer, hit, tiles, region);
2590
+ maps = patchEffectMaps(hit.maps, items, region, changed, effectsMargin(layer.effects), (r) => this.layerShape(layer, r), ctx);
2591
+ }
2592
+ if (maps) this.callStats.effectsPatched++;
2593
+ else this.callStats.effectsBuilt++;
2594
+ entry = { key, region, tiles, maps: _nullishCoalesce(maps, () => ( buildEffectMaps(items, this.layerShape(layer, region), region, ctx))), stamp: 0 };
2595
+ }
2596
+ entry.stamp = this.stamp;
2597
+ this.effectCache.set(layer.id, entry);
2598
+ if (entry !== hit) {
2446
2599
  let bytes = 0;
2447
2600
  for (const e of this.effectCache.values()) bytes += e.maps.bytes;
2448
2601
  while (this.effectCache.size > 1 && (this.effectCache.size > 32 || bytes > this.maxCacheBytes / 2)) {
@@ -2451,8 +2604,42 @@ var RasterCompositor = (_class = class {
2451
2604
  this.effectCache.delete(oldest);
2452
2605
  }
2453
2606
  }
2454
- this.effectMemo.set(layer, { stamp: this.stamp, maps });
2455
- return maps;
2607
+ this.effectMemo.set(layer, { stamp: this.stamp, maps: entry.maps });
2608
+ return entry.maps;
2609
+ }
2610
+ /**
2611
+ * Where a layer's shape may differ from the one `prev` was built from: the tiles whose signature
2612
+ * changed (a tile new to the region that reads nothing did not), narrowed to this call's `dirty`
2613
+ * rectangles when `prev` is from the previous call and they cover every change of the shape.
2614
+ */
2615
+ shapeChanges(layer, prev, tiles, region) {
2616
+ const out = [];
2617
+ for (const { tx, ty } of _chunkCRMAXIUVcjs.tilesInRect.call(void 0, region)) {
2618
+ const sig = tiles.get(_chunkCRMAXIUVcjs.tileKey.call(void 0, tx, ty));
2619
+ const old = prev.tiles.get(_chunkCRMAXIUVcjs.tileKey.call(void 0, tx, ty));
2620
+ if (old ? sameSig(old, sig) : sig.refs.every((r2) => r2 === null || r2 === 0)) continue;
2621
+ const r = intersectRect({ x: tx * _chunkCRMAXIUVcjs.TILE, y: ty * _chunkCRMAXIUVcjs.TILE, width: _chunkCRMAXIUVcjs.TILE, height: _chunkCRMAXIUVcjs.TILE }, region);
2622
+ if (r.width > 0 && r.height > 0) out.push(r);
2623
+ }
2624
+ const dirty = this.callDirty;
2625
+ if (!dirty || prev.stamp !== this.stamp - 1 || !this.dirtyCoversShape(layer)) return out;
2626
+ const narrowed = [];
2627
+ for (const r of out) {
2628
+ for (const d of dirty) {
2629
+ const a = intersectRect(r, { x: Math.floor(d.x), y: Math.floor(d.y), width: Math.ceil(d.x + d.width) - Math.floor(d.x), height: Math.ceil(d.y + d.height) - Math.floor(d.y) });
2630
+ if (a.width > 0 && a.height > 0) narrowed.push(a);
2631
+ }
2632
+ }
2633
+ return narrowed;
2634
+ }
2635
+ /**
2636
+ * Whether a change to a layer stays inside the `dirty` rectangles that name it: a pixel layer
2637
+ * without filters or a feathered or parametric mask (those spread a change beyond it).
2638
+ */
2639
+ dirtyCoversShape(layer) {
2640
+ if (!isPixel(layer) || _optionalChain([this, 'access', _26 => _26.profile, 'access', _27 => _27.pixelFilters, 'optionalCall', _28 => _28(layer)])) return false;
2641
+ const mask = layer.mask;
2642
+ return !mask || mask.enabled === false || !mask.source && !((_nullishCoalesce(mask.feather, () => ( 0))) > 0);
2456
2643
  }
2457
2644
  /** A layer's alpha over `r` after its mask (0..1, quantised to bytes as tiles see it). */
2458
2645
  layerShape(layer, r) {
@@ -2538,10 +2725,10 @@ var RasterCompositor = (_class = class {
2538
2725
  */
2539
2726
  layerPixels(layer) {
2540
2727
  const raw = this.sources.surfaceOf ? this.sources.surfaceOf(layer) : layer.surface;
2541
- const ox = _nullishCoalesce(_optionalChain([layer, 'access', _26 => _26.offset, 'optionalAccess', _27 => _27.x]), () => ( 0));
2542
- const oy = _nullishCoalesce(_optionalChain([layer, 'access', _28 => _28.offset, 'optionalAccess', _29 => _29.y]), () => ( 0));
2543
- const filters = _nullishCoalesce(_optionalChain([this, 'access', _30 => _30.profile, 'access', _31 => _31.pixelFilters, 'optionalCall', _32 => _32(layer)]), () => ( null));
2544
- if (!_optionalChain([filters, 'optionalAccess', _33 => _33.spatial])) return { surface: raw, dx: ox, dy: oy, perPixel: filters };
2728
+ const ox = _nullishCoalesce(_optionalChain([layer, 'access', _29 => _29.offset, 'optionalAccess', _30 => _30.x]), () => ( 0));
2729
+ const oy = _nullishCoalesce(_optionalChain([layer, 'access', _31 => _31.offset, 'optionalAccess', _32 => _32.y]), () => ( 0));
2730
+ const filters = _nullishCoalesce(_optionalChain([this, 'access', _33 => _33.profile, 'access', _34 => _34.pixelFilters, 'optionalCall', _35 => _35(layer)]), () => ( null));
2731
+ if (!_optionalChain([filters, 'optionalAccess', _36 => _36.spatial])) return { surface: raw, dx: ox, dy: oy, perPixel: filters };
2545
2732
  const fkey = `${ox},${oy}:${filters.key}`;
2546
2733
  let byKey = this.filteredCache.get(raw);
2547
2734
  if (!byKey) {
@@ -2558,7 +2745,7 @@ var RasterCompositor = (_class = class {
2558
2745
  }
2559
2746
  /** RGBA of a content layer over `r` (document space, inside one tile), or `null` when it has nothing there. */
2560
2747
  contentTile(layer, r) {
2561
- if (!isPixel(layer)) return _nullishCoalesce(_optionalChain([this, 'access', _34 => _34.profile, 'access', _35 => _35.contentTile, 'optionalCall', _36 => _36(layer, r, this.sources)]), () => ( null));
2748
+ if (!isPixel(layer)) return _nullishCoalesce(_optionalChain([this, 'access', _37 => _37.profile, 'access', _38 => _38.contentTile, 'optionalCall', _39 => _39(layer, r, this.sources)]), () => ( null));
2562
2749
  const { surface, dx, dy, perPixel } = this.layerPixels(layer);
2563
2750
  const src = { x: r.x - dx, y: r.y - dy, width: r.width, height: r.height };
2564
2751
  const covered = _chunkCRMAXIUVcjs.tilesInRect.call(void 0, src).map((t) => surface.getTile(t.tx, t.ty));
@@ -2573,7 +2760,7 @@ var RasterCompositor = (_class = class {
2573
2760
  let byKey = this.contentCache.get(anchor);
2574
2761
  const refsKey = `${r.x},${r.y}:${fkey}`;
2575
2762
  if (aligned) {
2576
- const hit = _optionalChain([byKey, 'optionalAccess', _37 => _37.get, 'call', _38 => _38(refsKey)]);
2763
+ const hit = _optionalChain([byKey, 'optionalAccess', _40 => _40.get, 'call', _41 => _41(refsKey)]);
2577
2764
  if (hit) return hit;
2578
2765
  }
2579
2766
  const data = _chunkCRMAXIUVcjs.readRegion.call(void 0, surface, src);
@@ -2611,7 +2798,7 @@ var RasterCompositor = (_class = class {
2611
2798
  const H = this.sources.height;
2612
2799
  let v = new Float32Array(W * H);
2613
2800
  if (mask.source) {
2614
- const generated = _nullishCoalesce(_optionalChain([this, 'access', _39 => _39.profile, 'access', _40 => _40.maskSourcePlane, 'optionalCall', _41 => _41(mask.source, W, H, this.sources)]), () => ( null));
2801
+ const generated = _nullishCoalesce(_optionalChain([this, 'access', _42 => _42.profile, 'access', _43 => _43.maskSourcePlane, 'optionalCall', _44 => _44(mask.source, W, H, this.sources)]), () => ( null));
2615
2802
  if (!generated || generated.length !== W * H) v.fill(1);
2616
2803
  else v.set(generated);
2617
2804
  } else {
@@ -2630,8 +2817,8 @@ var RasterCompositor = (_class = class {
2630
2817
  maskTile(layer, r) {
2631
2818
  const mask = layer.mask;
2632
2819
  if (!mask || mask.enabled === false) return null;
2633
- const ox = isPixel(layer) ? _nullishCoalesce(_optionalChain([layer, 'access', _42 => _42.offset, 'optionalAccess', _43 => _43.x]), () => ( 0)) : 0;
2634
- const oy = isPixel(layer) ? _nullishCoalesce(_optionalChain([layer, 'access', _44 => _44.offset, 'optionalAccess', _45 => _45.y]), () => ( 0)) : 0;
2820
+ const ox = isPixel(layer) ? _nullishCoalesce(_optionalChain([layer, 'access', _45 => _45.offset, 'optionalAccess', _46 => _46.x]), () => ( 0)) : 0;
2821
+ const oy = isPixel(layer) ? _nullishCoalesce(_optionalChain([layer, 'access', _47 => _47.offset, 'optionalAccess', _48 => _48.y]), () => ( 0)) : 0;
2635
2822
  const n = r.width * r.height;
2636
2823
  const out = new Float32Array(n);
2637
2824
  const density = _nullishCoalesce(mask.density, () => ( 1));
@@ -2705,7 +2892,7 @@ var RasterCompositor = (_class = class {
2705
2892
  return empty;
2706
2893
  }
2707
2894
  contentBlend(layer) {
2708
- return _nullishCoalesce(_optionalChain([this, 'access', _46 => _46.profile, 'access', _47 => _47.contentBlend, 'optionalCall', _48 => _48(layer)]), () => ( layer.blend));
2895
+ return _nullishCoalesce(_optionalChain([this, 'access', _49 => _49.profile, 'access', _50 => _50.contentBlend, 'optionalCall', _51 => _51(layer)]), () => ( layer.blend));
2709
2896
  }
2710
2897
  blendOnto(acc, src, empty, mode, alpha, coverage) {
2711
2898
  if (!(alpha > 0)) return empty;
@@ -2759,7 +2946,7 @@ var RasterCompositor = (_class = class {
2759
2946
  }
2760
2947
  visible.push(node);
2761
2948
  }
2762
- if (visible.length) _optionalChain([this, 'access', _49 => _49.profile, 'access', _50 => _50.applyFilterRun, 'optionalCall', _51 => _51(visible, acc, r, masks, this.sources)]);
2949
+ if (visible.length) _optionalChain([this, 'access', _52 => _52.profile, 'access', _53 => _53.applyFilterRun, 'optionalCall', _54 => _54(visible, acc, r, masks, this.sources)]);
2763
2950
  }
2764
2951
  }, _class);
2765
2952
  function filterOverCanvas(surface, ox, oy, filters, width, height) {
@@ -2779,6 +2966,9 @@ function copyRect(data, r, a) {
2779
2966
  function pasteRect(data, r, part, a) {
2780
2967
  for (let y = 0; y < a.height; y++) data.set(part.subarray(y * a.width * 4, (y + 1) * a.width * 4), ((a.y - r.y + y) * r.width + (a.x - r.x)) * 4);
2781
2968
  }
2969
+ function sameRect(a, b) {
2970
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
2971
+ }
2782
2972
  function inflateRect(r, m) {
2783
2973
  return { x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m };
2784
2974
  }
@@ -2834,13 +3024,13 @@ function fixedTables() {
2834
3024
  return [fixedLit, fixedDist];
2835
3025
  }
2836
3026
  var BitReader = (_class2 = class {
2837
- constructor(data) {;_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);
3027
+ constructor(data) {;_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);
2838
3028
  this.data = data;
2839
3029
  }
2840
3030
 
2841
- __init20() {this.pos = 0}
2842
- __init21() {this.bit = 0}
2843
- __init22() {this.bits = 0}
3031
+ __init22() {this.pos = 0}
3032
+ __init23() {this.bit = 0}
3033
+ __init24() {this.bits = 0}
2844
3034
  need(n) {
2845
3035
  while (this.bits < n) {
2846
3036
  if (this.pos >= this.data.length) throw new ZlibError("Unexpected end of compressed data");
@@ -2873,13 +3063,13 @@ var BitReader = (_class2 = class {
2873
3063
  }
2874
3064
  }, _class2);
2875
3065
  var Output = (_class3 = class {
2876
- constructor(hint, max) {;_class3.prototype.__init23.call(this);
3066
+ constructor(hint, max) {;_class3.prototype.__init25.call(this);
2877
3067
  this.max = max;
2878
3068
  this.buf = new Uint8Array(Math.max(1024, Math.min(hint, max)));
2879
3069
  }
2880
3070
 
2881
3071
 
2882
- __init23() {this.len = 0}
3072
+ __init25() {this.len = 0}
2883
3073
  ensure(n) {
2884
3074
  if (this.len + n > this.max) throw new ZlibError(`Decompressed data exceeds ${this.max} bytes`);
2885
3075
  if (this.len + n <= this.buf.length) return;
@@ -2999,10 +3189,10 @@ function inflate(data, options = {}) {
2999
3189
  }
3000
3190
  var BitWriter = (_class4 = class {
3001
3191
 
3002
- __init24() {this.len = 0}
3003
- __init25() {this.bit = 0}
3004
- __init26() {this.bits = 0}
3005
- constructor(hint) {;_class4.prototype.__init24.call(this);_class4.prototype.__init25.call(this);_class4.prototype.__init26.call(this);
3192
+ __init26() {this.len = 0}
3193
+ __init27() {this.bit = 0}
3194
+ __init28() {this.bits = 0}
3195
+ constructor(hint) {;_class4.prototype.__init26.call(this);_class4.prototype.__init27.call(this);_class4.prototype.__init28.call(this);
3006
3196
  this.buf = new Uint8Array(Math.max(1024, hint));
3007
3197
  }
3008
3198
  grow() {
@@ -3691,5 +3881,6 @@ function pngDataUrl(width, height, data, channels = 4) {
3691
3881
 
3692
3882
 
3693
3883
 
3694
- exports.DIRECT_GAUSSIAN_MAX_SIGMA = DIRECT_GAUSSIAN_MAX_SIGMA; exports.gaussianBlur = gaussianBlur; exports.flattenLayers = flattenLayers; exports.findLayer = findLayer; exports.mapLayer = mapLayer; exports.insertLayer = insertLayer; exports.isLayerLocked = isLayerLocked; exports.DEFAULT_GLOBAL_LIGHT = DEFAULT_GLOBAL_LIGHT; exports.LAYER_EFFECT_KINDS = LAYER_EFFECT_KINDS; exports.layerEffectLabel = layerEffectLabel; exports.isLayerEffectKind = isLayerEffectKind; exports.defaultEffectGradient = defaultEffectGradient; exports.newEffectId = newEffectId; exports.defaultLayerEffect = defaultLayerEffect; exports.hasActiveEffects = hasActiveEffects; exports.MAX_EFFECT_REACH = MAX_EFFECT_REACH; exports.effectReach = effectReach; exports.effectsMargin = effectsMargin; exports.effectsScale = effectsScale; exports.scaleLayerEffect = scaleLayerEffect; exports.parseEffectColor = parseEffectColor; exports.effectColorHex = effectColorHex; exports.LayerEffectError = LayerEffectError; exports.validateEffectGradient = validateEffectGradient; exports.layerEffectFields = layerEffectFields; exports.validateLayerEffect = validateLayerEffect; exports.mergeLayerEffect = mergeLayerEffect; exports.validateLayerEffects = validateLayerEffects; exports.normalizeLayerEffects = normalizeLayerEffects; exports.effectsFingerprint = effectsFingerprint; exports.PatternTile = PatternTile; exports.PatternPlacement = PatternPlacement; exports.builtinPatternIds = builtinPatternIds; exports.builtinPattern = builtinPattern; exports.hashNoise = hashNoise; exports.CURVE_LUT_SIZE = CURVE_LUT_SIZE; exports.curveLut = curveLut; exports.STROKE_CHAMFER_MAX = STROKE_CHAMFER_MAX; exports.contourLut = contourLut; exports.activeEffectItems = activeEffectItems; exports.effectsAreLocal = effectsAreLocal; exports.buildEffectMaps = buildEffectMaps; exports.gradientT = gradientT; exports.compositeLayerEffects = compositeLayerEffects; exports.renderLayerEffects = renderLayerEffects; exports.blendTile = blendTile; exports.RasterCompositor = RasterCompositor; exports.changedTiles = changedTiles; exports.ZlibError = ZlibError; exports.inflateRaw = inflateRaw; exports.adler32 = adler32; exports.inflate = inflate; exports.deflateRaw = deflateRaw; exports.deflate = deflate; exports.encodePng = encodePng; exports.isPng = isPng; exports.decodePng = decodePng; exports.bytesToBase64 = bytesToBase64; exports.base64ToBytes = base64ToBytes; exports.parseDataUrl = parseDataUrl; exports.pngDataUrl = pngDataUrl;
3695
- //# sourceMappingURL=chunk-T6H3JDGA.cjs.map
3884
+
3885
+ exports.DIRECT_GAUSSIAN_MAX_SIGMA = DIRECT_GAUSSIAN_MAX_SIGMA; exports.gaussianBlur = gaussianBlur; exports.flattenLayers = flattenLayers; exports.findLayer = findLayer; exports.mapLayer = mapLayer; exports.insertLayer = insertLayer; exports.isLayerLocked = isLayerLocked; exports.DEFAULT_GLOBAL_LIGHT = DEFAULT_GLOBAL_LIGHT; exports.LAYER_EFFECT_KINDS = LAYER_EFFECT_KINDS; exports.layerEffectLabel = layerEffectLabel; exports.isLayerEffectKind = isLayerEffectKind; exports.defaultEffectGradient = defaultEffectGradient; exports.newEffectId = newEffectId; exports.defaultLayerEffect = defaultLayerEffect; exports.hasActiveEffects = hasActiveEffects; exports.MAX_EFFECT_REACH = MAX_EFFECT_REACH; exports.effectReach = effectReach; exports.effectsMargin = effectsMargin; exports.effectsScale = effectsScale; exports.scaleLayerEffect = scaleLayerEffect; exports.parseEffectColor = parseEffectColor; exports.effectColorHex = effectColorHex; exports.LayerEffectError = LayerEffectError; exports.validateEffectGradient = validateEffectGradient; exports.layerEffectFields = layerEffectFields; exports.validateLayerEffect = validateLayerEffect; exports.mergeLayerEffect = mergeLayerEffect; exports.validateLayerEffects = validateLayerEffects; exports.normalizeLayerEffects = normalizeLayerEffects; exports.effectsFingerprint = effectsFingerprint; exports.PatternTile = PatternTile; exports.PatternPlacement = PatternPlacement; exports.builtinPatternIds = builtinPatternIds; exports.builtinPattern = builtinPattern; exports.hashNoise = hashNoise; exports.CURVE_LUT_SIZE = CURVE_LUT_SIZE; exports.curveLut = curveLut; exports.STROKE_CHAMFER_MAX = STROKE_CHAMFER_MAX; exports.contourLut = contourLut; exports.activeEffectItems = activeEffectItems; exports.effectsAreLocal = effectsAreLocal; exports.buildEffectMaps = buildEffectMaps; exports.patchEffectMaps = patchEffectMaps; exports.gradientT = gradientT; exports.compositeLayerEffects = compositeLayerEffects; exports.renderLayerEffects = renderLayerEffects; exports.blendTile = blendTile; exports.RasterCompositor = RasterCompositor; exports.changedTiles = changedTiles; exports.ZlibError = ZlibError; exports.inflateRaw = inflateRaw; exports.adler32 = adler32; exports.inflate = inflate; exports.deflateRaw = deflateRaw; exports.deflate = deflate; exports.encodePng = encodePng; exports.isPng = isPng; exports.decodePng = decodePng; exports.bytesToBase64 = bytesToBase64; exports.base64ToBytes = base64ToBytes; exports.parseDataUrl = parseDataUrl; exports.pngDataUrl = pngDataUrl;
3886
+ //# sourceMappingURL=chunk-4AB5BHYK.cjs.map