@pixerate/editor 0.276.0 → 0.277.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/animation-editor/index.cjs +59 -59
- package/dist/animation-editor/index.js +3 -3
- package/dist/brushes/index.cjs +69 -67
- package/dist/brushes/index.js +6 -4
- package/dist/brushes/patch.d.cts +4 -4
- package/dist/brushes/patch.d.ts +4 -4
- package/dist/brushes/profiles.d.cts +2 -0
- package/dist/brushes/profiles.d.ts +2 -0
- package/dist/brushes/schema.d.cts +1 -0
- package/dist/brushes/schema.d.ts +1 -0
- package/dist/brushes/types.d.cts +10 -0
- package/dist/brushes/types.d.ts +10 -0
- package/dist/{brushstrokeRender-I6MYFCOU.cjs → brushstrokeRender-7HOMKZNO.cjs} +15 -15
- package/dist/{brushstrokeRender-FK3ASUUJ.js → brushstrokeRender-EIK3625T.js} +3 -3
- package/dist/canvas/index.cjs +13 -13
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-U26LUXCJ.cjs → chunk-4NZTK7LM.cjs} +35 -35
- package/dist/{chunk-CVBTJVRN.js → chunk-DWPFGWW5.js} +1 -1
- package/dist/{chunk-XMSH66VU.js → chunk-FNIWHSLI.js} +1 -1
- package/dist/{chunk-QNNZA3V5.cjs → chunk-H3EME3S5.cjs} +2 -2
- package/dist/{chunk-4EM5NGCL.js → chunk-HHX7ST6Z.js} +1 -1
- package/dist/{chunk-Q33V5PEF.cjs → chunk-HRUK5TFW.cjs} +73 -12
- package/dist/{chunk-5HR5CSLC.cjs → chunk-JVFGK3DK.cjs} +69 -58
- package/dist/{chunk-JNMNM3SL.js → chunk-LZE2QZCT.js} +1 -1
- package/dist/{chunk-OS6OG7DN.js → chunk-N4MCWQZ7.js} +1 -1
- package/dist/{chunk-IJ6NSWKM.js → chunk-NJLPW4MM.js} +17 -6
- package/dist/{chunk-OY24XRYF.cjs → chunk-PEQWARCP.cjs} +12 -12
- package/dist/{chunk-XVPQNX7O.js → chunk-PPDUCZZV.js} +66 -5
- package/dist/{chunk-X7XBEX3O.cjs → chunk-QBZEC2JJ.cjs} +86 -86
- package/dist/{chunk-GHYJAMMS.js → chunk-ROURHJYT.js} +1 -1
- package/dist/{chunk-ZIE6EJGR.cjs → chunk-VNF7A2JA.cjs} +23 -23
- package/dist/{chunk-ISCAJBPQ.cjs → chunk-WFSYZO5Z.cjs} +1 -1
- package/dist/image-editor/index.cjs +788 -788
- package/dist/image-editor/index.js +4 -4
- package/dist/paint-editor/brush-model.d.cts +15 -1
- package/dist/paint-editor/brush-model.d.ts +15 -1
- package/dist/paint-editor/controller.d.cts +50 -1
- package/dist/paint-editor/controller.d.ts +50 -1
- package/dist/paint-editor/document.d.cts +8 -1
- package/dist/paint-editor/document.d.ts +8 -1
- package/dist/paint-editor/ferro-tools.d.cts +10 -0
- package/dist/paint-editor/ferro-tools.d.ts +10 -0
- package/dist/paint-editor/ferro.d.cts +134 -0
- package/dist/paint-editor/ferro.d.ts +134 -0
- package/dist/paint-editor/index.cjs +1878 -831
- package/dist/paint-editor/index.d.cts +2 -0
- package/dist/paint-editor/index.d.ts +2 -0
- package/dist/paint-editor/index.js +1184 -137
- package/dist/paint-editor/stroke.d.cts +14 -0
- package/dist/paint-editor/stroke.d.ts +14 -0
- package/dist/paint-editor/thick.d.cts +15 -2
- package/dist/paint-editor/thick.d.ts +15 -2
- package/dist/paint-editor/ui.d.cts +2 -0
- package/dist/paint-editor/ui.d.ts +2 -0
- package/dist/vector/index.cjs +998 -998
- package/dist/vector/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +1 -0
- package/skills/pixerate-paint-editor/SKILL.md +11 -29
- package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
- package/skills/pixerate-paint-editor/references/ferrofluid.md +36 -0
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
mergeBrushPatch,
|
|
15
15
|
multiColorSliders,
|
|
16
16
|
patchBrushValue
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-NJLPW4MM.js";
|
|
18
18
|
import {
|
|
19
19
|
createStickerTools,
|
|
20
20
|
describeStickerRef
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
BRUSH_PREVIEW_DARK_PAPER,
|
|
24
24
|
brushPreviewsOnDark
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-ROURHJYT.js";
|
|
26
26
|
import {
|
|
27
27
|
toVideoFfmpeg
|
|
28
28
|
} from "../chunk-S2XPXA47.js";
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
resolveExternalAssets,
|
|
43
43
|
serializeLayers,
|
|
44
44
|
serializeLayersExternal
|
|
45
|
-
} from "../chunk-
|
|
45
|
+
} from "../chunk-FNIWHSLI.js";
|
|
46
46
|
import "../chunk-ADMRNWR7.js";
|
|
47
47
|
import "../chunk-WJ4WQJCC.js";
|
|
48
48
|
import {
|
|
@@ -125,7 +125,7 @@ import {
|
|
|
125
125
|
wetPaperInput,
|
|
126
126
|
wetPigmentOf,
|
|
127
127
|
withStrokePath
|
|
128
|
-
} from "../chunk-
|
|
128
|
+
} from "../chunk-HHX7ST6Z.js";
|
|
129
129
|
import {
|
|
130
130
|
BRUSH_ID,
|
|
131
131
|
BRUSH_PRESETS,
|
|
@@ -137,6 +137,7 @@ import {
|
|
|
137
137
|
DEFAULT_BRISTLE,
|
|
138
138
|
DEFAULT_BRUSH_ID,
|
|
139
139
|
DEFAULT_DYNAMICS,
|
|
140
|
+
DEFAULT_FERRO,
|
|
140
141
|
DEFAULT_FLUORESCENCE,
|
|
141
142
|
DEFAULT_GHOST,
|
|
142
143
|
DEFAULT_IMPASTO,
|
|
@@ -182,6 +183,7 @@ import {
|
|
|
182
183
|
normalizeBristle,
|
|
183
184
|
normalizeBrushSpec,
|
|
184
185
|
normalizeDynamics,
|
|
186
|
+
normalizeFerro,
|
|
185
187
|
normalizeFluorescence,
|
|
186
188
|
normalizeGhost,
|
|
187
189
|
normalizeImpasto,
|
|
@@ -198,7 +200,7 @@ import {
|
|
|
198
200
|
readBrushSource,
|
|
199
201
|
resolveBrush,
|
|
200
202
|
usesPaintExchange
|
|
201
|
-
} from "../chunk-
|
|
203
|
+
} from "../chunk-PPDUCZZV.js";
|
|
202
204
|
import {
|
|
203
205
|
STAMP_KINDS,
|
|
204
206
|
StickersSlot,
|
|
@@ -412,6 +414,442 @@ function createPaintDocument(options = {}) {
|
|
|
412
414
|
return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
|
|
413
415
|
}
|
|
414
416
|
|
|
417
|
+
// src/paint-editor/ferro.ts
|
|
418
|
+
var DEFAULT_MAGNET_STRENGTH = 0.75;
|
|
419
|
+
var DEFAULT_MAGNET_RADIUS = 72;
|
|
420
|
+
var FERRO_CARRIER_RGB = [17, 24, 39];
|
|
421
|
+
var FERRO_DEFAULT_GLOSS = 224;
|
|
422
|
+
var FERRO_DEFAULT_METALLIC = 166;
|
|
423
|
+
var FERRO_DEFAULT_RIM = 64;
|
|
424
|
+
var SQRT3_HALF = 0.8660254037844386;
|
|
425
|
+
function normalizeMagnet(raw, base) {
|
|
426
|
+
const x = raw.x ?? base?.x;
|
|
427
|
+
const y = raw.y ?? base?.y;
|
|
428
|
+
const strength = raw.strength ?? base?.strength ?? DEFAULT_MAGNET_STRENGTH;
|
|
429
|
+
const radius = raw.radius ?? base?.radius ?? DEFAULT_MAGNET_RADIUS;
|
|
430
|
+
if (typeof x !== "number" || !Number.isFinite(x)) {
|
|
431
|
+
throw new Error(`x ${String(x)} is not a finite coordinate; pass a number in canvas pixels.`);
|
|
432
|
+
}
|
|
433
|
+
if (typeof y !== "number" || !Number.isFinite(y)) {
|
|
434
|
+
throw new Error(`y ${String(y)} is not a finite coordinate; pass a number in canvas pixels.`);
|
|
435
|
+
}
|
|
436
|
+
if (typeof strength !== "number" || !Number.isFinite(strength) || strength < -1 || strength > 1) {
|
|
437
|
+
throw new Error(`strength ${String(strength)} is out of range; use -1..1.`);
|
|
438
|
+
}
|
|
439
|
+
if (typeof radius !== "number" || !Number.isFinite(radius) || radius < 8 || radius > 2e3) {
|
|
440
|
+
throw new Error(`radius ${String(radius)} is out of range; use 8..2000 px.`);
|
|
441
|
+
}
|
|
442
|
+
return { id: raw.id, x, y, strength, radius };
|
|
443
|
+
}
|
|
444
|
+
function emptyFerro() {
|
|
445
|
+
return new TiledSurface(1, /* @__PURE__ */ new Map(), 0);
|
|
446
|
+
}
|
|
447
|
+
var full = (t, channels) => {
|
|
448
|
+
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
449
|
+
const out = new Uint8ClampedArray(TILE * TILE * channels);
|
|
450
|
+
const u = t.uniform ?? 0;
|
|
451
|
+
if (channels === 1) out.fill(u & 255);
|
|
452
|
+
else {
|
|
453
|
+
const bytes = [u >>> 24 & 255, u >>> 16 & 255, u >>> 8 & 255, u & 255];
|
|
454
|
+
for (let i = 0; i < out.length; i += 4) out.set(bytes, i);
|
|
455
|
+
}
|
|
456
|
+
return out;
|
|
457
|
+
};
|
|
458
|
+
function ferroToShadow(ferro) {
|
|
459
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
460
|
+
for (const [key, tile] of ferro.tiles) {
|
|
461
|
+
const f = full(tile, 1);
|
|
462
|
+
const d = new Uint8ClampedArray(TILE * TILE * 4);
|
|
463
|
+
for (let i = 0, j = 0; i < f.length; i++, j += 4) {
|
|
464
|
+
if (!f[i]) continue;
|
|
465
|
+
d[j] = d[j + 1] = d[j + 2] = 255;
|
|
466
|
+
d[j + 3] = f[i];
|
|
467
|
+
}
|
|
468
|
+
const t = finishTile(d, 4, 0);
|
|
469
|
+
if (t) tiles.set(key, t);
|
|
470
|
+
}
|
|
471
|
+
return new TiledSurface(4, tiles, 0);
|
|
472
|
+
}
|
|
473
|
+
function shadowToFerro(shadow) {
|
|
474
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
475
|
+
for (const [key, tile] of shadow.tiles) {
|
|
476
|
+
const s = full(tile, 4);
|
|
477
|
+
const d = new Uint8ClampedArray(TILE * TILE);
|
|
478
|
+
for (let i = 0, j = 0; i < d.length; i++, j += 4) {
|
|
479
|
+
if (s[j + 3]) d[i] = Math.round((s[j] + s[j + 1] + s[j + 2]) / 3 * s[j + 3] / 255);
|
|
480
|
+
}
|
|
481
|
+
const t = finishTile(d, 1, 0);
|
|
482
|
+
if (t) tiles.set(key, t);
|
|
483
|
+
}
|
|
484
|
+
return new TiledSurface(1, tiles, 0);
|
|
485
|
+
}
|
|
486
|
+
function ferroTwinBrush(brush) {
|
|
487
|
+
return normalizePaintBrush(
|
|
488
|
+
{
|
|
489
|
+
tip: { ...brush.tip, blend: "normal" },
|
|
490
|
+
behavior: { ...brush.behavior, mixing: "rgb" },
|
|
491
|
+
paint: null,
|
|
492
|
+
...brush.engine === "mixer" ? { mixer: { ...brush.mixer, mixing: "rgb" } } : {},
|
|
493
|
+
...brush.engine === "blender" ? { blender: { ...brush.blender, mixing: "rgb" } } : {},
|
|
494
|
+
...brush.engine === "smudge" ? { smudge: { ...brush.smudge, mixing: "rgb" } } : {}
|
|
495
|
+
},
|
|
496
|
+
brush
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
function scaleFerro(ferro, k) {
|
|
500
|
+
if (k >= 1) return ferro;
|
|
501
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
502
|
+
if (k > 0) {
|
|
503
|
+
for (const [key, tile] of ferro.tiles) {
|
|
504
|
+
const d = Uint8ClampedArray.from(full(tile, 1), (v) => Math.round(v * k));
|
|
505
|
+
const t = finishTile(d, 1, 0);
|
|
506
|
+
if (t) tiles.set(key, t);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return new TiledSurface(1, tiles, 0);
|
|
510
|
+
}
|
|
511
|
+
function mergeFerro(lower, upper, upperColor) {
|
|
512
|
+
if (!upper?.tiles.size) return lower;
|
|
513
|
+
const keys = /* @__PURE__ */ new Set([...lower?.tiles.keys() ?? [], ...upper.tiles.keys()]);
|
|
514
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
515
|
+
for (const key of keys) {
|
|
516
|
+
const lo = lower?.tiles.get(key);
|
|
517
|
+
const up = upper.tiles.get(key);
|
|
518
|
+
const col = upperColor.tiles.get(key);
|
|
519
|
+
const l = lo ? full(lo, 1) : null;
|
|
520
|
+
const u = up ? full(up, 1) : null;
|
|
521
|
+
const c = col ? full(col, 4) : null;
|
|
522
|
+
const d = new Uint8ClampedArray(TILE * TILE);
|
|
523
|
+
for (let i = 0; i < d.length; i++) {
|
|
524
|
+
const uf = u ? u[i] : 0;
|
|
525
|
+
const ca = c ? c[i * 4 + 3] : 0;
|
|
526
|
+
const a = Math.max(ca, uf) / 255;
|
|
527
|
+
d[i] = Math.min(255, Math.round(uf + (l ? l[i] : 0) * (1 - a)));
|
|
528
|
+
}
|
|
529
|
+
const t = finishTile(d, 1, 0);
|
|
530
|
+
if (t) tiles.set(key, t);
|
|
531
|
+
}
|
|
532
|
+
return new TiledSurface(1, tiles, 0);
|
|
533
|
+
}
|
|
534
|
+
function ferroFieldAt(x, y, magnets) {
|
|
535
|
+
let bx = 0;
|
|
536
|
+
let by = 0;
|
|
537
|
+
let bz = 0;
|
|
538
|
+
for (let i = 0; i < magnets.length; i++) {
|
|
539
|
+
const m = magnets[i];
|
|
540
|
+
if (m.strength === 0) continue;
|
|
541
|
+
const invR = 1 / Math.max(8, m.radius);
|
|
542
|
+
const u = (x - m.x) * invR;
|
|
543
|
+
const v = (y - m.y) * invR;
|
|
544
|
+
const r2 = u * u + v * v;
|
|
545
|
+
const s = 1 + r2;
|
|
546
|
+
const d = 1 / (s * s * Math.sqrt(s));
|
|
547
|
+
bx += m.strength * 1.5 * u * d;
|
|
548
|
+
by += m.strength * 1.5 * v * d;
|
|
549
|
+
bz += m.strength * (1 - 0.5 * r2) * d;
|
|
550
|
+
}
|
|
551
|
+
const magnitude = Math.hypot(bx, by, bz);
|
|
552
|
+
return { bx, by, bz, magnitude };
|
|
553
|
+
}
|
|
554
|
+
function hexWave(px, py, k) {
|
|
555
|
+
const c0 = Math.cos(k * px);
|
|
556
|
+
const c1 = Math.cos(k * (-0.5 * px + SQRT3_HALF * py));
|
|
557
|
+
const c2 = Math.cos(k * (-0.5 * px - SQRT3_HALF * py));
|
|
558
|
+
return (c0 + c1 + c2 + 1.5) / 4.5;
|
|
559
|
+
}
|
|
560
|
+
function ferroSpikeHeightAt(x, y, ferroByte, magnets, settings = DEFAULT_FERRO) {
|
|
561
|
+
if (ferroByte <= 0 || magnets.length === 0) return 0;
|
|
562
|
+
const field = ferroFieldAt(x, y, magnets);
|
|
563
|
+
const bc = Math.max(0.01, settings.threshold);
|
|
564
|
+
if (field.magnitude <= bc) return 0;
|
|
565
|
+
const amp = Math.sqrt(1 - bc / field.magnitude) * (ferroByte / 255);
|
|
566
|
+
const wl = Math.max(4, settings.wavelength);
|
|
567
|
+
const k = 2 * Math.PI / wl;
|
|
568
|
+
const w0 = hexWave(x, y, k);
|
|
569
|
+
const sz = field.bz < 0 ? -1 : 1;
|
|
570
|
+
const tx = sz * field.bx / field.magnitude;
|
|
571
|
+
const ty = sz * field.by / field.magnitude;
|
|
572
|
+
const shift = 0.32 * wl * amp * w0;
|
|
573
|
+
const w = Math.max(0, hexWave(x - tx * shift, y - ty * shift, k));
|
|
574
|
+
const profile = 0.15 + 0.85 * w * Math.sqrt(w);
|
|
575
|
+
return Math.min(255, Math.max(1, Math.round(255 * amp * profile)));
|
|
576
|
+
}
|
|
577
|
+
function advectFerro(ferro, magnets, offset = { x: 0, y: 0 }, steps = 4) {
|
|
578
|
+
if (!ferro.tiles.size || !magnets.length || steps <= 0) return ferro;
|
|
579
|
+
const b = ferro.bounds;
|
|
580
|
+
if (!b) return ferro;
|
|
581
|
+
let active = false;
|
|
582
|
+
for (let i = 0; i < magnets.length; i++) {
|
|
583
|
+
if (magnets[i].strength !== 0) {
|
|
584
|
+
active = true;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (!active) return ferro;
|
|
589
|
+
const pad = Math.min(32, Math.max(8, steps * 4));
|
|
590
|
+
const rect = {
|
|
591
|
+
x: b.x - pad,
|
|
592
|
+
y: b.y - pad,
|
|
593
|
+
width: b.width + 2 * pad,
|
|
594
|
+
height: b.height + 2 * pad
|
|
595
|
+
};
|
|
596
|
+
const w = rect.width;
|
|
597
|
+
const h = rect.height;
|
|
598
|
+
const raw = readRegion(ferro, rect);
|
|
599
|
+
let cur = new Float64Array(w * h);
|
|
600
|
+
for (let i = 0; i < raw.length; i++) cur[i] = raw[i];
|
|
601
|
+
const vx = new Float64Array(w * h);
|
|
602
|
+
const vy = new Float64Array(w * h);
|
|
603
|
+
for (let gy = 0; gy < h; gy++) {
|
|
604
|
+
const docY = rect.y + gy + offset.y + 0.5;
|
|
605
|
+
for (let gx = 0; gx < w; gx++) {
|
|
606
|
+
const docX = rect.x + gx + offset.x + 0.5;
|
|
607
|
+
let fx = 0;
|
|
608
|
+
let fy = 0;
|
|
609
|
+
for (let mIdx = 0; mIdx < magnets.length; mIdx++) {
|
|
610
|
+
const m = magnets[mIdx];
|
|
611
|
+
if (m.strength === 0) continue;
|
|
612
|
+
const dx = m.x - docX;
|
|
613
|
+
const dy = m.y - docY;
|
|
614
|
+
const r = Math.hypot(dx, dy);
|
|
615
|
+
if (r <= 1e-6) continue;
|
|
616
|
+
const R = Math.max(8, m.radius);
|
|
617
|
+
const u = r / R;
|
|
618
|
+
const s = 1 + u * u;
|
|
619
|
+
const damp = Math.min(1, r * 0.5);
|
|
620
|
+
const pull = Math.abs(m.strength) * damp * 1.5 / (s * Math.sqrt(s));
|
|
621
|
+
fx += dx / r * pull;
|
|
622
|
+
fy += dy / r * pull;
|
|
623
|
+
}
|
|
624
|
+
const mag = Math.hypot(fx, fy);
|
|
625
|
+
if (mag > 2.5) {
|
|
626
|
+
const scale = 2.5 / mag;
|
|
627
|
+
fx *= scale;
|
|
628
|
+
fy *= scale;
|
|
629
|
+
}
|
|
630
|
+
const idx = gy * w + gx;
|
|
631
|
+
vx[idx] = fx;
|
|
632
|
+
vy[idx] = fy;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (let step = 0; step < steps; step++) {
|
|
636
|
+
const next = new Float64Array(w * h);
|
|
637
|
+
for (let gy = 0; gy < h; gy++) {
|
|
638
|
+
for (let gx = 0; gx < w; gx++) {
|
|
639
|
+
const idx = gy * w + gx;
|
|
640
|
+
const val = cur[idx];
|
|
641
|
+
if (val <= 0) continue;
|
|
642
|
+
const nx = Math.min(w - 1, Math.max(0, gx + vx[idx]));
|
|
643
|
+
const ny = Math.min(h - 1, Math.max(0, gy + vy[idx]));
|
|
644
|
+
const x0 = Math.floor(nx);
|
|
645
|
+
const y0 = Math.floor(ny);
|
|
646
|
+
const x1 = Math.min(w - 1, x0 + 1);
|
|
647
|
+
const y1 = Math.min(h - 1, y0 + 1);
|
|
648
|
+
const tx = nx - x0;
|
|
649
|
+
const ty = ny - y0;
|
|
650
|
+
next[y0 * w + x0] += val * (1 - tx) * (1 - ty);
|
|
651
|
+
next[y0 * w + x1] += val * tx * (1 - ty);
|
|
652
|
+
next[y1 * w + x0] += val * (1 - tx) * ty;
|
|
653
|
+
next[y1 * w + x1] += val * tx * ty;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
for (let i = 0; i < next.length; i++) {
|
|
657
|
+
cur[i] = Math.min(255, next[i]);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const out = new Uint8ClampedArray(w * h);
|
|
661
|
+
for (let i = 0; i < out.length; i++) {
|
|
662
|
+
out[i] = Math.min(255, Math.max(0, Math.round(cur[i])));
|
|
663
|
+
}
|
|
664
|
+
return surfaceFromPixels({ width: w, height: h, data: out }, 1, rect.x, rect.y);
|
|
665
|
+
}
|
|
666
|
+
function ferroInfo(ferro, magnets = [], settings = DEFAULT_FERRO, offset = { x: 0, y: 0 }) {
|
|
667
|
+
if (!ferro || !ferro.tiles.size) {
|
|
668
|
+
return {
|
|
669
|
+
bounds: null,
|
|
670
|
+
peak: 0,
|
|
671
|
+
liquidPixels: 0,
|
|
672
|
+
maxSpikeHeight: 0,
|
|
673
|
+
spikePixels: 0,
|
|
674
|
+
magnetCount: magnets.length
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
let peak = 0;
|
|
678
|
+
let liquidPixels = 0;
|
|
679
|
+
let maxSpikeHeight = 0;
|
|
680
|
+
let spikePixels = 0;
|
|
681
|
+
for (const [key, tile] of ferro.tiles) {
|
|
682
|
+
const tx = key % 65536 - 32768;
|
|
683
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
684
|
+
const d = full(tile, 1);
|
|
685
|
+
for (let y = 0, i = 0; y < TILE; y++) {
|
|
686
|
+
const docY = ty * TILE + y + offset.y + 0.5;
|
|
687
|
+
for (let x = 0; x < TILE; x++, i++) {
|
|
688
|
+
const fb = d[i];
|
|
689
|
+
if (!fb) continue;
|
|
690
|
+
liquidPixels++;
|
|
691
|
+
if (fb > peak) peak = fb;
|
|
692
|
+
if (magnets.length > 0) {
|
|
693
|
+
const docX = tx * TILE + x + offset.x + 0.5;
|
|
694
|
+
const hs = ferroSpikeHeightAt(docX, docY, fb, magnets, settings);
|
|
695
|
+
if (hs > 0) {
|
|
696
|
+
spikePixels++;
|
|
697
|
+
if (hs > maxSpikeHeight) maxSpikeHeight = hs;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
bounds: peak ? ferro.bounds : null,
|
|
705
|
+
peak,
|
|
706
|
+
liquidPixels,
|
|
707
|
+
maxSpikeHeight,
|
|
708
|
+
spikePixels,
|
|
709
|
+
magnetCount: magnets.length
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function ferroCacheKey(ferro) {
|
|
713
|
+
if (!ferro || !ferro.surface.tiles.size) return "";
|
|
714
|
+
const mKey = ferro.magnets.map((m) => `${m.id}:${m.x.toFixed(2)},${m.y.toFixed(2)},${m.strength.toFixed(3)},${m.radius.toFixed(1)}`).join(";");
|
|
715
|
+
const s = ferro.settings;
|
|
716
|
+
return `${ferro.offset.x},${ferro.offset.y}|${s.wavelength.toFixed(2)},${s.threshold.toFixed(3)}|${mKey}`;
|
|
717
|
+
}
|
|
718
|
+
function bakeFerroIntoThick(color, ferro, thick, magnets, settings = DEFAULT_FERRO, offset = { x: 0, y: 0 }) {
|
|
719
|
+
const baseThick = thick ?? emptyThickChannels();
|
|
720
|
+
if (!ferro.tiles.size) {
|
|
721
|
+
return { color, thick: baseThick, maxSpikeHeight: 0, spikePixels: 0, curedPixels: 0 };
|
|
722
|
+
}
|
|
723
|
+
const colorTiles = new Map(color.tiles);
|
|
724
|
+
const heightTiles = new Map(baseThick.height.tiles);
|
|
725
|
+
const materialTiles = new Map(baseThick.material.tiles);
|
|
726
|
+
let maxSpikeHeight = 0;
|
|
727
|
+
let spikePixels = 0;
|
|
728
|
+
let curedPixels = 0;
|
|
729
|
+
for (const [key, fTile] of ferro.tiles) {
|
|
730
|
+
const tx = key % 65536 - 32768;
|
|
731
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
732
|
+
const fd = full(fTile, 1);
|
|
733
|
+
const cTile = colorTiles.get(key);
|
|
734
|
+
const hTile = heightTiles.get(key);
|
|
735
|
+
const mTile = materialTiles.get(key);
|
|
736
|
+
const cd = cTile ? new Uint8ClampedArray(full(cTile, 4)) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
737
|
+
const hd = hTile ? new Uint8ClampedArray(full(hTile, 1)) : new Uint8ClampedArray(TILE * TILE);
|
|
738
|
+
const md = mTile ? new Uint8ClampedArray(full(mTile, 4)) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
739
|
+
let changedColor = false;
|
|
740
|
+
let changedHeight = false;
|
|
741
|
+
let changedMaterial = false;
|
|
742
|
+
for (let y = 0, p = 0; y < TILE; y++) {
|
|
743
|
+
const docY = ty * TILE + y + offset.y + 0.5;
|
|
744
|
+
for (let x = 0; x < TILE; x++, p++) {
|
|
745
|
+
const fb = fd[p];
|
|
746
|
+
if (!fb) continue;
|
|
747
|
+
curedPixels++;
|
|
748
|
+
const i = p * 4;
|
|
749
|
+
if (cd[i + 3] < fb) {
|
|
750
|
+
const a0 = cd[i + 3] / fb;
|
|
751
|
+
cd[i] = Math.round(cd[i] * a0 + FERRO_CARRIER_RGB[0] * (1 - a0));
|
|
752
|
+
cd[i + 1] = Math.round(cd[i + 1] * a0 + FERRO_CARRIER_RGB[1] * (1 - a0));
|
|
753
|
+
cd[i + 2] = Math.round(cd[i + 2] * a0 + FERRO_CARRIER_RGB[2] * (1 - a0));
|
|
754
|
+
cd[i + 3] = fb;
|
|
755
|
+
changedColor = true;
|
|
756
|
+
}
|
|
757
|
+
const docX = tx * TILE + x + offset.x + 0.5;
|
|
758
|
+
const hs = magnets.length > 0 ? ferroSpikeHeightAt(docX, docY, fb, magnets, settings) : 0;
|
|
759
|
+
if (hs > 0) {
|
|
760
|
+
spikePixels++;
|
|
761
|
+
if (hs > maxSpikeHeight) maxSpikeHeight = hs;
|
|
762
|
+
}
|
|
763
|
+
const bead = Math.max(hs, Math.round(fb * 28 / 255));
|
|
764
|
+
const nh = Math.min(255, hd[p] + bead);
|
|
765
|
+
if (nh !== hd[p]) {
|
|
766
|
+
hd[p] = nh;
|
|
767
|
+
changedHeight = true;
|
|
768
|
+
}
|
|
769
|
+
const w = fb / 255;
|
|
770
|
+
const ng = Math.max(md[i], Math.round(FERRO_DEFAULT_GLOSS * w));
|
|
771
|
+
const nm = Math.max(md[i + 1], Math.round(FERRO_DEFAULT_METALLIC * w));
|
|
772
|
+
const nr = Math.max(md[i + 2], Math.round(FERRO_DEFAULT_RIM * w));
|
|
773
|
+
const na = md[i + 3] || 255;
|
|
774
|
+
if (ng !== md[i] || nm !== md[i + 1] || nr !== md[i + 2] || na !== md[i + 3]) {
|
|
775
|
+
md[i] = ng;
|
|
776
|
+
md[i + 1] = nm;
|
|
777
|
+
md[i + 2] = nr;
|
|
778
|
+
md[i + 3] = na;
|
|
779
|
+
changedMaterial = true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (changedColor) {
|
|
784
|
+
const ct = finishTile(cd, 4, 0);
|
|
785
|
+
if (ct) colorTiles.set(key, ct);
|
|
786
|
+
}
|
|
787
|
+
if (changedHeight) {
|
|
788
|
+
const ht = finishTile(hd, 1, 0);
|
|
789
|
+
if (ht) heightTiles.set(key, ht);
|
|
790
|
+
}
|
|
791
|
+
if (changedMaterial) {
|
|
792
|
+
const mt = finishTile(md, 4, 0);
|
|
793
|
+
if (mt) materialTiles.set(key, mt);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
color: new TiledSurface(4, colorTiles, color.defaultValue),
|
|
798
|
+
thick: {
|
|
799
|
+
height: new TiledSurface(1, heightTiles, 0),
|
|
800
|
+
material: new TiledSurface(4, materialTiles, 0),
|
|
801
|
+
film: baseThick.film
|
|
802
|
+
},
|
|
803
|
+
maxSpikeHeight,
|
|
804
|
+
spikePixels,
|
|
805
|
+
curedPixels
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function ferroToJSON(ferro, settings) {
|
|
809
|
+
const b = ferroInfo(ferro).bounds;
|
|
810
|
+
if (!b) return null;
|
|
811
|
+
const out = {
|
|
812
|
+
rect: { ...b },
|
|
813
|
+
ferro: pngDataUrl(b.width, b.height, readRegion(ferro, b), 1)
|
|
814
|
+
};
|
|
815
|
+
if (settings) {
|
|
816
|
+
if (settings.wavelength !== DEFAULT_FERRO.wavelength) out.wavelength = settings.wavelength;
|
|
817
|
+
if (settings.threshold !== DEFAULT_FERRO.threshold) out.threshold = settings.threshold;
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
function ferroFromJSON(raw) {
|
|
822
|
+
const r = raw;
|
|
823
|
+
if (!r || typeof r !== "object" || !r.rect || typeof r.ferro !== "string") {
|
|
824
|
+
throw new Error("not a saved ferrofluid channel");
|
|
825
|
+
}
|
|
826
|
+
const parsed = parseDataUrl(r.ferro);
|
|
827
|
+
if (!parsed) throw new Error("the ferrofluid channel is not a PNG data URL");
|
|
828
|
+
const img = decodePng(parsed.bytes, { keepGray: true });
|
|
829
|
+
const data = img.channels === 1 ? img.data : Uint8ClampedArray.from({ length: img.width * img.height }, (_, i) => img.data[i * 4]);
|
|
830
|
+
const surface = surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
|
|
831
|
+
const settings = normalizeFerro({
|
|
832
|
+
amount: 0,
|
|
833
|
+
wavelength: r.wavelength,
|
|
834
|
+
threshold: r.threshold
|
|
835
|
+
});
|
|
836
|
+
return { surface, settings };
|
|
837
|
+
}
|
|
838
|
+
function magnetsFromJSON(raw) {
|
|
839
|
+
if (!Array.isArray(raw)) return [];
|
|
840
|
+
const out = [];
|
|
841
|
+
for (const item of raw) {
|
|
842
|
+
if (!item || typeof item !== "object") continue;
|
|
843
|
+
const m = item;
|
|
844
|
+
if (typeof m.id !== "string" || !m.id || typeof m.x !== "number" || typeof m.y !== "number") continue;
|
|
845
|
+
try {
|
|
846
|
+
out.push(normalizeMagnet({ id: m.id, x: m.x, y: m.y, strength: m.strength, radius: m.radius }));
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
|
|
415
853
|
// src/paint-editor/thick.ts
|
|
416
854
|
var DEFAULT_LIGHTING = { azimuth: 315, elevation: 40, relief: 0.7 };
|
|
417
855
|
function normalizeLighting(raw = {}, base = DEFAULT_LIGHTING) {
|
|
@@ -551,8 +989,14 @@ function lightTile(color, height, heightAround, material, light, film = null, vi
|
|
|
551
989
|
}
|
|
552
990
|
var LitCache = class {
|
|
553
991
|
tiles = /* @__PURE__ */ new Map();
|
|
554
|
-
/**
|
|
555
|
-
|
|
992
|
+
/**
|
|
993
|
+
* The layer's pixels lit (tiles without relief, film or ferrofluid nearby are the colour tiles
|
|
994
|
+
* themselves). Without ferrofluid this is the thick-paint / thin-film pass; with it, the liquid
|
|
995
|
+
* ferrofluid adds its carrier colour, glossy metallic material and (near magnets) Rosensweig
|
|
996
|
+
* spike heights before the same lighting.
|
|
997
|
+
*/
|
|
998
|
+
lit(color, ch, light, viewTilt = DEFAULT_VIEW_TILT, ferro) {
|
|
999
|
+
if (ferro && ferro.surface.tiles.size > 0) return this.litFerro(color, ch, light, viewTilt, ferro);
|
|
556
1000
|
const hasRelief = ch.height.tiles.size > 0 && light.relief > 0;
|
|
557
1001
|
const hasFilm = ch.film.tiles.size > 0;
|
|
558
1002
|
if (!hasRelief && !hasFilm) return color;
|
|
@@ -601,6 +1045,101 @@ var LitCache = class {
|
|
|
601
1045
|
for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);
|
|
602
1046
|
return new TiledSurface(4, out, color.defaultValue);
|
|
603
1047
|
}
|
|
1048
|
+
/**
|
|
1049
|
+
* The ferrofluid lighting pass: every tile with ferrofluid or thick relief nearby is lit, with the
|
|
1050
|
+
* liquid's carrier colour and gloss / metal material blended in and its spikes added to the
|
|
1051
|
+
* height field. Thick relief is lit even when `light.relief` is 0 here (the spikes need it), and
|
|
1052
|
+
* a thin film on the same layer still colours the lit tiles.
|
|
1053
|
+
*/
|
|
1054
|
+
litFerro(color, ch, light, viewTilt, ferro) {
|
|
1055
|
+
const hasFerroSpikes = ferro.magnets.length > 0;
|
|
1056
|
+
const lk = `${lightingKey(light)}|${viewTiltKey(viewTilt)}`;
|
|
1057
|
+
const fk = ferroCacheKey(ferro);
|
|
1058
|
+
const out = new Map(color.tiles);
|
|
1059
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1060
|
+
for (const key of color.tiles.keys()) keys.add(key);
|
|
1061
|
+
for (const key of ferro.surface.tiles.keys()) keys.add(key);
|
|
1062
|
+
for (const key of keys) {
|
|
1063
|
+
const tile = color.tiles.get(key) ?? null;
|
|
1064
|
+
const tx = key % 65536 - 32768;
|
|
1065
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
1066
|
+
const aroundH = (dx, dy) => ch.height.tiles.get(tileKey(tx + dx, ty + dy)) ?? null;
|
|
1067
|
+
const aroundF = (dx, dy) => ferro.surface.tiles.get(tileKey(tx + dx, ty + dy)) ?? null;
|
|
1068
|
+
const film = ch.film.tiles.get(key) ?? null;
|
|
1069
|
+
let any = !!film;
|
|
1070
|
+
for (let dy = -1; dy <= 1 && !any; dy++) {
|
|
1071
|
+
for (let dx = -1; dx <= 1 && !any; dx++) {
|
|
1072
|
+
if (aroundH(dx, dy) || aroundF(dx, dy)) any = true;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (!any) continue;
|
|
1076
|
+
const m = ch.material.tiles.get(key) ?? null;
|
|
1077
|
+
const f = ferro.surface.tiles.get(key) ?? null;
|
|
1078
|
+
if (!tile && !f) continue;
|
|
1079
|
+
const inputs = [tile, lk, m, fk, film];
|
|
1080
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(aroundH(dx, dy));
|
|
1081
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(aroundF(dx, dy));
|
|
1082
|
+
const cached = this.tiles.get(key);
|
|
1083
|
+
if (cached && cached.inputs.length === inputs.length && cached.inputs.every((v, i) => v === inputs[i])) {
|
|
1084
|
+
out.set(key, cached.tile);
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
let cd = tile ? expand(tile, 4) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
1088
|
+
let md = m ? expand(m, 4) : null;
|
|
1089
|
+
if (f) {
|
|
1090
|
+
const fd = expand(f, 1);
|
|
1091
|
+
cd = new Uint8ClampedArray(cd);
|
|
1092
|
+
const mBuf = md ? new Uint8ClampedArray(md) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
1093
|
+
for (let p = 0; p < fd.length; p++) {
|
|
1094
|
+
const fb = fd[p];
|
|
1095
|
+
if (!fb) continue;
|
|
1096
|
+
const i = p * 4;
|
|
1097
|
+
if (cd[i + 3] < fb) {
|
|
1098
|
+
const a0 = cd[i + 3] / fb;
|
|
1099
|
+
cd[i] = Math.round(cd[i] * a0 + FERRO_CARRIER_RGB[0] * (1 - a0));
|
|
1100
|
+
cd[i + 1] = Math.round(cd[i + 1] * a0 + FERRO_CARRIER_RGB[1] * (1 - a0));
|
|
1101
|
+
cd[i + 2] = Math.round(cd[i + 2] * a0 + FERRO_CARRIER_RGB[2] * (1 - a0));
|
|
1102
|
+
cd[i + 3] = fb;
|
|
1103
|
+
}
|
|
1104
|
+
const w = fb / 255;
|
|
1105
|
+
mBuf[i] = Math.max(mBuf[i], Math.round(FERRO_DEFAULT_GLOSS * w));
|
|
1106
|
+
mBuf[i + 1] = Math.max(mBuf[i + 1], Math.round(FERRO_DEFAULT_METALLIC * w));
|
|
1107
|
+
mBuf[i + 2] = Math.max(mBuf[i + 2], Math.round(FERRO_DEFAULT_RIM * w));
|
|
1108
|
+
mBuf[i + 3] = mBuf[i + 3] || 255;
|
|
1109
|
+
}
|
|
1110
|
+
md = mBuf;
|
|
1111
|
+
}
|
|
1112
|
+
const buildHeightTile = (ntx, nty) => {
|
|
1113
|
+
const nkey = tileKey(ntx, nty);
|
|
1114
|
+
const ht = ch.height.tiles.get(nkey) ?? null;
|
|
1115
|
+
const ft = hasFerroSpikes ? ferro.surface.tiles.get(nkey) ?? null : null;
|
|
1116
|
+
if (!ht && !ft) return null;
|
|
1117
|
+
if (!ft) return expand(ht, 1);
|
|
1118
|
+
const hBase = ht ? expand(ht, 1) : null;
|
|
1119
|
+
const fData = expand(ft, 1);
|
|
1120
|
+
const outH = hBase ? new Uint8ClampedArray(hBase) : new Uint8ClampedArray(TILE * TILE);
|
|
1121
|
+
const ox = ntx * TILE + ferro.offset.x + 0.5;
|
|
1122
|
+
const oy = nty * TILE + ferro.offset.y + 0.5;
|
|
1123
|
+
for (let py = 0, p = 0; py < TILE; py++) {
|
|
1124
|
+
const docY = oy + py;
|
|
1125
|
+
for (let px = 0; px < TILE; px++, p++) {
|
|
1126
|
+
const fb = fData[p];
|
|
1127
|
+
if (!fb) continue;
|
|
1128
|
+
const hs = ferroSpikeHeightAt(ox + px, docY, fb, ferro.magnets, ferro.settings);
|
|
1129
|
+
if (hs > 0) outH[p] = Math.min(255, outH[p] + hs);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return outH;
|
|
1133
|
+
};
|
|
1134
|
+
const hd = buildHeightTile(tx, ty);
|
|
1135
|
+
const data = lightTile(cd, hd, (dx, dy) => buildHeightTile(tx + dx, ty + dy), md, light, film ? expand(film, 4) : null, viewTilt);
|
|
1136
|
+
const lit = { data, uniform: null };
|
|
1137
|
+
this.tiles.set(key, { inputs, tile: lit });
|
|
1138
|
+
out.set(key, lit);
|
|
1139
|
+
}
|
|
1140
|
+
for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);
|
|
1141
|
+
return new TiledSurface(4, out, color.defaultValue);
|
|
1142
|
+
}
|
|
604
1143
|
};
|
|
605
1144
|
function expand(t, channels) {
|
|
606
1145
|
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
@@ -924,27 +1463,27 @@ var ThickStrokeDriver = class {
|
|
|
924
1463
|
const u = ((dx + 0.5 - d.x) * nxs + (dy + 0.5 - d.y) * nys) / r;
|
|
925
1464
|
const h0 = this.getH(lx, ly);
|
|
926
1465
|
let h = h0;
|
|
927
|
-
const
|
|
1466
|
+
const full3 = 255 * thick * k;
|
|
928
1467
|
switch (im.tool) {
|
|
929
1468
|
case "brush": {
|
|
930
1469
|
const ridge = 0.6 + 0.4 * Math.sin(u * 9 + hash2(Math.round(u * 6), 3, seed) * 6.28) + 0.15 * (hash2(lx, ly, seed) - 0.5);
|
|
931
|
-
const target =
|
|
1470
|
+
const target = full3 * c * ridge;
|
|
932
1471
|
h = Math.max(h0 * (1 - 0.25 * c * im.drag), Math.min(255, h0 * 0.35 + target));
|
|
933
1472
|
break;
|
|
934
1473
|
}
|
|
935
1474
|
case "tube": {
|
|
936
1475
|
const dome = Math.sqrt(Math.max(0, c));
|
|
937
|
-
h = Math.max(h0, Math.min(255,
|
|
1476
|
+
h = Math.max(h0, Math.min(255, full3 * dome * 1.1));
|
|
938
1477
|
break;
|
|
939
1478
|
}
|
|
940
1479
|
case "gloop": {
|
|
941
1480
|
const dome = Math.sqrt(Math.max(0, 1 - Math.min(1, Math.abs(u)) ** 2));
|
|
942
|
-
h = Math.max(h0, Math.min(255,
|
|
1481
|
+
h = Math.max(h0, Math.min(255, full3 * dome * (c > 0.2 ? 1 : c * 5)));
|
|
943
1482
|
break;
|
|
944
1483
|
}
|
|
945
1484
|
case "roller": {
|
|
946
1485
|
const stipple = 0.75 + 0.5 * hash2(lx >> 1, ly >> 1, seed);
|
|
947
|
-
h = h0 + (Math.min(255,
|
|
1486
|
+
h = h0 + (Math.min(255, full3 * 0.6 * stipple) - h0) * Math.min(1, c * 1.5);
|
|
948
1487
|
break;
|
|
949
1488
|
}
|
|
950
1489
|
}
|
|
@@ -1068,7 +1607,7 @@ function normalizeLightSpill(raw, base = DEFAULT_LIGHT_SPILL) {
|
|
|
1068
1607
|
function emptyLight() {
|
|
1069
1608
|
return new TiledSurface(1, /* @__PURE__ */ new Map(), 0);
|
|
1070
1609
|
}
|
|
1071
|
-
var
|
|
1610
|
+
var full2 = (t, channels) => {
|
|
1072
1611
|
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
1073
1612
|
const out = new Uint8ClampedArray(TILE * TILE * channels);
|
|
1074
1613
|
const u = t.uniform ?? 0;
|
|
@@ -1082,7 +1621,7 @@ var full = (t, channels) => {
|
|
|
1082
1621
|
function lightToShadow(light) {
|
|
1083
1622
|
const tiles = /* @__PURE__ */ new Map();
|
|
1084
1623
|
for (const [key, tile] of light.tiles) {
|
|
1085
|
-
const l =
|
|
1624
|
+
const l = full2(tile, 1);
|
|
1086
1625
|
const d = new Uint8ClampedArray(TILE * TILE * 4);
|
|
1087
1626
|
for (let i = 0, j = 0; i < l.length; i++, j += 4) {
|
|
1088
1627
|
if (!l[i]) continue;
|
|
@@ -1097,7 +1636,7 @@ function lightToShadow(light) {
|
|
|
1097
1636
|
function shadowToLight(shadow) {
|
|
1098
1637
|
const tiles = /* @__PURE__ */ new Map();
|
|
1099
1638
|
for (const [key, tile] of shadow.tiles) {
|
|
1100
|
-
const s =
|
|
1639
|
+
const s = full2(tile, 4);
|
|
1101
1640
|
const d = new Uint8ClampedArray(TILE * TILE);
|
|
1102
1641
|
for (let i = 0, j = 0; i < d.length; i++, j += 4) if (s[j + 3]) d[i] = Math.round((s[j] + s[j + 1] + s[j + 2]) / 3 * s[j + 3] / 255);
|
|
1103
1642
|
const t = finishTile(d, 1, 0);
|
|
@@ -1132,7 +1671,7 @@ function scaleLight(light, k) {
|
|
|
1132
1671
|
const tiles = /* @__PURE__ */ new Map();
|
|
1133
1672
|
if (k > 0) {
|
|
1134
1673
|
for (const [key, tile] of light.tiles) {
|
|
1135
|
-
const d = Uint8ClampedArray.from(
|
|
1674
|
+
const d = Uint8ClampedArray.from(full2(tile, 1), (v) => Math.round(v * k));
|
|
1136
1675
|
const t = finishTile(d, 1, 0);
|
|
1137
1676
|
if (t) tiles.set(key, t);
|
|
1138
1677
|
}
|
|
@@ -1147,9 +1686,9 @@ function mergeLight(lower, upper, upperColor) {
|
|
|
1147
1686
|
const lo = lower?.tiles.get(key);
|
|
1148
1687
|
const up = upper.tiles.get(key);
|
|
1149
1688
|
const col = upperColor.tiles.get(key);
|
|
1150
|
-
const l = lo ?
|
|
1151
|
-
const u = up ?
|
|
1152
|
-
const c = col ?
|
|
1689
|
+
const l = lo ? full2(lo, 1) : null;
|
|
1690
|
+
const u = up ? full2(up, 1) : null;
|
|
1691
|
+
const c = col ? full2(col, 4) : null;
|
|
1153
1692
|
const d = new Uint8ClampedArray(TILE * TILE);
|
|
1154
1693
|
for (let i = 0; i < d.length; i++) {
|
|
1155
1694
|
const a = c ? c[i * 4 + 3] / 255 : 0;
|
|
@@ -1186,10 +1725,10 @@ var LightCache = class {
|
|
|
1186
1725
|
out.set(key, cached.tile);
|
|
1187
1726
|
continue;
|
|
1188
1727
|
}
|
|
1189
|
-
const c =
|
|
1190
|
-
const ld = l ?
|
|
1191
|
-
const md = m ?
|
|
1192
|
-
const hd = h ?
|
|
1728
|
+
const c = full2(tile, 4);
|
|
1729
|
+
const ld = l ? full2(l, 1) : null;
|
|
1730
|
+
const md = m ? full2(m, 4) : null;
|
|
1731
|
+
const hd = h ? full2(h, 1) : null;
|
|
1193
1732
|
const d = new Uint8ClampedArray(c);
|
|
1194
1733
|
for (let y = 0, i = 0; y < TILE; y++) {
|
|
1195
1734
|
for (let x = 0; x < TILE; x++, i++) {
|
|
@@ -2916,6 +3455,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2916
3455
|
this.invisibleDaylight = brush.fluorescence.amount > 0 && brush.fluorescence.invisible && brush.engine !== "eraser" && brush.engine !== "blender" && brush.engine !== "smudge";
|
|
2917
3456
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
2918
3457
|
if (init.light && !init.wet && !init.sticker) this.lightTwin = this.makeLightTwin(init.light.surface);
|
|
3458
|
+
if (init.ferro && !init.wet && !init.sticker) this.ferroTwin = this.makeFerroTwin(init.ferro.surface);
|
|
2919
3459
|
if (init.fluor && !init.wet && !init.sticker) this.fluorTwin = this.makeFluorTwin(init.fluor.surface);
|
|
2920
3460
|
if (init.thermo && !init.wet && !init.sticker) this.thermoTwin = this.makeThermoTwin(init.thermo.surface);
|
|
2921
3461
|
this.apply = {
|
|
@@ -2948,6 +3488,9 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2948
3488
|
/** Light paint: the stroke's twin over the light channel, and what it left there. */
|
|
2949
3489
|
lightTwin = null;
|
|
2950
3490
|
lightResult = null;
|
|
3491
|
+
/** Ferrofluid paint: the stroke's twin over the ferrofluid channel, and what it left there. */
|
|
3492
|
+
ferroTwin = null;
|
|
3493
|
+
ferroResult = null;
|
|
2951
3494
|
/** UV-fluorescent ink: the stroke's twin over the fluorescence channel, and what it left there. */
|
|
2952
3495
|
fluorTwin = null;
|
|
2953
3496
|
fluorResult = null;
|
|
@@ -3046,6 +3589,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3046
3589
|
paint,
|
|
3047
3590
|
effect,
|
|
3048
3591
|
light: null,
|
|
3592
|
+
ferro: null,
|
|
3049
3593
|
fluor: null,
|
|
3050
3594
|
thermo: null
|
|
3051
3595
|
});
|
|
@@ -3069,7 +3613,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3069
3613
|
inputFilter: void 0,
|
|
3070
3614
|
light: null,
|
|
3071
3615
|
fluor: null,
|
|
3072
|
-
thermo: null
|
|
3616
|
+
thermo: null,
|
|
3617
|
+
ferro: null
|
|
3073
3618
|
});
|
|
3074
3619
|
}
|
|
3075
3620
|
/** The stroke's twin over the 4-channel thermochromic surface (see `heat.ts`). */
|
|
@@ -3104,7 +3649,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3104
3649
|
effect: null,
|
|
3105
3650
|
light: null,
|
|
3106
3651
|
fluor: null,
|
|
3107
|
-
thermo: null
|
|
3652
|
+
thermo: null,
|
|
3653
|
+
ferro: null
|
|
3108
3654
|
});
|
|
3109
3655
|
}
|
|
3110
3656
|
/** Light paint: the layer's light channel after the stroke, or `null` when the stroke has no twin or left it unchanged. */
|
|
@@ -3119,6 +3665,44 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3119
3665
|
thermoChannel() {
|
|
3120
3666
|
return this.thermoResult;
|
|
3121
3667
|
}
|
|
3668
|
+
/** The stroke's twin over the ferrofluid channel: the same marks in grey at the brush's ferro amount (see `ferro.ts`). */
|
|
3669
|
+
makeFerroTwin(ferro) {
|
|
3670
|
+
const init = this.init;
|
|
3671
|
+
const grey = Math.round(init.brush.ferro.amount * 255);
|
|
3672
|
+
const g = grey / 255;
|
|
3673
|
+
const hex2 = "#" + grey.toString(16).padStart(2, "0").repeat(3);
|
|
3674
|
+
const host = {
|
|
3675
|
+
update: () => {
|
|
3676
|
+
},
|
|
3677
|
+
commit: (_s, surface) => {
|
|
3678
|
+
this.ferroResult = surface ? shadowToFerro(surface) : null;
|
|
3679
|
+
}
|
|
3680
|
+
};
|
|
3681
|
+
const effect = init.effect ? { source: init.effect.source, pixels: { ...init.effect.pixels, data: greyPixels(init.effect.pixels.data, grey) } } : null;
|
|
3682
|
+
const paint = init.paint ? { ...init.paint, stops: init.paint.stops.map((st) => ({ ...st, color: hex2 })) } : null;
|
|
3683
|
+
return new _PaintStrokeSession(host, {
|
|
3684
|
+
...init,
|
|
3685
|
+
surface: ferroToShadow(ferro),
|
|
3686
|
+
lockTransparency: false,
|
|
3687
|
+
brush: ferroTwinBrush(init.brush),
|
|
3688
|
+
color: [grey, grey, grey, 255],
|
|
3689
|
+
colorHex: hex2,
|
|
3690
|
+
secondary: [grey, grey, grey, 255],
|
|
3691
|
+
inputFilter: void 0,
|
|
3692
|
+
mixerLoad: { reservoir: init.mixerLoad.reservoir ? [g, g, g] : null, amount: init.mixerLoad.amount, pickup: null },
|
|
3693
|
+
thick: init.thick,
|
|
3694
|
+
paint,
|
|
3695
|
+
effect,
|
|
3696
|
+
light: null,
|
|
3697
|
+
ferro: null,
|
|
3698
|
+
fluor: null,
|
|
3699
|
+
thermo: null
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
/** Ferrofluid paint: the layer's ferrofluid channel after the stroke, or `null` when the stroke has no twin or left it unchanged. */
|
|
3703
|
+
ferroChannel() {
|
|
3704
|
+
return this.ferroResult;
|
|
3705
|
+
}
|
|
3122
3706
|
tileTarget() {
|
|
3123
3707
|
return {
|
|
3124
3708
|
offset: this.init.offset,
|
|
@@ -3149,6 +3733,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3149
3733
|
this.lastRaw = points[points.length - 1];
|
|
3150
3734
|
this.host.sampled?.(this, points);
|
|
3151
3735
|
this.lightTwin?.push(points);
|
|
3736
|
+
this.ferroTwin?.push(points);
|
|
3152
3737
|
this.fluorTwin?.push(points);
|
|
3153
3738
|
this.thermoTwin?.push(points);
|
|
3154
3739
|
if (this.wetDriver) {
|
|
@@ -3242,6 +3827,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3242
3827
|
this.update(this.dry ? this.dry.finish() : this.thickDriver ? this.thickDriver.finish() : this.stickerSpray ? this.stickerSpray.finish() : this.wet.finish());
|
|
3243
3828
|
this.closed = true;
|
|
3244
3829
|
if (this.lightTwin?.open) this.lightTwin.end();
|
|
3830
|
+
if (this.ferroTwin?.open) this.ferroTwin.end();
|
|
3245
3831
|
if (this.fluorTwin?.open) this.fluorTwin.end();
|
|
3246
3832
|
if (this.thermoTwin?.open) this.thermoTwin.end();
|
|
3247
3833
|
const ch = this.base.channels;
|
|
@@ -3276,6 +3862,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3276
3862
|
if (this.fluorTwin?.open) this.fluorTwin.cancel();
|
|
3277
3863
|
if (this.thermoTwin?.open) this.thermoTwin.cancel();
|
|
3278
3864
|
this.lightResult = null;
|
|
3865
|
+
if (this.ferroTwin?.open) this.ferroTwin.cancel();
|
|
3866
|
+
this.ferroResult = null;
|
|
3279
3867
|
this.fluorResult = null;
|
|
3280
3868
|
this.thermoResult = null;
|
|
3281
3869
|
this.host.commit(this, null, true);
|
|
@@ -3392,8 +3980,8 @@ function nearestOnStencil(s, p) {
|
|
|
3392
3980
|
if (s.kind === "arc") {
|
|
3393
3981
|
const c = s.center;
|
|
3394
3982
|
const ang = Math.atan2(p.y - c.y, p.x - c.x);
|
|
3395
|
-
const
|
|
3396
|
-
if (
|
|
3983
|
+
const full3 = Math.abs(s.endAngle - s.startAngle) >= 360;
|
|
3984
|
+
if (full3) {
|
|
3397
3985
|
const q = { x: c.x + Math.cos(ang) * s.radius, y: c.y + Math.sin(ang) * s.radius };
|
|
3398
3986
|
return { point: q, distance: Math.hypot(p.x - q.x, p.y - q.y) };
|
|
3399
3987
|
}
|
|
@@ -3836,7 +4424,7 @@ function deserializePaintDocument(raw) {
|
|
|
3836
4424
|
const wetEvents = Array.isArray(r.wetEvents) ? r.wetEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.clock) && typeof e.kind === "string") : [];
|
|
3837
4425
|
const state = { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) };
|
|
3838
4426
|
if (wetEvents.length) state.wetEvents = wetEvents;
|
|
3839
|
-
const paintEvents = Array.isArray(r.paintEvents) ? r.paintEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.at) && typeof e.layerId === "string" && ["fill", "fill-region", "clear", "flatten", "stickers", "live-strokes"].includes(e.kind)) : [];
|
|
4427
|
+
const paintEvents = Array.isArray(r.paintEvents) ? r.paintEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.at) && typeof e.layerId === "string" && ["fill", "fill-region", "clear", "flatten", "stickers", "live-strokes", "magnets", "cure-ferrofluid"].includes(e.kind)) : [];
|
|
3840
4428
|
if (paintEvents.length) state.paintEvents = paintEvents;
|
|
3841
4429
|
const o = r.origin;
|
|
3842
4430
|
if (o && typeof o === "object" && Number.isFinite(o.x) && Number.isFinite(o.y) && (o.x || o.y)) state.origin = { x: Math.round(o.x), y: Math.round(o.y) };
|
|
@@ -4768,6 +5356,7 @@ function liveStrokeRefusal(brush) {
|
|
|
4768
5356
|
if (brush.light.amount > 0) return `The ${brush.name} brush lays light, which a stroke layer doesn't redraw yet; paint light on a pixel layer (add_layer), or set light.amount to 0 (set_brush).`;
|
|
4769
5357
|
if (brush.fluorescence.amount > 0) return `The ${brush.name} brush lays UV-fluorescent ink, which a stroke layer doesn't redraw yet; paint it on a pixel layer (add_layer), or set fluorescence.amount to 0 (set_brush).`;
|
|
4770
5358
|
if (brush.thermochromic.hotColor !== null) return `The ${brush.name} brush lays thermochromic ink, which a stroke layer doesn't redraw yet; paint it on a pixel layer (add_layer), or remove thermochromic (set_brush).`;
|
|
5359
|
+
if (brush.ferro.amount > 0) return `The ${brush.name} brush lays ferrofluid, which a stroke layer doesn't redraw; paint ferrofluid on a pixel layer (add_layer), or set ferro.amount to 0 (set_brush).`;
|
|
4771
5360
|
if (brush.behavior.pickup > 0) return `The ${brush.name} brush picks up the paint under it (pickup ${brush.behavior.pickup}), so it can't be redrawn on its own; set behavior.pickup to 0 (set_brush) or paint on a pixel layer.`;
|
|
4772
5361
|
return null;
|
|
4773
5362
|
}
|
|
@@ -5036,6 +5625,12 @@ async function replayPainting(source, target, options = {}) {
|
|
|
5036
5625
|
if (strokes >= maxStrokes && item.kind === "stroke") break;
|
|
5037
5626
|
if (item.kind === "paint") {
|
|
5038
5627
|
const e = item.event;
|
|
5628
|
+
if (e.kind === "magnets") {
|
|
5629
|
+
const d = e.data;
|
|
5630
|
+
const list = (d?.magnets ?? []).map((m) => s === 1 ? m : { ...m, x: m.x * s, y: m.y * s, radius: Math.min(2e3, Math.max(8, m.radius * s)) });
|
|
5631
|
+
target.replaceMagnets(list, Boolean(d?.advect));
|
|
5632
|
+
continue;
|
|
5633
|
+
}
|
|
5039
5634
|
if (!target.getLayer(e.layerId)) continue;
|
|
5040
5635
|
if (e.kind === "fill") target.fillLayer({ layerId: e.layerId, ...e.data });
|
|
5041
5636
|
else if (e.kind === "fill-region") {
|
|
@@ -5050,6 +5645,7 @@ async function replayPainting(source, target, options = {}) {
|
|
|
5050
5645
|
...d.expand !== void 0 ? { expand: Math.max(-16, Math.min(16, d.expand * s)) } : {}
|
|
5051
5646
|
});
|
|
5052
5647
|
} else if (e.kind === "clear") target.clearLayer(e.layerId);
|
|
5648
|
+
else if (e.kind === "cure-ferrofluid") target.cureFerrofluid(e.layerId);
|
|
5053
5649
|
else if (e.kind === "live-strokes") {
|
|
5054
5650
|
const d = e.data;
|
|
5055
5651
|
if (d?.rasterized) target.replaceLiveStrokes(e.layerId, null);
|
|
@@ -6363,6 +6959,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6363
6959
|
litCaches = /* @__PURE__ */ new Map();
|
|
6364
6960
|
/** The live impasto stroke's channels. */
|
|
6365
6961
|
liveThick = null;
|
|
6962
|
+
/** The live ferrofluid stroke's channel. */
|
|
6963
|
+
liveFerro = null;
|
|
6964
|
+
nextMagnetId = 1;
|
|
6366
6965
|
/** The UV / blacklight setting (saved with the document, not undo steps). */
|
|
6367
6966
|
uvLight = { ...DEFAULT_UV_LIGHT };
|
|
6368
6967
|
uvCaches = /* @__PURE__ */ new Map();
|
|
@@ -6892,6 +7491,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6892
7491
|
paper: options.paper === void 0 ? { ...this.doc.paper, offset: void 0 } : this.resolvePaper(options.paper)
|
|
6893
7492
|
});
|
|
6894
7493
|
this.extensions = {};
|
|
7494
|
+
this.nextMagnetId = 1;
|
|
6895
7495
|
this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };
|
|
6896
7496
|
this.tracingVersion++;
|
|
6897
7497
|
if (this.wet.world || this.wet.patterns.size) {
|
|
@@ -7049,7 +7649,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7049
7649
|
describeLayer(l, parentId = findLayer(this.doc.layers, l.id)?.parent?.id ?? null, depth = findLayer(this.doc.layers, l.id)?.path.length ?? 0) {
|
|
7050
7650
|
let bounds = null;
|
|
7051
7651
|
if (l.type === "pixel") {
|
|
7052
|
-
const b = unionRect(contentBounds(l.surface), this.doc.fluor?.[l.id] ? contentBounds(this.doc.fluor[l.id]) : null);
|
|
7652
|
+
const b = unionRect(unionRect(contentBounds(l.surface), this.doc.fluor?.[l.id] ? contentBounds(this.doc.fluor[l.id]) : null), this.doc.ferro?.[l.id]?.bounds ?? null);
|
|
7053
7653
|
bounds = b ? { ...b, x: b.x + l.offset.x, y: b.y + l.offset.y } : null;
|
|
7054
7654
|
} else for (const c of flattenLayers(l.children)) if (c.type === "pixel") bounds = unionRect(bounds, this.describeLayer(c, l.id, depth + 1).bounds);
|
|
7055
7655
|
return {
|
|
@@ -7250,7 +7850,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7250
7850
|
...this.liveStrokesWithout(removedIds),
|
|
7251
7851
|
...this.lightWithout(removedIds),
|
|
7252
7852
|
...this.fluorWithout(removedIds),
|
|
7253
|
-
...this.thermoWithout(removedIds)
|
|
7853
|
+
...this.thermoWithout(removedIds),
|
|
7854
|
+
...this.ferroWithout(removedIds)
|
|
7254
7855
|
});
|
|
7255
7856
|
return { removed: id, activeLayerId: active };
|
|
7256
7857
|
}
|
|
@@ -7266,12 +7867,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7266
7867
|
const stickers = { ...this.doc.stickers ?? {} };
|
|
7267
7868
|
const liveStrokes = { ...this.doc.liveStrokes ?? {} };
|
|
7268
7869
|
const light = { ...this.doc.light ?? {} };
|
|
7870
|
+
const ferro = { ...this.doc.ferro ?? {} };
|
|
7871
|
+
const ferroSettings = { ...this.doc.ferroSettings ?? {} };
|
|
7269
7872
|
const fluor = { ...this.doc.fluor ?? {} };
|
|
7270
7873
|
const thermo = { ...this.doc.thermo ?? {} };
|
|
7271
7874
|
const copy = (l) => {
|
|
7272
7875
|
const id2 = newPaintLayerId();
|
|
7273
7876
|
if (light[l.id]) light[id2] = light[l.id];
|
|
7274
7877
|
if (this.lightSpills[l.id]) this.lightSpills[id2] = { ...this.lightSpills[l.id] };
|
|
7878
|
+
if (ferro[l.id]) ferro[id2] = ferro[l.id];
|
|
7879
|
+
if (ferroSettings[l.id]) ferroSettings[id2] = { ...ferroSettings[l.id] };
|
|
7275
7880
|
if (fluor[l.id]) fluor[id2] = fluor[l.id];
|
|
7276
7881
|
if (thermo[l.id]) thermo[id2] = thermo[l.id];
|
|
7277
7882
|
if (thick[l.id]) thick[id2] = thick[l.id];
|
|
@@ -7292,7 +7897,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7292
7897
|
...this.doc.liveStrokes ? { liveStrokes } : {},
|
|
7293
7898
|
...this.doc.light ? { light } : {},
|
|
7294
7899
|
...this.doc.fluor ? { fluor } : {},
|
|
7295
|
-
...this.doc.thermo ? { thermo } : {}
|
|
7900
|
+
...this.doc.thermo ? { thermo } : {},
|
|
7901
|
+
...this.doc.ferro ? { ferro } : {},
|
|
7902
|
+
...this.doc.ferroSettings ? { ferroSettings } : {}
|
|
7296
7903
|
})
|
|
7297
7904
|
);
|
|
7298
7905
|
return this.describeLayer(dup);
|
|
@@ -7345,6 +7952,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7345
7952
|
const mergedLight = this.doc.light?.[upper.id] || this.doc.light?.[below.id] ? mergeLight(toDoc(this.doc.light?.[below.id], below), upper.visible ? toDoc(this.doc.light?.[upper.id], upper) : void 0, upperDoc) : void 0;
|
|
7346
7953
|
const lightAfter = this.lightWithout([upper.id, below.id]).light ?? (this.doc.light ? { ...this.doc.light } : void 0);
|
|
7347
7954
|
const light = mergedLight ? { ...lightAfter ?? {}, [below.id]: mergedLight } : lightAfter;
|
|
7955
|
+
const mergedFerro = this.doc.ferro?.[upper.id] || this.doc.ferro?.[below.id] ? mergeFerro(toDoc(this.doc.ferro?.[below.id], below), upper.visible ? toDoc(this.doc.ferro?.[upper.id], upper) : void 0, translateSurface(upper.surface, upper.offset.x, upper.offset.y)) : void 0;
|
|
7956
|
+
const ferroRemoved = this.ferroWithout([upper.id, below.id]);
|
|
7957
|
+
const ferroAfter = ferroRemoved.ferro ?? (this.doc.ferro ? { ...this.doc.ferro } : void 0);
|
|
7958
|
+
const ferro = mergedFerro?.tiles.size ? { ...ferroAfter ?? {}, [below.id]: mergedFerro } : ferroAfter;
|
|
7959
|
+
const ferroSettingsAfter = ferroRemoved.ferroSettings ?? (this.doc.ferroSettings ? { ...this.doc.ferroSettings } : void 0);
|
|
7960
|
+
const mergedFerroSettings = upper.visible && this.doc.ferroSettings?.[upper.id] ? this.doc.ferroSettings[upper.id] : this.doc.ferroSettings?.[below.id];
|
|
7961
|
+
const ferroSettings = mergedFerro?.tiles.size && mergedFerroSettings ? { ...ferroSettingsAfter ?? {}, [below.id]: mergedFerroSettings } : ferroSettingsAfter;
|
|
7348
7962
|
const mergedFluor = this.doc.fluor?.[upper.id] || this.doc.fluor?.[below.id] ? mergeFourChannel(toDoc(this.doc.fluor?.[below.id], below), upper.visible ? toDoc(this.doc.fluor?.[upper.id], upper) : void 0) : void 0;
|
|
7349
7963
|
const fluorAfter = this.fluorWithout([upper.id, below.id]).fluor ?? (this.doc.fluor ? { ...this.doc.fluor } : void 0);
|
|
7350
7964
|
const fluor = mergedFluor ? { ...fluorAfter ?? {}, [below.id]: mergedFluor } : fluorAfter;
|
|
@@ -7362,7 +7976,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7362
7976
|
...this.liveStrokesWithout([upper.id, below.id]),
|
|
7363
7977
|
...light ? { light } : {},
|
|
7364
7978
|
...fluor ? { fluor } : {},
|
|
7365
|
-
...thermo ? { thermo } : {}
|
|
7979
|
+
...thermo ? { thermo } : {},
|
|
7980
|
+
...ferro !== void 0 ? { ferro } : {},
|
|
7981
|
+
...ferroSettings !== void 0 ? { ferroSettings } : {}
|
|
7366
7982
|
})
|
|
7367
7983
|
);
|
|
7368
7984
|
return this.describeLayer(merged);
|
|
@@ -7387,7 +8003,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7387
8003
|
...this.doc.liveStrokes?.[l.id] ? { liveStrokes: { ...this.doc.liveStrokes, [l.id]: [] } } : {},
|
|
7388
8004
|
...this.lightWithout([l.id]),
|
|
7389
8005
|
...this.fluorWithout([l.id]),
|
|
7390
|
-
...this.thermoWithout([l.id])
|
|
8006
|
+
...this.thermoWithout([l.id]),
|
|
8007
|
+
...this.ferroWithout([l.id])
|
|
7391
8008
|
},
|
|
7392
8009
|
{ kind: "clear", layerId: l.id }
|
|
7393
8010
|
)
|
|
@@ -7437,6 +8054,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7437
8054
|
const surface = this.applyCoverage(l, coverage, rect, apply);
|
|
7438
8055
|
const lit = this.doc.light?.[l.id];
|
|
7439
8056
|
const light = lit ? withLayerLight(this.doc.light, l.id, scaleLight(lit, 1 - opacity)) : void 0;
|
|
8057
|
+
const fer = this.doc.ferro?.[l.id];
|
|
8058
|
+
const ferro = fer ? withLayerFerro(this.doc.ferro, l.id, scaleFerro(fer, 1 - opacity)) : void 0;
|
|
8059
|
+
const ferroSettings = fer ? withLayerFerroSettings(this.doc.ferroSettings, ferro, l.id, this.doc.ferroSettings?.[l.id] ?? null) : void 0;
|
|
7440
8060
|
const fl = this.doc.fluor?.[l.id];
|
|
7441
8061
|
const fluor = fl ? withLayerLight(this.doc.fluor, l.id, scaleFluor(fl, 1 - opacity)) : void 0;
|
|
7442
8062
|
const th = this.doc.thermo?.[l.id];
|
|
@@ -7449,7 +8069,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7449
8069
|
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })),
|
|
7450
8070
|
...light ? { light } : {},
|
|
7451
8071
|
...fluor ? { fluor } : {},
|
|
7452
|
-
...thermo ? { thermo } : {}
|
|
8072
|
+
...thermo ? { thermo } : {},
|
|
8073
|
+
...fer ? { ferro, ferroSettings } : {}
|
|
7453
8074
|
}),
|
|
7454
8075
|
{ kind: "fill", layerId: l.id, data: { color, opacity, grain: grainAmount, pressure, blend } }
|
|
7455
8076
|
)
|
|
@@ -8625,6 +9246,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8625
9246
|
if (brush.thermochromic.hotColor !== null && brush.engine === "sticker") {
|
|
8626
9247
|
throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take thermochromic ink; paint thermochromic ink with a dab or particle brush (thermo-ink; list_brush_presets lists them), or set thermochromic.hotColor to null (set_brush).`);
|
|
8627
9248
|
}
|
|
9249
|
+
if (brush.ferro.amount > 0) {
|
|
9250
|
+
if (brush.engine === "sticker") throw new PaintEditorError(`Sticker brushes spray objects and cannot lay ferrofluid (${brush.name}); paint ferrofluid with a dab or particle brush (ferrofluid, ferro-ink; list_brush_presets lists them), or set ferro.amount to 0 (set_brush).`);
|
|
9251
|
+
if (brush.engine === "wet") throw new PaintEditorError(`Wet brushes cannot lay ferrofluid (${brush.name}); paint ferrofluid with a dry or impasto brush (ferrofluid, ferro-ink), or set ferro.amount to 0 (set_brush).`);
|
|
9252
|
+
}
|
|
8628
9253
|
if (doc.liveStrokes?.[layer.id]) {
|
|
8629
9254
|
const why = liveStrokeRefusal(brush);
|
|
8630
9255
|
if (why) throw new PaintEditorError(`Layer "${layer.name}" is a stroke layer. ${why}`);
|
|
@@ -8754,6 +9379,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8754
9379
|
thick: needThick ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
|
|
8755
9380
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
8756
9381
|
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
9382
|
+
// Ferrofluid paint: a twin of the stroke paints the liquid ferrofluid channel.
|
|
9383
|
+
ferro: !wet && brush.engine !== "sticker" && (brush.ferro.amount > 0 || doc.ferro?.[layer.id]) ? { surface: doc.ferro?.[layer.id] ?? emptyFerro() } : null,
|
|
8757
9384
|
fluor: brush.engine !== "sticker" && (brush.fluorescence.amount > 0 || doc.fluor?.[layer.id]) ? { surface: doc.fluor?.[layer.id] ?? emptyFluor() } : null,
|
|
8758
9385
|
thermo: brush.engine !== "sticker" && (brush.thermochromic.hotColor !== null || doc.thermo?.[layer.id]) ? { surface: doc.thermo?.[layer.id] ?? emptyThermo() } : null,
|
|
8759
9386
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
|
|
@@ -8804,6 +9431,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8804
9431
|
this.live = { layerId, surface };
|
|
8805
9432
|
const ch = this.session?.thickChannels();
|
|
8806
9433
|
this.liveThick = ch ? { layerId, channels: ch } : null;
|
|
9434
|
+
const fc = this.session?.ferroChannel();
|
|
9435
|
+
const fb = this.session?.init.brush.ferro;
|
|
9436
|
+
this.liveFerro = fc ? { layerId, surface: fc, settings: fb && fb.amount > 0 ? fb : this.doc.ferroSettings?.[layerId] ?? DEFAULT_FERRO } : null;
|
|
8807
9437
|
const fl = this.session?.fluorChannel();
|
|
8808
9438
|
this.liveFluor = fl ? { layerId, surface: fl } : null;
|
|
8809
9439
|
const th = this.session?.thermoChannel();
|
|
@@ -8826,6 +9456,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8826
9456
|
this.live = null;
|
|
8827
9457
|
this.liveLayers = null;
|
|
8828
9458
|
this.liveThick = null;
|
|
9459
|
+
this.liveFerro = null;
|
|
8829
9460
|
this.liveFluor = null;
|
|
8830
9461
|
this.liveThermo = null;
|
|
8831
9462
|
const wetStroke = Boolean(s.init.wet);
|
|
@@ -8874,6 +9505,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8874
9505
|
const stickers = sprayed ? { ...this.doc.stickers ?? {}, [l.id]: [...this.doc.stickers?.[l.id] ?? [], ...sprayed] } : this.doc.stickers;
|
|
8875
9506
|
const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
|
|
8876
9507
|
const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
|
|
9508
|
+
const fc = s.ferroChannel();
|
|
9509
|
+
const ferro = fc ? withLayerFerro(this.doc.ferro, l.id, fc) : this.doc.ferro;
|
|
9510
|
+
const ferroSettings = fc ? withLayerFerroSettings(this.doc.ferroSettings, ferro, l.id, brush.ferro.amount > 0 ? brush.ferro : this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO) : this.doc.ferroSettings;
|
|
8877
9511
|
const fluor = withLayerLight(this.doc.fluor, l.id, s.fluorChannel());
|
|
8878
9512
|
const thermo = withLayerLight(this.doc.thermo, l.id, s.thermoChannel());
|
|
8879
9513
|
this.commit(
|
|
@@ -8888,7 +9522,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8888
9522
|
...stickers ? { stickers } : {},
|
|
8889
9523
|
...light ? { light } : {},
|
|
8890
9524
|
...fluor ? { fluor } : {},
|
|
8891
|
-
...thermo ? { thermo } : {}
|
|
9525
|
+
...thermo ? { thermo } : {},
|
|
9526
|
+
...ferro ? { ferro } : { ferro: void 0 },
|
|
9527
|
+
...ferroSettings ? { ferroSettings } : { ferroSettings: void 0 }
|
|
8892
9528
|
},
|
|
8893
9529
|
stroke
|
|
8894
9530
|
)
|
|
@@ -8973,6 +9609,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8973
9609
|
this.live = null;
|
|
8974
9610
|
this.liveLayers = null;
|
|
8975
9611
|
this.liveThick = null;
|
|
9612
|
+
this.liveFerro = null;
|
|
8976
9613
|
this.liveFluor = null;
|
|
8977
9614
|
this.liveThermo = null;
|
|
8978
9615
|
const masks = s.init.selection ? (s.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === "mask") : [];
|
|
@@ -8993,6 +9630,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8993
9630
|
this.live = null;
|
|
8994
9631
|
this.liveLayers = null;
|
|
8995
9632
|
this.liveThick = null;
|
|
9633
|
+
this.liveFerro = null;
|
|
8996
9634
|
this.liveFluor = null;
|
|
8997
9635
|
this.liveThermo = null;
|
|
8998
9636
|
this.emit();
|
|
@@ -9037,6 +9675,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9037
9675
|
const thick = withLayerThick(out.thick, l.id, ch);
|
|
9038
9676
|
const sprayed = s.stickers();
|
|
9039
9677
|
const lit = s.lightChannel() ?? (s.init.wet && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null);
|
|
9678
|
+
const fc = s.ferroChannel();
|
|
9679
|
+
const nextFerro = fc ? withLayerFerro(out.ferro, l.id, fc) : out.ferro;
|
|
9680
|
+
const nextFerroSettings = fc ? withLayerFerroSettings(
|
|
9681
|
+
out.ferroSettings,
|
|
9682
|
+
nextFerro,
|
|
9683
|
+
l.id,
|
|
9684
|
+
s.init.brush.ferro.amount > 0 ? s.init.brush.ferro : out.ferroSettings?.[l.id] ?? DEFAULT_FERRO
|
|
9685
|
+
) : out.ferroSettings;
|
|
9040
9686
|
const fl = s.fluorChannel();
|
|
9041
9687
|
const th = s.thermoChannel();
|
|
9042
9688
|
out = withLiveStroke({
|
|
@@ -9046,6 +9692,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9046
9692
|
...thick ? { thick } : {},
|
|
9047
9693
|
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {},
|
|
9048
9694
|
...lit ? { light: withLayerLight(out.light, l.id, lit) } : {},
|
|
9695
|
+
...fc ? { ferro: nextFerro, ferroSettings: nextFerroSettings } : {},
|
|
9049
9696
|
...fl ? { fluor: withLayerLight(out.fluor, l.id, fl) } : {},
|
|
9050
9697
|
...th ? { thermo: withLayerLight(out.thermo, l.id, th) } : {}
|
|
9051
9698
|
}, stroke);
|
|
@@ -9440,6 +10087,178 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9440
10087
|
this.commit("Flatten relief", this.withPaintEvent({ ...this.doc, ...t }, { kind: "flatten", layerId: l.id }));
|
|
9441
10088
|
return { layerId: l.id, flattened: true };
|
|
9442
10089
|
}
|
|
10090
|
+
// --- Ferrofluid paint and magnets -----------------------------------------------
|
|
10091
|
+
/** The document's `ferro` and `ferroSettings` without these layers. */
|
|
10092
|
+
ferroWithout(ids) {
|
|
10093
|
+
const out = {};
|
|
10094
|
+
const f = this.doc.ferro;
|
|
10095
|
+
if (f && ids.some((id) => f[id])) {
|
|
10096
|
+
const next = { ...f };
|
|
10097
|
+
for (const id of ids) delete next[id];
|
|
10098
|
+
out.ferro = Object.keys(next).length ? next : void 0;
|
|
10099
|
+
}
|
|
10100
|
+
const fs = this.doc.ferroSettings;
|
|
10101
|
+
if (fs && ids.some((id) => fs[id])) {
|
|
10102
|
+
const next = { ...fs };
|
|
10103
|
+
for (const id of ids) delete next[id];
|
|
10104
|
+
out.ferroSettings = Object.keys(next).length ? next : void 0;
|
|
10105
|
+
}
|
|
10106
|
+
return out;
|
|
10107
|
+
}
|
|
10108
|
+
/** Runs deterministic advection on all pixel layers with liquid ferrofluid toward `magnets`. */
|
|
10109
|
+
advectDocumentFerro(doc, magnets, steps = 4) {
|
|
10110
|
+
const f = doc.ferro;
|
|
10111
|
+
if (!f || !magnets.length) return f ? { ...f } : void 0;
|
|
10112
|
+
const out = {};
|
|
10113
|
+
for (const [id, surf] of Object.entries(f)) {
|
|
10114
|
+
const l = findLayer(doc.layers, id)?.layer;
|
|
10115
|
+
if (isPixelLayer(l) && surf.tiles.size > 0) {
|
|
10116
|
+
const next = advectFerro(surf, magnets, l.offset, steps);
|
|
10117
|
+
if (next.tiles.size > 0) out[id] = next;
|
|
10118
|
+
} else if (surf.tiles.size > 0) {
|
|
10119
|
+
out[id] = surf;
|
|
10120
|
+
}
|
|
10121
|
+
}
|
|
10122
|
+
return Object.keys(out).length ? out : void 0;
|
|
10123
|
+
}
|
|
10124
|
+
/** Lists the magnets on the canvas. */
|
|
10125
|
+
listMagnets() {
|
|
10126
|
+
return (this.doc.magnets ?? []).map((m) => ({ ...m }));
|
|
10127
|
+
}
|
|
10128
|
+
/** Places a dipole magnet on the canvas and advects liquid ferrofluid toward it (one undo step). */
|
|
10129
|
+
addMagnet(input) {
|
|
10130
|
+
this.requireIdle("adding a magnet");
|
|
10131
|
+
const existing = this.doc.magnets ?? [];
|
|
10132
|
+
const usedIds = new Set(existing.map((m) => m.id));
|
|
10133
|
+
while (usedIds.has(`mag${this.nextMagnetId}`)) this.nextMagnetId++;
|
|
10134
|
+
const id = `mag${this.nextMagnetId++}`;
|
|
10135
|
+
let magnet;
|
|
10136
|
+
try {
|
|
10137
|
+
magnet = normalizeMagnet({ id, x: input.x, y: input.y, strength: input.strength, radius: input.radius });
|
|
10138
|
+
} catch (e) {
|
|
10139
|
+
throw new PaintEditorError(e.message);
|
|
10140
|
+
}
|
|
10141
|
+
const magnets = [...existing, magnet];
|
|
10142
|
+
const ferro = this.advectDocumentFerro(this.doc, magnets, 4);
|
|
10143
|
+
const nextDoc = this.withPaintEvent(
|
|
10144
|
+
{ ...this.doc, magnets, ...this.doc.ferro ? { ferro } : {} },
|
|
10145
|
+
{ kind: "magnets", layerId: "", data: { magnets: magnets.map((m) => ({ ...m })), advect: true } }
|
|
10146
|
+
);
|
|
10147
|
+
this.commit("Add magnet", nextDoc);
|
|
10148
|
+
return { ...magnet };
|
|
10149
|
+
}
|
|
10150
|
+
/** Moves or retunes a magnet and advects liquid ferrofluid toward the magnets (one undo step). */
|
|
10151
|
+
updateMagnet(id, update, commitToHistory = true) {
|
|
10152
|
+
this.requireIdle("updating a magnet");
|
|
10153
|
+
if (update.x === void 0 && update.y === void 0 && update.strength === void 0 && update.radius === void 0) {
|
|
10154
|
+
throw new PaintEditorError("Pass at least one change: x, y, strength or radius.");
|
|
10155
|
+
}
|
|
10156
|
+
const existing = this.doc.magnets ?? [];
|
|
10157
|
+
const idx = existing.findIndex((m) => m.id === id);
|
|
10158
|
+
if (idx < 0) throw new PaintEditorError(`Unknown magnet "${id}". Call list_magnets for the ids.`);
|
|
10159
|
+
const current = existing[idx];
|
|
10160
|
+
let magnet;
|
|
10161
|
+
try {
|
|
10162
|
+
magnet = normalizeMagnet(
|
|
10163
|
+
{
|
|
10164
|
+
id,
|
|
10165
|
+
x: update.x ?? current.x,
|
|
10166
|
+
y: update.y ?? current.y,
|
|
10167
|
+
strength: update.strength ?? current.strength,
|
|
10168
|
+
radius: update.radius ?? current.radius
|
|
10169
|
+
},
|
|
10170
|
+
current
|
|
10171
|
+
);
|
|
10172
|
+
} catch (e) {
|
|
10173
|
+
throw new PaintEditorError(e.message);
|
|
10174
|
+
}
|
|
10175
|
+
const magnets = existing.map((m, i) => i === idx ? magnet : m);
|
|
10176
|
+
const last = this.undoStack[this.undoStack.length - 1];
|
|
10177
|
+
const amending = !commitToHistory && Boolean(last && last.label === "Update magnet" && !last.wet && !this.wetOp && !this.grouping);
|
|
10178
|
+
const baseDoc = amending && last ? last.before : this.doc;
|
|
10179
|
+
const ferro = this.advectDocumentFerro(baseDoc, magnets, 4);
|
|
10180
|
+
const nextDoc = this.withPaintEvent(
|
|
10181
|
+
{ ...this.doc, paintEvents: baseDoc.paintEvents, magnets, ...baseDoc.ferro ? { ferro } : {} },
|
|
10182
|
+
{ kind: "magnets", layerId: "", data: { magnets: magnets.map((m) => ({ ...m })), advect: true } }
|
|
10183
|
+
);
|
|
10184
|
+
this.commitOrAmend("Update magnet", nextDoc, commitToHistory);
|
|
10185
|
+
return { ...magnet };
|
|
10186
|
+
}
|
|
10187
|
+
/** Removes magnets by id, or all magnets when `ids` is omitted or empty (one undo step). Returns the removed ids. */
|
|
10188
|
+
removeMagnets(ids) {
|
|
10189
|
+
this.requireIdle("removing magnets");
|
|
10190
|
+
const existing = this.doc.magnets ?? [];
|
|
10191
|
+
if (ids?.length) {
|
|
10192
|
+
for (const id of ids) {
|
|
10193
|
+
if (!existing.some((m) => m.id === id)) throw new PaintEditorError(`Unknown magnet "${id}". Call list_magnets for the ids.`);
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
10196
|
+
const removed = ids?.length ? [...ids] : existing.map((m) => m.id);
|
|
10197
|
+
if (!removed.length) return [];
|
|
10198
|
+
const next = existing.filter((m) => !removed.includes(m.id));
|
|
10199
|
+
const nextDoc = this.withPaintEvent(
|
|
10200
|
+
{ ...this.doc, magnets: next.length ? next : void 0 },
|
|
10201
|
+
{ kind: "magnets", layerId: "", data: { magnets: next.map((m) => ({ ...m })), advect: false } }
|
|
10202
|
+
);
|
|
10203
|
+
this.commit("Remove magnets", nextDoc);
|
|
10204
|
+
return removed;
|
|
10205
|
+
}
|
|
10206
|
+
/** Replaces the canvas magnets (used by replay). */
|
|
10207
|
+
replaceMagnets(magnets, advect = false) {
|
|
10208
|
+
const next = magnets.map((m) => normalizeMagnet(m));
|
|
10209
|
+
const ferro = advect ? this.advectDocumentFerro(this.doc, next, 4) : this.doc.ferro;
|
|
10210
|
+
this.commit("Magnets", { ...this.doc, magnets: next.length ? next : void 0, ...this.doc.ferro ? { ferro } : {} });
|
|
10211
|
+
}
|
|
10212
|
+
/** Reads a layer's liquid ferrofluid coverage and active Rosensweig spike relief under the canvas magnets. */
|
|
10213
|
+
getFerrofluid(layerId) {
|
|
10214
|
+
const l = this.wetLayer(layerId, "read the ferrofluid of");
|
|
10215
|
+
const settings = this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO;
|
|
10216
|
+
const magnets = this.doc.magnets ?? [];
|
|
10217
|
+
const info = ferroInfo(this.doc.ferro?.[l.id], magnets, settings, l.offset);
|
|
10218
|
+
const bounds = info.bounds ? { ...info.bounds, x: info.bounds.x + l.offset.x, y: info.bounds.y + l.offset.y } : null;
|
|
10219
|
+
return {
|
|
10220
|
+
layerId: l.id,
|
|
10221
|
+
ferro: info.peak > 0,
|
|
10222
|
+
bounds,
|
|
10223
|
+
peak: info.peak,
|
|
10224
|
+
coverage: info.liquidPixels,
|
|
10225
|
+
activeSpikeArea: info.spikePixels,
|
|
10226
|
+
maxSpikeHeight: info.maxSpikeHeight,
|
|
10227
|
+
magnetCount: info.magnetCount,
|
|
10228
|
+
wavelength: settings.wavelength,
|
|
10229
|
+
threshold: settings.threshold
|
|
10230
|
+
};
|
|
10231
|
+
}
|
|
10232
|
+
/** Cures a layer's liquid ferrofluid: bakes the current Rosensweig spikes and glossy metallic material into `doc.thick` and clears `doc.ferro` (one undo step). */
|
|
10233
|
+
cureFerrofluid(layerId) {
|
|
10234
|
+
this.requireIdle("curing ferrofluid");
|
|
10235
|
+
const l = this.paintTarget(layerId, "cure the ferrofluid of");
|
|
10236
|
+
const fSurf = this.doc.ferro?.[l.id];
|
|
10237
|
+
if (!fSurf || !fSurf.tiles.size) {
|
|
10238
|
+
return { layerId: l.id, cured: false, bounds: null, maxSpikeHeight: 0, activeSpikeArea: 0 };
|
|
10239
|
+
}
|
|
10240
|
+
const settings = this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO;
|
|
10241
|
+
const magnets = this.doc.magnets ?? [];
|
|
10242
|
+
const fb = ferroInfo(fSurf, magnets, settings, l.offset).bounds;
|
|
10243
|
+
const bounds = fb ? { ...fb, x: fb.x + l.offset.x, y: fb.y + l.offset.y } : null;
|
|
10244
|
+
const baked = bakeFerroIntoThick(l.surface, fSurf, this.doc.thick?.[l.id], magnets, settings, l.offset);
|
|
10245
|
+
const removed = this.ferroWithout([l.id]);
|
|
10246
|
+
const nextDoc = {
|
|
10247
|
+
...this.doc,
|
|
10248
|
+
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: baked.color })),
|
|
10249
|
+
thick: { ...this.doc.thick ?? {}, [l.id]: baked.thick },
|
|
10250
|
+
ferro: removed.ferro,
|
|
10251
|
+
ferroSettings: removed.ferroSettings
|
|
10252
|
+
};
|
|
10253
|
+
this.commit("Cure ferrofluid", this.withPaintEvent(nextDoc, { kind: "cure-ferrofluid", layerId: l.id }));
|
|
10254
|
+
return {
|
|
10255
|
+
layerId: l.id,
|
|
10256
|
+
cured: true,
|
|
10257
|
+
bounds,
|
|
10258
|
+
maxSpikeHeight: baked.maxSpikeHeight,
|
|
10259
|
+
activeSpikeArea: baked.spikePixels
|
|
10260
|
+
};
|
|
10261
|
+
}
|
|
9443
10262
|
// --- Replay: time-lapse and high-resolution export ------------------------------
|
|
9444
10263
|
/**
|
|
9445
10264
|
* A fresh, headless editor for replaying this painting at `scale` (canvas, dpi and paper grain
|
|
@@ -9942,20 +10761,33 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9942
10761
|
};
|
|
9943
10762
|
return visit(layers);
|
|
9944
10763
|
}
|
|
9945
|
-
/** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */
|
|
10764
|
+
/** The layers with thick paint and ferrofluid spikes lit (each thick or ferrofluid layer's pixels replaced by its lit tiles). */
|
|
9946
10765
|
litLayers(layers) {
|
|
9947
10766
|
const thick = this.doc.thick;
|
|
9948
10767
|
const live = this.liveThick;
|
|
9949
|
-
|
|
10768
|
+
const ferro = this.doc.ferro;
|
|
10769
|
+
const liveFerro = this.liveFerro;
|
|
10770
|
+
if (!thick && !live && !ferro && !liveFerro) return layers;
|
|
9950
10771
|
let out = layers;
|
|
9951
|
-
const ids = /* @__PURE__ */ new Set([
|
|
10772
|
+
const ids = /* @__PURE__ */ new Set([
|
|
10773
|
+
...Object.keys(thick ?? {}),
|
|
10774
|
+
...live ? [live.layerId] : [],
|
|
10775
|
+
...Object.keys(ferro ?? {}),
|
|
10776
|
+
...liveFerro ? [liveFerro.layerId] : []
|
|
10777
|
+
]);
|
|
10778
|
+
const magnets = this.doc.magnets ?? [];
|
|
9952
10779
|
for (const id of ids) {
|
|
9953
|
-
const
|
|
9954
|
-
|
|
9955
|
-
|
|
10780
|
+
const l = findLayer(out, id)?.layer;
|
|
10781
|
+
if (!isPixelLayer(l)) continue;
|
|
10782
|
+
const ch = (live?.layerId === id ? live.channels : thick?.[id]) ?? emptyThickChannels();
|
|
10783
|
+
const fSurf = liveFerro?.layerId === id ? liveFerro.surface : ferro?.[id];
|
|
10784
|
+
const fSet = (liveFerro?.layerId === id ? liveFerro.settings : this.doc.ferroSettings?.[id]) ?? DEFAULT_FERRO;
|
|
10785
|
+
const hasThick = ch.height.tiles.size > 0 || ch.material.tiles.size > 0 || ch.film.tiles.size > 0;
|
|
10786
|
+
const hasFerro = Boolean(fSurf && fSurf.tiles.size > 0);
|
|
10787
|
+
if (!hasThick && !hasFerro) continue;
|
|
9956
10788
|
let cache = this.litCaches.get(id);
|
|
9957
10789
|
if (!cache) this.litCaches.set(id, cache = new LitCache());
|
|
9958
|
-
const lit = cache.lit(l.surface, ch, this.lighting, this.viewTilt);
|
|
10790
|
+
const lit = cache.lit(l.surface, ch, this.lighting, this.viewTilt, hasFerro ? { surface: fSurf, magnets, settings: fSet, offset: l.offset } : null);
|
|
9959
10791
|
if (lit !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: lit }));
|
|
9960
10792
|
}
|
|
9961
10793
|
return out;
|
|
@@ -10044,13 +10876,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10044
10876
|
}
|
|
10045
10877
|
/** The canvas (or some layers), scaled so the longest side is at most `maxSide`. */
|
|
10046
10878
|
renderPreview(options = {}) {
|
|
10047
|
-
const
|
|
10879
|
+
const full3 = this.compositePixels({ ...options, wet: options.wet ?? "display" });
|
|
10048
10880
|
const side = Math.max(1, Math.round(options.maxSide ?? 512));
|
|
10049
|
-
const k = Math.min(1, side / Math.max(
|
|
10050
|
-
if (k >= 1) return
|
|
10051
|
-
const w = Math.max(1, Math.round(
|
|
10052
|
-
const h = Math.max(1, Math.round(
|
|
10053
|
-
return { width: w, height: h, data: downsampleRgba(
|
|
10881
|
+
const k = Math.min(1, side / Math.max(full3.width, full3.height));
|
|
10882
|
+
if (k >= 1) return full3;
|
|
10883
|
+
const w = Math.max(1, Math.round(full3.width * k));
|
|
10884
|
+
const h = Math.max(1, Math.round(full3.height * k));
|
|
10885
|
+
return { width: w, height: h, data: downsampleRgba(full3.data, full3.width, full3.height, w, h) };
|
|
10054
10886
|
}
|
|
10055
10887
|
// --- Save, load and export -------------------------------------------------------
|
|
10056
10888
|
/** The document as JSON with inline PNG assets. */
|
|
@@ -10166,6 +10998,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10166
10998
|
const spills = Object.fromEntries(Object.entries(this.lightSpills).filter(([id]) => findLayer(this.doc.layers, id)));
|
|
10167
10999
|
if (Object.keys(spills).length) ext.lightSpill = spills;
|
|
10168
11000
|
else delete ext.lightSpill;
|
|
11001
|
+
const ferro = {};
|
|
11002
|
+
for (const [id, f] of Object.entries(this.doc.ferro ?? {})) {
|
|
11003
|
+
if (!findLayer(this.doc.layers, id)) continue;
|
|
11004
|
+
const saved = ferroToJSON(f, this.doc.ferroSettings?.[id]);
|
|
11005
|
+
if (saved) ferro[id] = saved;
|
|
11006
|
+
}
|
|
11007
|
+
if (Object.keys(ferro).length) ext.ferro = ferro;
|
|
11008
|
+
else delete ext.ferro;
|
|
11009
|
+
if (this.doc.magnets?.length) ext.magnets = this.doc.magnets.map((m) => ({ ...m }));
|
|
11010
|
+
else delete ext.magnets;
|
|
10169
11011
|
const effectLayers = effectLayersToJSON(this.doc.effectLayers ?? {}, (id) => isPixelLayer(findLayer(this.doc.layers, id)?.layer));
|
|
10170
11012
|
if (effectLayers) ext.effectLayers = effectLayers;
|
|
10171
11013
|
else delete ext.effectLayers;
|
|
@@ -10303,6 +11145,25 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10303
11145
|
if (heat) state = { ...state, heat };
|
|
10304
11146
|
else problems.push("The heat field could not be read; the canvas loads cool.");
|
|
10305
11147
|
}
|
|
11148
|
+
if (ext.ferro && typeof ext.ferro === "object") {
|
|
11149
|
+
const ferro = {};
|
|
11150
|
+
const ferroSettings = {};
|
|
11151
|
+
for (const [id, v] of Object.entries(ext.ferro)) {
|
|
11152
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
11153
|
+
try {
|
|
11154
|
+
const loaded = ferroFromJSON(v);
|
|
11155
|
+
ferro[id] = loaded.surface;
|
|
11156
|
+
ferroSettings[id] = loaded.settings;
|
|
11157
|
+
} catch (e) {
|
|
11158
|
+
problems.push(`The ferrofluid of layer "${id}" could not be read (${e.message}); it loads without ferrofluid.`);
|
|
11159
|
+
}
|
|
11160
|
+
}
|
|
11161
|
+
if (Object.keys(ferro).length) state = { ...state, ferro, ferroSettings };
|
|
11162
|
+
}
|
|
11163
|
+
if (ext.magnets !== void 0) {
|
|
11164
|
+
const magnets = magnetsFromJSON(ext.magnets);
|
|
11165
|
+
if (magnets.length) state = { ...state, magnets };
|
|
11166
|
+
}
|
|
10306
11167
|
const raw = ext.layerChannels;
|
|
10307
11168
|
if (!raw || typeof raw !== "object") return state;
|
|
10308
11169
|
const thick = {};
|
|
@@ -10337,6 +11198,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10337
11198
|
this.redoStack = [];
|
|
10338
11199
|
this.resetMutations();
|
|
10339
11200
|
this.doc = this.readThick(loaded.state, loaded.extensions, loaded.problems);
|
|
11201
|
+
this.nextMagnetId = 1;
|
|
11202
|
+
for (const m of this.doc.magnets ?? []) {
|
|
11203
|
+
const n = Number(/^mag(\d+)$/.exec(m.id)?.[1] ?? 0);
|
|
11204
|
+
if (n >= this.nextMagnetId) this.nextMagnetId = n + 1;
|
|
11205
|
+
}
|
|
10340
11206
|
for (const list of Object.values(this.doc.liveStrokes ?? {})) {
|
|
10341
11207
|
for (const s of list) {
|
|
10342
11208
|
const n = Number(/^s(\d+)$/.exec(s.id)?.[1] ?? 0);
|
|
@@ -10347,6 +11213,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10347
11213
|
delete this.extensions.layerChannels;
|
|
10348
11214
|
delete this.extensions.light;
|
|
10349
11215
|
delete this.extensions.lightSpill;
|
|
11216
|
+
delete this.extensions.ferro;
|
|
11217
|
+
delete this.extensions.magnets;
|
|
10350
11218
|
delete this.extensions.lighting;
|
|
10351
11219
|
delete this.extensions.stickers;
|
|
10352
11220
|
delete this.extensions.liveStrokes;
|
|
@@ -10387,13 +11255,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10387
11255
|
return encodePng(img.width, img.height, img.data);
|
|
10388
11256
|
}
|
|
10389
11257
|
scaled(options) {
|
|
10390
|
-
const
|
|
11258
|
+
const full3 = this.compositePixels(options);
|
|
10391
11259
|
const s = options.scale ?? 1;
|
|
10392
11260
|
if (!(s > 0 && s <= 1)) throw new PaintEditorError(`scale ${s} is out of range; use above 0 up to 1 (exports never upscale pixels).`);
|
|
10393
|
-
if (s === 1) return
|
|
10394
|
-
const w = Math.max(1, Math.round(
|
|
10395
|
-
const h = Math.max(1, Math.round(
|
|
10396
|
-
return { width: w, height: h, data: downsampleRgba(
|
|
11261
|
+
if (s === 1) return full3;
|
|
11262
|
+
const w = Math.max(1, Math.round(full3.width * s));
|
|
11263
|
+
const h = Math.max(1, Math.round(full3.height * s));
|
|
11264
|
+
return { width: w, height: h, data: downsampleRgba(full3.data, full3.width, full3.height, w, h) };
|
|
10397
11265
|
}
|
|
10398
11266
|
/**
|
|
10399
11267
|
* The canvas as PNG, JPEG or WebP bytes. JPEG has no alpha, so it always includes the paper.
|
|
@@ -11596,11 +12464,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11596
12464
|
width,
|
|
11597
12465
|
height
|
|
11598
12466
|
) ?? { x: 0, y: 0, width, height };
|
|
11599
|
-
const
|
|
12467
|
+
const full3 = this.compositePixels({ wet: "display" });
|
|
11600
12468
|
const cropData = new Uint8ClampedArray(cropRect.width * cropRect.height * 4);
|
|
11601
12469
|
for (let y = 0; y < cropRect.height; y++) {
|
|
11602
12470
|
const srcRow = ((cropRect.y + y) * width + cropRect.x) * 4;
|
|
11603
|
-
cropData.set(
|
|
12471
|
+
cropData.set(full3.data.subarray(srcRow, srcRow + cropRect.width * 4), y * cropRect.width * 4);
|
|
11604
12472
|
}
|
|
11605
12473
|
const cropPngBase64 = bytesToBase64(encodePng(cropRect.width, cropRect.height, cropData));
|
|
11606
12474
|
const preview = this.renderPreview({ wet: "display", maxSide: options.maxSide ?? 512 });
|
|
@@ -12231,6 +13099,19 @@ function withLayerThick(thick, layerId, next) {
|
|
|
12231
13099
|
else delete out[layerId];
|
|
12232
13100
|
return out;
|
|
12233
13101
|
}
|
|
13102
|
+
function withLayerFerro(ferro, layerId, next) {
|
|
13103
|
+
if (!next) return ferro ? { ...ferro } : void 0;
|
|
13104
|
+
const out = { ...ferro ?? {} };
|
|
13105
|
+
if (next.tiles.size) out[layerId] = next;
|
|
13106
|
+
else delete out[layerId];
|
|
13107
|
+
return Object.keys(out).length ? out : void 0;
|
|
13108
|
+
}
|
|
13109
|
+
function withLayerFerroSettings(settings, ferro, layerId, next) {
|
|
13110
|
+
const out = { ...settings ?? {} };
|
|
13111
|
+
if (next && ferro?.[layerId]?.tiles.size) out[layerId] = { amount: next.amount, wavelength: next.wavelength, threshold: next.threshold };
|
|
13112
|
+
else if (!ferro?.[layerId]?.tiles.size) delete out[layerId];
|
|
13113
|
+
return Object.keys(out).length ? out : void 0;
|
|
13114
|
+
}
|
|
12234
13115
|
var BACKLIGHT_LAYER_ID = "__backlight";
|
|
12235
13116
|
function insertAfter(layers, id, node) {
|
|
12236
13117
|
const i = layers.findIndex((l) => l.id === id);
|
|
@@ -12272,6 +13153,7 @@ function shiftDocument(doc, dx, dy) {
|
|
|
12272
13153
|
for (const l of flattenLayers(doc.layers)) if (l.type === "pixel") layers = mapLayer(layers, l.id, () => ({ ...l, offset: movePoint(l.offset, dx, dy) }));
|
|
12273
13154
|
const m = [1, 0, 0, 1, dx, dy];
|
|
12274
13155
|
const moveStickers = (list) => list.map((s) => ({ ...s, x: Math.round((s.x + dx) * 100) / 100, y: Math.round((s.y + dy) * 100) / 100 }));
|
|
13156
|
+
const moveMagnets = (list) => list.map((mag) => ({ ...mag, x: Math.round((mag.x + dx) * 100) / 100, y: Math.round((mag.y + dy) * 100) / 100 }));
|
|
12275
13157
|
const out = {
|
|
12276
13158
|
...doc,
|
|
12277
13159
|
layers,
|
|
@@ -12287,11 +13169,14 @@ function shiftDocument(doc, dx, dy) {
|
|
|
12287
13169
|
return s.stencils ? { ...moved, stencils: s.stencils.map((x) => translateStencil(x, dx, dy)) } : moved;
|
|
12288
13170
|
};
|
|
12289
13171
|
if (doc.stickers) out.stickers = Object.fromEntries(Object.entries(doc.stickers).map(([id, list]) => [id, moveStickers(list)]));
|
|
13172
|
+
if (doc.magnets) out.magnets = moveMagnets(doc.magnets);
|
|
12290
13173
|
if (doc.liveStrokes) out.liveStrokes = Object.fromEntries(Object.entries(doc.liveStrokes).map(([id, list]) => [id, list.map(moveStroke)]));
|
|
12291
13174
|
if (doc.paintEvents) {
|
|
12292
13175
|
out.paintEvents = doc.paintEvents.map((e) => {
|
|
12293
13176
|
const list = e.kind === "stickers" ? e.data?.stickers : void 0;
|
|
12294
13177
|
if (list) return { ...e, data: { ...e.data, stickers: moveStickers(list) } };
|
|
13178
|
+
const magnets = e.kind === "magnets" ? e.data?.magnets : void 0;
|
|
13179
|
+
if (magnets) return { ...e, data: { ...e.data, magnets: moveMagnets(magnets) } };
|
|
12295
13180
|
const strokes = e.kind === "live-strokes" ? e.data?.strokes : void 0;
|
|
12296
13181
|
if (strokes) return { ...e, data: { ...e.data, strokes: strokes.map(moveStroke) } };
|
|
12297
13182
|
if (e.kind === "fill-region" && e.data) {
|
|
@@ -12647,7 +13532,7 @@ function unionRect2(a, b) {
|
|
|
12647
13532
|
|
|
12648
13533
|
// src/paint-editor/playback.ts
|
|
12649
13534
|
var WET = /^(advance_wet_paint|dry_paint|wet_paper|set_wet_settings|set_flow_map|set_canvas_tilt|pause_wet_paint|set_wet_engine|set_water_overlay)$/;
|
|
12650
|
-
var PAPER = /^(set_paper|set_lighting|flatten_relief|set_instant_dry)$/;
|
|
13535
|
+
var PAPER = /^(set_paper|set_lighting|flatten_relief|set_instant_dry|add_magnet|update_magnet|remove_magnets|cure_ferrofluid)$/;
|
|
12651
13536
|
var LAYERS = /^(add_layer|update_layer|set_active_layer|remove_layer|duplicate_layer|move_layer|merge_layer_down|clear_layer|update_sticker|arrange_stickers|remove_stickers)$/;
|
|
12652
13537
|
var PAD = /^(mixing_pad_stroke|pick_mixing_pad_color|clear_mixing_pad|mix_on_pad|save_mixing_pad|load_mixing_pad|remove_mixing_pad|mix_colors)$/;
|
|
12653
13538
|
var COLOR = /^(set_color|set_paint|color_harmony|add_swatch|remove_swatch|load_mixer_brush|clean_mixer_brush)$/;
|
|
@@ -13838,8 +14723,111 @@ function createPaintThickTools(c) {
|
|
|
13838
14723
|
];
|
|
13839
14724
|
}
|
|
13840
14725
|
|
|
13841
|
-
// src/paint-editor/
|
|
14726
|
+
// src/paint-editor/ferro-tools.ts
|
|
13842
14727
|
var READ5 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14728
|
+
var SET4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14729
|
+
var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
14730
|
+
var EMPTY3 = { type: "object", properties: {}, additionalProperties: false };
|
|
14731
|
+
var LAYER_ID4 = { type: "string", description: "Pixel layer id from list_layers (default the active layer)." };
|
|
14732
|
+
function createFerroTools(c) {
|
|
14733
|
+
return [
|
|
14734
|
+
{
|
|
14735
|
+
name: "add_magnet",
|
|
14736
|
+
title: "Place a magnet",
|
|
14737
|
+
description: "Places a dipole magnet on the canvas at (x, y) in document pixels. Liquid ferrofluid on every layer gathers toward the magnet (deterministic advection steps) and rises into hexagonal Rosensweig relief spikes along the magnetic field lines wherever |B| exceeds the layer\u2019s threshold, shaded live by set_lighting. One undo step. Returns the magnet (id, x, y, strength, radius).",
|
|
14738
|
+
inputSchema: {
|
|
14739
|
+
type: "object",
|
|
14740
|
+
properties: {
|
|
14741
|
+
x: { type: "number", description: "Magnet centre x in document pixels." },
|
|
14742
|
+
y: { type: "number", description: "Magnet centre y in document pixels." },
|
|
14743
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Dipole strength -1..1 (default 0.8; negative flips the pole)." },
|
|
14744
|
+
radius: { type: "number", minimum: 4, maximum: 2048, description: "Dipole falloff radius in pixels (default 80)." }
|
|
14745
|
+
},
|
|
14746
|
+
required: ["x", "y"],
|
|
14747
|
+
additionalProperties: false
|
|
14748
|
+
},
|
|
14749
|
+
annotations: { title: "Place a magnet", ...WRITE5 },
|
|
14750
|
+
coordinates: { in: [""], out: [""] },
|
|
14751
|
+
handler: (a) => c.addMagnet(a)
|
|
14752
|
+
},
|
|
14753
|
+
{
|
|
14754
|
+
name: "update_magnet",
|
|
14755
|
+
title: "Move or tune a magnet",
|
|
14756
|
+
description: "Moves a magnet or changes its dipole strength (-1..1) or falloff radius (4..2048 px) by id from list_magnets. Liquid ferrofluid gathers toward the updated field and the lit relief spikes update at once. One undo step. Returns the updated magnet.",
|
|
14757
|
+
inputSchema: {
|
|
14758
|
+
type: "object",
|
|
14759
|
+
properties: {
|
|
14760
|
+
id: { type: "string", description: "Magnet id from list_magnets." },
|
|
14761
|
+
x: { type: "number", description: "Magnet centre x in document pixels." },
|
|
14762
|
+
y: { type: "number", description: "Magnet centre y in document pixels." },
|
|
14763
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Dipole strength -1..1." },
|
|
14764
|
+
radius: { type: "number", minimum: 4, maximum: 2048, description: "Dipole falloff radius in pixels (4..2048)." }
|
|
14765
|
+
},
|
|
14766
|
+
required: ["id"],
|
|
14767
|
+
additionalProperties: false
|
|
14768
|
+
},
|
|
14769
|
+
annotations: { title: "Move or tune a magnet", ...SET4 },
|
|
14770
|
+
coordinates: { in: [""], out: [""] },
|
|
14771
|
+
handler: ({ id, ...u }) => c.updateMagnet(id, u)
|
|
14772
|
+
},
|
|
14773
|
+
{
|
|
14774
|
+
name: "remove_magnets",
|
|
14775
|
+
title: "Remove magnets",
|
|
14776
|
+
description: "Removes magnets by id from list_magnets, or every magnet on the canvas when ids is omitted or empty. Uncured ferrofluid relaxes as the field changes; cured relief stays. One undo step. Returns the removed magnet ids.",
|
|
14777
|
+
inputSchema: {
|
|
14778
|
+
type: "object",
|
|
14779
|
+
properties: {
|
|
14780
|
+
ids: {
|
|
14781
|
+
type: "array",
|
|
14782
|
+
items: { type: "string", description: "Magnet id from list_magnets." },
|
|
14783
|
+
description: "Magnet ids to remove (default: remove all magnets)."
|
|
14784
|
+
}
|
|
14785
|
+
},
|
|
14786
|
+
additionalProperties: false
|
|
14787
|
+
},
|
|
14788
|
+
annotations: { title: "Remove magnets", ...WRITE5 },
|
|
14789
|
+
handler: ({ ids }) => ({ removed: c.removeMagnets(ids) })
|
|
14790
|
+
},
|
|
14791
|
+
{
|
|
14792
|
+
name: "list_magnets",
|
|
14793
|
+
title: "List magnets",
|
|
14794
|
+
description: "Lists the magnets placed on the canvas in order: id, x, y (document pixels), strength (-1..1) and radius (px). Changes nothing.",
|
|
14795
|
+
inputSchema: EMPTY3,
|
|
14796
|
+
annotations: { title: "List magnets", ...READ5 },
|
|
14797
|
+
coordinates: { out: ["magnets[]"] },
|
|
14798
|
+
handler: () => ({ magnets: c.listMagnets() })
|
|
14799
|
+
},
|
|
14800
|
+
{
|
|
14801
|
+
name: "get_ferrofluid",
|
|
14802
|
+
title: "Read a layer\u2019s ferrofluid and spikes",
|
|
14803
|
+
description: "Reads a layer\u2019s liquid ferrofluid (laid by brushes with ferro.amount above 0, such as ferrofluid and ferro-ink) and its live Rosensweig spikes under the current magnets: whether it has any (ferro), its bounds in document pixels, peak liquid level (0..255), liquid coverage in pixels, activeSpikeArea (pixels currently raised into spikes), maxSpikeHeight (0..255), magnetCount, and the layer\u2019s capillary wavelength and threshold. Changes nothing.",
|
|
14804
|
+
inputSchema: {
|
|
14805
|
+
type: "object",
|
|
14806
|
+
properties: { layerId: LAYER_ID4 },
|
|
14807
|
+
additionalProperties: false
|
|
14808
|
+
},
|
|
14809
|
+
annotations: { title: "Read a layer\u2019s ferrofluid and spikes", ...READ5 },
|
|
14810
|
+
coordinates: { out: ["bounds"] },
|
|
14811
|
+
handler: ({ layerId }) => c.getFerrofluid(layerId)
|
|
14812
|
+
},
|
|
14813
|
+
{
|
|
14814
|
+
name: "cure_ferrofluid",
|
|
14815
|
+
title: "Cure ferrofluid into solid relief",
|
|
14816
|
+
description: "Cures a layer\u2019s liquid ferrofluid: bakes its current magnetic Rosensweig spikes into the layer\u2019s solid impasto height and dark glossy metallic material, and clears the liquid ferrofluid channel so moving or removing magnets no longer changes this layer. One undo step. Returns the layer id, whether anything was cured, the cured bounds, maxSpikeHeight and activeSpikeArea.",
|
|
14817
|
+
inputSchema: {
|
|
14818
|
+
type: "object",
|
|
14819
|
+
properties: { layerId: LAYER_ID4 },
|
|
14820
|
+
additionalProperties: false
|
|
14821
|
+
},
|
|
14822
|
+
annotations: { title: "Cure ferrofluid into solid relief", ...WRITE5 },
|
|
14823
|
+
coordinates: { out: ["bounds"] },
|
|
14824
|
+
handler: ({ layerId }) => c.cureFerrofluid(layerId)
|
|
14825
|
+
}
|
|
14826
|
+
];
|
|
14827
|
+
}
|
|
14828
|
+
|
|
14829
|
+
// src/paint-editor/replay-tools.ts
|
|
14830
|
+
var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13843
14831
|
function createPaintReplayTools(c) {
|
|
13844
14832
|
return [
|
|
13845
14833
|
{
|
|
@@ -13858,7 +14846,7 @@ function createPaintReplayTools(c) {
|
|
|
13858
14846
|
},
|
|
13859
14847
|
additionalProperties: false
|
|
13860
14848
|
},
|
|
13861
|
-
annotations: { title: "Export a time-lapse", ...
|
|
14849
|
+
annotations: { title: "Export a time-lapse", ...READ6 },
|
|
13862
14850
|
handler: async (a) => {
|
|
13863
14851
|
await c.wetIdle();
|
|
13864
14852
|
const tl = await c.timeLapse({ frames: a.frames, fps: a.fps, maxSide: a.maxSide, wetStepsPerFrame: a.wetStepsPerFrame, holdMs: a.holdMs });
|
|
@@ -13890,7 +14878,7 @@ function createPaintReplayTools(c) {
|
|
|
13890
14878
|
required: ["scale"],
|
|
13891
14879
|
additionalProperties: false
|
|
13892
14880
|
},
|
|
13893
|
-
annotations: { title: "Export at a higher resolution", ...
|
|
14881
|
+
annotations: { title: "Export at a higher resolution", ...READ6 },
|
|
13894
14882
|
handler: async ({ scale, paper }) => {
|
|
13895
14883
|
await c.wetIdle();
|
|
13896
14884
|
const img = await c.renderHighRes({ scale, paper });
|
|
@@ -13902,9 +14890,9 @@ function createPaintReplayTools(c) {
|
|
|
13902
14890
|
}
|
|
13903
14891
|
|
|
13904
14892
|
// src/paint-editor/mutation-tools.ts
|
|
13905
|
-
var
|
|
13906
|
-
var
|
|
13907
|
-
var
|
|
14893
|
+
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14894
|
+
var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
14895
|
+
var SET5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13908
14896
|
var POINTS = {
|
|
13909
14897
|
type: "array",
|
|
13910
14898
|
minItems: 1,
|
|
@@ -13943,7 +14931,7 @@ function createMutationTools(c, options = {}) {
|
|
|
13943
14931
|
},
|
|
13944
14932
|
additionalProperties: false
|
|
13945
14933
|
},
|
|
13946
|
-
annotations: { title: "List pending mutations", ...
|
|
14934
|
+
annotations: { title: "List pending mutations", ...READ7 },
|
|
13947
14935
|
coordinates: { out: ["mutations[].bounds"] },
|
|
13948
14936
|
handler: (a) => ({ mutations: c.listMutations({ includeAll: Boolean(a.includeAll) }) })
|
|
13949
14937
|
},
|
|
@@ -13974,7 +14962,7 @@ function createMutationTools(c, options = {}) {
|
|
|
13974
14962
|
required: ["id"],
|
|
13975
14963
|
additionalProperties: false
|
|
13976
14964
|
},
|
|
13977
|
-
annotations: { title: "Read and claim a mutation", ...
|
|
14965
|
+
annotations: { title: "Read and claim a mutation", ...WRITE6 },
|
|
13978
14966
|
coordinates: { out: ["bounds", "strokes[].bounds", "strokes[].points[]", "cropPng.rect"] },
|
|
13979
14967
|
handler: (a) => c.readMutation(a.id, {
|
|
13980
14968
|
actor: options.actor?.id,
|
|
@@ -14051,7 +15039,7 @@ function createMutationTools(c, options = {}) {
|
|
|
14051
15039
|
required: ["id", "strokes"],
|
|
14052
15040
|
additionalProperties: false
|
|
14053
15041
|
},
|
|
14054
|
-
annotations: { title: "Answer a mutation with strokes", ...
|
|
15042
|
+
annotations: { title: "Answer a mutation with strokes", ...WRITE6 },
|
|
14055
15043
|
coordinates: { in: ["strokes[].points[]"], out: ["bounds"] },
|
|
14056
15044
|
handler: async (a) => {
|
|
14057
15045
|
await c.wetIdle();
|
|
@@ -14105,7 +15093,7 @@ function createMutationTools(c, options = {}) {
|
|
|
14105
15093
|
required: ["id"],
|
|
14106
15094
|
additionalProperties: false
|
|
14107
15095
|
},
|
|
14108
|
-
annotations: { title: "Decline a mutation", ...
|
|
15096
|
+
annotations: { title: "Decline a mutation", ...SET5 },
|
|
14109
15097
|
coordinates: { out: ["bounds"] },
|
|
14110
15098
|
handler: (a) => c.declineMutation(a.id, a.reason)
|
|
14111
15099
|
}
|
|
@@ -14113,8 +15101,8 @@ function createMutationTools(c, options = {}) {
|
|
|
14113
15101
|
}
|
|
14114
15102
|
|
|
14115
15103
|
// src/paint-editor/evolution-tools.ts
|
|
14116
|
-
var
|
|
14117
|
-
var
|
|
15104
|
+
var READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15105
|
+
var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
14118
15106
|
var UNIT2 = (description) => ({ type: "number", minimum: 0, maximum: 1, description });
|
|
14119
15107
|
var EVOLUTION_MUTATION_SCHEMA = {
|
|
14120
15108
|
type: "object",
|
|
@@ -14159,7 +15147,7 @@ function createPaintEvolutionTools(c) {
|
|
|
14159
15147
|
required: ["strokeIds"],
|
|
14160
15148
|
additionalProperties: false
|
|
14161
15149
|
},
|
|
14162
|
-
annotations: { title: "Breed stroke variants", ...
|
|
15150
|
+
annotations: { title: "Breed stroke variants", ...WRITE7 },
|
|
14163
15151
|
coordinates: { out: ["proposals[].bounds"] },
|
|
14164
15152
|
handler: ({ strokeIds, count, mutation, seed }, context) => {
|
|
14165
15153
|
const actor = context?.actor?.id;
|
|
@@ -14202,7 +15190,7 @@ function createPaintEvolutionTools(c) {
|
|
|
14202
15190
|
required: ["proposalIds"],
|
|
14203
15191
|
additionalProperties: false
|
|
14204
15192
|
},
|
|
14205
|
-
annotations: { title: "Breed from paint proposals", ...
|
|
15193
|
+
annotations: { title: "Breed from paint proposals", ...WRITE7 },
|
|
14206
15194
|
coordinates: { out: ["proposals[].bounds"] },
|
|
14207
15195
|
handler: ({ proposalIds, count, mutation, seed }, context) => {
|
|
14208
15196
|
const actor = context?.actor?.id;
|
|
@@ -14230,7 +15218,7 @@ function createPaintEvolutionTools(c) {
|
|
|
14230
15218
|
required: ["proposalId"],
|
|
14231
15219
|
additionalProperties: false
|
|
14232
15220
|
},
|
|
14233
|
-
annotations: { title: "Get a proposal\u2019s stroke genome", ...
|
|
15221
|
+
annotations: { title: "Get a proposal\u2019s stroke genome", ...READ8 },
|
|
14234
15222
|
coordinates: { out: ["bounds", "strokes[].points[]"] },
|
|
14235
15223
|
handler: ({ proposalId }) => c.getProposalGenome(proposalId)
|
|
14236
15224
|
}
|
|
@@ -14249,13 +15237,13 @@ function createPaintEvolutionToolProvider(controller, options = {}) {
|
|
|
14249
15237
|
}
|
|
14250
15238
|
|
|
14251
15239
|
// src/paint-editor/tools.ts
|
|
14252
|
-
var
|
|
14253
|
-
var
|
|
14254
|
-
var
|
|
15240
|
+
var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15241
|
+
var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
15242
|
+
var SET6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14255
15243
|
var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
14256
|
-
var
|
|
15244
|
+
var EMPTY4 = { type: "object", properties: {}, additionalProperties: false };
|
|
14257
15245
|
var COLOR3 = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
|
|
14258
|
-
var
|
|
15246
|
+
var LAYER_ID5 = { type: "string", description: "Layer id from list_layers (default the active layer)." };
|
|
14259
15247
|
var TIME = { type: "number", minimum: 0, description: "Draw live effect paint (strokes on stroke layers painted with live effect paint) this many ms into its animation (default: as painted, its own time)." };
|
|
14260
15248
|
var POINTS2 = {
|
|
14261
15249
|
type: "array",
|
|
@@ -14298,8 +15286,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14298
15286
|
name: "get_canvas_info",
|
|
14299
15287
|
title: "Get the canvas",
|
|
14300
15288
|
description: "Reads the canvas: size in pixels and dpi, its origin (where (0, 0) is relative to the canvas it was extended from), bounds (the rectangle the painting covers, in the coordinates every other tool takes), the paper (preset, colour, grain depth), the active layer, the active brush id, the paint colour, the view (zoom, rotation, mirror), the hand, how many strokes are recorded, the undo / redo labels, and the blend modes. Call it first.",
|
|
14301
|
-
inputSchema:
|
|
14302
|
-
annotations: { title: "Get the canvas", ...
|
|
15289
|
+
inputSchema: EMPTY4,
|
|
15290
|
+
annotations: { title: "Get the canvas", ...READ9 },
|
|
14303
15291
|
coordinates: { out: ["bounds"] },
|
|
14304
15292
|
handler: () => {
|
|
14305
15293
|
const s = c.getState();
|
|
@@ -14360,7 +15348,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14360
15348
|
},
|
|
14361
15349
|
additionalProperties: false
|
|
14362
15350
|
},
|
|
14363
|
-
annotations: { title: "Resize the canvas", ...
|
|
15351
|
+
annotations: { title: "Resize the canvas", ...SET6 },
|
|
14364
15352
|
handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
|
|
14365
15353
|
});
|
|
14366
15354
|
const EXTEND_SIDE = (side) => ({ type: "integer", minimum: 0, maximum: MAX_CANVAS_SIDE - 1, description: `Pixels to add on the ${side} (default 0).` });
|
|
@@ -14373,15 +15361,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14373
15361
|
properties: { left: EXTEND_SIDE("left"), top: EXTEND_SIDE("top"), right: EXTEND_SIDE("right"), bottom: EXTEND_SIDE("bottom") },
|
|
14374
15362
|
additionalProperties: false
|
|
14375
15363
|
},
|
|
14376
|
-
annotations: { title: "Extend the canvas", ...
|
|
15364
|
+
annotations: { title: "Extend the canvas", ...WRITE8 },
|
|
14377
15365
|
handler: (a) => c.extendCanvas({ left: a.left, top: a.top, right: a.right, bottom: a.bottom })
|
|
14378
15366
|
});
|
|
14379
15367
|
add({
|
|
14380
15368
|
name: "list_papers",
|
|
14381
15369
|
title: "List papers",
|
|
14382
15370
|
description: "Lists the built-in paper library (generated grain, no textures): id, name, family, grain pattern, colour, grain depth and the wet-media properties (absorption, permeability, roughness). Use an id with set_paper or new_canvas.",
|
|
14383
|
-
inputSchema:
|
|
14384
|
-
annotations: { title: "List papers", ...
|
|
15371
|
+
inputSchema: EMPTY4,
|
|
15372
|
+
annotations: { title: "List papers", ...READ9 },
|
|
14385
15373
|
handler: () => ({
|
|
14386
15374
|
papers: PAPER_LIBRARY.map((p) => ({ id: p.preset, name: p.name, family: p.family, pattern: p.pattern, color: p.color, depth: p.depth, absorption: p.absorption, permeability: p.permeability, roughness: p.roughness })),
|
|
14387
15375
|
families: [...PAPER_FAMILIES],
|
|
@@ -14392,8 +15380,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14392
15380
|
name: "get_paper",
|
|
14393
15381
|
title: "Get the paper",
|
|
14394
15382
|
description: "Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness, and metallic, backlight and backlightColor when set).",
|
|
14395
|
-
inputSchema:
|
|
14396
|
-
annotations: { title: "Get the paper", ...
|
|
15383
|
+
inputSchema: EMPTY4,
|
|
15384
|
+
annotations: { title: "Get the paper", ...READ9 },
|
|
14397
15385
|
handler: () => JSON.parse(JSON.stringify(c.document.paper))
|
|
14398
15386
|
});
|
|
14399
15387
|
const tuning = {
|
|
@@ -14431,15 +15419,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14431
15419
|
},
|
|
14432
15420
|
additionalProperties: false
|
|
14433
15421
|
},
|
|
14434
|
-
annotations: { title: "Set the paper", ...
|
|
15422
|
+
annotations: { title: "Set the paper", ...SET6 },
|
|
14435
15423
|
handler: (a) => c.setPaper(a)
|
|
14436
15424
|
});
|
|
14437
15425
|
add({
|
|
14438
15426
|
name: "list_layers",
|
|
14439
15427
|
title: "List layers",
|
|
14440
15428
|
description: "Lists the layers top first: id, name, type (pixel or group), visibility, opacity, blend, clipping, locks, whether it is active, the painted bounds in document pixels (null when empty), parent and depth.",
|
|
14441
|
-
inputSchema:
|
|
14442
|
-
annotations: { title: "List layers", ...
|
|
15429
|
+
inputSchema: EMPTY4,
|
|
15430
|
+
annotations: { title: "List layers", ...READ9 },
|
|
14443
15431
|
coordinates: { out: ["layers[].bounds"] },
|
|
14444
15432
|
handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId })
|
|
14445
15433
|
});
|
|
@@ -14457,7 +15445,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14457
15445
|
},
|
|
14458
15446
|
additionalProperties: false
|
|
14459
15447
|
},
|
|
14460
|
-
annotations: { title: "Add a layer", ...
|
|
15448
|
+
annotations: { title: "Add a layer", ...WRITE8 },
|
|
14461
15449
|
coordinates: { out: ["bounds"] },
|
|
14462
15450
|
handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId })
|
|
14463
15451
|
});
|
|
@@ -14493,7 +15481,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14493
15481
|
required: ["layerId"],
|
|
14494
15482
|
additionalProperties: false
|
|
14495
15483
|
},
|
|
14496
|
-
annotations: { title: "Change a layer", ...
|
|
15484
|
+
annotations: { title: "Change a layer", ...SET6 },
|
|
14497
15485
|
coordinates: { out: ["bounds"] },
|
|
14498
15486
|
handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
|
|
14499
15487
|
});
|
|
@@ -14501,8 +15489,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14501
15489
|
name: "list_blend_modes",
|
|
14502
15490
|
title: "List blend modes",
|
|
14503
15491
|
description: "Lists every layer and brush blend mode: id, label, group (standard Photoshop modes, pigment, or painterly), what the pigment and painterly modes do, and the standard mode PSD export writes in their place. Changes nothing. Read it before choosing a painterly mode for update_layer or a brush tip.",
|
|
14504
|
-
inputSchema:
|
|
14505
|
-
annotations: { title: "List blend modes", ...
|
|
15492
|
+
inputSchema: EMPTY4,
|
|
15493
|
+
annotations: { title: "List blend modes", ...READ9 },
|
|
14506
15494
|
handler: () => ({
|
|
14507
15495
|
modes: BLEND_MODES.map((m) => ({
|
|
14508
15496
|
id: m.id,
|
|
@@ -14518,7 +15506,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14518
15506
|
title: "Choose the active layer",
|
|
14519
15507
|
description: "Makes a layer active: strokes, fills and clears without a layerId go to it. Editor state, not an undo step. Returns the layer.",
|
|
14520
15508
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
14521
|
-
annotations: { title: "Choose the active layer", ...
|
|
15509
|
+
annotations: { title: "Choose the active layer", ...SET6 },
|
|
14522
15510
|
coordinates: { out: ["bounds"] },
|
|
14523
15511
|
handler: ({ layerId }) => {
|
|
14524
15512
|
c.setActiveLayer(layerId);
|
|
@@ -14538,7 +15526,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14538
15526
|
title: "Duplicate a layer",
|
|
14539
15527
|
description: "Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.",
|
|
14540
15528
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
14541
|
-
annotations: { title: "Duplicate a layer", ...
|
|
15529
|
+
annotations: { title: "Duplicate a layer", ...WRITE8 },
|
|
14542
15530
|
coordinates: { out: ["bounds"] },
|
|
14543
15531
|
handler: ({ layerId }) => c.duplicateLayer(layerId)
|
|
14544
15532
|
});
|
|
@@ -14556,7 +15544,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14556
15544
|
required: ["layerId", "index"],
|
|
14557
15545
|
additionalProperties: false
|
|
14558
15546
|
},
|
|
14559
|
-
annotations: { title: "Reorder a layer", ...
|
|
15547
|
+
annotations: { title: "Reorder a layer", ...WRITE8 },
|
|
14560
15548
|
coordinates: { out: ["bounds"] },
|
|
14561
15549
|
handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId)
|
|
14562
15550
|
});
|
|
@@ -14576,7 +15564,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14576
15564
|
inputSchema: {
|
|
14577
15565
|
type: "object",
|
|
14578
15566
|
properties: {
|
|
14579
|
-
layerId:
|
|
15567
|
+
layerId: LAYER_ID5,
|
|
14580
15568
|
color: { ...COLOR3, description: "Fill colour #rrggbb (default the paint colour)." },
|
|
14581
15569
|
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
|
|
14582
15570
|
grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
|
|
@@ -14585,7 +15573,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14585
15573
|
},
|
|
14586
15574
|
additionalProperties: false
|
|
14587
15575
|
},
|
|
14588
|
-
annotations: { title: "Fill a layer", ...
|
|
15576
|
+
annotations: { title: "Fill a layer", ...WRITE8 },
|
|
14589
15577
|
coordinates: { out: ["bounds"] },
|
|
14590
15578
|
handler: (a) => act({ kind: "fill", fill: a }, () => c.fillLayer(a))
|
|
14591
15579
|
});
|
|
@@ -14613,7 +15601,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14613
15601
|
additionalProperties: false
|
|
14614
15602
|
}
|
|
14615
15603
|
},
|
|
14616
|
-
layerId:
|
|
15604
|
+
layerId: LAYER_ID5,
|
|
14617
15605
|
reference: {
|
|
14618
15606
|
type: "string",
|
|
14619
15607
|
description: 'Which pixels stop the fill: "layer" (default, the target layer itself), "all" or "composite" (all visible layers without paper), or a layer id from list_layers (e.g. an ink line-art layer above the flats layer).'
|
|
@@ -14632,7 +15620,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14632
15620
|
},
|
|
14633
15621
|
additionalProperties: false
|
|
14634
15622
|
},
|
|
14635
|
-
annotations: { title: "Fill a region (bucket / flats)", ...
|
|
15623
|
+
annotations: { title: "Fill a region (bucket / flats)", ...WRITE8 },
|
|
14636
15624
|
coordinates: { in: ["", "points[]"], out: ["bounds", "regions[].point", "regions[].bounds"] },
|
|
14637
15625
|
handler: (a) => act({ kind: "fill-region", fill: a }, () => c.fillRegion(a))
|
|
14638
15626
|
});
|
|
@@ -14656,7 +15644,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14656
15644
|
},
|
|
14657
15645
|
additionalProperties: false
|
|
14658
15646
|
},
|
|
14659
|
-
annotations: { title: "Find enclosed unfilled regions", ...
|
|
15647
|
+
annotations: { title: "Find enclosed unfilled regions", ...READ9 },
|
|
14660
15648
|
coordinates: { out: ["regions[].point", "regions[].centroid", "regions[].bounds"] },
|
|
14661
15649
|
handler: (a) => c.findUnfilledRegions(a)
|
|
14662
15650
|
});
|
|
@@ -14664,7 +15652,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14664
15652
|
name: "clear_layer",
|
|
14665
15653
|
title: "Clear a layer",
|
|
14666
15654
|
description: "Removes every pixel of a pixel layer. One undo step. Returns the layer id and the bounds that were cleared (null if it was empty).",
|
|
14667
|
-
inputSchema: { type: "object", properties: { layerId:
|
|
15655
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, additionalProperties: false },
|
|
14668
15656
|
annotations: { title: "Clear a layer", ...DESTRUCTIVE3 },
|
|
14669
15657
|
coordinates: { out: ["bounds"] },
|
|
14670
15658
|
handler: ({ layerId }) => c.clearLayer(layerId)
|
|
@@ -14674,7 +15662,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14674
15662
|
title: "Get a brush",
|
|
14675
15663
|
description: "Reads a brush as facets (kind, medium, variety or tool, tip, marks, dynamics, behaviour, light, motion, colour: the brush library's form, as describe_brush returns it), with the engine it paints with and whether it is an unchanged built-in preset. Without id, the active brush as edited. Also returns strokePaint: what a stroke with it would paint now (its gradient colour, else the editor's paint from set_paint). Changes nothing.",
|
|
14676
15664
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." } }, additionalProperties: false },
|
|
14677
|
-
annotations: { title: "Get a brush", ...
|
|
15665
|
+
annotations: { title: "Get a brush", ...READ9 },
|
|
14678
15666
|
handler: ({ id }) => {
|
|
14679
15667
|
const st = c.getState();
|
|
14680
15668
|
const b = id === void 0 ? st.brush : c.getBrush(id);
|
|
@@ -14688,7 +15676,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14688
15676
|
title: "Set the brush",
|
|
14689
15677
|
description: "Chooses the active brush (id, from list_brush_presets) and / or changes it with facets laid over it (objects merge, lists and values replace, marks of another kind replace the marks; the brush library's create_brush fields); unset removes whole facets first (unset colour drops a brush's own gradient). Strokes without a brush use it. Editor state, not an undo step: the changes stay on the active brush until the library's create_brush or update_brush stores a brush. Returns the active brush as facets, the engine it paints with, and problems (values clamped or dropped).",
|
|
14690
15678
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brush_presets to start from (default the active brush)." }, name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." }, ...BRUSH_PATCH_PROPERTIES, unset: BRUSH_UNSET_PROPERTY }, additionalProperties: false },
|
|
14691
|
-
annotations: { title: "Set the brush", ...
|
|
15679
|
+
annotations: { title: "Set the brush", ...SET6 },
|
|
14692
15680
|
handler: async ({ id, unset, ...patch }) => {
|
|
14693
15681
|
const stamps = patch.tip?.stamps;
|
|
14694
15682
|
if (stamps) await c.prepareStickers(stamps);
|
|
@@ -14709,7 +15697,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14709
15697
|
},
|
|
14710
15698
|
additionalProperties: false
|
|
14711
15699
|
},
|
|
14712
|
-
annotations: { title: "Set the paint colour", ...
|
|
15700
|
+
annotations: { title: "Set the paint colour", ...SET6 },
|
|
14713
15701
|
handler: ({ color, secondaryColor, locked }) => {
|
|
14714
15702
|
if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
|
|
14715
15703
|
if (color !== void 0) c.setColor(color);
|
|
@@ -14724,7 +15712,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14724
15712
|
title: "Set the paint (colour or gradient)",
|
|
14725
15713
|
description: 'Sets what strokes paint with: one colour, or gradient paint that changes colour as the stroke goes. Gradient paint: 2..16 stops sorted by offset (#rrggbb, or "paint" / "secondary" for the two editor colours), space pigment (default: mixes like paint) / oklab / srgb, and map: distance (default: the colour changes as the stroke is dragged out, 300 px then mirrored back), pressure, tilt, velocity, direction, time, across-tip or random. Every stroking brush samples it per dab (dab, mixer, watercolour and gouache, oil and impasto, particle, bristle, sticker); erasers, blenders and smudges ignore it, and a brush with its own gradient (get_brush paint) paints that instead. It also ends effect paint (set_effect_paint); set_color goes back to one colour. Editor state, not an undo step. Returns paint, color and secondaryColor.',
|
|
14726
15714
|
inputSchema: { type: "object", properties: { paint: PAINT_SOURCE_JSON_SCHEMA }, required: ["paint"], additionalProperties: false },
|
|
14727
|
-
annotations: { title: "Set the paint (colour or gradient)", ...
|
|
15715
|
+
annotations: { title: "Set the paint (colour or gradient)", ...SET6 },
|
|
14728
15716
|
handler: ({ paint }) => {
|
|
14729
15717
|
const p = c.setPaint(paint);
|
|
14730
15718
|
const s = c.getState();
|
|
@@ -14745,7 +15733,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14745
15733
|
required: ["x", "y"],
|
|
14746
15734
|
additionalProperties: false
|
|
14747
15735
|
},
|
|
14748
|
-
annotations: { title: "Pick a colour", ...
|
|
15736
|
+
annotations: { title: "Pick a colour", ...READ9 },
|
|
14749
15737
|
coordinates: { in: [""] },
|
|
14750
15738
|
handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId })
|
|
14751
15739
|
});
|
|
@@ -14753,8 +15741,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14753
15741
|
name: "get_mixer_brush",
|
|
14754
15742
|
title: "Get the Mixer Brush load",
|
|
14755
15743
|
description: "Reads what the brush carries between strokes: reservoir #rrggbb or null (the Mixer Brush's loaded paint), amount 0..1 left, pickup #rrggbb or null (paint picked up from the canvas, kept by dirty brushes and a Mixer Brush that does not clean each stroke). Call before painting with mixer-brush when its loadEachStroke is false, or with a dirty brush.",
|
|
14756
|
-
inputSchema:
|
|
14757
|
-
annotations: { title: "Get the Mixer Brush load", ...
|
|
15744
|
+
inputSchema: EMPTY4,
|
|
15745
|
+
annotations: { title: "Get the Mixer Brush load", ...READ9 },
|
|
14758
15746
|
handler: () => loadResult(c.getState().mixerLoad)
|
|
14759
15747
|
});
|
|
14760
15748
|
add({
|
|
@@ -14762,7 +15750,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14762
15750
|
title: "Load the Mixer Brush",
|
|
14763
15751
|
description: "Fills the Mixer Brush reservoir with a colour (amount 1), keeping what it picked up. Editor state, not an undo step. Returns the load.",
|
|
14764
15752
|
inputSchema: { type: "object", properties: { color: { ...COLOR3, description: "Colour #rrggbb (default the paint colour)." } }, additionalProperties: false },
|
|
14765
|
-
annotations: { title: "Load the Mixer Brush", ...
|
|
15753
|
+
annotations: { title: "Load the Mixer Brush", ...SET6 },
|
|
14766
15754
|
handler: ({ color }) => loadResult(c.loadMixerBrush(color))
|
|
14767
15755
|
});
|
|
14768
15756
|
add({
|
|
@@ -14770,7 +15758,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14770
15758
|
title: "Clean the Mixer Brush",
|
|
14771
15759
|
description: "Cleans the brush: empties the picked-up paint a dirty brush or the Mixer Brush carries (and with emptyReservoir the Mixer Brush paint too). Editor state, not an undo step. Returns the load.",
|
|
14772
15760
|
inputSchema: { type: "object", properties: { emptyReservoir: { type: "boolean", description: "Also drop the loaded paint (default false)." } }, additionalProperties: false },
|
|
14773
|
-
annotations: { title: "Clean the Mixer Brush", ...
|
|
15761
|
+
annotations: { title: "Clean the Mixer Brush", ...SET6 },
|
|
14774
15762
|
handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
|
|
14775
15763
|
});
|
|
14776
15764
|
add({
|
|
@@ -14781,7 +15769,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14781
15769
|
type: "object",
|
|
14782
15770
|
properties: {
|
|
14783
15771
|
points: POINTS2,
|
|
14784
|
-
layerId:
|
|
15772
|
+
layerId: LAYER_ID5,
|
|
14785
15773
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
14786
15774
|
brush: { type: "object", description: "Facets to change for this stroke only, laid over brushId or the active brush (as set_brush takes them).", properties: BRUSH_PATCH_PROPERTIES, additionalProperties: false },
|
|
14787
15775
|
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
@@ -14792,7 +15780,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14792
15780
|
required: ["points"],
|
|
14793
15781
|
additionalProperties: false
|
|
14794
15782
|
},
|
|
14795
|
-
annotations: { title: "Paint a brush stroke", ...
|
|
15783
|
+
annotations: { title: "Paint a brush stroke", ...WRITE8 },
|
|
14796
15784
|
coordinates: { in: ["points[]"], out: ["bounds"] },
|
|
14797
15785
|
handler: async (a) => {
|
|
14798
15786
|
await c.wetIdle();
|
|
@@ -14819,7 +15807,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14819
15807
|
spacing: { type: "number", minimum: 0.25, maximum: 100, description: "Distance between samples along the path in document pixels (default 2); the brush spaces its own dabs, so this only sets how closely curves are followed." },
|
|
14820
15808
|
pressure: { type: "array", minItems: 1, maxItems: 64, items: { type: "number", minimum: 0, maximum: 1 }, description: "Pen pressure along each subpath, spread evenly from start to end (default full pressure); [0.2, 1, 0.2] tapers both ends." },
|
|
14821
15809
|
speed: { type: "number", minimum: 0.01, maximum: 100, description: "Drawing speed in document pixels per millisecond (optional; gives each sample a time t so playback has a pace and speed-sensitive brushes respond)." },
|
|
14822
|
-
layerId:
|
|
15810
|
+
layerId: LAYER_ID5,
|
|
14823
15811
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
14824
15812
|
brush: { type: "object", description: "Facets to change for this stroke only, laid over brushId or the active brush (as set_brush takes them).", properties: BRUSH_PATCH_PROPERTIES, additionalProperties: false },
|
|
14825
15813
|
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
@@ -14829,7 +15817,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14829
15817
|
required: ["path"],
|
|
14830
15818
|
additionalProperties: false
|
|
14831
15819
|
},
|
|
14832
|
-
annotations: { title: "Brush along a path", ...
|
|
15820
|
+
annotations: { title: "Brush along a path", ...WRITE8 },
|
|
14833
15821
|
coordinates: { out: ["bounds"] },
|
|
14834
15822
|
handler: async (a) => {
|
|
14835
15823
|
await c.wetIdle();
|
|
@@ -14883,7 +15871,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14883
15871
|
properties: { limit: { type: "integer", minimum: 1, maximum: 5e3, description: "Most recent samples to return (default 500)." } },
|
|
14884
15872
|
additionalProperties: false
|
|
14885
15873
|
},
|
|
14886
|
-
annotations: { title: "Get the stroke being painted", ...
|
|
15874
|
+
annotations: { title: "Get the stroke being painted", ...READ9 },
|
|
14887
15875
|
handler: (args) => {
|
|
14888
15876
|
const live = c.getLiveStroke();
|
|
14889
15877
|
if (!live) return { live: false };
|
|
@@ -14905,7 +15893,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14905
15893
|
},
|
|
14906
15894
|
additionalProperties: false
|
|
14907
15895
|
},
|
|
14908
|
-
annotations: { title: "List recorded strokes", ...
|
|
15896
|
+
annotations: { title: "List recorded strokes", ...READ9 },
|
|
14909
15897
|
coordinates: { out: ["strokes[].bounds", "strokes[].points[]"] },
|
|
14910
15898
|
handler: ({ ids, last, live, layerId }) => {
|
|
14911
15899
|
if (layerId !== void 0 && !c.getLayer(layerId)) throw new Error(`Unknown layer "${layerId}". Call list_layers for the ids.`);
|
|
@@ -14934,7 +15922,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14934
15922
|
required: ["x", "y"],
|
|
14935
15923
|
additionalProperties: false
|
|
14936
15924
|
},
|
|
14937
|
-
annotations: { title: "Find the stroke at a point", ...
|
|
15925
|
+
annotations: { title: "Find the stroke at a point", ...READ9 },
|
|
14938
15926
|
coordinates: { in: [""], out: ["stroke.bounds"] },
|
|
14939
15927
|
handler: ({ layerId, x, y }) => {
|
|
14940
15928
|
const s = c.liveStrokeAt(layerId, x, y);
|
|
@@ -14958,7 +15946,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14958
15946
|
required: ["id"],
|
|
14959
15947
|
additionalProperties: false
|
|
14960
15948
|
},
|
|
14961
|
-
annotations: { title: "Change a stroke", ...
|
|
15949
|
+
annotations: { title: "Change a stroke", ...WRITE8 },
|
|
14962
15950
|
coordinates: { out: ["bounds", "stroke.bounds"] },
|
|
14963
15951
|
handler: ({ id, ...update }) => {
|
|
14964
15952
|
if (!Object.keys(update).length) throw new Error("Pass at least one change: brushId, brush, color, paint or seed.");
|
|
@@ -14997,7 +15985,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14997
15985
|
required: ["id"],
|
|
14998
15986
|
additionalProperties: false
|
|
14999
15987
|
},
|
|
15000
|
-
annotations: { title: "Move or reshape a stroke", ...
|
|
15988
|
+
annotations: { title: "Move or reshape a stroke", ...WRITE8 },
|
|
15001
15989
|
coordinates: { in: ["points[]", "transform.origin"], out: ["bounds", "stroke.bounds", "stroke.points[]"] },
|
|
15002
15990
|
handler: async ({ id, points, transform }) => {
|
|
15003
15991
|
await c.prepareStrokeEdit(id, { points, transform });
|
|
@@ -15015,7 +16003,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15015
16003
|
required: ["id", "index"],
|
|
15016
16004
|
additionalProperties: false
|
|
15017
16005
|
},
|
|
15018
|
-
annotations: { title: "Move a stroke up or down", ...
|
|
16006
|
+
annotations: { title: "Move a stroke up or down", ...WRITE8 },
|
|
15019
16007
|
handler: ({ id, index }) => ({ order: c.reorderStroke(id, index) })
|
|
15020
16008
|
});
|
|
15021
16009
|
add({
|
|
@@ -15037,7 +16025,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15037
16025
|
title: "Make a layer a stroke layer",
|
|
15038
16026
|
description: 'Makes an empty pixel layer a stroke layer, so strokes painted on it stay editable. Refuses a layer with paint (add_layer {type: "strokes"} instead). One undo step. Returns the layer.',
|
|
15039
16027
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers (default the active layer)." } }, additionalProperties: false },
|
|
15040
|
-
annotations: { title: "Make a layer a stroke layer", ...
|
|
16028
|
+
annotations: { title: "Make a layer a stroke layer", ...WRITE8 },
|
|
15041
16029
|
coordinates: { out: ["bounds"] },
|
|
15042
16030
|
handler: ({ layerId }) => c.convertToLiveStrokes(layerId)
|
|
15043
16031
|
});
|
|
@@ -15079,7 +16067,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15079
16067
|
required: ["strokeIds"],
|
|
15080
16068
|
additionalProperties: false
|
|
15081
16069
|
},
|
|
15082
|
-
annotations: { title: "Replay strokes, varied", ...
|
|
16070
|
+
annotations: { title: "Replay strokes, varied", ...WRITE8 },
|
|
15083
16071
|
coordinates: { in: ["origin"], out: ["bounds"] },
|
|
15084
16072
|
handler: async (a) => {
|
|
15085
16073
|
const replay = a;
|
|
@@ -15101,7 +16089,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15101
16089
|
},
|
|
15102
16090
|
additionalProperties: false
|
|
15103
16091
|
},
|
|
15104
|
-
annotations: { title: "Rotate, mirror or zoom the view", ...
|
|
16092
|
+
annotations: { title: "Rotate, mirror or zoom the view", ...SET6 },
|
|
15105
16093
|
handler: ({ reset, ...v }) => {
|
|
15106
16094
|
if (reset) c.resetView();
|
|
15107
16095
|
return c.setView(v);
|
|
@@ -15112,15 +16100,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15112
16100
|
title: "Set the drawing hand",
|
|
15113
16101
|
description: "Says which hand holds the pen; palm rejection ignores touches on that side of the pen. Editor state, not an undo step. Returns the hand.",
|
|
15114
16102
|
inputSchema: { type: "object", properties: { hand: { type: "string", enum: ["right", "left"], description: "right or left." } }, required: ["hand"], additionalProperties: false },
|
|
15115
|
-
annotations: { title: "Set the drawing hand", ...
|
|
16103
|
+
annotations: { title: "Set the drawing hand", ...SET6 },
|
|
15116
16104
|
handler: ({ hand }) => ({ hand: c.setHand(hand) })
|
|
15117
16105
|
});
|
|
15118
16106
|
add({
|
|
15119
16107
|
name: "undo_paint",
|
|
15120
16108
|
title: "Undo",
|
|
15121
16109
|
description: "Undoes the last step (a stroke, a fill, a layer or paper change). Returns the label undone (null when there was nothing) and what can be undone and redone next.",
|
|
15122
|
-
inputSchema:
|
|
15123
|
-
annotations: { title: "Undo", ...
|
|
16110
|
+
inputSchema: EMPTY4,
|
|
16111
|
+
annotations: { title: "Undo", ...WRITE8 },
|
|
15124
16112
|
handler: () => {
|
|
15125
16113
|
const undone = c.undo();
|
|
15126
16114
|
const s = c.getState();
|
|
@@ -15131,8 +16119,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15131
16119
|
name: "redo_paint",
|
|
15132
16120
|
title: "Redo",
|
|
15133
16121
|
description: "Redoes the last undone step. Returns the label redone (null when there was nothing) and what can be undone and redone next.",
|
|
15134
|
-
inputSchema:
|
|
15135
|
-
annotations: { title: "Redo", ...
|
|
16122
|
+
inputSchema: EMPTY4,
|
|
16123
|
+
annotations: { title: "Redo", ...WRITE8 },
|
|
15136
16124
|
handler: () => {
|
|
15137
16125
|
const redone = c.redo();
|
|
15138
16126
|
const s = c.getState();
|
|
@@ -15152,7 +16140,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15152
16140
|
},
|
|
15153
16141
|
additionalProperties: false
|
|
15154
16142
|
},
|
|
15155
|
-
annotations: { title: "Render a preview", ...
|
|
16143
|
+
annotations: { title: "Render a preview", ...READ9 },
|
|
15156
16144
|
handler: ({ maxSide, layerIds, paper }) => {
|
|
15157
16145
|
if (layerIds) {
|
|
15158
16146
|
for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer "${id}". Call list_layers for the ids.`);
|
|
@@ -15176,7 +16164,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15176
16164
|
},
|
|
15177
16165
|
additionalProperties: false
|
|
15178
16166
|
},
|
|
15179
|
-
annotations: { title: "Export the painting", ...
|
|
16167
|
+
annotations: { title: "Export the painting", ...READ9 },
|
|
15180
16168
|
handler: async ({ format, quality, scale, paper, time }) => {
|
|
15181
16169
|
const run = () => c.exportImage({ format, quality, scale, paper });
|
|
15182
16170
|
const bytes = time === void 0 ? await run() : await c.withEffectTime(time, run);
|
|
@@ -15188,7 +16176,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15188
16176
|
title: "Export as PSD",
|
|
15189
16177
|
description: 'Writes the painting as an 8-bit RGB PSD (PSB above 30000 px) through the shared PSD writer: every layer and group with its opacity, blend, mask and locks, the paper as a locked "Paper" pixel layer at the bottom, and the composite as the merged image. Changes nothing. Returns the file as base64, its size, whether it is a PSB, and warnings (pigment and painterly blend modes have no PSD equivalent: they are written as the nearest standard mode, see list_blend_modes).',
|
|
15190
16178
|
inputSchema: { type: "object", properties: { time: TIME }, additionalProperties: false },
|
|
15191
|
-
annotations: { title: "Export as PSD", ...
|
|
16179
|
+
annotations: { title: "Export as PSD", ...READ9 },
|
|
15192
16180
|
handler: async ({ time }) => {
|
|
15193
16181
|
const r = time === void 0 ? c.exportPsd() : await c.withEffectTime(time, () => c.exportPsd());
|
|
15194
16182
|
return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
|
|
@@ -15197,6 +16185,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15197
16185
|
tools.push(...createPaintGuideTools(c));
|
|
15198
16186
|
tools.push(...createPaintWetTools(c));
|
|
15199
16187
|
tools.push(...createPaintThickTools(c));
|
|
16188
|
+
tools.push(...createFerroTools(c));
|
|
15200
16189
|
tools.push(...createPaintReplayTools(c));
|
|
15201
16190
|
tools.push(...createPaintEvolutionTools(c));
|
|
15202
16191
|
tools.push(...createPaintStickerTools(c));
|
|
@@ -15920,6 +16909,10 @@ var EVOLUTION_SLIDERS = [
|
|
|
15920
16909
|
{ path: "brush", label: "Brush", min: 0, max: 1, step: 0.05 },
|
|
15921
16910
|
{ path: "seed", label: "Seed", min: 0, max: 1, step: 0.05 }
|
|
15922
16911
|
];
|
|
16912
|
+
var MAGNET_SLIDERS = [
|
|
16913
|
+
{ path: "strength", label: "Strength", min: -1, max: 1, step: 0.05 },
|
|
16914
|
+
{ path: "radius", label: "Radius", min: 4, max: 512, step: 1 }
|
|
16915
|
+
];
|
|
15923
16916
|
function quickStencil(kind, canvas) {
|
|
15924
16917
|
const { width: w, height: h } = canvas;
|
|
15925
16918
|
switch (kind) {
|
|
@@ -16205,8 +17198,8 @@ function createPaintEditorDocument(controller, options = {}) {
|
|
|
16205
17198
|
}
|
|
16206
17199
|
|
|
16207
17200
|
// src/paint-editor/collaboration-tools.ts
|
|
16208
|
-
var
|
|
16209
|
-
var
|
|
17201
|
+
var READ10 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
17202
|
+
var WRITE9 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
16210
17203
|
var proposalIdArg = {
|
|
16211
17204
|
type: "object",
|
|
16212
17205
|
properties: { proposalId: { type: "string", minLength: 1, description: "Id of a pending paint proposal, from propose_paint or list_paint_proposals." } },
|
|
@@ -16284,7 +17277,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
16284
17277
|
required: ["title", "ops"],
|
|
16285
17278
|
additionalProperties: false
|
|
16286
17279
|
},
|
|
16287
|
-
annotations: { title: "Propose paint", ...
|
|
17280
|
+
annotations: { title: "Propose paint", ...WRITE9 },
|
|
16288
17281
|
handler: (args, context) => {
|
|
16289
17282
|
const origin = context.actor?.id ? `agent:${context.actor.id}` : options.fallbackOrigin;
|
|
16290
17283
|
const proposal = proposals.propose({ ...args, ...origin ? { origin } : {} });
|
|
@@ -16297,7 +17290,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
16297
17290
|
title: "List paint proposals",
|
|
16298
17291
|
description: "Lists the pending paint proposals (ghost paint not yet on the canvas): id, title, rationale, who proposed it, its op kinds, where it goes, where the ghost sits, the strokes it echoes and any of those since removed (missing: it can no longer be accepted), and whether accepting is allowed. Call before accept_paint_proposal or reject_paint_proposal.",
|
|
16299
17292
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
16300
|
-
annotations: { title: "List paint proposals", ...
|
|
17293
|
+
annotations: { title: "List paint proposals", ...READ10 },
|
|
16301
17294
|
handler: () => ({ proposals: proposals.getState().proposals.map(describe), readOnly: readOnlyReason() })
|
|
16302
17295
|
};
|
|
16303
17296
|
const acceptTool = {
|
|
@@ -16305,7 +17298,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
16305
17298
|
title: "Accept a paint proposal",
|
|
16306
17299
|
description: "Paints a pending proposal on a new layer as one undo step, recorded as made by whoever proposed it, and drops it from the pending list; the user's active layer stays theirs. Refuses, changing nothing, when the painting is read-only or a stroke it echoes was removed. Returns the new layer id, the stroke ids painted and the undo step name.",
|
|
16307
17300
|
inputSchema: proposalIdArg,
|
|
16308
|
-
annotations: { title: "Accept a paint proposal", ...
|
|
17301
|
+
annotations: { title: "Accept a paint proposal", ...WRITE9 },
|
|
16309
17302
|
handler: (args) => {
|
|
16310
17303
|
const reason = readOnlyReason();
|
|
16311
17304
|
if (reason) throw new Error(reason);
|
|
@@ -16317,7 +17310,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
16317
17310
|
title: "Reject a paint proposal",
|
|
16318
17311
|
description: "Dismisses a pending paint proposal: its ghost disappears and the painting and its history are unchanged. Use it to withdraw a suggestion that no longer fits. Returns the dismissed proposal id and title.",
|
|
16319
17312
|
inputSchema: proposalIdArg,
|
|
16320
|
-
annotations: { title: "Reject a paint proposal", ...
|
|
17313
|
+
annotations: { title: "Reject a paint proposal", ...WRITE9 },
|
|
16321
17314
|
handler: (args) => {
|
|
16322
17315
|
const p = proposals.reject(args.proposalId);
|
|
16323
17316
|
return { proposalId: p.id, title: p.title, rejected: true };
|
|
@@ -16390,6 +17383,33 @@ var PAINT_DOCUMENT_JSON_SCHEMA = {
|
|
|
16390
17383
|
required: ["id", "layerId", "points", "brush"]
|
|
16391
17384
|
}
|
|
16392
17385
|
},
|
|
17386
|
+
ferro: { type: "object", additionalProperties: { type: "string" }, description: "Layer id \u2192 asset id of its liquid ferrofluid map." },
|
|
17387
|
+
ferroSettings: {
|
|
17388
|
+
type: "object",
|
|
17389
|
+
additionalProperties: {
|
|
17390
|
+
type: "object",
|
|
17391
|
+
properties: {
|
|
17392
|
+
wavelength: { type: "number", description: "Rosensweig spike spacing in pixels." },
|
|
17393
|
+
threshold: { type: "number", description: "Critical magnetic field threshold." }
|
|
17394
|
+
}
|
|
17395
|
+
},
|
|
17396
|
+
description: "Layer id \u2192 ferrofluid spike settings."
|
|
17397
|
+
},
|
|
17398
|
+
magnets: {
|
|
17399
|
+
type: "array",
|
|
17400
|
+
description: "Magnets on the canvas that pull liquid ferrofluid into spikes.",
|
|
17401
|
+
items: {
|
|
17402
|
+
type: "object",
|
|
17403
|
+
properties: {
|
|
17404
|
+
id: { type: "string", description: "Magnet id." },
|
|
17405
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
17406
|
+
y: { type: "number", description: "Document y in pixels." },
|
|
17407
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Magnet dipole strength (-1..1)." },
|
|
17408
|
+
radius: { type: "number", minimum: 4, maximum: 512, description: "Magnet field falloff radius in pixels." }
|
|
17409
|
+
},
|
|
17410
|
+
required: ["id", "x", "y", "strength", "radius"]
|
|
17411
|
+
}
|
|
17412
|
+
},
|
|
16393
17413
|
fluor: {
|
|
16394
17414
|
type: "object",
|
|
16395
17415
|
description: "Per-layer UV-fluorescent ink channels (layerId \u2192 bounds + PNG data URL).",
|
|
@@ -16440,7 +17460,7 @@ function uiTables() {
|
|
|
16440
17460
|
brushOptions: BRUSH_OPTION_CONTROLS,
|
|
16441
17461
|
blendMenu: BLEND_MENU_GROUPS,
|
|
16442
17462
|
layerEffects: LAYER_EFFECT_MENU.map(({ kind, label }) => ({ kind, label, controls: layerEffectControls(defaultLayerEffect(kind, kind)) })),
|
|
16443
|
-
sliders: { paper: PAPER_SLIDERS, wet: WET_PARAM_SLIDERS, wetTilt: WET_TILT_SLIDERS, wetSpeed: WET_SPEED_SLIDER, light: LIGHT_SLIDERS, uvLight: UV_LIGHT_SLIDERS, viewTilt: VIEW_TILT_SLIDERS, ghost: GHOST_SLIDERS, mutation: MUTATION_SLIDERS, livePatterns: LIVE_PATTERN_SLIDERS, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS },
|
|
17463
|
+
sliders: { paper: PAPER_SLIDERS, wet: WET_PARAM_SLIDERS, wetTilt: WET_TILT_SLIDERS, wetSpeed: WET_SPEED_SLIDER, light: LIGHT_SLIDERS, uvLight: UV_LIGHT_SLIDERS, viewTilt: VIEW_TILT_SLIDERS, ghost: GHOST_SLIDERS, mutation: MUTATION_SLIDERS, livePatterns: LIVE_PATTERN_SLIDERS, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS, magnet: MAGNET_SLIDERS },
|
|
16444
17464
|
pathStrokeTapers: PATH_STROKE_TAPERS,
|
|
16445
17465
|
padSampleSizes: PAD_SAMPLE_SIZES,
|
|
16446
17466
|
wetGpu: WET_GPU_OPTIONS,
|
|
@@ -16517,6 +17537,7 @@ export {
|
|
|
16517
17537
|
DEFAULT_BRUSH_ID,
|
|
16518
17538
|
DEFAULT_DPI,
|
|
16519
17539
|
DEFAULT_DYNAMICS,
|
|
17540
|
+
DEFAULT_FERRO,
|
|
16520
17541
|
DEFAULT_FLOW,
|
|
16521
17542
|
DEFAULT_FLUORESCENCE,
|
|
16522
17543
|
DEFAULT_GHOST,
|
|
@@ -16525,6 +17546,8 @@ export {
|
|
|
16525
17546
|
DEFAULT_LIGHT,
|
|
16526
17547
|
DEFAULT_LIGHTING,
|
|
16527
17548
|
DEFAULT_LIVE_PATTERNS,
|
|
17549
|
+
DEFAULT_MAGNET_RADIUS,
|
|
17550
|
+
DEFAULT_MAGNET_STRENGTH,
|
|
16528
17551
|
DEFAULT_MULTI_COLOR,
|
|
16529
17552
|
DEFAULT_MUTATION,
|
|
16530
17553
|
DEFAULT_MUTATION_RATES,
|
|
@@ -16552,6 +17575,10 @@ export {
|
|
|
16552
17575
|
EVOLUTION_BRUSH_FACETS,
|
|
16553
17576
|
EVOLUTION_MUTATION_SCHEMA,
|
|
16554
17577
|
EVOLUTION_SLIDERS,
|
|
17578
|
+
FERRO_CARRIER_RGB,
|
|
17579
|
+
FERRO_DEFAULT_GLOSS,
|
|
17580
|
+
FERRO_DEFAULT_METALLIC,
|
|
17581
|
+
FERRO_DEFAULT_RIM,
|
|
16555
17582
|
FilmBuffer,
|
|
16556
17583
|
GHOST_SLIDERS,
|
|
16557
17584
|
GHOST_TRIGGERS,
|
|
@@ -16576,6 +17603,7 @@ export {
|
|
|
16576
17603
|
LIVE_PATTERN_SLIDERS,
|
|
16577
17604
|
LIVE_STROKE_ENGINES,
|
|
16578
17605
|
LitCache,
|
|
17606
|
+
MAGNET_SLIDERS,
|
|
16579
17607
|
MAX_BRUSH_SIZE,
|
|
16580
17608
|
MAX_CANVAS_SIDE,
|
|
16581
17609
|
MAX_COORDINATE,
|
|
@@ -16688,9 +17716,11 @@ export {
|
|
|
16688
17716
|
addPaintGradientStop,
|
|
16689
17717
|
addPolygonArea,
|
|
16690
17718
|
advanceHeatField,
|
|
17719
|
+
advectFerro,
|
|
16691
17720
|
affineCopy,
|
|
16692
17721
|
applyAffine,
|
|
16693
17722
|
backlightOf,
|
|
17723
|
+
bakeFerroIntoThick,
|
|
16694
17724
|
brushEngineControls,
|
|
16695
17725
|
brushLoadColor,
|
|
16696
17726
|
brushLoadForStroke,
|
|
@@ -16707,6 +17737,7 @@ export {
|
|
|
16707
17737
|
computeUvSpill,
|
|
16708
17738
|
contactSheet,
|
|
16709
17739
|
createEvolutionTools,
|
|
17740
|
+
createFerroTools,
|
|
16710
17741
|
createHeatField,
|
|
16711
17742
|
createMutationTools,
|
|
16712
17743
|
createPaintAgentStrokes,
|
|
@@ -16751,11 +17782,20 @@ export {
|
|
|
16751
17782
|
effectPaintKey,
|
|
16752
17783
|
effectPaintSize,
|
|
16753
17784
|
emptiedLayers,
|
|
17785
|
+
emptyFerro,
|
|
16754
17786
|
emptyFluor,
|
|
16755
17787
|
emptyThermo,
|
|
16756
17788
|
emptyThickChannels,
|
|
16757
17789
|
engineBrushSourceIds,
|
|
16758
17790
|
exportProposalContactSheetPng,
|
|
17791
|
+
ferroCacheKey,
|
|
17792
|
+
ferroFieldAt,
|
|
17793
|
+
ferroFromJSON,
|
|
17794
|
+
ferroInfo,
|
|
17795
|
+
ferroSpikeHeightAt,
|
|
17796
|
+
ferroToJSON,
|
|
17797
|
+
ferroToShadow,
|
|
17798
|
+
ferroTwinBrush,
|
|
16759
17799
|
fillRegionMask,
|
|
16760
17800
|
finalizeThermoStroke,
|
|
16761
17801
|
findUnfilledRegionsMask,
|
|
@@ -16792,7 +17832,9 @@ export {
|
|
|
16792
17832
|
lightingKey,
|
|
16793
17833
|
liveStrokeBounds,
|
|
16794
17834
|
liveStrokeRefusal,
|
|
17835
|
+
magnetsFromJSON,
|
|
16795
17836
|
mapPenPoint,
|
|
17837
|
+
mergeFerro,
|
|
16796
17838
|
mergeFourChannel,
|
|
16797
17839
|
mix32,
|
|
16798
17840
|
mixColorOklch,
|
|
@@ -16814,6 +17856,8 @@ export {
|
|
|
16814
17856
|
normalizeBrushStroke,
|
|
16815
17857
|
normalizeCanvas,
|
|
16816
17858
|
normalizeDynamics,
|
|
17859
|
+
normalizeFerro,
|
|
17860
|
+
normalizeFerro as normalizeFerroSettings,
|
|
16817
17861
|
normalizeFlow,
|
|
16818
17862
|
normalizeFluorescence,
|
|
16819
17863
|
normalizeGhost,
|
|
@@ -16822,6 +17866,7 @@ export {
|
|
|
16822
17866
|
normalizeLight,
|
|
16823
17867
|
normalizeLighting,
|
|
16824
17868
|
normalizeLivePatterns,
|
|
17869
|
+
normalizeMagnet,
|
|
16825
17870
|
normalizeMultiColor,
|
|
16826
17871
|
normalizeMutation,
|
|
16827
17872
|
normalizeMutationRates,
|
|
@@ -16903,6 +17948,7 @@ export {
|
|
|
16903
17948
|
sampleHeatAt,
|
|
16904
17949
|
samplePath,
|
|
16905
17950
|
samplePressure,
|
|
17951
|
+
scaleFerro,
|
|
16906
17952
|
scaleFluor,
|
|
16907
17953
|
scalePaintGradient,
|
|
16908
17954
|
scaleStencil,
|
|
@@ -16911,6 +17957,7 @@ export {
|
|
|
16911
17957
|
seededStrokeVariation,
|
|
16912
17958
|
serializePaintDocument,
|
|
16913
17959
|
serializePaintDocumentExternal,
|
|
17960
|
+
shadowToFerro,
|
|
16914
17961
|
shiftHeatField,
|
|
16915
17962
|
solidPaint,
|
|
16916
17963
|
squaredDistanceTransform,
|