@pixerate/editor 0.274.0 → 0.275.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 +60 -60
- package/dist/animation-editor/index.js +4 -4
- package/dist/brushes/index.cjs +102 -74
- package/dist/brushes/index.js +34 -6
- package/dist/brushes/patch.d.cts +1 -1
- package/dist/brushes/patch.d.ts +1 -1
- package/dist/brushes/preview.d.cts +3 -0
- package/dist/brushes/preview.d.ts +3 -0
- package/dist/brushes/profiles.d.cts +2 -0
- package/dist/brushes/profiles.d.ts +2 -0
- package/dist/brushes/schema.d.cts +4 -0
- package/dist/brushes/schema.d.ts +4 -0
- package/dist/brushes/types.d.cts +47 -0
- package/dist/brushes/types.d.ts +47 -0
- package/dist/{brushstrokeRender-5YBCLFDQ.cjs → brushstrokeRender-2RQL46WB.cjs} +15 -15
- package/dist/{brushstrokeRender-DZGFU3J4.js → brushstrokeRender-WGY4DZHF.js} +3 -3
- package/dist/canvas/index.cjs +13 -13
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-2XDNF73E.js → chunk-43XJC3PP.js} +1 -1
- package/dist/{chunk-GHGBIAWQ.cjs → chunk-6QNDGMK7.cjs} +263 -45
- package/dist/{chunk-2F3YDXCN.js → chunk-75SYKUOT.js} +1 -1
- package/dist/{chunk-M3RZOF3G.cjs → chunk-C6VB7CPY.cjs} +1 -1
- package/dist/{chunk-N5IMDAHM.cjs → chunk-DHC4E2DU.cjs} +89 -37
- package/dist/{chunk-NAHJV3O6.js → chunk-G5TCMKX5.js} +28 -11
- package/dist/{chunk-DZVRH63Y.js → chunk-IEEY5RJV.js} +1 -1
- package/dist/{chunk-7W2EQQBP.cjs → chunk-JDXWLCXF.cjs} +6 -6
- package/dist/{chunk-N3SKPQEH.js → chunk-LMG57L5A.js} +62 -10
- package/dist/{chunk-5HP2VSAS.js → chunk-MSWLJI4G.js} +1 -1
- package/dist/{chunk-SNAFFCAX.cjs → chunk-OLZXXAER.cjs} +221 -196
- package/dist/{chunk-YJANF2BL.cjs → chunk-RAPNRK4R.cjs} +86 -86
- package/dist/{chunk-CLB2I2FU.js → chunk-RJGW3RQS.js} +2 -2
- package/dist/{chunk-FXVCB6YG.cjs → chunk-T6KNJVBP.cjs} +35 -35
- package/dist/{chunk-ZEI7ZEA4.js → chunk-Y2ISSVPH.js} +34 -9
- package/dist/{chunk-3JKCREEU.cjs → chunk-Y354ETAE.cjs} +76 -59
- package/dist/{chunk-TNLKFW7D.cjs → chunk-YYZC2EQO.cjs} +23 -23
- package/dist/{chunk-UXYCY3HP.js → chunk-ZM7IKQ4X.js} +239 -21
- package/dist/image-editor/index.cjs +789 -789
- package/dist/image-editor/index.js +5 -5
- package/dist/paint/index.cjs +2 -2
- package/dist/paint/index.js +1 -1
- package/dist/paint/wet/world.d.cts +15 -2
- package/dist/paint/wet/world.d.ts +15 -2
- package/dist/paint-editor/brush-controls.d.cts +7 -0
- package/dist/paint-editor/brush-controls.d.ts +7 -0
- package/dist/paint-editor/brush-model.d.cts +59 -1
- package/dist/paint-editor/brush-model.d.ts +59 -1
- package/dist/paint-editor/controller.d.cts +168 -1
- package/dist/paint-editor/controller.d.ts +168 -1
- package/dist/paint-editor/dab-stroke.d.cts +3 -0
- package/dist/paint-editor/dab-stroke.d.ts +3 -0
- package/dist/paint-editor/document.d.cts +13 -3
- package/dist/paint-editor/document.d.ts +13 -3
- package/dist/paint-editor/engine.d.cts +3 -0
- package/dist/paint-editor/engine.d.ts +3 -0
- package/dist/paint-editor/heat.d.cts +156 -0
- package/dist/paint-editor/heat.d.ts +156 -0
- package/dist/paint-editor/index.cjs +3386 -855
- package/dist/paint-editor/index.d.cts +3 -0
- package/dist/paint-editor/index.d.ts +3 -0
- package/dist/paint-editor/index.js +2731 -200
- package/dist/paint-editor/mutation-tools.d.cts +13 -0
- package/dist/paint-editor/mutation-tools.d.ts +13 -0
- package/dist/paint-editor/mutation.d.cts +67 -0
- package/dist/paint-editor/mutation.d.ts +67 -0
- package/dist/paint-editor/presence.d.cts +7 -1
- package/dist/paint-editor/presence.d.ts +7 -1
- package/dist/paint-editor/procedural.d.cts +3 -0
- package/dist/paint-editor/procedural.d.ts +3 -0
- package/dist/paint-editor/stroke.d.cts +32 -2
- package/dist/paint-editor/stroke.d.ts +32 -2
- package/dist/paint-editor/thick.d.cts +48 -7
- package/dist/paint-editor/thick.d.ts +48 -7
- package/dist/paint-editor/ui.d.cts +34 -1
- package/dist/paint-editor/ui.d.ts +34 -1
- package/dist/paint-editor/wet.d.cts +8 -1
- package/dist/paint-editor/wet.d.ts +8 -1
- package/dist/vector/index.cjs +999 -999
- package/dist/vector/index.js +5 -5
- package/package.json +1 -1
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
BRUSH_SLIDERS,
|
|
8
8
|
BRUSH_UNSET_PROPERTY,
|
|
9
9
|
GHOST_SLIDERS,
|
|
10
|
+
IRIDESCENCE_SLIDERS,
|
|
11
|
+
MUTATION_SLIDERS,
|
|
10
12
|
brushEngineControls,
|
|
11
13
|
createBrushLibraryToolProvider,
|
|
12
14
|
mergeBrushPatch,
|
|
13
15
|
multiColorSliders,
|
|
14
16
|
patchBrushValue
|
|
15
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-G5TCMKX5.js";
|
|
16
18
|
import {
|
|
17
19
|
createStickerTools,
|
|
18
20
|
describeStickerRef
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
import {
|
|
21
23
|
BRUSH_PREVIEW_DARK_PAPER,
|
|
22
24
|
brushPreviewsOnDark
|
|
23
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-RJGW3RQS.js";
|
|
24
26
|
import {
|
|
25
27
|
toVideoFfmpeg
|
|
26
28
|
} from "../chunk-S2XPXA47.js";
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
resolveExternalAssets,
|
|
41
43
|
serializeLayers,
|
|
42
44
|
serializeLayersExternal
|
|
43
|
-
} from "../chunk-
|
|
45
|
+
} from "../chunk-MSWLJI4G.js";
|
|
44
46
|
import "../chunk-ADMRNWR7.js";
|
|
45
47
|
import "../chunk-WJ4WQJCC.js";
|
|
46
48
|
import {
|
|
@@ -123,7 +125,7 @@ import {
|
|
|
123
125
|
wetPaperInput,
|
|
124
126
|
wetPigmentOf,
|
|
125
127
|
withStrokePath
|
|
126
|
-
} from "../chunk-
|
|
128
|
+
} from "../chunk-Y2ISSVPH.js";
|
|
127
129
|
import {
|
|
128
130
|
BRUSH_ID,
|
|
129
131
|
BRUSH_PRESETS,
|
|
@@ -135,17 +137,22 @@ import {
|
|
|
135
137
|
DEFAULT_BRISTLE,
|
|
136
138
|
DEFAULT_BRUSH_ID,
|
|
137
139
|
DEFAULT_DYNAMICS,
|
|
140
|
+
DEFAULT_FLUORESCENCE,
|
|
138
141
|
DEFAULT_GHOST,
|
|
139
142
|
DEFAULT_IMPASTO,
|
|
143
|
+
DEFAULT_IRIDESCENCE,
|
|
140
144
|
DEFAULT_LIGHT,
|
|
141
145
|
DEFAULT_MULTI_COLOR,
|
|
146
|
+
DEFAULT_MUTATION,
|
|
142
147
|
DEFAULT_PARTICLE,
|
|
143
148
|
DEFAULT_SPRAY,
|
|
144
149
|
DEFAULT_STICKER,
|
|
150
|
+
DEFAULT_THERMOCHROMIC,
|
|
145
151
|
DEFAULT_TIP_GRAIN,
|
|
146
152
|
DEFAULT_WET_BRUSH,
|
|
147
153
|
DOT_ROTATIONS,
|
|
148
154
|
DOT_TEXTURE_ORDERS,
|
|
155
|
+
GHOST_TRIGGERS,
|
|
149
156
|
IMPASTO_TOOLS,
|
|
150
157
|
MAX_DOT_TEXTURES,
|
|
151
158
|
MAX_STICKER_STAMPS,
|
|
@@ -175,20 +182,23 @@ import {
|
|
|
175
182
|
normalizeBristle,
|
|
176
183
|
normalizeBrushSpec,
|
|
177
184
|
normalizeDynamics,
|
|
185
|
+
normalizeFluorescence,
|
|
178
186
|
normalizeGhost,
|
|
179
187
|
normalizeImpasto,
|
|
180
188
|
normalizeLight,
|
|
181
189
|
normalizeMultiColor,
|
|
190
|
+
normalizeMutation,
|
|
182
191
|
normalizePaintBrush,
|
|
183
192
|
normalizeParticle,
|
|
184
193
|
normalizeStickerSettings,
|
|
194
|
+
normalizeThermochromic,
|
|
185
195
|
normalizeWetBrush,
|
|
186
196
|
paintToMultiColor,
|
|
187
197
|
parseBrushLibrary,
|
|
188
198
|
readBrushSource,
|
|
189
199
|
resolveBrush,
|
|
190
200
|
usesPaintExchange
|
|
191
|
-
} from "../chunk-
|
|
201
|
+
} from "../chunk-ZM7IKQ4X.js";
|
|
192
202
|
import {
|
|
193
203
|
STAMP_KINDS,
|
|
194
204
|
StickersSlot,
|
|
@@ -238,7 +248,7 @@ import {
|
|
|
238
248
|
normalizeWetParams,
|
|
239
249
|
pigmentOf,
|
|
240
250
|
wetSupport
|
|
241
|
-
} from "../chunk-
|
|
251
|
+
} from "../chunk-LMG57L5A.js";
|
|
242
252
|
import "../chunk-W6JDUXP3.js";
|
|
243
253
|
import {
|
|
244
254
|
CoverageMap,
|
|
@@ -408,8 +418,22 @@ function normalizeLighting(raw = {}, base = DEFAULT_LIGHTING) {
|
|
|
408
418
|
relief: Math.min(1, Math.max(0, num(raw.relief, base.relief)))
|
|
409
419
|
};
|
|
410
420
|
}
|
|
421
|
+
var DEFAULT_VIEW_TILT = { x: 0, y: 0 };
|
|
422
|
+
function normalizeViewTilt(raw = {}, base = DEFAULT_VIEW_TILT) {
|
|
423
|
+
const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
424
|
+
return {
|
|
425
|
+
x: Math.min(1, Math.max(-1, num(raw.x, base.x))),
|
|
426
|
+
y: Math.min(1, Math.max(-1, num(raw.y, base.y)))
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function viewTiltKey(v) {
|
|
430
|
+
return `${v.x.toFixed(3)}|${v.y.toFixed(3)}`;
|
|
431
|
+
}
|
|
411
432
|
function emptyThickChannels() {
|
|
412
|
-
return { height: new TiledSurface(1, /* @__PURE__ */ new Map(), 0), material: new TiledSurface(4, /* @__PURE__ */ new Map(), 0) };
|
|
433
|
+
return { height: new TiledSurface(1, /* @__PURE__ */ new Map(), 0), material: new TiledSurface(4, /* @__PURE__ */ new Map(), 0), film: new TiledSurface(4, /* @__PURE__ */ new Map(), 0) };
|
|
434
|
+
}
|
|
435
|
+
function hasThickChannels(ch) {
|
|
436
|
+
return Boolean(ch && (ch.height.tiles.size > 0 || ch.material.tiles.size > 0 || ch.film.tiles.size > 0));
|
|
413
437
|
}
|
|
414
438
|
var RELIEF_PX = 7;
|
|
415
439
|
var AMBIENT = 0.38;
|
|
@@ -418,14 +442,31 @@ function lightVector(l) {
|
|
|
418
442
|
const el = l.elevation * Math.PI / 180;
|
|
419
443
|
return [Math.cos(el) * Math.sin(az), -Math.cos(el) * Math.cos(az), Math.sin(el)];
|
|
420
444
|
}
|
|
445
|
+
function viewVector(v) {
|
|
446
|
+
const vx = v.x * 1.25;
|
|
447
|
+
const vy = v.y * 1.25;
|
|
448
|
+
const vz = 1;
|
|
449
|
+
const vl = Math.hypot(vx, vy, vz);
|
|
450
|
+
return [vx / vl, vy / vl, vz / vl];
|
|
451
|
+
}
|
|
452
|
+
function thinFilmRgb(opd) {
|
|
453
|
+
const twoPi = Math.PI * 2;
|
|
454
|
+
return [
|
|
455
|
+
0.5 - 0.5 * Math.cos(twoPi * opd / 630),
|
|
456
|
+
0.5 - 0.5 * Math.cos(twoPi * opd / 532),
|
|
457
|
+
0.5 - 0.5 * Math.cos(twoPi * opd / 465)
|
|
458
|
+
];
|
|
459
|
+
}
|
|
421
460
|
function lightingKey(l) {
|
|
422
461
|
return `${l.azimuth.toFixed(2)}|${l.elevation.toFixed(2)}|${l.relief.toFixed(3)}`;
|
|
423
462
|
}
|
|
424
|
-
function lightTile(color, height, heightAround, material, light) {
|
|
463
|
+
function lightTile(color, height, heightAround, material, light, film = null, viewTilt = DEFAULT_VIEW_TILT) {
|
|
425
464
|
const out = new Uint8ClampedArray(color);
|
|
426
|
-
|
|
465
|
+
const hasRelief = Boolean(height && light.relief > 0);
|
|
466
|
+
if (!hasRelief && !film) return out;
|
|
427
467
|
const [lx, ly, lz] = lightVector(light);
|
|
428
|
-
const
|
|
468
|
+
const [vx, vy, vz] = viewVector(viewTilt);
|
|
469
|
+
const hv = [lx + vx, ly + vy, lz + vz];
|
|
429
470
|
const hl = Math.hypot(hv[0], hv[1], hv[2]);
|
|
430
471
|
const hx = hv[0] / hl;
|
|
431
472
|
const hy = hv[1] / hl;
|
|
@@ -433,6 +474,7 @@ function lightTile(color, height, heightAround, material, light) {
|
|
|
433
474
|
const k = RELIEF_PX / 255 * light.relief * 2;
|
|
434
475
|
const flatDiffuse = AMBIENT + (1 - AMBIENT) * lz;
|
|
435
476
|
const read = (x, y) => {
|
|
477
|
+
if (!height) return 0;
|
|
436
478
|
if (x >= 0 && y >= 0 && x < TILE && y < TILE) return height[y * TILE + x];
|
|
437
479
|
const dx = x < 0 ? -1 : x >= TILE ? 1 : 0;
|
|
438
480
|
const dy = y < 0 ? -1 : y >= TILE ? 1 : 0;
|
|
@@ -445,28 +487,59 @@ function lightTile(color, height, heightAround, material, light) {
|
|
|
445
487
|
const p = y * TILE + x;
|
|
446
488
|
const i = p * 4;
|
|
447
489
|
if (color[i + 3] === 0) continue;
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
490
|
+
const fa = film ? film[i + 3] / 255 : 0;
|
|
491
|
+
const h = hasRelief ? height[p] : 0;
|
|
492
|
+
const gx = hasRelief ? (read(x + 1, y) - read(x - 1, y)) * 0.5 : 0;
|
|
493
|
+
const gy = hasRelief ? (read(x, y + 1) - read(x, y - 1)) * 0.5 : 0;
|
|
494
|
+
const hasSlope = hasRelief && (h !== 0 || gx !== 0 || gy !== 0);
|
|
495
|
+
if (!hasSlope && fa <= 0) continue;
|
|
496
|
+
let nx = 0;
|
|
497
|
+
let ny = 0;
|
|
498
|
+
let nz = 1;
|
|
499
|
+
let shade = 1;
|
|
500
|
+
let gloss = 0.3;
|
|
501
|
+
let metal = 0;
|
|
502
|
+
let spec = 0;
|
|
503
|
+
if (hasSlope) {
|
|
504
|
+
nx = -gx * k;
|
|
505
|
+
ny = -gy * k;
|
|
506
|
+
const nl = Math.hypot(nx, ny, 1);
|
|
507
|
+
nx /= nl;
|
|
508
|
+
ny /= nl;
|
|
509
|
+
nz = 1 / nl;
|
|
510
|
+
const diffuse = Math.max(0, nx * lx + ny * ly + nz * lz);
|
|
511
|
+
shade = (AMBIENT + (1 - AMBIENT) * diffuse) / flatDiffuse;
|
|
512
|
+
gloss = material ? material[i] / 255 : 0.3;
|
|
513
|
+
metal = material ? material[i + 1] / 255 : 0;
|
|
514
|
+
const shininess = 6 + 90 * gloss;
|
|
515
|
+
const nh = Math.max(0, nx * hx + ny * hy + nz * hz);
|
|
516
|
+
spec = Math.max(0, Math.pow(nh, shininess) - Math.pow(hz, shininess)) * (0.25 + 0.75 * gloss) * light.relief;
|
|
517
|
+
}
|
|
518
|
+
let filmRgb = null;
|
|
519
|
+
if (fa > 0 && film) {
|
|
520
|
+
const d = 100 + film[i] / 255 * 900;
|
|
521
|
+
const ior = 1.2 + film[i + 1] / 255 * 0.8;
|
|
522
|
+
const nh = Math.max(0, nx * hx + ny * hy + nz * hz);
|
|
523
|
+
const nv = Math.max(0, nx * vx + ny * vy + nz * vz);
|
|
524
|
+
const cosThetaI = Math.max(0, Math.min(1, nh * nv));
|
|
525
|
+
const sin2ThetaT = (1 - cosThetaI * cosThetaI) / (ior * ior);
|
|
526
|
+
const cosThetaT = Math.sqrt(Math.max(0, 1 - sin2ThetaT));
|
|
527
|
+
const opd = 2 * ior * d * cosThetaT;
|
|
528
|
+
filmRgb = thinFilmRgb(opd);
|
|
529
|
+
}
|
|
465
530
|
for (let c = 0; c < 3; c++) {
|
|
466
531
|
const a = SRGB_TO_LINEAR[color[i + c]];
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
532
|
+
const fc = filmRgb ? filmRgb[c] : a;
|
|
533
|
+
if (hasSlope) {
|
|
534
|
+
const tint = metal * a + (1 - metal);
|
|
535
|
+
const diff = fa > 0 ? a * (1 - fa * 0.75) + fc * fa * (0.45 + 0.55 * a) : a;
|
|
536
|
+
const specTint = fa > 0 ? tint * (1 - fa) + fc * fa : tint;
|
|
537
|
+
const v = diff * shade * (1 - 0.35 * metal) + spec * specTint * (1 + metal);
|
|
538
|
+
out[i + c] = linearToSrgb8(v);
|
|
539
|
+
} else {
|
|
540
|
+
const v = a * (1 - fa * 0.75) + fc * fa * (0.45 + 0.55 * a);
|
|
541
|
+
out[i + c] = linearToSrgb8(v);
|
|
542
|
+
}
|
|
470
543
|
}
|
|
471
544
|
}
|
|
472
545
|
}
|
|
@@ -474,10 +547,12 @@ function lightTile(color, height, heightAround, material, light) {
|
|
|
474
547
|
}
|
|
475
548
|
var LitCache = class {
|
|
476
549
|
tiles = /* @__PURE__ */ new Map();
|
|
477
|
-
/** The layer's pixels lit (tiles without relief nearby are the colour tiles themselves). */
|
|
478
|
-
lit(color, ch, light) {
|
|
479
|
-
|
|
480
|
-
const
|
|
550
|
+
/** The layer's pixels lit (tiles without relief or film nearby are the colour tiles themselves). */
|
|
551
|
+
lit(color, ch, light, viewTilt = DEFAULT_VIEW_TILT) {
|
|
552
|
+
const hasRelief = ch.height.tiles.size > 0 && light.relief > 0;
|
|
553
|
+
const hasFilm = ch.film.tiles.size > 0;
|
|
554
|
+
if (!hasRelief && !hasFilm) return color;
|
|
555
|
+
const lk = `${lightingKey(light)}|${viewTiltKey(viewTilt)}`;
|
|
481
556
|
const out = new Map(color.tiles);
|
|
482
557
|
const keys = /* @__PURE__ */ new Set();
|
|
483
558
|
for (const key of color.tiles.keys()) keys.add(key);
|
|
@@ -486,12 +561,15 @@ var LitCache = class {
|
|
|
486
561
|
const ty = Math.floor(key / 65536) - 32768;
|
|
487
562
|
const around = (dx, dy) => ch.height.tiles.get(tileKey(tx + dx, ty + dy))?.data ?? null;
|
|
488
563
|
let any = false;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
564
|
+
if (hasRelief) {
|
|
565
|
+
for (let dy = -1; dy <= 1 && !any; dy++) for (let dx = -1; dx <= 1 && !any; dx++) if (around(dx, dy)) any = true;
|
|
566
|
+
}
|
|
567
|
+
const f = ch.film.tiles.get(key) ?? null;
|
|
568
|
+
if (!any && !f) continue;
|
|
569
|
+
const h = hasRelief ? ch.height.tiles.get(key) ?? null : null;
|
|
570
|
+
const m = hasRelief ? ch.material.tiles.get(key) ?? null : null;
|
|
571
|
+
const inputs = [tile, lk, m, f];
|
|
572
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(hasRelief ? ch.height.tiles.get(tileKey(tx + dx, ty + dy)) ?? null : null);
|
|
495
573
|
const cached = this.tiles.get(key);
|
|
496
574
|
if (cached && cached.inputs.length === inputs.length && cached.inputs.every((v, i) => v === inputs[i])) {
|
|
497
575
|
out.set(key, cached.tile);
|
|
@@ -499,10 +577,19 @@ var LitCache = class {
|
|
|
499
577
|
}
|
|
500
578
|
const hd = h ? expand(h, 1) : null;
|
|
501
579
|
const md = m ? expand(m, 4) : null;
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
580
|
+
const fd = f ? expand(f, 4) : null;
|
|
581
|
+
const data = lightTile(
|
|
582
|
+
expand(tile, 4),
|
|
583
|
+
hd,
|
|
584
|
+
(dx, dy) => {
|
|
585
|
+
const t = ch.height.tiles.get(tileKey(tx + dx, ty + dy));
|
|
586
|
+
return t ? expand(t, 1) : null;
|
|
587
|
+
},
|
|
588
|
+
md,
|
|
589
|
+
light,
|
|
590
|
+
fd,
|
|
591
|
+
viewTilt
|
|
592
|
+
);
|
|
506
593
|
const lit = { data, uniform: null };
|
|
507
594
|
this.tiles.set(key, { inputs, tile: lit });
|
|
508
595
|
out.set(key, lit);
|
|
@@ -522,6 +609,122 @@ function expand(t, channels) {
|
|
|
522
609
|
}
|
|
523
610
|
return out;
|
|
524
611
|
}
|
|
612
|
+
var FilmBuffer = class {
|
|
613
|
+
constructor(base, offset = { x: 0, y: 0 }, mask = null) {
|
|
614
|
+
this.base = base;
|
|
615
|
+
this.offset = offset;
|
|
616
|
+
this.mask = mask;
|
|
617
|
+
}
|
|
618
|
+
base;
|
|
619
|
+
offset;
|
|
620
|
+
mask;
|
|
621
|
+
tiles = /* @__PURE__ */ new Map();
|
|
622
|
+
maskAt(wx, wy) {
|
|
623
|
+
const sel = this.mask;
|
|
624
|
+
if (!sel) return 1;
|
|
625
|
+
return wx >= 0 && wy >= 0 && wx < sel.width && wy < sel.height ? sel.data[wy * sel.width + wx] / 255 : 0;
|
|
626
|
+
}
|
|
627
|
+
tile(tx, ty) {
|
|
628
|
+
const key = tileKey(tx, ty);
|
|
629
|
+
let t = this.tiles.get(key);
|
|
630
|
+
if (!t) {
|
|
631
|
+
const b = this.base.tiles.get(key);
|
|
632
|
+
t = b ? new Uint8ClampedArray(expand(b, 4)) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
633
|
+
this.tiles.set(key, t);
|
|
634
|
+
}
|
|
635
|
+
return t;
|
|
636
|
+
}
|
|
637
|
+
/** Deposits thin-film iridescence over `rect` with per-pixel `coverage` (0..1) scaled by `strength` (0..1). */
|
|
638
|
+
deposit(rect, coverage, strength, ir, pressure = 0.5) {
|
|
639
|
+
if (strength <= 0) return;
|
|
640
|
+
const nm = Math.max(100, Math.min(1e3, ir.thickness * (1 + ir.pressureToThickness * (pressure - 0.5) * 2)));
|
|
641
|
+
const rByte = Math.round((nm - 100) / 900 * 255);
|
|
642
|
+
const gByte = Math.round((ir.ior - 1.2) / 0.8 * 255);
|
|
643
|
+
const ox = this.offset.x;
|
|
644
|
+
const oy = this.offset.y;
|
|
645
|
+
for (let yy = 0, p = 0; yy < rect.height; yy++) {
|
|
646
|
+
for (let xx = 0; xx < rect.width; xx++, p++) {
|
|
647
|
+
const wx = rect.x + xx;
|
|
648
|
+
const wy = rect.y + yy;
|
|
649
|
+
const m = this.maskAt(wx, wy);
|
|
650
|
+
const c = Math.min(1, coverage[p] * strength * m);
|
|
651
|
+
if (c <= 0) continue;
|
|
652
|
+
const lx = wx - ox;
|
|
653
|
+
const ly = wy - oy;
|
|
654
|
+
const t = this.tile(Math.floor(lx / TILE), Math.floor(ly / TILE));
|
|
655
|
+
const i = ((ly & 255) * TILE + (lx & 255)) * 4;
|
|
656
|
+
const prevA = t[i + 3] / 255;
|
|
657
|
+
const nextA = prevA + c * (1 - prevA);
|
|
658
|
+
const w = nextA > 0 ? c / nextA : 1;
|
|
659
|
+
t[i] = Math.round(t[i] + (rByte - t[i]) * w);
|
|
660
|
+
t[i + 1] = Math.round(t[i + 1] + (gByte - t[i + 1]) * w);
|
|
661
|
+
t[i + 2] = 0;
|
|
662
|
+
t[i + 3] = Math.round(nextA * 255);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/** Stamps a soft disc of thin-film iridescence at `(cx, cy)` (document pixels) with `radius` and `alpha` (0..1). */
|
|
667
|
+
stampDisc(cx, cy, radius, alpha, ir, pressure = 0.5) {
|
|
668
|
+
if (alpha <= 0 || radius <= 0) return;
|
|
669
|
+
const r = Math.max(0.5, radius);
|
|
670
|
+
const x0 = Math.floor(cx - r - 1);
|
|
671
|
+
const y0 = Math.floor(cy - r - 1);
|
|
672
|
+
const x1 = Math.ceil(cx + r + 1);
|
|
673
|
+
const y1 = Math.ceil(cy + r + 1);
|
|
674
|
+
const w = x1 - x0;
|
|
675
|
+
const h = y1 - y0;
|
|
676
|
+
if (w <= 0 || h <= 0) return;
|
|
677
|
+
const cov = new Float32Array(w * h);
|
|
678
|
+
for (let yy = 0, p = 0; yy < h; yy++) {
|
|
679
|
+
for (let xx = 0; xx < w; xx++, p++) {
|
|
680
|
+
const d = Math.hypot(x0 + xx + 0.5 - cx, y0 + yy + 0.5 - cy);
|
|
681
|
+
if (d < r + 0.5) cov[p] = d <= r - 0.5 ? 1 : Math.max(0, r + 0.5 - d);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
this.deposit({ x: x0, y: y0, width: w, height: h }, cov, Math.min(1, alpha), ir, pressure);
|
|
685
|
+
}
|
|
686
|
+
/** Erases thin-film coverage over `rect` by `coverage[p] * strength`. */
|
|
687
|
+
erase(rect, coverage, strength) {
|
|
688
|
+
if (strength <= 0) return;
|
|
689
|
+
const ox = this.offset.x;
|
|
690
|
+
const oy = this.offset.y;
|
|
691
|
+
for (let yy = 0, p = 0; yy < rect.height; yy++) {
|
|
692
|
+
for (let xx = 0; xx < rect.width; xx++, p++) {
|
|
693
|
+
const wx = rect.x + xx;
|
|
694
|
+
const wy = rect.y + yy;
|
|
695
|
+
const m = this.maskAt(wx, wy);
|
|
696
|
+
const c = Math.min(1, coverage[p] * strength * m);
|
|
697
|
+
if (c <= 0) continue;
|
|
698
|
+
const lx = wx - ox;
|
|
699
|
+
const ly = wy - oy;
|
|
700
|
+
const tx = Math.floor(lx / TILE);
|
|
701
|
+
const ty = Math.floor(ly / TILE);
|
|
702
|
+
const key = tileKey(tx, ty);
|
|
703
|
+
if (!this.tiles.has(key) && !this.base.tiles.has(key)) continue;
|
|
704
|
+
const t = this.tile(tx, ty);
|
|
705
|
+
const i = ((ly & 255) * TILE + (lx & 255)) * 4;
|
|
706
|
+
const a = Math.round(t[i + 3] * (1 - c));
|
|
707
|
+
if (a <= 0) {
|
|
708
|
+
t[i] = 0;
|
|
709
|
+
t[i + 1] = 0;
|
|
710
|
+
t[i + 2] = 0;
|
|
711
|
+
t[i + 3] = 0;
|
|
712
|
+
} else {
|
|
713
|
+
t[i + 3] = a;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
surface() {
|
|
719
|
+
const out = new Map(this.base.tiles);
|
|
720
|
+
for (const [k, d] of this.tiles) {
|
|
721
|
+
const t = finishTile(new Uint8ClampedArray(d), 4, 0);
|
|
722
|
+
if (t) out.set(k, t);
|
|
723
|
+
else out.delete(k);
|
|
724
|
+
}
|
|
725
|
+
return new TiledSurface(4, out, 0);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
525
728
|
var ThickStrokeDriver = class {
|
|
526
729
|
constructor(init) {
|
|
527
730
|
this.init = init;
|
|
@@ -538,6 +741,7 @@ var ThickStrokeDriver = class {
|
|
|
538
741
|
amount: 1
|
|
539
742
|
}));
|
|
540
743
|
this.medium = new TileMedium(init.target, {});
|
|
744
|
+
this.film = b.iridescence ? new FilmBuffer(init.channels.film, init.target.offset) : null;
|
|
541
745
|
this.sampler = init.paint ? new PaintGradientSampler(init.paint.map.by === "across-tip" ? { ...init.paint, map: { by: "distance", length: 300, repeat: "mirror" } } : init.paint, init.seed) : null;
|
|
542
746
|
}
|
|
543
747
|
init;
|
|
@@ -547,6 +751,7 @@ var ThickStrokeDriver = class {
|
|
|
547
751
|
medium;
|
|
548
752
|
height = /* @__PURE__ */ new Map();
|
|
549
753
|
material = /* @__PURE__ */ new Map();
|
|
754
|
+
film;
|
|
550
755
|
paint = [0, 0, 0];
|
|
551
756
|
centre = [0, 0, 0];
|
|
552
757
|
tip = new Float32Array(0);
|
|
@@ -575,7 +780,11 @@ var ThickStrokeDriver = class {
|
|
|
575
780
|
else m.delete(k);
|
|
576
781
|
}
|
|
577
782
|
void this.version;
|
|
578
|
-
return {
|
|
783
|
+
return {
|
|
784
|
+
height: new TiledSurface(1, h, 0),
|
|
785
|
+
material: new TiledSurface(4, m, 0),
|
|
786
|
+
film: this.film ? this.film.surface() : this.init.channels.film
|
|
787
|
+
};
|
|
579
788
|
}
|
|
580
789
|
heightTile(tx, ty, write) {
|
|
581
790
|
const key = tileKey(tx, ty);
|
|
@@ -671,6 +880,7 @@ var ThickStrokeDriver = class {
|
|
|
671
880
|
st.smudge.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: k }, rect, tip);
|
|
672
881
|
this.knifeHeight(st, d, rect, tip, k, im.drag);
|
|
673
882
|
this.paintMaterial(rect, tip, k * 0.6, im.gloss, im.metallic, im.rim, im.clarity);
|
|
883
|
+
if (this.film && b.iridescence) this.film.deposit(rect, tip, k * 0.6, b.iridescence, d.pressure);
|
|
674
884
|
return;
|
|
675
885
|
}
|
|
676
886
|
const ex = st.exchange;
|
|
@@ -738,6 +948,7 @@ var ThickStrokeDriver = class {
|
|
|
738
948
|
}
|
|
739
949
|
}
|
|
740
950
|
this.paintMaterial(rect, tip, k, im.gloss * (1 - 0.3 * im.thinner), im.metallic, im.rim, im.clarity);
|
|
951
|
+
if (this.film && b.iridescence) this.film.deposit(rect, tip, k, b.iridescence, d.pressure);
|
|
741
952
|
}
|
|
742
953
|
/** The knife: flattens the relief under it and carries some of it along (a patch, like a smudge). */
|
|
743
954
|
knifeHeight(st, d, rect, tip, k, drag) {
|
|
@@ -790,18 +1001,33 @@ var ThickStrokeDriver = class {
|
|
|
790
1001
|
}
|
|
791
1002
|
}
|
|
792
1003
|
};
|
|
1004
|
+
function unionRects(a, b) {
|
|
1005
|
+
if (!a) return b ? { ...b } : null;
|
|
1006
|
+
if (!b) return { ...a };
|
|
1007
|
+
const x = Math.min(a.x, b.x);
|
|
1008
|
+
const y = Math.min(a.y, yOf(b));
|
|
1009
|
+
return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
|
|
1010
|
+
}
|
|
1011
|
+
var yOf = (r) => r.y;
|
|
793
1012
|
function reliefInfo(ch) {
|
|
794
1013
|
let max = 0;
|
|
795
1014
|
for (const t of ch.height.tiles.values()) {
|
|
796
1015
|
if (t.uniform !== null) max = Math.max(max, t.uniform & 255);
|
|
797
1016
|
else for (let i = 0; i < t.data.length; i++) if (t.data[i] > max) max = t.data[i];
|
|
798
1017
|
}
|
|
799
|
-
|
|
1018
|
+
const bounds = unionRects(ch.height.bounds, ch.film.bounds);
|
|
1019
|
+
return { bounds, maxHeight: max, tiles: ch.height.tiles.size || ch.film.tiles.size };
|
|
800
1020
|
}
|
|
801
1021
|
function thickToJSON(ch) {
|
|
802
|
-
const b = ch.height.bounds;
|
|
1022
|
+
const b = unionRects(ch.height.bounds, ch.film.bounds);
|
|
803
1023
|
if (!b) return null;
|
|
804
|
-
|
|
1024
|
+
const out = {
|
|
1025
|
+
rect: { ...b },
|
|
1026
|
+
height: pngDataUrl(b.width, b.height, readRegion(ch.height, b), 1),
|
|
1027
|
+
material: pngDataUrl(b.width, b.height, readRegion(ch.material, b), 4)
|
|
1028
|
+
};
|
|
1029
|
+
if (ch.film.bounds) out.film = pngDataUrl(b.width, b.height, readRegion(ch.film, b), 4);
|
|
1030
|
+
return out;
|
|
805
1031
|
}
|
|
806
1032
|
function thickFromJSON(raw) {
|
|
807
1033
|
const r = raw;
|
|
@@ -814,9 +1040,14 @@ function thickFromJSON(raw) {
|
|
|
814
1040
|
const h = decode(r.height, true);
|
|
815
1041
|
const m = decode(r.material, false);
|
|
816
1042
|
const hd = h.channels === 1 ? h.data : Uint8ClampedArray.from({ length: h.width * h.height }, (_, i) => h.data[i * 4]);
|
|
1043
|
+
const film = typeof r.film === "string" ? (() => {
|
|
1044
|
+
const f = decode(r.film, false);
|
|
1045
|
+
return surfaceFromPixels({ width: f.width, height: f.height, data: f.data }, 4, r.rect.x, r.rect.y);
|
|
1046
|
+
})() : new TiledSurface(4, /* @__PURE__ */ new Map(), 0);
|
|
817
1047
|
return {
|
|
818
1048
|
height: surfaceFromPixels({ width: h.width, height: h.height, data: hd }, 1, r.rect.x, r.rect.y),
|
|
819
|
-
material: surfaceFromPixels({ width: m.width, height: m.height, data: m.data }, 4, r.rect.x, r.rect.y)
|
|
1049
|
+
material: surfaceFromPixels({ width: m.width, height: m.height, data: m.data }, 4, r.rect.x, r.rect.y),
|
|
1050
|
+
film
|
|
820
1051
|
};
|
|
821
1052
|
}
|
|
822
1053
|
|
|
@@ -1075,6 +1306,567 @@ function lightFromJSON(raw) {
|
|
|
1075
1306
|
return surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
|
|
1076
1307
|
}
|
|
1077
1308
|
|
|
1309
|
+
// src/paint-editor/heat.ts
|
|
1310
|
+
var DEFAULT_UV_LIGHT = { on: false, strength: 1, ambient: 0.15 };
|
|
1311
|
+
function normalizeUvLight(raw = {}, base = DEFAULT_UV_LIGHT) {
|
|
1312
|
+
const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
1313
|
+
return {
|
|
1314
|
+
on: typeof raw.on === "boolean" ? raw.on : base.on,
|
|
1315
|
+
strength: Math.min(2, Math.max(0, num(raw.strength, base.strength))),
|
|
1316
|
+
ambient: Math.min(1, Math.max(0, num(raw.ambient, base.ambient)))
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function uvLightKey(uv) {
|
|
1320
|
+
return `${uv.on ? 1 : 0}|${uv.strength.toFixed(3)}|${uv.ambient.toFixed(3)}`;
|
|
1321
|
+
}
|
|
1322
|
+
var uvPapers = /* @__PURE__ */ new WeakMap();
|
|
1323
|
+
function uvPaper(paper, uv) {
|
|
1324
|
+
if (!uv.on || uv.ambient >= 1) return paper;
|
|
1325
|
+
const cached = uvPapers.get(paper);
|
|
1326
|
+
if (cached && cached.ambient === uv.ambient) return cached.paper;
|
|
1327
|
+
const [r, g, b] = hexToRgb8(paper.color);
|
|
1328
|
+
const toHex = (v) => Math.round(Math.min(255, Math.max(0, v * uv.ambient))).toString(16).padStart(2, "0");
|
|
1329
|
+
const next = { ...paper, color: `#${toHex(r)}${toHex(g)}${toHex(b)}` };
|
|
1330
|
+
uvPapers.set(paper, { ambient: uv.ambient, paper: next });
|
|
1331
|
+
return next;
|
|
1332
|
+
}
|
|
1333
|
+
function emptyFluor() {
|
|
1334
|
+
return new TiledSurface(4, /* @__PURE__ */ new Map(), 0);
|
|
1335
|
+
}
|
|
1336
|
+
function emptyThermo() {
|
|
1337
|
+
return new TiledSurface(4, /* @__PURE__ */ new Map(), 0);
|
|
1338
|
+
}
|
|
1339
|
+
function expand4(t) {
|
|
1340
|
+
if (t.data.length === TILE * TILE * 4) return t.data;
|
|
1341
|
+
const out = new Uint8ClampedArray(TILE * TILE * 4);
|
|
1342
|
+
const u = t.uniform ?? 0;
|
|
1343
|
+
const bytes = [u >>> 24 & 255, u >>> 16 & 255, u >>> 8 & 255, u & 255];
|
|
1344
|
+
for (let i = 0; i < out.length; i += 4) out.set(bytes, i);
|
|
1345
|
+
return out;
|
|
1346
|
+
}
|
|
1347
|
+
function fluorTwinBrush(brush) {
|
|
1348
|
+
const f = brush.fluorescence.amount;
|
|
1349
|
+
const base = { ...brush, light: { amount: 0, pressure: false }, fluorescence: { amount: 0, invisible: false }, thermochromic: { hotColor: null, threshold: 0.5 }, tip: { ...brush.tip, blend: "normal" } };
|
|
1350
|
+
if (brush.engine === "eraser" || brush.engine === "smudge" || brush.engine === "blender") return base;
|
|
1351
|
+
const move = brush.engine === "impasto" ? brush.impasto.tool === "knife" ? "smudge" : "dab" : brush.engine;
|
|
1352
|
+
if (f > 0) {
|
|
1353
|
+
return normalizePaintBrush(
|
|
1354
|
+
{
|
|
1355
|
+
...base,
|
|
1356
|
+
engine: move,
|
|
1357
|
+
tip: { ...base.tip, opacity: brush.tip.opacity * f, blend: "normal" }
|
|
1358
|
+
},
|
|
1359
|
+
base
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
if (brush.engine === "mixer" || brush.behavior.pickup > 0 || brush.engine === "impasto" && brush.impasto.tool === "knife") {
|
|
1363
|
+
return normalizePaintBrush({ ...base, engine: move }, base);
|
|
1364
|
+
}
|
|
1365
|
+
return normalizePaintBrush(
|
|
1366
|
+
{
|
|
1367
|
+
...base,
|
|
1368
|
+
engine: "eraser",
|
|
1369
|
+
tip: { ...base.tip, opacity: brush.tip.opacity },
|
|
1370
|
+
paint: null
|
|
1371
|
+
},
|
|
1372
|
+
base
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
function thermoTwinBrush(brush) {
|
|
1376
|
+
const active = brush.thermochromic.hotColor !== null;
|
|
1377
|
+
const base = {
|
|
1378
|
+
...brush,
|
|
1379
|
+
light: { amount: 0, pressure: false },
|
|
1380
|
+
fluorescence: { amount: 0, invisible: false },
|
|
1381
|
+
thermochromic: { hotColor: null, threshold: 0.5 },
|
|
1382
|
+
paint: null,
|
|
1383
|
+
tip: { ...brush.tip, blend: "normal" }
|
|
1384
|
+
};
|
|
1385
|
+
if (brush.engine === "eraser" || brush.engine === "smudge" || brush.engine === "blender") return base;
|
|
1386
|
+
const move = brush.engine === "impasto" ? brush.impasto.tool === "knife" ? "smudge" : "dab" : brush.engine;
|
|
1387
|
+
if (active) {
|
|
1388
|
+
return normalizePaintBrush({ ...base, engine: move, behavior: { ...base.behavior, pickup: 0 } }, base);
|
|
1389
|
+
}
|
|
1390
|
+
if (brush.engine === "mixer" || brush.behavior.pickup > 0 || brush.engine === "impasto" && brush.impasto.tool === "knife") {
|
|
1391
|
+
return normalizePaintBrush({ ...base, engine: move }, base);
|
|
1392
|
+
}
|
|
1393
|
+
return normalizePaintBrush({ ...base, engine: "eraser", tip: { ...base.tip, opacity: brush.tip.opacity } }, base);
|
|
1394
|
+
}
|
|
1395
|
+
function finalizeThermoStroke(twinOut, before, hotRgb, threshold) {
|
|
1396
|
+
const thByte = Math.max(1, Math.min(255, Math.round(threshold * 255)));
|
|
1397
|
+
const out = new Map(before.tiles);
|
|
1398
|
+
const keys = /* @__PURE__ */ new Set([...before.tiles.keys(), ...twinOut.tiles.keys()]);
|
|
1399
|
+
for (const key of keys) {
|
|
1400
|
+
const afterTile = twinOut.tiles.get(key);
|
|
1401
|
+
const beforeTile = before.tiles.get(key);
|
|
1402
|
+
if (afterTile === beforeTile) continue;
|
|
1403
|
+
if (!afterTile) {
|
|
1404
|
+
out.delete(key);
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
const aData = expand4(afterTile);
|
|
1408
|
+
const bData = beforeTile ? expand4(beforeTile) : null;
|
|
1409
|
+
const dst = new Uint8ClampedArray(TILE * TILE * 4);
|
|
1410
|
+
for (let i = 0; i < dst.length; i += 4) {
|
|
1411
|
+
const cov = aData[i + 3] / 255;
|
|
1412
|
+
const prevTh = bData ? bData[i + 3] : 0;
|
|
1413
|
+
if (cov <= 0 && prevTh === 0) continue;
|
|
1414
|
+
if (prevTh === 0) {
|
|
1415
|
+
dst[i] = hotRgb[0];
|
|
1416
|
+
dst[i + 1] = hotRgb[1];
|
|
1417
|
+
dst[i + 2] = hotRgb[2];
|
|
1418
|
+
dst[i + 3] = thByte;
|
|
1419
|
+
} else if (cov <= 0) {
|
|
1420
|
+
dst[i] = bData[i];
|
|
1421
|
+
dst[i + 1] = bData[i + 1];
|
|
1422
|
+
dst[i + 2] = bData[i + 2];
|
|
1423
|
+
dst[i + 3] = prevTh;
|
|
1424
|
+
} else {
|
|
1425
|
+
dst[i] = Math.round(bData[i] * (1 - cov) + hotRgb[0] * cov);
|
|
1426
|
+
dst[i + 1] = Math.round(bData[i + 1] * (1 - cov) + hotRgb[1] * cov);
|
|
1427
|
+
dst[i + 2] = Math.round(bData[i + 2] * (1 - cov) + hotRgb[2] * cov);
|
|
1428
|
+
dst[i + 3] = Math.max(1, Math.round(prevTh * (1 - cov) + thByte * cov));
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const ft = finishTile(dst, 4, 0);
|
|
1432
|
+
if (ft) out.set(key, ft);
|
|
1433
|
+
else out.delete(key);
|
|
1434
|
+
}
|
|
1435
|
+
return new TiledSurface(4, out, 0);
|
|
1436
|
+
}
|
|
1437
|
+
function scaleFluor(surface, k) {
|
|
1438
|
+
if (k <= 0 || !surface.tiles.size) return emptyFluor();
|
|
1439
|
+
if (k >= 1) return new TiledSurface(4, new Map(surface.tiles), 0);
|
|
1440
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
1441
|
+
for (const [key, tile] of surface.tiles) {
|
|
1442
|
+
const src = expand4(tile);
|
|
1443
|
+
const dst = new Uint8ClampedArray(src);
|
|
1444
|
+
for (let i = 3; i < dst.length; i += 4) dst[i] = Math.round(dst[i] * k);
|
|
1445
|
+
const ft = finishTile(dst, 4, 0);
|
|
1446
|
+
if (ft) tiles.set(key, ft);
|
|
1447
|
+
}
|
|
1448
|
+
return new TiledSurface(4, tiles, 0);
|
|
1449
|
+
}
|
|
1450
|
+
function mergeFourChannel(below, above) {
|
|
1451
|
+
if (!below?.tiles.size && !above?.tiles.size) return null;
|
|
1452
|
+
if (!below?.tiles.size) return new TiledSurface(4, new Map(above.tiles), 0);
|
|
1453
|
+
if (!above?.tiles.size) return new TiledSurface(4, new Map(below.tiles), 0);
|
|
1454
|
+
const keys = /* @__PURE__ */ new Set([...below.tiles.keys(), ...above.tiles.keys()]);
|
|
1455
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
1456
|
+
for (const key of keys) {
|
|
1457
|
+
const b = below.tiles.get(key);
|
|
1458
|
+
const a = above.tiles.get(key);
|
|
1459
|
+
if (!a) {
|
|
1460
|
+
tiles.set(key, b);
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (!b) {
|
|
1464
|
+
tiles.set(key, a);
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
const bd = expand4(b);
|
|
1468
|
+
const ad = expand4(a);
|
|
1469
|
+
const dst = new Uint8ClampedArray(TILE * TILE * 4);
|
|
1470
|
+
for (let i = 0; i < dst.length; i += 4) {
|
|
1471
|
+
const sa = ad[i + 3] / 255;
|
|
1472
|
+
const da = bd[i + 3] / 255;
|
|
1473
|
+
const oa = sa + da * (1 - sa);
|
|
1474
|
+
if (oa <= 0) continue;
|
|
1475
|
+
for (let c = 0; c < 3; c++) dst[i + c] = Math.round((ad[i + c] * sa + bd[i + c] * da * (1 - sa)) / oa);
|
|
1476
|
+
dst[i + 3] = Math.round(oa * 255);
|
|
1477
|
+
}
|
|
1478
|
+
const ft = finishTile(dst, 4, 0);
|
|
1479
|
+
if (ft) tiles.set(key, ft);
|
|
1480
|
+
}
|
|
1481
|
+
return tiles.size ? new TiledSurface(4, tiles, 0) : null;
|
|
1482
|
+
}
|
|
1483
|
+
function fluorIntensitySurface(fluor) {
|
|
1484
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
1485
|
+
for (const [key, tile] of fluor.tiles) {
|
|
1486
|
+
const src = expand4(tile);
|
|
1487
|
+
const dst = new Uint8ClampedArray(TILE * TILE);
|
|
1488
|
+
for (let p = 0; p < TILE * TILE; p++) dst[p] = src[p * 4 + 3];
|
|
1489
|
+
const ft = finishTile(dst, 1, 0);
|
|
1490
|
+
if (ft) tiles.set(key, ft);
|
|
1491
|
+
}
|
|
1492
|
+
return new TiledSurface(1, tiles, 0);
|
|
1493
|
+
}
|
|
1494
|
+
function computeUvSpill(fluor, strength) {
|
|
1495
|
+
if (!fluor.tiles.size || strength <= 0) return null;
|
|
1496
|
+
const intensity = fluorIntensitySurface(fluor);
|
|
1497
|
+
if (!intensity.tiles.size) return null;
|
|
1498
|
+
return computeSpill(fluor, intensity, { radius: 20, strength: Math.min(2, 0.9 * strength) });
|
|
1499
|
+
}
|
|
1500
|
+
var UvCache = class {
|
|
1501
|
+
tiles = /* @__PURE__ */ new Map();
|
|
1502
|
+
spillCache = null;
|
|
1503
|
+
render(color, fluor, uv) {
|
|
1504
|
+
if (!uv.on) return color;
|
|
1505
|
+
const uk = uvLightKey(uv);
|
|
1506
|
+
const keys = /* @__PURE__ */ new Set([...color.tiles.keys(), ...fluor ? fluor.tiles.keys() : []]);
|
|
1507
|
+
const out = /* @__PURE__ */ new Map();
|
|
1508
|
+
for (const key of keys) {
|
|
1509
|
+
const cTile = color.tiles.get(key) ?? null;
|
|
1510
|
+
const fTile = fluor?.tiles.get(key) ?? null;
|
|
1511
|
+
if (!cTile && (!fTile || uv.strength <= 0)) continue;
|
|
1512
|
+
const inputs = [cTile, fTile, uk];
|
|
1513
|
+
const cached = this.tiles.get(key);
|
|
1514
|
+
if (cached && cached.inputs.length === inputs.length && cached.inputs.every((v, i) => v === inputs[i])) {
|
|
1515
|
+
out.set(key, cached.tile);
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
const cd = cTile ? expand4(cTile) : null;
|
|
1519
|
+
const fd = fTile && uv.strength > 0 ? expand4(fTile) : null;
|
|
1520
|
+
const dst = new Uint8ClampedArray(TILE * TILE * 4);
|
|
1521
|
+
const amb = uv.ambient;
|
|
1522
|
+
const str = uv.strength;
|
|
1523
|
+
for (let i = 0; i < dst.length; i += 4) {
|
|
1524
|
+
const ca = cd ? cd[i + 3] : 0;
|
|
1525
|
+
const fa = fd ? fd[i + 3] : 0;
|
|
1526
|
+
if (ca === 0 && fa === 0) continue;
|
|
1527
|
+
const fGain = fa / 255 * str;
|
|
1528
|
+
const fCov = Math.min(1, fGain);
|
|
1529
|
+
for (let c = 0; c < 3; c++) {
|
|
1530
|
+
const baseLin = ca > 0 ? SRGB_TO_LINEAR[cd[i + c]] * amb : 0;
|
|
1531
|
+
const emLin = fa > 0 ? SRGB_TO_LINEAR[fd[i + c]] * (1 + 1.15 * fGain) : 0;
|
|
1532
|
+
dst[i + c] = linearToSrgb8(baseLin * (1 - fCov * 0.5) + emLin * fCov);
|
|
1533
|
+
}
|
|
1534
|
+
dst[i + 3] = Math.min(255, Math.max(ca, Math.round(fa * Math.min(1, str))));
|
|
1535
|
+
}
|
|
1536
|
+
const ft = finishTile(dst, 4, 0);
|
|
1537
|
+
if (ft) {
|
|
1538
|
+
this.tiles.set(key, { inputs, tile: ft });
|
|
1539
|
+
out.set(key, ft);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);
|
|
1543
|
+
return new TiledSurface(4, out, color.defaultValue);
|
|
1544
|
+
}
|
|
1545
|
+
spill(fluor, uv) {
|
|
1546
|
+
if (!uv.on || uv.strength <= 0 || !fluor.tiles.size) return null;
|
|
1547
|
+
if (this.spillCache && this.spillCache.fluor === fluor && this.spillCache.strength === uv.strength) {
|
|
1548
|
+
return this.spillCache.surface;
|
|
1549
|
+
}
|
|
1550
|
+
const surface = computeUvSpill(fluor, uv.strength);
|
|
1551
|
+
this.spillCache = { fluor, strength: uv.strength, surface };
|
|
1552
|
+
return surface;
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
var HEAT_SCALE = 4;
|
|
1556
|
+
var HEAT_DIFFUSION = 0.18;
|
|
1557
|
+
var HEAT_COOLING = 0.03;
|
|
1558
|
+
var HEAT_DRY_THRESHOLD = 0.5;
|
|
1559
|
+
function createHeatField(width, height) {
|
|
1560
|
+
const gridWidth = Math.max(1, Math.ceil(width / HEAT_SCALE));
|
|
1561
|
+
const gridHeight = Math.max(1, Math.ceil(height / HEAT_SCALE));
|
|
1562
|
+
return {
|
|
1563
|
+
width,
|
|
1564
|
+
height,
|
|
1565
|
+
gridWidth,
|
|
1566
|
+
gridHeight,
|
|
1567
|
+
cells: new Float64Array(gridWidth * gridHeight),
|
|
1568
|
+
max: 0,
|
|
1569
|
+
revision: 0
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function cloneHeatField(field) {
|
|
1573
|
+
return {
|
|
1574
|
+
width: field.width,
|
|
1575
|
+
height: field.height,
|
|
1576
|
+
gridWidth: field.gridWidth,
|
|
1577
|
+
gridHeight: field.gridHeight,
|
|
1578
|
+
cells: new Float64Array(field.cells),
|
|
1579
|
+
max: field.max,
|
|
1580
|
+
revision: field.revision
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
function distToSegment(px, py, ax, ay, bx, by) {
|
|
1584
|
+
const dx = bx - ax;
|
|
1585
|
+
const dy = by - ay;
|
|
1586
|
+
const len2 = dx * dx + dy * dy;
|
|
1587
|
+
if (len2 <= 1e-9) return Math.hypot(px - ax, py - ay);
|
|
1588
|
+
const t = Math.min(1, Math.max(0, ((px - ax) * dx + (py - ay) * dy) / len2));
|
|
1589
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
1590
|
+
}
|
|
1591
|
+
var round3 = (n) => Math.round(n * 1e3) / 1e3;
|
|
1592
|
+
function heatStats(field) {
|
|
1593
|
+
if (!field) return { max: 0, mean: 0, hotCells: 0, bounds: null };
|
|
1594
|
+
let max = 0;
|
|
1595
|
+
let sum = 0;
|
|
1596
|
+
let hot = 0;
|
|
1597
|
+
const n = field.cells.length;
|
|
1598
|
+
for (let i = 0; i < n; i++) {
|
|
1599
|
+
const v = field.cells[i];
|
|
1600
|
+
if (v > max) max = v;
|
|
1601
|
+
sum += v;
|
|
1602
|
+
if (v >= 0.05) hot++;
|
|
1603
|
+
}
|
|
1604
|
+
field.max = round3(max);
|
|
1605
|
+
return {
|
|
1606
|
+
max: field.max,
|
|
1607
|
+
mean: n > 0 ? round3(sum / n) : 0,
|
|
1608
|
+
hotCells: hot,
|
|
1609
|
+
bounds: hot > 0 ? hotCellsBounds(field, 0.05) : null
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
function hotCellsBounds(field, threshold = HEAT_DRY_THRESHOLD) {
|
|
1613
|
+
let x0 = field.gridWidth;
|
|
1614
|
+
let y0 = field.gridHeight;
|
|
1615
|
+
let x1 = -1;
|
|
1616
|
+
let y1 = -1;
|
|
1617
|
+
for (let cy = 0; cy < field.gridHeight; cy++) {
|
|
1618
|
+
for (let cx = 0; cx < field.gridWidth; cx++) {
|
|
1619
|
+
if (field.cells[cy * field.gridWidth + cx] >= threshold) {
|
|
1620
|
+
if (cx < x0) x0 = cx;
|
|
1621
|
+
if (cy < y0) y0 = cy;
|
|
1622
|
+
if (cx > x1) x1 = cx;
|
|
1623
|
+
if (cy > y1) y1 = cy;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
if (x1 < x0) return null;
|
|
1628
|
+
const rx = x0 * HEAT_SCALE;
|
|
1629
|
+
const ry = y0 * HEAT_SCALE;
|
|
1630
|
+
const rw = Math.min(field.width - rx, (x1 - x0 + 1) * HEAT_SCALE);
|
|
1631
|
+
const rh = Math.min(field.height - ry, (y1 - y0 + 1) * HEAT_SCALE);
|
|
1632
|
+
return rw > 0 && rh > 0 ? { x: rx, y: ry, width: rw, height: rh } : null;
|
|
1633
|
+
}
|
|
1634
|
+
function paintHeatField(field, points, radius = 32, temperature = 0.85) {
|
|
1635
|
+
const r = Math.min(512, Math.max(1, radius));
|
|
1636
|
+
const temp = Math.min(1, Math.max(0, temperature));
|
|
1637
|
+
const gw = field.gridWidth;
|
|
1638
|
+
const gh = field.gridHeight;
|
|
1639
|
+
let minCx = gw;
|
|
1640
|
+
let minCy = gh;
|
|
1641
|
+
let maxCx = -1;
|
|
1642
|
+
let maxCy = -1;
|
|
1643
|
+
const segCount = Math.max(1, points.length - 1);
|
|
1644
|
+
for (let s = 0; s < segCount; s++) {
|
|
1645
|
+
const a = points[s];
|
|
1646
|
+
const b = points[Math.min(points.length - 1, s + 1)];
|
|
1647
|
+
const x0 = Math.max(0, Math.floor((Math.min(a.x, b.x) - r) / HEAT_SCALE));
|
|
1648
|
+
const y0 = Math.max(0, Math.floor((Math.min(a.y, b.y) - r) / HEAT_SCALE));
|
|
1649
|
+
const x1 = Math.min(gw - 1, Math.ceil((Math.max(a.x, b.x) + r) / HEAT_SCALE));
|
|
1650
|
+
const y1 = Math.min(gh - 1, Math.ceil((Math.max(a.y, b.y) + r) / HEAT_SCALE));
|
|
1651
|
+
for (let cy = y0; cy <= y1; cy++) {
|
|
1652
|
+
const gy = (cy + 0.5) * HEAT_SCALE;
|
|
1653
|
+
for (let cx = x0; cx <= x1; cx++) {
|
|
1654
|
+
const gx = (cx + 0.5) * HEAT_SCALE;
|
|
1655
|
+
const d = distToSegment(gx, gy, a.x, a.y, b.x, b.y);
|
|
1656
|
+
if (d >= r) continue;
|
|
1657
|
+
const u = 1 - d / r;
|
|
1658
|
+
const w = u * u * (3 - 2 * u);
|
|
1659
|
+
const idx = cy * gw + cx;
|
|
1660
|
+
const cur = field.cells[idx];
|
|
1661
|
+
const next = cur + (temp - cur) * w;
|
|
1662
|
+
field.cells[idx] = next < 1e-4 ? 0 : Math.min(1, Math.max(0, next));
|
|
1663
|
+
if (cx < minCx) minCx = cx;
|
|
1664
|
+
if (cy < minCy) minCy = cy;
|
|
1665
|
+
if (cx > maxCx) maxCx = cx;
|
|
1666
|
+
if (cy > maxCy) maxCy = cy;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
field.revision++;
|
|
1671
|
+
const bounds = maxCx >= minCx ? {
|
|
1672
|
+
x: minCx * HEAT_SCALE,
|
|
1673
|
+
y: minCy * HEAT_SCALE,
|
|
1674
|
+
width: Math.min(field.width - minCx * HEAT_SCALE, (maxCx - minCx + 1) * HEAT_SCALE),
|
|
1675
|
+
height: Math.min(field.height - minCy * HEAT_SCALE, (maxCy - minCy + 1) * HEAT_SCALE)
|
|
1676
|
+
} : { x: 0, y: 0, width: 0, height: 0 };
|
|
1677
|
+
return { bounds, stats: heatStats(field) };
|
|
1678
|
+
}
|
|
1679
|
+
function advanceHeatField(field, steps = 10) {
|
|
1680
|
+
const n = Math.min(1e3, Math.max(1, Math.round(steps)));
|
|
1681
|
+
const gw = field.gridWidth;
|
|
1682
|
+
const gh = field.gridHeight;
|
|
1683
|
+
let src = field.cells;
|
|
1684
|
+
let dst = new Float64Array(src.length);
|
|
1685
|
+
const decay = 1 - HEAT_COOLING;
|
|
1686
|
+
for (let s = 0; s < n; s++) {
|
|
1687
|
+
for (let y = 0; y < gh; y++) {
|
|
1688
|
+
const yUp = Math.max(0, y - 1) * gw;
|
|
1689
|
+
const yCur = y * gw;
|
|
1690
|
+
const yDn = Math.min(gh - 1, y + 1) * gw;
|
|
1691
|
+
for (let x = 0; x < gw; x++) {
|
|
1692
|
+
const xL = Math.max(0, x - 1);
|
|
1693
|
+
const xR = Math.min(gw - 1, x + 1);
|
|
1694
|
+
const c = src[yCur + x];
|
|
1695
|
+
const avg = (src[yCur + xL] + src[yCur + xR] + src[yUp + x] + src[yDn + x]) * 0.25;
|
|
1696
|
+
const v = (c + HEAT_DIFFUSION * (avg - c)) * decay;
|
|
1697
|
+
dst[yCur + x] = v < 1e-4 ? 0 : v;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const tmp = src;
|
|
1701
|
+
src = dst;
|
|
1702
|
+
dst = tmp;
|
|
1703
|
+
}
|
|
1704
|
+
if (src !== field.cells) field.cells.set(src);
|
|
1705
|
+
field.revision++;
|
|
1706
|
+
return heatStats(field);
|
|
1707
|
+
}
|
|
1708
|
+
function clearHeatField(field) {
|
|
1709
|
+
let had = false;
|
|
1710
|
+
for (let i = 0; i < field.cells.length; i++) {
|
|
1711
|
+
if (field.cells[i] > 0) {
|
|
1712
|
+
had = true;
|
|
1713
|
+
field.cells[i] = 0;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
field.max = 0;
|
|
1717
|
+
if (had) field.revision++;
|
|
1718
|
+
return had;
|
|
1719
|
+
}
|
|
1720
|
+
function sampleHeatAt(field, x, y) {
|
|
1721
|
+
const gx = x / HEAT_SCALE - 0.5;
|
|
1722
|
+
const gy = y / HEAT_SCALE - 0.5;
|
|
1723
|
+
const gw = field.gridWidth;
|
|
1724
|
+
const gh = field.gridHeight;
|
|
1725
|
+
const x0 = Math.max(0, Math.min(gw - 1, Math.floor(gx)));
|
|
1726
|
+
const y0 = Math.max(0, Math.min(gh - 1, Math.floor(gy)));
|
|
1727
|
+
const x1 = Math.max(0, Math.min(gw - 1, x0 + 1));
|
|
1728
|
+
const y1 = Math.max(0, Math.min(gh - 1, y0 + 1));
|
|
1729
|
+
const fx = Math.min(1, Math.max(0, gx - Math.floor(gx)));
|
|
1730
|
+
const fy = Math.min(1, Math.max(0, gy - Math.floor(gy)));
|
|
1731
|
+
const c00 = field.cells[y0 * gw + x0];
|
|
1732
|
+
const c10 = field.cells[y0 * gw + x1];
|
|
1733
|
+
const c01 = field.cells[y1 * gw + x0];
|
|
1734
|
+
const c11 = field.cells[y1 * gw + x1];
|
|
1735
|
+
const top = c00 * (1 - fx) + c10 * fx;
|
|
1736
|
+
const bot = c01 * (1 - fx) + c11 * fx;
|
|
1737
|
+
return top * (1 - fy) + bot * fy;
|
|
1738
|
+
}
|
|
1739
|
+
function shiftHeatField(field, dx, dy, newWidth, newHeight) {
|
|
1740
|
+
const next = createHeatField(newWidth, newHeight);
|
|
1741
|
+
for (let cy = 0; cy < next.gridHeight; cy++) {
|
|
1742
|
+
const docY = (cy + 0.5) * HEAT_SCALE - dy;
|
|
1743
|
+
if (docY < 0 || docY >= field.height) continue;
|
|
1744
|
+
for (let cx = 0; cx < next.gridWidth; cx++) {
|
|
1745
|
+
const docX = (cx + 0.5) * HEAT_SCALE - dx;
|
|
1746
|
+
if (docX < 0 || docX >= field.width) continue;
|
|
1747
|
+
next.cells[cy * next.gridWidth + cx] = sampleHeatAt(field, docX, docY);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
next.revision = field.revision + 1;
|
|
1751
|
+
heatStats(next);
|
|
1752
|
+
return next;
|
|
1753
|
+
}
|
|
1754
|
+
var ThermoCache = class {
|
|
1755
|
+
tiles = /* @__PURE__ */ new Map();
|
|
1756
|
+
render(color, thermo, offset, heat) {
|
|
1757
|
+
if (!thermo?.tiles.size) return color;
|
|
1758
|
+
let anyHeat = false;
|
|
1759
|
+
for (let i = 0; i < heat.cells.length; i++) {
|
|
1760
|
+
if (heat.cells[i] > 0) {
|
|
1761
|
+
anyHeat = true;
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
if (!anyHeat) return color;
|
|
1766
|
+
const out = new Map(color.tiles);
|
|
1767
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1768
|
+
for (const key of color.tiles.keys()) keys.add(key);
|
|
1769
|
+
for (const [key, cTile] of color.tiles) {
|
|
1770
|
+
const tTile = thermo.tiles.get(key);
|
|
1771
|
+
if (!tTile) continue;
|
|
1772
|
+
const inputs = [cTile, tTile, offset.x, offset.y, heat.revision];
|
|
1773
|
+
const cached = this.tiles.get(key);
|
|
1774
|
+
if (cached && cached.inputs.length === inputs.length && cached.inputs.every((v, i) => v === inputs[i])) {
|
|
1775
|
+
out.set(key, cached.tile);
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
const tx = key % 65536 - 32768;
|
|
1779
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
1780
|
+
const cd = expand4(cTile);
|
|
1781
|
+
const td = expand4(tTile);
|
|
1782
|
+
const dst = new Uint8ClampedArray(cd);
|
|
1783
|
+
const band = 0.12;
|
|
1784
|
+
for (let y = 0; y < TILE; y++) {
|
|
1785
|
+
const docY = ty * TILE + y + offset.y + 0.5;
|
|
1786
|
+
for (let x = 0; x < TILE; x++) {
|
|
1787
|
+
const i = (y * TILE + x) * 4;
|
|
1788
|
+
const ca = cd[i + 3];
|
|
1789
|
+
const thByte = td[i + 3];
|
|
1790
|
+
if (ca === 0 || thByte === 0) continue;
|
|
1791
|
+
const docX = tx * TILE + x + offset.x + 0.5;
|
|
1792
|
+
const temp = sampleHeatAt(heat, docX, docY);
|
|
1793
|
+
if (temp <= 0) continue;
|
|
1794
|
+
const threshold = thByte / 255;
|
|
1795
|
+
const u = Math.min(1, Math.max(0, (temp - (threshold - band)) / (2 * band)));
|
|
1796
|
+
const t = u * u * (3 - 2 * u);
|
|
1797
|
+
if (t <= 0) continue;
|
|
1798
|
+
dst[i] = Math.round(cd[i] * (1 - t) + td[i] * t);
|
|
1799
|
+
dst[i + 1] = Math.round(cd[i + 1] * (1 - t) + td[i + 1] * t);
|
|
1800
|
+
dst[i + 2] = Math.round(cd[i + 2] * (1 - t) + td[i + 2] * t);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const shadedTile = { data: dst, uniform: null };
|
|
1804
|
+
this.tiles.set(key, { inputs, tile: shadedTile });
|
|
1805
|
+
out.set(key, shadedTile);
|
|
1806
|
+
}
|
|
1807
|
+
for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);
|
|
1808
|
+
return new TiledSurface(4, out, color.defaultValue);
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
function fluorToJSON(fluor) {
|
|
1812
|
+
const b = fluor?.bounds;
|
|
1813
|
+
if (!fluor || !b) return null;
|
|
1814
|
+
return { rect: { ...b }, fluor: pngDataUrl(b.width, b.height, readRegion(fluor, b), 4) };
|
|
1815
|
+
}
|
|
1816
|
+
function fluorFromJSON(raw) {
|
|
1817
|
+
const r = raw;
|
|
1818
|
+
if (!r || typeof r !== "object" || !r.rect || typeof r.fluor !== "string") throw new Error("not a saved fluorescence channel");
|
|
1819
|
+
const parsed = parseDataUrl(r.fluor);
|
|
1820
|
+
if (!parsed) throw new Error("the fluorescence channel is not a PNG data URL");
|
|
1821
|
+
const img = decodePng(parsed.bytes);
|
|
1822
|
+
return surfaceFromPixels({ width: img.width, height: img.height, data: img.data }, 4, r.rect.x, r.rect.y);
|
|
1823
|
+
}
|
|
1824
|
+
function thermoToJSON(thermo) {
|
|
1825
|
+
const b = thermo?.bounds;
|
|
1826
|
+
if (!thermo || !b) return null;
|
|
1827
|
+
return { rect: { ...b }, thermo: pngDataUrl(b.width, b.height, readRegion(thermo, b), 4) };
|
|
1828
|
+
}
|
|
1829
|
+
function thermoFromJSON(raw) {
|
|
1830
|
+
const r = raw;
|
|
1831
|
+
if (!r || typeof r !== "object" || !r.rect || typeof r.thermo !== "string") throw new Error("not a saved thermochromic channel");
|
|
1832
|
+
const parsed = parseDataUrl(r.thermo);
|
|
1833
|
+
if (!parsed) throw new Error("the thermochromic channel is not a PNG data URL");
|
|
1834
|
+
const img = decodePng(parsed.bytes);
|
|
1835
|
+
return surfaceFromPixels({ width: img.width, height: img.height, data: img.data }, 4, r.rect.x, r.rect.y);
|
|
1836
|
+
}
|
|
1837
|
+
function heatToJSON(field) {
|
|
1838
|
+
if (!field) return null;
|
|
1839
|
+
let any = false;
|
|
1840
|
+
for (let i = 0; i < field.cells.length; i++) {
|
|
1841
|
+
if (field.cells[i] > 0) {
|
|
1842
|
+
any = true;
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (!any) return null;
|
|
1847
|
+
return {
|
|
1848
|
+
gridWidth: field.gridWidth,
|
|
1849
|
+
gridHeight: field.gridHeight,
|
|
1850
|
+
cells: Array.from(field.cells, (v) => Math.round(v * 1e4) / 1e4)
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function heatFromJSON(raw, width, height) {
|
|
1854
|
+
const r = raw;
|
|
1855
|
+
if (!r || typeof r !== "object" || typeof r.gridWidth !== "number" || typeof r.gridHeight !== "number" || !Array.isArray(r.cells)) {
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
const field = createHeatField(width, height);
|
|
1859
|
+
if (r.gridWidth === field.gridWidth && r.gridHeight === field.gridHeight && r.cells.length === field.cells.length) {
|
|
1860
|
+
for (let i = 0; i < field.cells.length; i++) {
|
|
1861
|
+
const v = r.cells[i];
|
|
1862
|
+
field.cells[i] = typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0;
|
|
1863
|
+
}
|
|
1864
|
+
field.revision = 1;
|
|
1865
|
+
heatStats(field);
|
|
1866
|
+
}
|
|
1867
|
+
return field;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1078
1870
|
// src/paint-editor/procedural.ts
|
|
1079
1871
|
var STEP_PX = 1.5;
|
|
1080
1872
|
var MarkStroke = class {
|
|
@@ -1223,6 +2015,7 @@ var MarkStroke = class {
|
|
|
1223
2015
|
if (color && color.alpha < 1) for (let k = 0; k < n; k++) vals[k] *= color.alpha;
|
|
1224
2016
|
this.coverage.accumulate(rect, vals, opacity, color ? color.rgb : null);
|
|
1225
2017
|
}
|
|
2018
|
+
if (this.brush.iridescence && this.options.film) this.options.film.deposit(rect, vals, opacity * this.brush.tip.opacity, this.brush.iridescence, pressure);
|
|
1226
2019
|
rects.push(rect);
|
|
1227
2020
|
this.dabCount++;
|
|
1228
2021
|
}
|
|
@@ -2116,8 +2909,11 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2116
2909
|
this.live = new Map(this.base.tiles);
|
|
2117
2910
|
this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
2118
2911
|
const brush = init.brush;
|
|
2912
|
+
this.invisibleDaylight = brush.fluorescence.amount > 0 && brush.fluorescence.invisible && brush.engine !== "eraser" && brush.engine !== "blender" && brush.engine !== "smudge";
|
|
2119
2913
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
2120
2914
|
if (init.light && !init.wet && !init.sticker) this.lightTwin = this.makeLightTwin(init.light.surface);
|
|
2915
|
+
if (init.fluor && !init.wet && !init.sticker) this.fluorTwin = this.makeFluorTwin(init.fluor.surface);
|
|
2916
|
+
if (init.thermo && !init.wet && !init.sticker) this.thermoTwin = this.makeThermoTwin(init.thermo.surface);
|
|
2121
2917
|
this.apply = {
|
|
2122
2918
|
mode: brush.engine === "eraser" ? "erase" : "paint",
|
|
2123
2919
|
color: init.color,
|
|
@@ -2143,10 +2939,18 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2143
2939
|
wet = null;
|
|
2144
2940
|
wetDriver = null;
|
|
2145
2941
|
thickDriver = null;
|
|
2942
|
+
filmBuffer = null;
|
|
2146
2943
|
stickerSpray = null;
|
|
2147
2944
|
/** Light paint: the stroke's twin over the light channel, and what it left there. */
|
|
2148
2945
|
lightTwin = null;
|
|
2149
2946
|
lightResult = null;
|
|
2947
|
+
/** UV-fluorescent ink: the stroke's twin over the fluorescence channel, and what it left there. */
|
|
2948
|
+
fluorTwin = null;
|
|
2949
|
+
fluorResult = null;
|
|
2950
|
+
/** Thermochromic ink: the stroke's twin over the thermochromic channel, and what it left there. */
|
|
2951
|
+
thermoTwin = null;
|
|
2952
|
+
thermoResult = null;
|
|
2953
|
+
invisibleDaylight;
|
|
2150
2954
|
/** A wet stroke that ended while the simulation was mid-batch: it commits when the driver catches up. */
|
|
2151
2955
|
wetEndPending = false;
|
|
2152
2956
|
apply;
|
|
@@ -2161,30 +2965,36 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2161
2965
|
build(effect) {
|
|
2162
2966
|
const init = this.init;
|
|
2163
2967
|
const brush = init.brush;
|
|
2164
|
-
this.dry = this.wet = this.thickDriver = this.stickerSpray = null;
|
|
2968
|
+
this.dry = this.wet = this.thickDriver = this.stickerSpray = this.filmBuffer = null;
|
|
2165
2969
|
if (init.sticker) {
|
|
2166
2970
|
this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null, effect, art: init.sticker.art, variant: init.sticker.variant });
|
|
2167
|
-
} else if (init.thick) {
|
|
2971
|
+
} else if (brush.engine === "impasto" && init.thick) {
|
|
2168
2972
|
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
2169
2973
|
this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect });
|
|
2170
|
-
} else if (brush.engine === "mixer" || brush.engine === "blender" || brush.engine === "smudge" || usesPaintExchange(brush, Boolean(init.paint))) {
|
|
2171
|
-
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
2172
|
-
this.wet = new PaintDabStroke(brush, {
|
|
2173
|
-
target: this.tileTarget(),
|
|
2174
|
-
color: rgb(init.color),
|
|
2175
|
-
secondary: rgb(init.secondary ?? init.background),
|
|
2176
|
-
load: init.mixerLoad,
|
|
2177
|
-
grain: init.grain,
|
|
2178
|
-
paperDepth: init.paperDepth,
|
|
2179
|
-
seed: init.seed,
|
|
2180
|
-
copies: this.copies,
|
|
2181
|
-
paint: init.paint ?? null,
|
|
2182
|
-
effect,
|
|
2183
|
-
stampArt: init.stampArt
|
|
2184
|
-
});
|
|
2185
2974
|
} else {
|
|
2186
|
-
|
|
2187
|
-
|
|
2975
|
+
if (init.thick && (brush.iridescence || brush.engine === "eraser")) {
|
|
2976
|
+
this.filmBuffer = new FilmBuffer(init.thick.channels.film, init.offset, init.selection ?? null);
|
|
2977
|
+
}
|
|
2978
|
+
if (brush.engine === "mixer" || brush.engine === "blender" || brush.engine === "smudge" || usesPaintExchange(brush, Boolean(init.paint))) {
|
|
2979
|
+
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
2980
|
+
this.wet = new PaintDabStroke(brush, {
|
|
2981
|
+
target: this.tileTarget(),
|
|
2982
|
+
color: rgb(init.color),
|
|
2983
|
+
secondary: rgb(init.secondary ?? init.background),
|
|
2984
|
+
load: init.mixerLoad,
|
|
2985
|
+
grain: init.grain,
|
|
2986
|
+
paperDepth: init.paperDepth,
|
|
2987
|
+
seed: init.seed,
|
|
2988
|
+
copies: this.copies,
|
|
2989
|
+
paint: init.paint ?? null,
|
|
2990
|
+
effect,
|
|
2991
|
+
stampArt: init.stampArt,
|
|
2992
|
+
film: this.filmBuffer
|
|
2993
|
+
});
|
|
2994
|
+
} else {
|
|
2995
|
+
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect, stampArt: init.stampArt, film: this.filmBuffer };
|
|
2996
|
+
this.dry = brush.engine === "particle" ? new ParticleStroke(brush, opts) : brush.engine === "bristle" ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);
|
|
2997
|
+
}
|
|
2188
2998
|
}
|
|
2189
2999
|
}
|
|
2190
3000
|
/**
|
|
@@ -2228,16 +3038,83 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2228
3038
|
secondary: [grey, grey, grey, 255],
|
|
2229
3039
|
inputFilter: void 0,
|
|
2230
3040
|
mixerLoad: { reservoir: init.mixerLoad.reservoir ? [g, g, g] : null, amount: init.mixerLoad.amount, pickup: null },
|
|
2231
|
-
thick:
|
|
3041
|
+
thick: void 0,
|
|
2232
3042
|
paint,
|
|
2233
3043
|
effect,
|
|
2234
|
-
light: null
|
|
3044
|
+
light: null,
|
|
3045
|
+
fluor: null,
|
|
3046
|
+
thermo: null
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
/** The stroke's twin over the 4-channel fluorescence surface (see `heat.ts`). */
|
|
3050
|
+
makeFluorTwin(fluor) {
|
|
3051
|
+
const init = this.init;
|
|
3052
|
+
const host = {
|
|
3053
|
+
update: (_id, surface) => {
|
|
3054
|
+
this.fluorResult = surface;
|
|
3055
|
+
},
|
|
3056
|
+
commit: (_s, surface) => {
|
|
3057
|
+
this.fluorResult = surface;
|
|
3058
|
+
}
|
|
3059
|
+
};
|
|
3060
|
+
return new _PaintStrokeSession(host, {
|
|
3061
|
+
...init,
|
|
3062
|
+
surface: fluor,
|
|
3063
|
+
lockTransparency: false,
|
|
3064
|
+
brush: fluorTwinBrush(init.brush),
|
|
3065
|
+
inputFilter: void 0,
|
|
3066
|
+
light: null,
|
|
3067
|
+
fluor: null,
|
|
3068
|
+
thermo: null
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
/** The stroke's twin over the 4-channel thermochromic surface (see `heat.ts`). */
|
|
3072
|
+
makeThermoTwin(thermo) {
|
|
3073
|
+
const init = this.init;
|
|
3074
|
+
const hotHex = init.brush.thermochromic.hotColor;
|
|
3075
|
+
const hotRgb = hotHex ? hexToRgb8(hotHex) : [0, 0, 0];
|
|
3076
|
+
const threshold = init.brush.thermochromic.threshold;
|
|
3077
|
+
const host = {
|
|
3078
|
+
update: (_id, surface) => {
|
|
3079
|
+
this.thermoResult = hotHex ? finalizeThermoStroke(surface, thermo, hotRgb, threshold) : surface;
|
|
3080
|
+
},
|
|
3081
|
+
commit: (_s, surface) => {
|
|
3082
|
+
if (!surface) {
|
|
3083
|
+
this.thermoResult = null;
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
this.thermoResult = hotHex ? finalizeThermoStroke(surface, thermo, hotRgb, threshold) : surface;
|
|
3087
|
+
}
|
|
3088
|
+
};
|
|
3089
|
+
return new _PaintStrokeSession(host, {
|
|
3090
|
+
...init,
|
|
3091
|
+
surface: hotHex ? emptyThermo() : thermo,
|
|
3092
|
+
lockTransparency: false,
|
|
3093
|
+
brush: thermoTwinBrush(init.brush),
|
|
3094
|
+
color: [hotRgb[0], hotRgb[1], hotRgb[2], 255],
|
|
3095
|
+
colorHex: hotHex ?? "#000000",
|
|
3096
|
+
secondary: [hotRgb[0], hotRgb[1], hotRgb[2], 255],
|
|
3097
|
+
inputFilter: void 0,
|
|
3098
|
+
mixerLoad: hotHex ? { reservoir: [hotRgb[0] / 255, hotRgb[1] / 255, hotRgb[2] / 255], amount: 1, pickup: null } : init.mixerLoad,
|
|
3099
|
+
paint: null,
|
|
3100
|
+
effect: null,
|
|
3101
|
+
light: null,
|
|
3102
|
+
fluor: null,
|
|
3103
|
+
thermo: null
|
|
2235
3104
|
});
|
|
2236
3105
|
}
|
|
2237
3106
|
/** Light paint: the layer's light channel after the stroke, or `null` when the stroke has no twin or left it unchanged. */
|
|
2238
3107
|
lightChannel() {
|
|
2239
3108
|
return this.lightResult;
|
|
2240
3109
|
}
|
|
3110
|
+
/** UV-fluorescent ink: the layer's fluorescence channel after the stroke, or `null` when unchanged. */
|
|
3111
|
+
fluorChannel() {
|
|
3112
|
+
return this.fluorResult;
|
|
3113
|
+
}
|
|
3114
|
+
/** Thermochromic ink: the layer's thermochromic channel after the stroke, or `null` when unchanged. */
|
|
3115
|
+
thermoChannel() {
|
|
3116
|
+
return this.thermoResult;
|
|
3117
|
+
}
|
|
2241
3118
|
tileTarget() {
|
|
2242
3119
|
return {
|
|
2243
3120
|
offset: this.init.offset,
|
|
@@ -2268,6 +3145,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2268
3145
|
this.lastRaw = points[points.length - 1];
|
|
2269
3146
|
this.host.sampled?.(this, points);
|
|
2270
3147
|
this.lightTwin?.push(points);
|
|
3148
|
+
this.fluorTwin?.push(points);
|
|
3149
|
+
this.thermoTwin?.push(points);
|
|
2271
3150
|
if (this.wetDriver) {
|
|
2272
3151
|
this.wetUpdate(this.wetDriver.push(points));
|
|
2273
3152
|
return;
|
|
@@ -2278,9 +3157,12 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2278
3157
|
stickers() {
|
|
2279
3158
|
return this.stickerSpray ? this.stickerSpray.stickers.map((s) => ({ ...s })) : null;
|
|
2280
3159
|
}
|
|
2281
|
-
/** Impasto strokes: the layer's thick-paint channels with the stroke so far. */
|
|
3160
|
+
/** Impasto or iridescent strokes: the layer's thick-paint channels with the stroke so far. */
|
|
2282
3161
|
thickChannels() {
|
|
2283
|
-
return this.thickDriver
|
|
3162
|
+
if (this.thickDriver) return this.thickDriver.channels();
|
|
3163
|
+
const f = this.filmBuffer ?? this.wetDriver?.init.film ?? null;
|
|
3164
|
+
if (f && this.init.thick) return { ...this.init.thick.channels, film: f.surface() };
|
|
3165
|
+
return null;
|
|
2284
3166
|
}
|
|
2285
3167
|
/** Shows a wet stroke's progress (the wet layer's display). */
|
|
2286
3168
|
wetUpdate(rects) {
|
|
@@ -2299,6 +3181,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2299
3181
|
this.finishWet();
|
|
2300
3182
|
}
|
|
2301
3183
|
finishWet() {
|
|
3184
|
+
if (this.fluorTwin?.open) this.fluorTwin.end();
|
|
3185
|
+
if (this.thermoTwin?.open) this.thermoTwin.end();
|
|
2302
3186
|
const d = this.wetDriver;
|
|
2303
3187
|
const changed = d.dabCount > 0;
|
|
2304
3188
|
this.result = { layerId: this.layerId, bounds: changed ? d.bounds : null, dabs: d.dabCount, strokeId: null, wet: true };
|
|
@@ -2309,6 +3193,10 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2309
3193
|
update(rects) {
|
|
2310
3194
|
if (!rects.length) return;
|
|
2311
3195
|
for (const r of rects) this.bounds = union(this.bounds, r);
|
|
3196
|
+
if (this.invisibleDaylight) {
|
|
3197
|
+
this.host.update(this.layerId, this.base, rects);
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
2312
3200
|
const ch = this.base.channels;
|
|
2313
3201
|
if (!this.dry) {
|
|
2314
3202
|
for (const key of this.fresh) this.live.set(key, { data: this.live.get(key).data, uniform: null });
|
|
@@ -2350,18 +3238,24 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2350
3238
|
this.update(this.dry ? this.dry.finish() : this.thickDriver ? this.thickDriver.finish() : this.stickerSpray ? this.stickerSpray.finish() : this.wet.finish());
|
|
2351
3239
|
this.closed = true;
|
|
2352
3240
|
if (this.lightTwin?.open) this.lightTwin.end();
|
|
3241
|
+
if (this.fluorTwin?.open) this.fluorTwin.end();
|
|
3242
|
+
if (this.thermoTwin?.open) this.thermoTwin.end();
|
|
2353
3243
|
const ch = this.base.channels;
|
|
2354
3244
|
const tiles = new Map(this.base.tiles);
|
|
2355
3245
|
let changed = false;
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
3246
|
+
if (this.invisibleDaylight) {
|
|
3247
|
+
changed = this.fluorResult !== null || this.thermoResult !== null || this.lightResult !== null;
|
|
3248
|
+
} else {
|
|
3249
|
+
for (const key of this.touched) {
|
|
3250
|
+
const tile = finishTile(this.live.get(key).data, ch, this.base.defaultValue);
|
|
3251
|
+
const prev = this.base.tiles.get(key);
|
|
3252
|
+
if (tile) tiles.set(key, tile);
|
|
3253
|
+
else tiles.delete(key);
|
|
3254
|
+
if (!sameTile(prev, tile)) changed = true;
|
|
3255
|
+
}
|
|
2362
3256
|
}
|
|
2363
3257
|
if (this.thickDriver && this.thickDriver.dabCount > 0) changed = true;
|
|
2364
|
-
const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;
|
|
3258
|
+
const surface = changed ? this.invisibleDaylight ? this.base : new TiledSurface(ch, tiles, this.base.defaultValue) : null;
|
|
2365
3259
|
const dabs = this.dry ? this.dry.dabCount : this.thickDriver ? this.thickDriver.dabCount : this.stickerSpray ? this.stickerSpray.dabCount : this.wet.dabCount;
|
|
2366
3260
|
this.result = { layerId: this.layerId, bounds: changed ? this.bounds : null, dabs, strokeId: null };
|
|
2367
3261
|
if (this.stickerSpray?.skipped.size) this.result.skippedStickers = [...this.stickerSpray.skipped];
|
|
@@ -2375,7 +3269,11 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2375
3269
|
if (this.closed) return;
|
|
2376
3270
|
this.closed = true;
|
|
2377
3271
|
if (this.lightTwin?.open) this.lightTwin.cancel();
|
|
3272
|
+
if (this.fluorTwin?.open) this.fluorTwin.cancel();
|
|
3273
|
+
if (this.thermoTwin?.open) this.thermoTwin.cancel();
|
|
2378
3274
|
this.lightResult = null;
|
|
3275
|
+
this.fluorResult = null;
|
|
3276
|
+
this.thermoResult = null;
|
|
2379
3277
|
this.host.commit(this, null, true);
|
|
2380
3278
|
}
|
|
2381
3279
|
};
|
|
@@ -3105,6 +4003,7 @@ function paintWetTestStroke(brush, paper, color, width, height) {
|
|
|
3105
4003
|
w.paint("thumb", { x: 0, y: 0, width, height: half }, null, 0.4, Array.from(pigmentOf([r / 255, g / 255, b / 255], 0), (v) => v * 0.05));
|
|
3106
4004
|
w.paint("thumb", { x: 0, y: half, width, height: height - half }, null, 0.4, Array.from(pigmentOf([232 / 255, 196 / 255, 64 / 255], 0), (v) => v * 0.05));
|
|
3107
4005
|
}
|
|
4006
|
+
const film = brush.iridescence && brush.wet.tool === "paint" ? new FilmBuffer(emptySurface(4, 0)) : null;
|
|
3108
4007
|
const driver = new WetStrokeDriver({
|
|
3109
4008
|
world: w,
|
|
3110
4009
|
laneId: "thumb",
|
|
@@ -3115,12 +4014,14 @@ function paintWetTestStroke(brush, paper, color, width, height) {
|
|
|
3115
4014
|
paperDepth: paper.depth,
|
|
3116
4015
|
paperColor: hexToRgb8(paper.color),
|
|
3117
4016
|
seed: 7,
|
|
3118
|
-
copies: [IDENTITY_COPY]
|
|
4017
|
+
copies: [IDENTITY_COPY],
|
|
4018
|
+
film
|
|
3119
4019
|
});
|
|
3120
4020
|
driver.push(thumbnailStroke(width, height));
|
|
3121
4021
|
driver.finish();
|
|
3122
4022
|
if (brush.wet.tool === "blow") w.stepSync(4);
|
|
3123
|
-
|
|
4023
|
+
const surf = w.display("thumb").surface;
|
|
4024
|
+
return film ? new LitCache().lit(surf, { ...emptyThickChannels(), film: film.surface() }, DEFAULT_LIGHTING) : surf;
|
|
3124
4025
|
}
|
|
3125
4026
|
function thumbnailStroke(width, height) {
|
|
3126
4027
|
const pts = [];
|
|
@@ -3139,6 +4040,7 @@ function paintTestStroke(surface, brush, paper, color, width, height, stampArt)
|
|
|
3139
4040
|
let result = surface;
|
|
3140
4041
|
let thick = null;
|
|
3141
4042
|
let light = null;
|
|
4043
|
+
let fluor = null;
|
|
3142
4044
|
const session = new PaintStrokeSession(
|
|
3143
4045
|
{
|
|
3144
4046
|
update: () => {
|
|
@@ -3147,6 +4049,7 @@ function paintTestStroke(surface, brush, paper, color, width, height, stampArt)
|
|
|
3147
4049
|
result = s ?? result;
|
|
3148
4050
|
thick = sess.thickChannels();
|
|
3149
4051
|
light = sess.lightChannel();
|
|
4052
|
+
fluor = sess.fluorChannel();
|
|
3150
4053
|
}
|
|
3151
4054
|
},
|
|
3152
4055
|
{
|
|
@@ -3164,16 +4067,19 @@ function paintTestStroke(surface, brush, paper, color, width, height, stampArt)
|
|
|
3164
4067
|
affineCopies: [],
|
|
3165
4068
|
copies: [IDENTITY_COPY],
|
|
3166
4069
|
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),
|
|
3167
|
-
thick: brush.engine === "impasto" ? { channels: emptyThickChannels() } : void 0,
|
|
4070
|
+
thick: brush.engine === "impasto" || brush.iridescence ? { channels: emptyThickChannels() } : void 0,
|
|
3168
4071
|
sticker: brush.engine === "sticker" ? { nextId: 1 } : void 0,
|
|
3169
4072
|
light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyLight() } : null,
|
|
4073
|
+
fluor: brush.fluorescence.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyFluor() } : null,
|
|
3170
4074
|
stampArt
|
|
3171
4075
|
}
|
|
3172
4076
|
);
|
|
3173
4077
|
session.push(thumbnailStroke(width, height));
|
|
3174
4078
|
session.end();
|
|
3175
4079
|
const lit = thick ? new LitCache().lit(result, thick, DEFAULT_LIGHTING) : result;
|
|
3176
|
-
|
|
4080
|
+
const glowed = light || thick ? new LightCache().glow(lit, light ?? void 0, thick ?? void 0) : lit;
|
|
4081
|
+
const uvLit = fluor ? new UvCache().render(glowed, fluor, { on: true, strength: 1, ambient: 0.15 }) : glowed;
|
|
4082
|
+
return { surface: uvLit, light, fluor };
|
|
3177
4083
|
}
|
|
3178
4084
|
function renderBrushThumbnail(brush, options) {
|
|
3179
4085
|
const width = Math.max(16, Math.min(512, Math.round(options.width ?? 160)));
|
|
@@ -3191,11 +4097,12 @@ function renderBrushThumbnail(brush, options) {
|
|
|
3191
4097
|
surface = fillRect(surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);
|
|
3192
4098
|
}
|
|
3193
4099
|
let light = null;
|
|
4100
|
+
let fluor = null;
|
|
3194
4101
|
if (b.engine === "wet") surface = paintWetTestStroke(b, options.paper, options.color, width, height);
|
|
3195
|
-
else ({ surface, light } = paintTestStroke(surface, b, options.paper, options.color, width, height, options.stampArt));
|
|
4102
|
+
else ({ surface, light, fluor } = paintTestStroke(surface, b, options.paper, options.color, width, height, options.stampArt));
|
|
3196
4103
|
const layer = { type: "pixel", id: "thumbnail", name: "thumbnail", visible: true, opacity: 1, blend: "normal", surface, offset: { x: 0, y: 0 } };
|
|
3197
|
-
const paper = light && brushPreviewsOnDark(b) ? { ...options.paper, color: BRUSH_PREVIEW_DARK_PAPER } : options.paper;
|
|
3198
|
-
const spill = light ? computeSpill(surface, light, { radius: Math.max(4, height / 6), strength: 1 }) : null;
|
|
4104
|
+
const paper = (light || fluor) && brushPreviewsOnDark(b) ? { ...options.paper, color: BRUSH_PREVIEW_DARK_PAPER } : options.paper;
|
|
4105
|
+
const spill = light ? computeSpill(surface, light, { radius: Math.max(4, height / 6), strength: 1 }) : fluor ? computeUvSpill(fluor, 1) : null;
|
|
3199
4106
|
const layers = [paperNode(paper), layer];
|
|
3200
4107
|
if (spill) layers.push({ type: "pixel", id: "thumbnail-light", name: "light", visible: true, opacity: 1, blend: "linear-dodge", surface: spill, offset: { x: 0, y: 0 } });
|
|
3201
4108
|
const data = createPaintCompositor().composite(layers, { x: 0, y: 0, width, height }, { width, height });
|
|
@@ -3725,6 +4632,8 @@ function engineChanges(base, next) {
|
|
|
3725
4632
|
return JSON.stringify(a) === JSON.stringify(b) ? void 0 : copyJSON(b);
|
|
3726
4633
|
};
|
|
3727
4634
|
const { id: _i, builtin: _b, ...rest } = diff(base, next) ?? {};
|
|
4635
|
+
if (base.iridescence && !next.iridescence) rest.iridescence = null;
|
|
4636
|
+
if (base.tipGrain && !next.tipGrain) rest.tipGrain = null;
|
|
3728
4637
|
return rest;
|
|
3729
4638
|
}
|
|
3730
4639
|
|
|
@@ -3851,7 +4760,10 @@ function liveStrokeRefusal(brush) {
|
|
|
3851
4760
|
case "smudge":
|
|
3852
4761
|
return `The ${brush.name} brush moves the paint under it, so it can't be redrawn on its own; rasterize the layer first (rasterize_stroke_layer) or blend on a pixel layer.`;
|
|
3853
4762
|
}
|
|
4763
|
+
if (brush.mutation) return `The ${brush.name} brush paints mutation strokes (which fade once an agent reads them), so it goes on a pixel layer (add_layer).`;
|
|
3854
4764
|
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).`;
|
|
4765
|
+
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).`;
|
|
4766
|
+
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).`;
|
|
3855
4767
|
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.`;
|
|
3856
4768
|
return null;
|
|
3857
4769
|
}
|
|
@@ -4179,6 +5091,15 @@ async function replayPainting(source, target, options = {}) {
|
|
|
4179
5091
|
if (e.layerId && target.getLayer(e.layerId)) target.wetPaper({ layerId: e.layerId, rect: sr(d.rect), amount: d.amount });
|
|
4180
5092
|
break;
|
|
4181
5093
|
}
|
|
5094
|
+
case "ghost-release": {
|
|
5095
|
+
const d = e.data;
|
|
5096
|
+
if (e.layerId && target.getLayer(e.layerId)) target.wet.releaseGhost(e.layerId, d?.linger);
|
|
5097
|
+
break;
|
|
5098
|
+
}
|
|
5099
|
+
case "ghost-keep": {
|
|
5100
|
+
if (e.layerId && target.getLayer(e.layerId)) target.wet.keepGhost(e.layerId);
|
|
5101
|
+
break;
|
|
5102
|
+
}
|
|
4182
5103
|
}
|
|
4183
5104
|
} catch {
|
|
4184
5105
|
}
|
|
@@ -4421,6 +5342,27 @@ function effectLayersToJSON(map, keep) {
|
|
|
4421
5342
|
return Object.keys(out).length ? out : void 0;
|
|
4422
5343
|
}
|
|
4423
5344
|
|
|
5345
|
+
// src/paint-editor/mutation.ts
|
|
5346
|
+
function mutationBrushRefusal(brush) {
|
|
5347
|
+
if (!brush.mutation) return null;
|
|
5348
|
+
if (brush.engine === "eraser" || brush.engine === "blender" || brush.engine === "smudge" || brush.engine === "mixer" || brush.engine === "sticker") {
|
|
5349
|
+
return `Mutation paint adds strokes that fade when an agent reads them; ${brush.engine} brushes cannot use mutation.`;
|
|
5350
|
+
}
|
|
5351
|
+
if ((brush.behavior?.pickup ?? 0) > 0 || brush.behavior?.dirty) {
|
|
5352
|
+
return "Mutation brushes cannot pick up canvas colour (behavior.pickup must be 0 and behavior.dirty false).";
|
|
5353
|
+
}
|
|
5354
|
+
if (brush.engine === "wet") {
|
|
5355
|
+
const tool = brush.wet?.tool ?? "paint";
|
|
5356
|
+
if (tool !== "paint") {
|
|
5357
|
+
return `Wet mutation brushes must use wet.tool: "paint" (got "${tool}").`;
|
|
5358
|
+
}
|
|
5359
|
+
if ((brush.light?.amount ?? 0) > 0) {
|
|
5360
|
+
return "Wet mutation brushes use the wet light channel to track fading paint, so light.amount must be 0.";
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5363
|
+
return null;
|
|
5364
|
+
}
|
|
5365
|
+
|
|
4424
5366
|
// src/paint-editor/collaboration.ts
|
|
4425
5367
|
var LAYER_FIELDS = ["name", "visible", "opacity", "blend"];
|
|
4426
5368
|
function createPaintChangeFeed(controller, options = {}) {
|
|
@@ -5392,9 +6334,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5392
6334
|
wetOptions;
|
|
5393
6335
|
/** Where the light comes from for thick paint (saved with the document, not undo steps). */
|
|
5394
6336
|
lighting = { ...DEFAULT_LIGHTING };
|
|
6337
|
+
/** Where the viewer looks from for thin-film iridescence and specular highlights (editor state, not saved with the document). */
|
|
6338
|
+
viewTilt = { ...DEFAULT_VIEW_TILT };
|
|
5395
6339
|
litCaches = /* @__PURE__ */ new Map();
|
|
5396
6340
|
/** The live impasto stroke's channels. */
|
|
5397
6341
|
liveThick = null;
|
|
6342
|
+
/** The UV / blacklight setting (saved with the document, not undo steps). */
|
|
6343
|
+
uvLight = { ...DEFAULT_UV_LIGHT };
|
|
6344
|
+
uvCaches = /* @__PURE__ */ new Map();
|
|
6345
|
+
thermoCaches = /* @__PURE__ */ new Map();
|
|
6346
|
+
liveFluor = null;
|
|
6347
|
+
liveThermo = null;
|
|
5398
6348
|
/** Set while an operation that changes wet paint runs: the simulation before it (for undo). */
|
|
5399
6349
|
wetOp = null;
|
|
5400
6350
|
/** The effects provider slot (see `setEffectsProvider`). */
|
|
@@ -5409,6 +6359,15 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5409
6359
|
rasterizeSticker;
|
|
5410
6360
|
effectsRenderer = new PaintEffectsRenderer();
|
|
5411
6361
|
paperEffectNodeCache = null;
|
|
6362
|
+
mutations = [];
|
|
6363
|
+
nextMutationId = 1;
|
|
6364
|
+
mutationListeners = /* @__PURE__ */ new Set();
|
|
6365
|
+
mutationAgentConnected = false;
|
|
6366
|
+
mutationDirector = null;
|
|
6367
|
+
heldEvaporation = /* @__PURE__ */ new Map();
|
|
6368
|
+
tempLayerTargets = /* @__PURE__ */ new Map();
|
|
6369
|
+
answeringMutation = false;
|
|
6370
|
+
lastMutationTick = null;
|
|
5412
6371
|
constructor(options = {}) {
|
|
5413
6372
|
this.historyLimit = Math.max(1, options.historyLimit ?? 200);
|
|
5414
6373
|
this.rasterizeSticker = options.rasterizeSticker ?? defaultStickerRasterizer;
|
|
@@ -5673,6 +6632,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5673
6632
|
effectPaintRendering: Boolean(this.effectPaintPending || this.pathStroke),
|
|
5674
6633
|
mixerLoad: this.mixerLoad,
|
|
5675
6634
|
view: this.view,
|
|
6635
|
+
viewTilt: { ...this.viewTilt },
|
|
5676
6636
|
hand: this.hand,
|
|
5677
6637
|
effectPlayback: { ...this.effectPlaybackState, time: this.effectTime },
|
|
5678
6638
|
selectedStroke: this.selectedStroke,
|
|
@@ -5698,6 +6658,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5698
6658
|
gpu: this.wet.gpuMode,
|
|
5699
6659
|
gpuActive: Boolean(this.wet.world?.gpuActive)
|
|
5700
6660
|
},
|
|
6661
|
+
uvLight: { ...this.uvLight },
|
|
6662
|
+
heat: heatStats(this.doc.heat),
|
|
6663
|
+
mutations: this.mutations.map((m) => this.toMutationInfo(m)),
|
|
6664
|
+
hasMutationAgent: this.mutationListeners.size > 0 || this.mutationAgentConnected,
|
|
5701
6665
|
version: this.version
|
|
5702
6666
|
};
|
|
5703
6667
|
return this.cached;
|
|
@@ -5804,7 +6768,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5804
6768
|
const wetIds = ids.filter((id) => this.wet.wetLayerIds().includes(id));
|
|
5805
6769
|
if (!wetIds.length) return [];
|
|
5806
6770
|
this.beginWetOp();
|
|
5807
|
-
for (const id of wetIds)
|
|
6771
|
+
for (const id of wetIds) {
|
|
6772
|
+
this.wet.dry(id);
|
|
6773
|
+
this.finishWetMutationsOnDry(id);
|
|
6774
|
+
}
|
|
5808
6775
|
this.logWet({ kind: "dry", layerId: wetIds.join(",") });
|
|
5809
6776
|
return wetIds;
|
|
5810
6777
|
}
|
|
@@ -5840,6 +6807,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5840
6807
|
this.wet.restore(e.wet.lost ? null : e.wet.before);
|
|
5841
6808
|
if (paper !== this.doc.paper) this.wet.paperChanged();
|
|
5842
6809
|
}
|
|
6810
|
+
this.reconcileMutationsAfterUndo();
|
|
5843
6811
|
this.notifyCommit({ kind: "undo", label: e.label, origin: this.getOrigin() });
|
|
5844
6812
|
this.emit();
|
|
5845
6813
|
return e.label;
|
|
@@ -5896,6 +6864,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5896
6864
|
this.beginWetOp();
|
|
5897
6865
|
this.wet.reset();
|
|
5898
6866
|
}
|
|
6867
|
+
this.resetMutations();
|
|
5899
6868
|
this.commit("New canvas", next);
|
|
5900
6869
|
return next;
|
|
5901
6870
|
}
|
|
@@ -5907,11 +6876,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5907
6876
|
height: update.height ?? this.doc.canvas.height,
|
|
5908
6877
|
dpi: update.dpi ?? this.doc.canvas.dpi
|
|
5909
6878
|
});
|
|
5910
|
-
|
|
6879
|
+
const resized = canvas.width !== this.doc.canvas.width || canvas.height !== this.doc.canvas.height;
|
|
6880
|
+
if (this.wet.world && resized) {
|
|
5911
6881
|
this.dryLayersFirst(this.wet.wetLayerIds());
|
|
5912
6882
|
this.wet.reset();
|
|
5913
6883
|
}
|
|
5914
|
-
this.
|
|
6884
|
+
const heat = resized && this.doc.heat ? shiftHeatField(this.doc.heat, canvas.width, canvas.height, 0, 0) : this.doc.heat;
|
|
6885
|
+
this.commit("Canvas size", this.withWetEvents({ ...this.doc, canvas, ...heat ? { heat } : {} }));
|
|
5915
6886
|
return canvas;
|
|
5916
6887
|
}
|
|
5917
6888
|
/**
|
|
@@ -5956,9 +6927,30 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5956
6927
|
}
|
|
5957
6928
|
}
|
|
5958
6929
|
let next = { ...shiftDocument(this.withWetEvents(this.doc), left, top), canvas: { ...was, width, height } };
|
|
6930
|
+
if (next.heat) next = { ...next, heat: shiftHeatField(next.heat, left, top, width, height) };
|
|
5959
6931
|
for (const id of keptWet) {
|
|
5960
6932
|
const l = findLayer(next.layers, id)?.layer;
|
|
5961
|
-
if (isPixelLayer(l))
|
|
6933
|
+
if (isPixelLayer(l)) {
|
|
6934
|
+
const ox = l.offset.x;
|
|
6935
|
+
const oy = l.offset.y;
|
|
6936
|
+
next = { ...next, layers: mapLayer(next.layers, id, () => ({ ...l, surface: translateSurface(l.surface, ox, oy), offset: { x: 0, y: 0 } })) };
|
|
6937
|
+
const tc = next.thick?.[id];
|
|
6938
|
+
if (tc && (ox || oy)) {
|
|
6939
|
+
next = {
|
|
6940
|
+
...next,
|
|
6941
|
+
thick: {
|
|
6942
|
+
...next.thick,
|
|
6943
|
+
[id]: {
|
|
6944
|
+
height: translateSurface(tc.height, ox, oy),
|
|
6945
|
+
material: translateSurface(tc.material, ox, oy),
|
|
6946
|
+
film: translateSurface(tc.film, ox, oy)
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
};
|
|
6950
|
+
}
|
|
6951
|
+
if (next.fluor?.[id] && (ox || oy)) next = { ...next, fluor: { ...next.fluor, [id]: translateSurface(next.fluor[id], ox, oy) } };
|
|
6952
|
+
if (next.thermo?.[id] && (ox || oy)) next = { ...next, thermo: { ...next.thermo, [id]: translateSurface(next.thermo[id], ox, oy) } };
|
|
6953
|
+
}
|
|
5962
6954
|
}
|
|
5963
6955
|
if (this.wet.world) {
|
|
5964
6956
|
if (keptWet.length) this.wet.extend(width, height, left, top, next.paper);
|
|
@@ -6023,7 +7015,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6023
7015
|
describeLayer(l, parentId = findLayer(this.doc.layers, l.id)?.parent?.id ?? null, depth = findLayer(this.doc.layers, l.id)?.path.length ?? 0) {
|
|
6024
7016
|
let bounds = null;
|
|
6025
7017
|
if (l.type === "pixel") {
|
|
6026
|
-
const b = contentBounds(l.surface);
|
|
7018
|
+
const b = unionRect(contentBounds(l.surface), this.doc.fluor?.[l.id] ? contentBounds(this.doc.fluor[l.id]) : null);
|
|
6027
7019
|
bounds = b ? { ...b, x: b.x + l.offset.x, y: b.y + l.offset.y } : null;
|
|
6028
7020
|
} else for (const c of flattenLayers(l.children)) if (c.type === "pixel") bounds = unionRect(bounds, this.describeLayer(c, l.id, depth + 1).bounds);
|
|
6029
7021
|
return {
|
|
@@ -6039,6 +7031,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6039
7031
|
bounds,
|
|
6040
7032
|
...this.doc.liveStrokes?.[l.id] ? { liveStrokes: this.doc.liveStrokes[l.id].length } : {},
|
|
6041
7033
|
...l.effects?.items.length ? { effects: l.effects.items.length } : {},
|
|
7034
|
+
...l.type === "pixel" && l.temporary ? { temporary: true } : {},
|
|
6042
7035
|
parentId,
|
|
6043
7036
|
depth
|
|
6044
7037
|
};
|
|
@@ -6059,7 +7052,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6059
7052
|
return l;
|
|
6060
7053
|
}
|
|
6061
7054
|
setActiveLayer(id) {
|
|
6062
|
-
this.requireLayer(id);
|
|
7055
|
+
const l = this.requireLayer(id);
|
|
7056
|
+
if (isPixelLayer(l) && l.temporary) throw new PaintEditorError("Temporary mutation layers fade away on their own; paint on the layer below.");
|
|
6063
7057
|
if (this.doc.activeLayerId === id) return;
|
|
6064
7058
|
this.doc = { ...this.doc, activeLayerId: id };
|
|
6065
7059
|
this.emit();
|
|
@@ -6212,7 +7206,18 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6212
7206
|
const layers = mapLayer(this.doc.layers, id, () => null);
|
|
6213
7207
|
let active = this.doc.activeLayerId;
|
|
6214
7208
|
if (active && !findLayer(layers, active)) active = flattenLayers(layers).filter((x) => x.type === "pixel").pop()?.id ?? null;
|
|
6215
|
-
|
|
7209
|
+
const removedIds = flattenLayers([l]).map((x) => x.id);
|
|
7210
|
+
this.commit("Remove layer", {
|
|
7211
|
+
...this.doc,
|
|
7212
|
+
layers,
|
|
7213
|
+
activeLayerId: active,
|
|
7214
|
+
...this.thickWithout(removedIds),
|
|
7215
|
+
...this.stickersWithout(removedIds),
|
|
7216
|
+
...this.liveStrokesWithout(removedIds),
|
|
7217
|
+
...this.lightWithout(removedIds),
|
|
7218
|
+
...this.fluorWithout(removedIds),
|
|
7219
|
+
...this.thermoWithout(removedIds)
|
|
7220
|
+
});
|
|
6216
7221
|
return { removed: id, activeLayerId: active };
|
|
6217
7222
|
}
|
|
6218
7223
|
duplicateLayer(id) {
|
|
@@ -6227,10 +7232,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6227
7232
|
const stickers = { ...this.doc.stickers ?? {} };
|
|
6228
7233
|
const liveStrokes = { ...this.doc.liveStrokes ?? {} };
|
|
6229
7234
|
const light = { ...this.doc.light ?? {} };
|
|
7235
|
+
const fluor = { ...this.doc.fluor ?? {} };
|
|
7236
|
+
const thermo = { ...this.doc.thermo ?? {} };
|
|
6230
7237
|
const copy = (l) => {
|
|
6231
7238
|
const id2 = newPaintLayerId();
|
|
6232
7239
|
if (light[l.id]) light[id2] = light[l.id];
|
|
6233
7240
|
if (this.lightSpills[l.id]) this.lightSpills[id2] = { ...this.lightSpills[l.id] };
|
|
7241
|
+
if (fluor[l.id]) fluor[id2] = fluor[l.id];
|
|
7242
|
+
if (thermo[l.id]) thermo[id2] = thermo[l.id];
|
|
6234
7243
|
if (thick[l.id]) thick[id2] = thick[l.id];
|
|
6235
7244
|
if (stickers[l.id]) stickers[id2] = stickers[l.id].map((x) => ({ ...x }));
|
|
6236
7245
|
if (liveStrokes[l.id]) liveStrokes[id2] = liveStrokes[l.id].map((x) => ({ ...x, id: `s${this.strokeCounter++}`, layerId: id2 }));
|
|
@@ -6238,7 +7247,20 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6238
7247
|
};
|
|
6239
7248
|
const dup = { ...copy(loc.layer), name: `${loc.layer.name} copy` };
|
|
6240
7249
|
const layers = insertLayer(this.doc.layers, dup, loc.parent?.id ?? null, loc.index + 1);
|
|
6241
|
-
this.commit(
|
|
7250
|
+
this.commit(
|
|
7251
|
+
"Duplicate layer",
|
|
7252
|
+
this.withWetEvents({
|
|
7253
|
+
...this.doc,
|
|
7254
|
+
layers,
|
|
7255
|
+
activeLayerId: dup.id,
|
|
7256
|
+
...this.doc.thick ? { thick } : {},
|
|
7257
|
+
...this.doc.stickers ? { stickers } : {},
|
|
7258
|
+
...this.doc.liveStrokes ? { liveStrokes } : {},
|
|
7259
|
+
...this.doc.light ? { light } : {},
|
|
7260
|
+
...this.doc.fluor ? { fluor } : {},
|
|
7261
|
+
...this.doc.thermo ? { thermo } : {}
|
|
7262
|
+
})
|
|
7263
|
+
);
|
|
6242
7264
|
return this.describeLayer(dup);
|
|
6243
7265
|
}
|
|
6244
7266
|
/** Moves a layer to `index` (0 = bottom) within `parentId` (default its own parent; `null` = top level). */
|
|
@@ -6285,10 +7307,30 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6285
7307
|
const merged = { ...below, surface, offset: { x: 0, y: 0 } };
|
|
6286
7308
|
const layers = mapLayer(mapLayer(this.doc.layers, upper.id, () => null), below.id, () => merged);
|
|
6287
7309
|
const toDoc = (t, l) => t ? translateSurface(t, l.offset.x, l.offset.y) : void 0;
|
|
6288
|
-
const
|
|
7310
|
+
const upperDoc = translateSurface(upper.surface, upper.offset.x, upper.offset.y);
|
|
7311
|
+
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;
|
|
6289
7312
|
const lightAfter = this.lightWithout([upper.id, below.id]).light ?? (this.doc.light ? { ...this.doc.light } : void 0);
|
|
6290
7313
|
const light = mergedLight ? { ...lightAfter ?? {}, [below.id]: mergedLight } : lightAfter;
|
|
6291
|
-
|
|
7314
|
+
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;
|
|
7315
|
+
const fluorAfter = this.fluorWithout([upper.id, below.id]).fluor ?? (this.doc.fluor ? { ...this.doc.fluor } : void 0);
|
|
7316
|
+
const fluor = mergedFluor ? { ...fluorAfter ?? {}, [below.id]: mergedFluor } : fluorAfter;
|
|
7317
|
+
const mergedThermo = this.doc.thermo?.[upper.id] || this.doc.thermo?.[below.id] ? mergeFourChannel(toDoc(this.doc.thermo?.[below.id], below), upper.visible ? toDoc(this.doc.thermo?.[upper.id], upper) : void 0) : void 0;
|
|
7318
|
+
const thermoAfter = this.thermoWithout([upper.id, below.id]).thermo ?? (this.doc.thermo ? { ...this.doc.thermo } : void 0);
|
|
7319
|
+
const thermo = mergedThermo ? { ...thermoAfter ?? {}, [below.id]: mergedThermo } : thermoAfter;
|
|
7320
|
+
this.commit(
|
|
7321
|
+
"Merge down",
|
|
7322
|
+
this.withWetEvents({
|
|
7323
|
+
...this.doc,
|
|
7324
|
+
layers,
|
|
7325
|
+
activeLayerId: below.id,
|
|
7326
|
+
...this.thickWithout([upper.id]),
|
|
7327
|
+
...this.stickersWithout([upper.id, below.id]),
|
|
7328
|
+
...this.liveStrokesWithout([upper.id, below.id]),
|
|
7329
|
+
...light ? { light } : {},
|
|
7330
|
+
...fluor ? { fluor } : {},
|
|
7331
|
+
...thermo ? { thermo } : {}
|
|
7332
|
+
})
|
|
7333
|
+
);
|
|
6292
7334
|
return this.describeLayer(merged);
|
|
6293
7335
|
}
|
|
6294
7336
|
/** Removes every pixel of a layer. */
|
|
@@ -6300,7 +7342,22 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6300
7342
|
this.wet.world.removeLane(l.id);
|
|
6301
7343
|
}
|
|
6302
7344
|
const bounds = this.describeLayer(l).bounds;
|
|
6303
|
-
this.commit(
|
|
7345
|
+
this.commit(
|
|
7346
|
+
"Clear layer",
|
|
7347
|
+
this.withPaintEvent(
|
|
7348
|
+
{
|
|
7349
|
+
...this.doc,
|
|
7350
|
+
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: emptySurface(4) })),
|
|
7351
|
+
...this.thickWithout([l.id]),
|
|
7352
|
+
...this.stickersWithout([l.id]),
|
|
7353
|
+
...this.doc.liveStrokes?.[l.id] ? { liveStrokes: { ...this.doc.liveStrokes, [l.id]: [] } } : {},
|
|
7354
|
+
...this.lightWithout([l.id]),
|
|
7355
|
+
...this.fluorWithout([l.id]),
|
|
7356
|
+
...this.thermoWithout([l.id])
|
|
7357
|
+
},
|
|
7358
|
+
{ kind: "clear", layerId: l.id }
|
|
7359
|
+
)
|
|
7360
|
+
);
|
|
6304
7361
|
return { layerId: l.id, bounds };
|
|
6305
7362
|
}
|
|
6306
7363
|
/**
|
|
@@ -6346,7 +7403,23 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6346
7403
|
const surface = this.applyCoverage(l, coverage, rect, apply);
|
|
6347
7404
|
const lit = this.doc.light?.[l.id];
|
|
6348
7405
|
const light = lit ? withLayerLight(this.doc.light, l.id, scaleLight(lit, 1 - opacity)) : void 0;
|
|
6349
|
-
|
|
7406
|
+
const fl = this.doc.fluor?.[l.id];
|
|
7407
|
+
const fluor = fl ? withLayerLight(this.doc.fluor, l.id, scaleFluor(fl, 1 - opacity)) : void 0;
|
|
7408
|
+
const th = this.doc.thermo?.[l.id];
|
|
7409
|
+
const thermo = th && opacity >= 0.5 ? this.thermoWithout([l.id]).thermo : void 0;
|
|
7410
|
+
this.commit(
|
|
7411
|
+
"Fill layer",
|
|
7412
|
+
this.withPaintEvent(
|
|
7413
|
+
this.withWetEvents({
|
|
7414
|
+
...this.doc,
|
|
7415
|
+
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })),
|
|
7416
|
+
...light ? { light } : {},
|
|
7417
|
+
...fluor ? { fluor } : {},
|
|
7418
|
+
...thermo ? { thermo } : {}
|
|
7419
|
+
}),
|
|
7420
|
+
{ kind: "fill", layerId: l.id, data: { color, opacity, grain: grainAmount, pressure, blend } }
|
|
7421
|
+
)
|
|
7422
|
+
);
|
|
6350
7423
|
return { layerId: l.id, bounds: rect };
|
|
6351
7424
|
}
|
|
6352
7425
|
/**
|
|
@@ -7237,6 +8310,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7237
8310
|
}
|
|
7238
8311
|
useBrush(next, spec, builtin) {
|
|
7239
8312
|
next = normalizePaintBrush(next, next);
|
|
8313
|
+
const refusal = mutationBrushRefusal(next);
|
|
8314
|
+
if (refusal) throw new PaintEditorError(refusal);
|
|
7240
8315
|
if (builtin) next.builtin = true;
|
|
7241
8316
|
else delete next.builtin;
|
|
7242
8317
|
this.brush = next;
|
|
@@ -7485,6 +8560,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7485
8560
|
return l;
|
|
7486
8561
|
})();
|
|
7487
8562
|
const brush = this.strokeBrush(init);
|
|
8563
|
+
const mutRefusal = mutationBrushRefusal(brush);
|
|
8564
|
+
if (mutRefusal) throw new PaintEditorError(mutRefusal);
|
|
7488
8565
|
if (this.previewing && isWetBrush(brush)) throw new PaintEditorError(`Wet paint can't be previewed; the ${brush.name} brush paints wet media. Suggest it with a dry brush.`);
|
|
7489
8566
|
if (brush.ghost) {
|
|
7490
8567
|
if (brush.light.amount > 0) throw new PaintEditorError(`Ghost paint (${brush.name}) shares the wet light channel and can't emit light; set light.amount to 0 or ghost to null (set_brush).`);
|
|
@@ -7508,6 +8585,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7508
8585
|
throw new PaintEditorError(`Layer "${layer.name}" has ghost paint fading on it, which uses the wet light channel; paint light on another layer (add_layer), or finish the ghost first (dry_paint).`);
|
|
7509
8586
|
}
|
|
7510
8587
|
}
|
|
8588
|
+
if (brush.fluorescence.amount > 0 && brush.engine === "sticker") {
|
|
8589
|
+
throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take fluorescence; paint UV ink with a dab or particle brush (uv-marker; list_brush_presets lists them), or set fluorescence.amount to 0 (set_brush).`);
|
|
8590
|
+
}
|
|
8591
|
+
if (brush.thermochromic.hotColor !== null && brush.engine === "sticker") {
|
|
8592
|
+
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).`);
|
|
8593
|
+
}
|
|
7511
8594
|
if (doc.liveStrokes?.[layer.id]) {
|
|
7512
8595
|
const why = liveStrokeRefusal(brush);
|
|
7513
8596
|
if (why) throw new PaintEditorError(`Layer "${layer.name}" is a stroke layer. ${why}`);
|
|
@@ -7529,9 +8612,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7529
8612
|
if (isWetBrush(brush)) {
|
|
7530
8613
|
this.requireWetIdle("painting");
|
|
7531
8614
|
if (layer.offset.x || layer.offset.y) {
|
|
7532
|
-
const
|
|
7533
|
-
|
|
7534
|
-
|
|
8615
|
+
const ox = layer.offset.x;
|
|
8616
|
+
const oy = layer.offset.y;
|
|
8617
|
+
const moved = { ...layer, surface: translateSurface(layer.surface, ox, oy), offset: { x: 0, y: 0 } };
|
|
8618
|
+
const moveThick = (t) => {
|
|
8619
|
+
const tc = t?.[layer.id];
|
|
8620
|
+
if (!tc) return t;
|
|
8621
|
+
return { ...t, [layer.id]: { height: translateSurface(tc.height, ox, oy), material: translateSurface(tc.material, ox, oy), film: translateSurface(tc.film, ox, oy) } };
|
|
8622
|
+
};
|
|
8623
|
+
this.doc = { ...this.doc, layers: mapLayer(this.doc.layers, layer.id, () => moved), ...this.doc.thick?.[layer.id] ? { thick: moveThick(this.doc.thick) } : {} };
|
|
8624
|
+
if (doc !== this.doc) doc = { ...doc, layers: mapLayer(doc.layers, layer.id, () => moved), ...doc.thick?.[layer.id] ? { thick: moveThick(doc.thick) } : {} };
|
|
7535
8625
|
layer = moved;
|
|
7536
8626
|
}
|
|
7537
8627
|
if (!this.wet.wetLayerIds().includes(layer.id) && this.wet.wetLayerIds().length >= MAX_WET_LANES) {
|
|
@@ -7546,6 +8636,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7546
8636
|
const load = init.load ?? brushLoadForStroke(brush, this.mixerLoad, [r / 255, g / 255, b / 255]);
|
|
7547
8637
|
const seedW = init.seed ?? mix32(this.seedCounter * 2654435761 >>> 0) & 2147483647;
|
|
7548
8638
|
const copiesW = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
|
|
8639
|
+
const wetMask = init.maskStencils ? this.stencilSelection(init.maskStencils) : init.stencils !== false ? this.stencilSelection() : null;
|
|
8640
|
+
const needWetFilm = Boolean(brush.wet.tool === "paint" && brush.iridescence || brush.wet.tool === "blot" && doc.thick?.[id]?.film.tiles.size);
|
|
8641
|
+
const wetFilm = needWetFilm ? new FilmBuffer((doc.thick?.[id] ?? emptyThickChannels()).film, layer.offset, wetMask) : null;
|
|
7549
8642
|
const driver = new WetStrokeDriver({
|
|
7550
8643
|
world,
|
|
7551
8644
|
laneId: id,
|
|
@@ -7557,9 +8650,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7557
8650
|
paperColor: hexToRgb8(doc.paper.color),
|
|
7558
8651
|
seed: seedW,
|
|
7559
8652
|
copies: [IDENTITY_AFFINE, ...copiesW].map(affineCopy),
|
|
7560
|
-
mask:
|
|
8653
|
+
mask: wetMask,
|
|
7561
8654
|
paint: effect ? null : wetGradient,
|
|
7562
8655
|
effect: effect?.pixels ?? null,
|
|
8656
|
+
film: wetFilm,
|
|
7563
8657
|
speed: this.wet.speed
|
|
7564
8658
|
});
|
|
7565
8659
|
init = { ...init, seed: seedW, load };
|
|
@@ -7569,6 +8663,23 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7569
8663
|
const dried = findLayer(this.doc.layers, layer.id).layer;
|
|
7570
8664
|
layer = dried;
|
|
7571
8665
|
}
|
|
8666
|
+
if (brush.mutation && !isWetBrush(brush) && !this.answeringMutation && !this.previewing && !this.getOrigin().startsWith("agent:")) {
|
|
8667
|
+
const targetLayer = layer;
|
|
8668
|
+
const existing = this.mutations.find((m) => m.state === "settling" && m.medium === "dry" && m.layerId === targetLayer.id && m.tempLayerId && findLayer(doc.layers, m.tempLayerId));
|
|
8669
|
+
if (existing?.tempLayerId) {
|
|
8670
|
+
layer = findLayer(doc.layers, existing.tempLayerId).layer;
|
|
8671
|
+
} else {
|
|
8672
|
+
const temp = {
|
|
8673
|
+
...createPaintLayer("Mutation"),
|
|
8674
|
+
clipped: targetLayer.clipped,
|
|
8675
|
+
temporary: true
|
|
8676
|
+
};
|
|
8677
|
+
this.tempLayerTargets.set(temp.id, targetLayer.id);
|
|
8678
|
+
this.doc = { ...this.doc, layers: insertAfter(this.doc.layers, targetLayer.id, temp) };
|
|
8679
|
+
if (doc !== this.doc) doc = { ...doc, layers: insertAfter(doc.layers, targetLayer.id, temp) };
|
|
8680
|
+
layer = temp;
|
|
8681
|
+
}
|
|
8682
|
+
}
|
|
7572
8683
|
let color = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
7573
8684
|
const tracing = this.guides.tracing;
|
|
7574
8685
|
if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
|
|
@@ -7585,6 +8696,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7585
8696
|
const rgba = rgbaOf(color);
|
|
7586
8697
|
const seed = init.seed ?? mix32(this.seedCounter++ * 2654435761 >>> 0) & 2147483647;
|
|
7587
8698
|
const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
|
|
8699
|
+
const hasThick = Boolean(doc.thick?.[layer.id]);
|
|
8700
|
+
const needThick = brush.engine === "impasto" || Boolean(brush.iridescence) || brush.engine === "eraser" && hasThick || Boolean(wet?.driver.init.film);
|
|
7588
8701
|
const session = new PaintStrokeSession(host, {
|
|
7589
8702
|
layerId: layer.id,
|
|
7590
8703
|
surface: layer.surface,
|
|
@@ -7604,9 +8717,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7604
8717
|
mixerLoad: init.load ?? brushLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]),
|
|
7605
8718
|
secondary: rgbaOf(this.secondaryColor),
|
|
7606
8719
|
wet,
|
|
7607
|
-
thick:
|
|
8720
|
+
thick: needThick ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
|
|
7608
8721
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
7609
8722
|
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
8723
|
+
fluor: brush.engine !== "sticker" && (brush.fluorescence.amount > 0 || doc.fluor?.[layer.id]) ? { surface: doc.fluor?.[layer.id] ?? emptyFluor() } : null,
|
|
8724
|
+
thermo: brush.engine !== "sticker" && (brush.thermochromic.hotColor !== null || doc.thermo?.[layer.id]) ? { surface: doc.thermo?.[layer.id] ?? emptyThermo() } : null,
|
|
7610
8725
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
|
|
7611
8726
|
stampArt: this.stampArt,
|
|
7612
8727
|
paint: effect ? null : this.strokeGradient(init, brush, color),
|
|
@@ -7647,12 +8762,18 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7647
8762
|
const host = {
|
|
7648
8763
|
update: (layerId, surface, rects) => {
|
|
7649
8764
|
if (this.session?.init.wet) {
|
|
8765
|
+
const ch2 = this.session.thickChannels();
|
|
8766
|
+
this.liveThick = ch2 ? { layerId, channels: ch2 } : null;
|
|
7650
8767
|
this.wet.syncDisplays();
|
|
7651
8768
|
return;
|
|
7652
8769
|
}
|
|
7653
8770
|
this.live = { layerId, surface };
|
|
7654
8771
|
const ch = this.session?.thickChannels();
|
|
7655
8772
|
this.liveThick = ch ? { layerId, channels: ch } : null;
|
|
8773
|
+
const fl = this.session?.fluorChannel();
|
|
8774
|
+
this.liveFluor = fl ? { layerId, surface: fl } : null;
|
|
8775
|
+
const th = this.session?.thermoChannel();
|
|
8776
|
+
this.liveThermo = th ? { layerId, surface: th } : null;
|
|
7656
8777
|
this.emit(rects);
|
|
7657
8778
|
},
|
|
7658
8779
|
sampled: (s, points) => {
|
|
@@ -7671,6 +8792,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7671
8792
|
this.live = null;
|
|
7672
8793
|
this.liveLayers = null;
|
|
7673
8794
|
this.liveThick = null;
|
|
8795
|
+
this.liveFluor = null;
|
|
8796
|
+
this.liveThermo = null;
|
|
7674
8797
|
const wetStroke = Boolean(s.init.wet);
|
|
7675
8798
|
if (wetStroke) this.wet.driver = null;
|
|
7676
8799
|
const brush = s.init.brush;
|
|
@@ -7683,6 +8806,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7683
8806
|
if (wetStroke) this.wet.restore(snap);
|
|
7684
8807
|
this.doc = { ...this.doc, layers: before.layers };
|
|
7685
8808
|
this.wet.syncDisplays();
|
|
8809
|
+
} else if (this.doc.layers !== before.layers) {
|
|
8810
|
+
this.doc = { ...this.doc, layers: before.layers };
|
|
7686
8811
|
}
|
|
7687
8812
|
this.emit();
|
|
7688
8813
|
this.notifySample(cancelled ? "cancel" : "end", s, [], cancelled ? void 0 : null);
|
|
@@ -7698,13 +8823,33 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7698
8823
|
s.result.wet = this.wet.isWet(s.layerId);
|
|
7699
8824
|
}
|
|
7700
8825
|
const l = this.getLayer(s.layerId);
|
|
7701
|
-
const
|
|
7702
|
-
const thick = ch ? { ...this.doc.thick ?? {}, [l.id]: ch } : this.doc.thick;
|
|
8826
|
+
const thick = withLayerThick(this.doc.thick, l.id, s.thickChannels());
|
|
7703
8827
|
const sprayed = s.stickers();
|
|
7704
8828
|
const stickers = sprayed ? { ...this.doc.stickers ?? {}, [l.id]: [...this.doc.stickers?.[l.id] ?? [], ...sprayed] } : this.doc.stickers;
|
|
7705
8829
|
const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
|
|
7706
8830
|
const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
|
|
7707
|
-
|
|
8831
|
+
const fluor = withLayerLight(this.doc.fluor, l.id, s.fluorChannel());
|
|
8832
|
+
const thermo = withLayerLight(this.doc.thermo, l.id, s.thermoChannel());
|
|
8833
|
+
this.commit(
|
|
8834
|
+
`${brush.name} stroke`,
|
|
8835
|
+
this.withWetEvents(
|
|
8836
|
+
withLiveStroke(
|
|
8837
|
+
{
|
|
8838
|
+
...this.doc,
|
|
8839
|
+
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })),
|
|
8840
|
+
strokes: [...this.doc.strokes, stroke],
|
|
8841
|
+
...thick ? { thick } : {},
|
|
8842
|
+
...stickers ? { stickers } : {},
|
|
8843
|
+
...light ? { light } : {},
|
|
8844
|
+
...fluor ? { fluor } : {},
|
|
8845
|
+
...thermo ? { thermo } : {}
|
|
8846
|
+
},
|
|
8847
|
+
stroke
|
|
8848
|
+
)
|
|
8849
|
+
),
|
|
8850
|
+
before
|
|
8851
|
+
);
|
|
8852
|
+
this.recordMutationStroke(stroke, s.result?.bounds ?? null);
|
|
7708
8853
|
this.notifySample("end", s, [], stroke.id);
|
|
7709
8854
|
}
|
|
7710
8855
|
};
|
|
@@ -7782,6 +8927,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7782
8927
|
this.live = null;
|
|
7783
8928
|
this.liveLayers = null;
|
|
7784
8929
|
this.liveThick = null;
|
|
8930
|
+
this.liveFluor = null;
|
|
8931
|
+
this.liveThermo = null;
|
|
7785
8932
|
const masks = s.init.selection ? (s.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === "mask") : [];
|
|
7786
8933
|
const load = s.init.mixerLoad;
|
|
7787
8934
|
const result = this.paintStroke({ layerId: s.layerId, points: s.points, color: s.init.colorHex, brush: brushToJSON(s.init.brush), seed: s.init.seed, copies: s.init.affineCopies, ...load.reservoir || load.pickup ? { load } : {}, paint, snapToPerspective: false, stencils: false, maskStencils: masks });
|
|
@@ -7800,6 +8947,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7800
8947
|
this.live = null;
|
|
7801
8948
|
this.liveLayers = null;
|
|
7802
8949
|
this.liveThick = null;
|
|
8950
|
+
this.liveFluor = null;
|
|
8951
|
+
this.liveThermo = null;
|
|
7803
8952
|
this.emit();
|
|
7804
8953
|
this.notifySample("cancel", s, []);
|
|
7805
8954
|
throw e;
|
|
@@ -7820,18 +8969,27 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7820
8969
|
const stroke = this.record(s);
|
|
7821
8970
|
s.result.strokeId = stroke.id;
|
|
7822
8971
|
this.paintedBounds.set(stroke, s.result?.bounds ?? null);
|
|
8972
|
+
const targetId = this.tempLayerTargets.get(s.layerId);
|
|
8973
|
+
if (targetId && !findLayer(out.layers, s.layerId)) {
|
|
8974
|
+
const temp = findLayer(this.doc.layers, s.layerId)?.layer;
|
|
8975
|
+
if (temp) out = { ...out, layers: insertAfter(out.layers, targetId, temp) };
|
|
8976
|
+
}
|
|
7823
8977
|
const l = findLayer(out.layers, s.layerId).layer;
|
|
7824
8978
|
const offset = s.init.wet ? { x: 0, y: 0 } : l.offset;
|
|
7825
|
-
const
|
|
8979
|
+
const thick = withLayerThick(out.thick, l.id, s.thickChannels());
|
|
7826
8980
|
const sprayed = s.stickers();
|
|
7827
8981
|
const lit = s.lightChannel() ?? (s.init.wet && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null);
|
|
8982
|
+
const fl = s.fluorChannel();
|
|
8983
|
+
const th = s.thermoChannel();
|
|
7828
8984
|
out = withLiveStroke({
|
|
7829
8985
|
...out,
|
|
7830
8986
|
layers: mapLayer(out.layers, l.id, () => ({ ...l, surface, offset })),
|
|
7831
8987
|
strokes: [...out.strokes, stroke],
|
|
7832
|
-
...
|
|
8988
|
+
...thick ? { thick } : {},
|
|
7833
8989
|
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {},
|
|
7834
|
-
...lit ? { light: withLayerLight(out.light, l.id, lit) } : {}
|
|
8990
|
+
...lit ? { light: withLayerLight(out.light, l.id, lit) } : {},
|
|
8991
|
+
...fl ? { fluor: withLayerLight(out.fluor, l.id, fl) } : {},
|
|
8992
|
+
...th ? { thermo: withLayerLight(out.thermo, l.id, th) } : {}
|
|
7835
8993
|
}, stroke);
|
|
7836
8994
|
}
|
|
7837
8995
|
};
|
|
@@ -7864,8 +9022,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7864
9022
|
}
|
|
7865
9023
|
const { doc, result, brush } = run;
|
|
7866
9024
|
if (result.mixerLoad) this.keepLoad(brush, result.mixerLoad);
|
|
7867
|
-
if (doc !== before || this.wetOp)
|
|
7868
|
-
|
|
9025
|
+
if (doc !== before || this.wetOp) {
|
|
9026
|
+
this.commit(`${brush.name} stroke`, this.withWetEvents(doc), before);
|
|
9027
|
+
const lastStroke = this.doc.strokes[this.doc.strokes.length - 1];
|
|
9028
|
+
if (lastStroke && lastStroke.id === result.strokeId) this.recordMutationStroke(lastStroke, result.bounds);
|
|
9029
|
+
} else this.emit([]);
|
|
7869
9030
|
return result;
|
|
7870
9031
|
}
|
|
7871
9032
|
/**
|
|
@@ -7904,8 +9065,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7904
9065
|
}
|
|
7905
9066
|
const run = last;
|
|
7906
9067
|
if (run?.result.mixerLoad) this.keepLoad(run.brush, run.result.mixerLoad);
|
|
7907
|
-
if (doc !== before || this.wetOp)
|
|
7908
|
-
|
|
9068
|
+
if (doc !== before || this.wetOp) {
|
|
9069
|
+
this.commit(`${run?.brush.name ?? "Brush"} path stroke`, this.withWetEvents(doc), before);
|
|
9070
|
+
for (const sid of strokeIds) {
|
|
9071
|
+
const recorded = this.doc.strokes.find((s) => s.id === sid);
|
|
9072
|
+
if (recorded) this.recordMutationStroke(recorded, this.paintedBounds.get(recorded) ?? null);
|
|
9073
|
+
}
|
|
9074
|
+
} else this.emit([]);
|
|
7909
9075
|
const layerId = run?.result.layerId ?? stroke.layerId ?? this.doc.activeLayerId ?? "";
|
|
7910
9076
|
return { layerId, bounds, dabs, strokeIds, ...run?.result.mixerLoad ? { mixerLoad: run.result.mixerLoad } : {}, ...run?.result.wet !== void 0 ? { wet: run.result.wet } : {} };
|
|
7911
9077
|
}
|
|
@@ -8029,6 +9195,137 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8029
9195
|
this.commit("Clear light", { ...this.doc, light: t.light });
|
|
8030
9196
|
return { layerId: l.id, cleared: true };
|
|
8031
9197
|
}
|
|
9198
|
+
// --- UV blacklight and thermochromic heat ---------------------------------------
|
|
9199
|
+
fluorWithout(ids) {
|
|
9200
|
+
const t = this.doc.fluor;
|
|
9201
|
+
if (!t || !ids.some((id) => t[id])) return {};
|
|
9202
|
+
const out = { ...t };
|
|
9203
|
+
for (const id of ids) delete out[id];
|
|
9204
|
+
return { fluor: out };
|
|
9205
|
+
}
|
|
9206
|
+
thermoWithout(ids) {
|
|
9207
|
+
const t = this.doc.thermo;
|
|
9208
|
+
if (!t || !ids.some((id) => t[id])) return {};
|
|
9209
|
+
const out = { ...t };
|
|
9210
|
+
for (const id of ids) delete out[id];
|
|
9211
|
+
return { thermo: out };
|
|
9212
|
+
}
|
|
9213
|
+
/** The UV / blacklight view setting. */
|
|
9214
|
+
getUvLight() {
|
|
9215
|
+
return { ...this.uvLight };
|
|
9216
|
+
}
|
|
9217
|
+
/** Layers that carry UV-fluorescent ink. */
|
|
9218
|
+
fluorescentLayerIds() {
|
|
9219
|
+
return Object.entries(this.doc.fluor ?? {}).filter(([id, s]) => s.tiles.size > 0 && Boolean(findLayer(this.doc.layers, id))).map(([id]) => id);
|
|
9220
|
+
}
|
|
9221
|
+
/** Layers that carry thermochromic ink. */
|
|
9222
|
+
thermochromicLayerIds() {
|
|
9223
|
+
return Object.entries(this.doc.thermo ?? {}).filter(([id, s]) => s.tiles.size > 0 && Boolean(findLayer(this.doc.layers, id))).map(([id]) => id);
|
|
9224
|
+
}
|
|
9225
|
+
/**
|
|
9226
|
+
* Switches or tunes the virtual UV / blacklight (`on`, `strength` 0..2, `ambient` 0..1). When
|
|
9227
|
+
* `on` is true, the composite dims to `ambient` and fluorescent ink (`brush.fluorescence`) glows
|
|
9228
|
+
* in its stroke colour with a Linear Dodge halo. Saved with the document, not an undo step.
|
|
9229
|
+
*/
|
|
9230
|
+
setUvLight(update) {
|
|
9231
|
+
if (update.on !== void 0 && typeof update.on !== "boolean") throw new PaintEditorError("on must be true or false.");
|
|
9232
|
+
if (update.strength !== void 0 && !(typeof update.strength === "number" && Number.isFinite(update.strength) && update.strength >= 0 && update.strength <= 2)) {
|
|
9233
|
+
throw new PaintEditorError(`strength ${update.strength} is out of range; use 0..2.`);
|
|
9234
|
+
}
|
|
9235
|
+
if (update.ambient !== void 0 && !(typeof update.ambient === "number" && Number.isFinite(update.ambient) && update.ambient >= 0 && update.ambient <= 1)) {
|
|
9236
|
+
throw new PaintEditorError(`ambient ${update.ambient} is out of range; use 0..1.`);
|
|
9237
|
+
}
|
|
9238
|
+
this.uvLight = normalizeUvLight(update, this.uvLight);
|
|
9239
|
+
this.emit("all");
|
|
9240
|
+
return { ...this.uvLight };
|
|
9241
|
+
}
|
|
9242
|
+
/**
|
|
9243
|
+
* Reads the document's heat field: its temperature summary (`peak`, `mean`, `hotCells`) and,
|
|
9244
|
+
* when `points` are given (document pixels), the bilinearly sampled temperature (0..1) at each.
|
|
9245
|
+
*/
|
|
9246
|
+
getHeat(points = []) {
|
|
9247
|
+
const samples = points.map((p, i) => {
|
|
9248
|
+
if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) throw new PaintEditorError(`points[${i}] needs finite x and y in document pixels.`);
|
|
9249
|
+
return { x: p.x, y: p.y, temperature: this.doc.heat ? Math.round(sampleHeatAt(this.doc.heat, p.x, p.y) * 1e3) / 1e3 : 0 };
|
|
9250
|
+
});
|
|
9251
|
+
return { ...heatStats(this.doc.heat), samples, thermochromicLayerIds: this.thermochromicLayerIds() };
|
|
9252
|
+
}
|
|
9253
|
+
/**
|
|
9254
|
+
* Warms (or with `temperature` 0 cools) the canvas heat field along `points` (document pixels)
|
|
9255
|
+
* with a soft brush of `radius` px (default 28) towards `temperature` 0..1 (default 1). Where
|
|
9256
|
+
* temperature reaches `HEAT_DRY_THRESHOLD` (0.5), any wet paint on those tiles dries at once.
|
|
9257
|
+
* One undo step.
|
|
9258
|
+
*/
|
|
9259
|
+
paintHeat(options) {
|
|
9260
|
+
this.requireIdle("painting heat");
|
|
9261
|
+
this.requireWetIdle("painting heat");
|
|
9262
|
+
const points = this.checkPoints(options.points);
|
|
9263
|
+
const radius = options.radius ?? 28;
|
|
9264
|
+
if (!(typeof radius === "number" && Number.isFinite(radius) && radius >= 1 && radius <= 512)) {
|
|
9265
|
+
throw new PaintEditorError(`radius ${options.radius} is out of range; use 1..512 px.`);
|
|
9266
|
+
}
|
|
9267
|
+
const temperature = options.temperature ?? 1;
|
|
9268
|
+
if (!(typeof temperature === "number" && Number.isFinite(temperature) && temperature >= 0 && temperature <= 1)) {
|
|
9269
|
+
throw new PaintEditorError(`temperature ${options.temperature} is out of range; use 0..1 (0 cools back to ambient).`);
|
|
9270
|
+
}
|
|
9271
|
+
const before = this.doc;
|
|
9272
|
+
const { width, height } = this.doc.canvas;
|
|
9273
|
+
const field = this.doc.heat ? cloneHeatField(this.doc.heat) : createHeatField(width, height);
|
|
9274
|
+
const { stats } = paintHeatField(field, points, radius, temperature);
|
|
9275
|
+
const dried = this.dryWetFromHeat(field);
|
|
9276
|
+
const next = { ...this.doc, ...stats.max > 0 ? { heat: field } : {} };
|
|
9277
|
+
if (stats.max === 0) delete next.heat;
|
|
9278
|
+
if (next !== before || this.wetOp) this.commit("Paint heat", this.withWetEvents(next), before);
|
|
9279
|
+
else this.emit([]);
|
|
9280
|
+
return { ...stats, dried };
|
|
9281
|
+
}
|
|
9282
|
+
/**
|
|
9283
|
+
* Advances the heat field by `steps` simulation steps (1..200, default 10): heat diffuses into
|
|
9284
|
+
* neighbouring cells and cools towards 0 by Newtonian cooling. Newly heated tiles above the dry
|
|
9285
|
+
* threshold dry any wet paint on them. Not an undo step (time passing never is).
|
|
9286
|
+
*/
|
|
9287
|
+
advanceHeat(stepsOrOptions = 10) {
|
|
9288
|
+
this.requireIdle("advancing heat");
|
|
9289
|
+
this.requireWetIdle("advancing heat");
|
|
9290
|
+
const rawSteps = typeof stepsOrOptions === "number" ? stepsOrOptions : stepsOrOptions?.steps;
|
|
9291
|
+
const steps = rawSteps ?? 10;
|
|
9292
|
+
if (!(Number.isInteger(steps) && steps >= 1 && steps <= 200)) {
|
|
9293
|
+
throw new PaintEditorError(`steps ${rawSteps} is out of range; use a whole number 1..200.`);
|
|
9294
|
+
}
|
|
9295
|
+
if (!this.doc.heat || heatStats(this.doc.heat).max === 0) {
|
|
9296
|
+
return { steps, max: 0, mean: 0, hotCells: 0, bounds: null, dried: [] };
|
|
9297
|
+
}
|
|
9298
|
+
const field = cloneHeatField(this.doc.heat);
|
|
9299
|
+
const stats = advanceHeatField(field, steps);
|
|
9300
|
+
const dried = this.dryWetFromHeat(field);
|
|
9301
|
+
const next = { ...this.doc, ...stats.max > 0 ? { heat: field } : {} };
|
|
9302
|
+
if (stats.max === 0) delete next.heat;
|
|
9303
|
+
this.doc = this.withWetEvents(next);
|
|
9304
|
+
this.docRevision++;
|
|
9305
|
+
this.emit("all");
|
|
9306
|
+
return { steps, ...stats, dried };
|
|
9307
|
+
}
|
|
9308
|
+
/** Cools the whole canvas heat field to 0 at once (one undo step when anything was warm). */
|
|
9309
|
+
clearHeat() {
|
|
9310
|
+
this.requireIdle("clearing heat");
|
|
9311
|
+
if (!this.doc.heat || heatStats(this.doc.heat).max === 0) {
|
|
9312
|
+
return { cleared: false };
|
|
9313
|
+
}
|
|
9314
|
+
const { heat: _gone, ...rest } = this.doc;
|
|
9315
|
+
this.commit("Clear heat", rest);
|
|
9316
|
+
return { cleared: true };
|
|
9317
|
+
}
|
|
9318
|
+
/** Dries any wet paint on tiles where `field` is at or above `HEAT_DRY_THRESHOLD`. */
|
|
9319
|
+
dryWetFromHeat(field) {
|
|
9320
|
+
const ids = this.wet.wetLayerIds();
|
|
9321
|
+
if (!ids.length) return [];
|
|
9322
|
+
const hotRect = hotCellsBounds(field, HEAT_DRY_THRESHOLD);
|
|
9323
|
+
if (!hotRect) return [];
|
|
9324
|
+
this.beginWetOp();
|
|
9325
|
+
const dried = ids.flatMap((id) => this.wet.dry(id, hotRect)).filter((d) => d.bounds !== null);
|
|
9326
|
+
if (dried.length) this.logWet({ kind: "dry", data: hotRect });
|
|
9327
|
+
return dried;
|
|
9328
|
+
}
|
|
8032
9329
|
// --- Thick paint and light -------------------------------------------------------
|
|
8033
9330
|
/** The document's `thick` without these layers (as a spread: `{}` when unchanged). */
|
|
8034
9331
|
thickWithout(ids) {
|
|
@@ -8050,13 +9347,29 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8050
9347
|
this.emit("all");
|
|
8051
9348
|
return { ...this.lighting };
|
|
8052
9349
|
}
|
|
9350
|
+
/** Where the viewer looks from for thin-film iridescence and specular highlights (-1..1; editor state, not saved with the document). */
|
|
9351
|
+
getViewTilt() {
|
|
9352
|
+
return { ...this.viewTilt };
|
|
9353
|
+
}
|
|
9354
|
+
/** Tilts the view vector for thin-film iridescence and specular highlights (-1..1; editor state, not saved with the document, not an undo step). */
|
|
9355
|
+
setViewTilt(update) {
|
|
9356
|
+
if (update.x !== void 0 && !(Number.isFinite(update.x) && update.x >= -1 && update.x <= 1)) {
|
|
9357
|
+
throw new PaintEditorError(`x ${update.x} is out of range; use -1..1.`);
|
|
9358
|
+
}
|
|
9359
|
+
if (update.y !== void 0 && !(Number.isFinite(update.y) && update.y >= -1 && update.y <= 1)) {
|
|
9360
|
+
throw new PaintEditorError(`y ${update.y} is out of range; use -1..1.`);
|
|
9361
|
+
}
|
|
9362
|
+
this.viewTilt = normalizeViewTilt(update, this.viewTilt);
|
|
9363
|
+
this.emit("all");
|
|
9364
|
+
return { ...this.viewTilt };
|
|
9365
|
+
}
|
|
8053
9366
|
/** A layer's relief: where it is, its highest point (0..255) and its tiles; `null` when it has none. */
|
|
8054
9367
|
reliefOf(layerId) {
|
|
8055
9368
|
const id = layerId ?? this.doc.activeLayerId;
|
|
8056
9369
|
if (!id) return null;
|
|
8057
9370
|
this.requireLayer(id);
|
|
8058
9371
|
const ch = this.doc.thick?.[id];
|
|
8059
|
-
if (!ch) return null;
|
|
9372
|
+
if (!ch || !hasThickChannels(ch)) return null;
|
|
8060
9373
|
const r = reliefInfo(ch);
|
|
8061
9374
|
return { layerId: id, bounds: r.bounds, maxHeight: r.maxHeight };
|
|
8062
9375
|
}
|
|
@@ -8087,6 +9400,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8087
9400
|
const paper = { ...this.doc.paper, scale: Math.min(8, this.doc.paper.scale * scale) };
|
|
8088
9401
|
target.adopt({ state: { canvas: target.doc.canvas, paper, layers: emptiedLayers(this.doc.layers), activeLayerId: this.doc.activeLayerId, strokes: [] }, extensions: {}, problems: [] });
|
|
8089
9402
|
target.lighting = { ...this.lighting };
|
|
9403
|
+
target.uvLight = { ...this.uvLight };
|
|
8090
9404
|
target.effects.set(this.effects.provider);
|
|
8091
9405
|
if (w === width && h === height) for (const [key, img] of this.effectPaintPixels) target.effectPaintPixels.set(key, img);
|
|
8092
9406
|
return target;
|
|
@@ -8130,6 +9444,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8130
9444
|
* bindings do). Steps at the wet speed unless paused; resolves when the frame's steps are done.
|
|
8131
9445
|
*/
|
|
8132
9446
|
tickWet(now) {
|
|
9447
|
+
this.tickMutations(now);
|
|
8133
9448
|
return this.wet.tick(now);
|
|
8134
9449
|
}
|
|
8135
9450
|
/** Resolves when the wet kernel is as fast as it will get here (`null` when nothing is wet yet). */
|
|
@@ -8204,7 +9519,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8204
9519
|
async advanceWet(options) {
|
|
8205
9520
|
const steps = options.steps ?? Math.round((options.seconds ?? 1) * this.wet.speed);
|
|
8206
9521
|
if (!(Number.isInteger(steps) && steps >= 0 && steps <= 1e5)) throw new PaintEditorError(`steps ${steps} is out of range; use 0..100000 (or seconds).`);
|
|
9522
|
+
const seconds = options.seconds ?? steps / this.wet.speed;
|
|
8207
9523
|
const run = await this.wet.advance(steps);
|
|
9524
|
+
if (seconds > 0) this.advanceMutations(seconds);
|
|
8208
9525
|
this.emit();
|
|
8209
9526
|
const st = this.wetState();
|
|
8210
9527
|
return { steps: run, clock: st.clock, stillWet: st.wet, layers: st.layers };
|
|
@@ -8223,7 +9540,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8223
9540
|
if (!ids.length) return { dried: [], stillWet: this.wet.isWet() };
|
|
8224
9541
|
const before = this.doc;
|
|
8225
9542
|
this.beginWetOp();
|
|
8226
|
-
const dried = ids.flatMap((id) =>
|
|
9543
|
+
const dried = ids.flatMap((id) => {
|
|
9544
|
+
const r = this.wet.dry(id, rect);
|
|
9545
|
+
if (!rect) this.finishWetMutationsOnDry(id);
|
|
9546
|
+
return r;
|
|
9547
|
+
});
|
|
8227
9548
|
this.logWet({ kind: "dry", layerId: options.layerId, data: rect });
|
|
8228
9549
|
this.commit("Dry paint", this.withWetEvents(this.doc), before);
|
|
8229
9550
|
return { dried, stillWet: this.wet.isWet() };
|
|
@@ -8257,6 +9578,15 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8257
9578
|
setWetParams(layerId, update) {
|
|
8258
9579
|
const l = this.wetLayer(layerId, "change the wet settings of");
|
|
8259
9580
|
this.requireWetIdle("changing wet settings");
|
|
9581
|
+
if (this.heldEvaporation.has(l.id)) {
|
|
9582
|
+
const current = this.wet.info(l.id).params;
|
|
9583
|
+
const intended = normalizeWetParams({ ...current, evaporation: this.heldEvaporation.get(l.id), ...update });
|
|
9584
|
+
this.heldEvaporation.set(l.id, intended.evaporation);
|
|
9585
|
+
this.wet.setParams(l.id, { ...update, evaporation: 0 });
|
|
9586
|
+
this.recordWetSetting({ kind: "params", layerId: l.id, data: intended });
|
|
9587
|
+
this.emit([]);
|
|
9588
|
+
return intended;
|
|
9589
|
+
}
|
|
8260
9590
|
const params = this.wet.setParams(l.id, update);
|
|
8261
9591
|
this.recordWetSetting({ kind: "params", layerId: l.id, data: params });
|
|
8262
9592
|
this.emit([]);
|
|
@@ -8362,6 +9692,55 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8362
9692
|
}
|
|
8363
9693
|
return out;
|
|
8364
9694
|
}
|
|
9695
|
+
/** The layers with thermochromic paint shifted toward its hot colour where the canvas is warm. */
|
|
9696
|
+
thermoLayers(layers) {
|
|
9697
|
+
const thermo = this.doc.thermo;
|
|
9698
|
+
const heat = this.doc.heat;
|
|
9699
|
+
if (!thermo || !heat || heat.max === 0) return layers;
|
|
9700
|
+
let out = layers;
|
|
9701
|
+
for (const [id, th] of Object.entries(thermo)) {
|
|
9702
|
+
const l = findLayer(out, id)?.layer;
|
|
9703
|
+
if (!isPixelLayer(l) || !th.tiles.size) continue;
|
|
9704
|
+
let cache = this.thermoCaches.get(id);
|
|
9705
|
+
if (!cache) this.thermoCaches.set(id, cache = new ThermoCache());
|
|
9706
|
+
const shifted = cache.render(l.surface, th, l.offset, heat);
|
|
9707
|
+
if (shifted !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: shifted }));
|
|
9708
|
+
}
|
|
9709
|
+
return out;
|
|
9710
|
+
}
|
|
9711
|
+
/**
|
|
9712
|
+
* Under the UV blacklight: every pixel layer's daylight paint dims to `uvLight.ambient`, any
|
|
9713
|
+
* fluorescent ink (`doc.fluor[id]`) glows in its own colour, and each fluorescent layer casts a
|
|
9714
|
+
* Linear Dodge (`::uv`) spill layer directly above it.
|
|
9715
|
+
*/
|
|
9716
|
+
uvLayers(layers) {
|
|
9717
|
+
if (!this.uvLight.on) return layers;
|
|
9718
|
+
const fluor = this.doc.fluor ?? {};
|
|
9719
|
+
const visit = (list) => {
|
|
9720
|
+
let out = [];
|
|
9721
|
+
for (const l of list) {
|
|
9722
|
+
if (l.type === "group") {
|
|
9723
|
+
out.push({ ...l, children: visit(l.children) });
|
|
9724
|
+
continue;
|
|
9725
|
+
}
|
|
9726
|
+
if (!isPixelLayer(l)) {
|
|
9727
|
+
out.push(l);
|
|
9728
|
+
continue;
|
|
9729
|
+
}
|
|
9730
|
+
let cache = this.uvCaches.get(l.id);
|
|
9731
|
+
if (!cache) this.uvCaches.set(l.id, cache = new UvCache());
|
|
9732
|
+
const fl = fluor[l.id];
|
|
9733
|
+
const surface = cache.render(l.surface, fl, this.uvLight);
|
|
9734
|
+
out.push(surface !== l.surface ? { ...l, surface } : l);
|
|
9735
|
+
const spill = fl ? cache.spill(fl, this.uvLight) : null;
|
|
9736
|
+
if (spill) {
|
|
9737
|
+
out.push({ type: "pixel", id: `${l.id}${UV_SPILL_SUFFIX}`, name: `${l.name} UV`, visible: l.visible, opacity: l.opacity, blend: "linear-dodge", surface: spill, offset: { ...l.offset } });
|
|
9738
|
+
}
|
|
9739
|
+
}
|
|
9740
|
+
return out;
|
|
9741
|
+
};
|
|
9742
|
+
return visit(layers);
|
|
9743
|
+
}
|
|
8365
9744
|
/** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */
|
|
8366
9745
|
litLayers(layers) {
|
|
8367
9746
|
const thick = this.doc.thick;
|
|
@@ -8375,7 +9754,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8375
9754
|
if (!ch || !isPixelLayer(l)) continue;
|
|
8376
9755
|
let cache = this.litCaches.get(id);
|
|
8377
9756
|
if (!cache) this.litCaches.set(id, cache = new LitCache());
|
|
8378
|
-
const lit = cache.lit(l.surface, ch, this.lighting);
|
|
9757
|
+
const lit = cache.lit(l.surface, ch, this.lighting, this.viewTilt);
|
|
8379
9758
|
if (lit !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: lit }));
|
|
8380
9759
|
}
|
|
8381
9760
|
return out;
|
|
@@ -8433,7 +9812,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8433
9812
|
}
|
|
8434
9813
|
renderLayers(options, view = false) {
|
|
8435
9814
|
const base = (options.wet ?? "display") === "dried" ? this.driedLayers() : this.currentLayers();
|
|
8436
|
-
let layers = this.lightLayers(this.litLayers(base));
|
|
9815
|
+
let layers = this.lightLayers(this.uvLayers(this.litLayers(this.thermoLayers(base))));
|
|
8437
9816
|
const backlight = options.paper !== false && !options.layerIds ? this.backlightLayer(base) : null;
|
|
8438
9817
|
if (backlight) layers = [...layers, backlight];
|
|
8439
9818
|
if (options.layerIds) {
|
|
@@ -8441,7 +9820,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8441
9820
|
for (const id of options.layerIds) picked.push(findLayer(layers, id)?.layer ?? this.requireLayer(id));
|
|
8442
9821
|
layers = picked.map((l) => ({ ...l, visible: true }));
|
|
8443
9822
|
}
|
|
8444
|
-
const under = options.paper === false ? [] : [paperNode(this.doc.paper, this.lighting)];
|
|
9823
|
+
const under = options.paper === false ? [] : [paperNode(uvPaper(this.doc.paper, this.uvLight), this.lighting)];
|
|
8445
9824
|
if (options.paper !== false && this.doc.paperEffect) under.push(this.paperEffectNode(this.doc.paperEffect));
|
|
8446
9825
|
if (view) under.push(...this.underlayLayers());
|
|
8447
9826
|
return [...under, ...layers];
|
|
@@ -8476,8 +9855,36 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8476
9855
|
/** The document as JSON with inline PNG assets. */
|
|
8477
9856
|
/** The document as saved: wet paint dried where it is (deterministically); wet settings as data. */
|
|
8478
9857
|
savedState() {
|
|
8479
|
-
|
|
8480
|
-
|
|
9858
|
+
let state = this.doc;
|
|
9859
|
+
for (const m of this.mutations) {
|
|
9860
|
+
if (m.medium !== "dry" || !m.tempLayerId || !findLayer(state.layers, m.tempLayerId)) continue;
|
|
9861
|
+
if (m.state === "claimed" || m.state === "answered") {
|
|
9862
|
+
const tempId = m.tempLayerId;
|
|
9863
|
+
state = {
|
|
9864
|
+
...state,
|
|
9865
|
+
layers: mapLayer(state.layers, tempId, () => null),
|
|
9866
|
+
strokes: state.strokes.filter((s) => s.layerId !== tempId)
|
|
9867
|
+
};
|
|
9868
|
+
} else {
|
|
9869
|
+
state = this.mergeTempLayerIntoTarget(state, m.tempLayerId, m.layerId);
|
|
9870
|
+
}
|
|
9871
|
+
}
|
|
9872
|
+
for (const l of flattenLayers(state.layers)) {
|
|
9873
|
+
if (isPixelLayer(l) && l.temporary) {
|
|
9874
|
+
const targetId = this.tempLayerTargets.get(l.id);
|
|
9875
|
+
if (targetId && findLayer(state.layers, targetId)) {
|
|
9876
|
+
state = this.mergeTempLayerIntoTarget(state, l.id, targetId);
|
|
9877
|
+
} else {
|
|
9878
|
+
state = {
|
|
9879
|
+
...state,
|
|
9880
|
+
layers: mapLayer(state.layers, l.id, () => null),
|
|
9881
|
+
strokes: state.strokes.filter((s) => s.layerId !== l.id)
|
|
9882
|
+
};
|
|
9883
|
+
}
|
|
9884
|
+
}
|
|
9885
|
+
}
|
|
9886
|
+
const layers = this.driedLayers(state.layers);
|
|
9887
|
+
return layers === state.layers ? state : { ...state, layers };
|
|
8481
9888
|
}
|
|
8482
9889
|
/** Wet settings saved with the document (`null` when they are all defaults). */
|
|
8483
9890
|
wetJSON() {
|
|
@@ -8485,7 +9892,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8485
9892
|
for (const [id, set] of this.wet.settings) {
|
|
8486
9893
|
if (!findLayer(this.doc.layers, id)) continue;
|
|
8487
9894
|
const ghost = this.wet.world?.hasLane(id) ? this.wet.world.getGhost(id) : set.ghost ?? null;
|
|
8488
|
-
|
|
9895
|
+
const savedGhost = ghost?.trigger === "agent" ? null : ghost;
|
|
9896
|
+
const heldEvap = this.heldEvaporation.get(id);
|
|
9897
|
+
const params = heldEvap !== void 0 ? { ...set.params, evaporation: heldEvap } : { ...set.params };
|
|
9898
|
+
layers[id] = { params, flow: { ...set.flow }, ...savedGhost ? { ghost: savedGhost } : {} };
|
|
8489
9899
|
}
|
|
8490
9900
|
const flat = this.wet.tilt.angle === 0;
|
|
8491
9901
|
if (!Object.keys(layers).length && flat) return void 0;
|
|
@@ -8550,13 +9960,37 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8550
9960
|
else delete ext.paperEffect;
|
|
8551
9961
|
if (this.lighting.azimuth !== DEFAULT_LIGHTING.azimuth || this.lighting.elevation !== DEFAULT_LIGHTING.elevation || this.lighting.relief !== DEFAULT_LIGHTING.relief) ext.lighting = { ...this.lighting };
|
|
8552
9962
|
else delete ext.lighting;
|
|
9963
|
+
const fluor = {};
|
|
9964
|
+
for (const [id, f] of Object.entries(this.doc.fluor ?? {})) {
|
|
9965
|
+
if (!findLayer(this.doc.layers, id)) continue;
|
|
9966
|
+
const saved = fluorToJSON(f);
|
|
9967
|
+
if (saved) fluor[id] = saved;
|
|
9968
|
+
}
|
|
9969
|
+
if (Object.keys(fluor).length) ext.fluor = fluor;
|
|
9970
|
+
else delete ext.fluor;
|
|
9971
|
+
const thermo = {};
|
|
9972
|
+
for (const [id, th] of Object.entries(this.doc.thermo ?? {})) {
|
|
9973
|
+
if (!findLayer(this.doc.layers, id)) continue;
|
|
9974
|
+
const saved = thermoToJSON(th);
|
|
9975
|
+
if (saved) thermo[id] = saved;
|
|
9976
|
+
}
|
|
9977
|
+
if (Object.keys(thermo).length) ext.thermo = thermo;
|
|
9978
|
+
else delete ext.thermo;
|
|
9979
|
+
const savedHeat = this.doc.heat ? heatToJSON(this.doc.heat) : null;
|
|
9980
|
+
if (savedHeat) ext.heat = savedHeat;
|
|
9981
|
+
else delete ext.heat;
|
|
9982
|
+
if (this.uvLight.on !== DEFAULT_UV_LIGHT.on || this.uvLight.strength !== DEFAULT_UV_LIGHT.strength || this.uvLight.ambient !== DEFAULT_UV_LIGHT.ambient) ext.uvLight = { ...this.uvLight };
|
|
9983
|
+
else delete ext.uvLight;
|
|
8553
9984
|
return ext;
|
|
8554
9985
|
}
|
|
8555
9986
|
/** Reads `layerChannels` and `lighting` from loaded extensions into the document. */
|
|
8556
9987
|
readThick(state, ext, problems) {
|
|
8557
9988
|
if (Array.isArray(ext.brushSources)) this.brushSources.set(ext.brushSources.map((x, i) => readBrushSource(x, `brushSources[${i}]`, problems)).filter((x) => x !== null));
|
|
8558
9989
|
this.lighting = normalizeLighting(ext.lighting ?? {}, DEFAULT_LIGHTING);
|
|
9990
|
+
this.uvLight = normalizeUvLight(ext.uvLight ?? {}, DEFAULT_UV_LIGHT);
|
|
8559
9991
|
this.litCaches.clear();
|
|
9992
|
+
this.uvCaches.clear();
|
|
9993
|
+
this.thermoCaches.clear();
|
|
8560
9994
|
if (ext.stickers && typeof ext.stickers === "object") {
|
|
8561
9995
|
const stickers = {};
|
|
8562
9996
|
for (const [id, list] of Object.entries(ext.stickers)) {
|
|
@@ -8627,6 +10061,35 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8627
10061
|
}
|
|
8628
10062
|
if (Object.keys(light).length) state = { ...state, light };
|
|
8629
10063
|
}
|
|
10064
|
+
if (ext.fluor && typeof ext.fluor === "object") {
|
|
10065
|
+
const fluor = {};
|
|
10066
|
+
for (const [id, v] of Object.entries(ext.fluor)) {
|
|
10067
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
10068
|
+
try {
|
|
10069
|
+
fluor[id] = fluorFromJSON(v);
|
|
10070
|
+
} catch (e) {
|
|
10071
|
+
problems.push(`The fluorescent ink of layer "${id}" could not be read (${e.message}); it loads without fluorescence.`);
|
|
10072
|
+
}
|
|
10073
|
+
}
|
|
10074
|
+
if (Object.keys(fluor).length) state = { ...state, fluor };
|
|
10075
|
+
}
|
|
10076
|
+
if (ext.thermo && typeof ext.thermo === "object") {
|
|
10077
|
+
const thermo = {};
|
|
10078
|
+
for (const [id, v] of Object.entries(ext.thermo)) {
|
|
10079
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
10080
|
+
try {
|
|
10081
|
+
thermo[id] = thermoFromJSON(v);
|
|
10082
|
+
} catch (e) {
|
|
10083
|
+
problems.push(`The thermochromic ink of layer "${id}" could not be read (${e.message}); it loads without thermochromic shift.`);
|
|
10084
|
+
}
|
|
10085
|
+
}
|
|
10086
|
+
if (Object.keys(thermo).length) state = { ...state, thermo };
|
|
10087
|
+
}
|
|
10088
|
+
if (ext.heat !== void 0) {
|
|
10089
|
+
const heat = heatFromJSON(ext.heat, state.canvas.width, state.canvas.height);
|
|
10090
|
+
if (heat) state = { ...state, heat };
|
|
10091
|
+
else problems.push("The heat field could not be read; the canvas loads cool.");
|
|
10092
|
+
}
|
|
8630
10093
|
const raw = ext.layerChannels;
|
|
8631
10094
|
if (!raw || typeof raw !== "object") return state;
|
|
8632
10095
|
const thick = {};
|
|
@@ -8659,6 +10122,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8659
10122
|
}
|
|
8660
10123
|
this.undoStack = [];
|
|
8661
10124
|
this.redoStack = [];
|
|
10125
|
+
this.resetMutations();
|
|
8662
10126
|
this.doc = this.readThick(loaded.state, loaded.extensions, loaded.problems);
|
|
8663
10127
|
for (const list of Object.values(this.doc.liveStrokes ?? {})) {
|
|
8664
10128
|
for (const s of list) {
|
|
@@ -8676,6 +10140,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8676
10140
|
delete this.extensions.brushSources;
|
|
8677
10141
|
delete this.extensions.effectLayers;
|
|
8678
10142
|
delete this.extensions.paperEffect;
|
|
10143
|
+
delete this.extensions.fluor;
|
|
10144
|
+
delete this.extensions.thermo;
|
|
10145
|
+
delete this.extensions.heat;
|
|
10146
|
+
delete this.extensions.uvLight;
|
|
8679
10147
|
this.viewCompositor.clear();
|
|
8680
10148
|
this.emit();
|
|
8681
10149
|
const refs = Object.values(this.doc.stickers ?? {}).flatMap((list) => list.map((x) => x.ref));
|
|
@@ -8737,7 +10205,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8737
10205
|
exportPsd() {
|
|
8738
10206
|
const driedBase = this.driedLayers(this.doc.layers);
|
|
8739
10207
|
const backlight = this.backlightLayer(driedBase);
|
|
8740
|
-
const dried = [...this.lightLayers(this.litLayers(driedBase)), ...backlight ? [backlight] : []];
|
|
10208
|
+
const dried = [...this.lightLayers(this.uvLayers(this.litLayers(this.thermoLayers(driedBase)))), ...backlight ? [backlight] : []];
|
|
8741
10209
|
const { width, height, dpi } = this.doc.canvas;
|
|
8742
10210
|
const paperPixels = this.compositePixels({ layerIds: [], paper: true });
|
|
8743
10211
|
const paperLayer = {
|
|
@@ -9420,8 +10888,24 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9420
10888
|
withLiveStrokes(doc, layer, list, dirty) {
|
|
9421
10889
|
if (isLayerLocked(layer, "pixels")) throw new PaintEditorError(`Layer "${layer.name}" has its pixels locked; unlock it (update_layer locks) first.`);
|
|
9422
10890
|
const current = findLayer(doc.layers, layer.id)?.layer;
|
|
9423
|
-
const
|
|
9424
|
-
const
|
|
10891
|
+
const hasFilm = list.some((s) => Boolean(s.brush.iridescence)) || Boolean(doc.thick?.[layer.id]);
|
|
10892
|
+
const surface = this.drawLiveStrokes(current ?? layer, list, hasFilm ? null : dirty);
|
|
10893
|
+
let thick = doc.thick;
|
|
10894
|
+
if (hasFilm) {
|
|
10895
|
+
const plain = { ...current ?? layer, visible: true, locks: void 0, offset: { x: 0, y: 0 } };
|
|
10896
|
+
const plainDoc = { ...doc, liveStrokes: void 0, stickers: void 0 };
|
|
10897
|
+
let s = emptySurface(4);
|
|
10898
|
+
let ch = list.some((st) => Boolean(st.brush.iridescence)) ? emptyThickChannels() : null;
|
|
10899
|
+
if (ch) {
|
|
10900
|
+
for (const st of list) {
|
|
10901
|
+
const step = this.drawLiveStroke(plainDoc, plain, s, st, ch);
|
|
10902
|
+
s = step.surface;
|
|
10903
|
+
ch = step.thick ?? ch;
|
|
10904
|
+
}
|
|
10905
|
+
}
|
|
10906
|
+
thick = withLayerThick(doc.thick, layer.id, ch);
|
|
10907
|
+
}
|
|
10908
|
+
const next = { ...doc, layers: mapLayer(doc.layers, layer.id, (l) => ({ ...l, surface, offset: { x: 0, y: 0 } })), liveStrokes: { ...doc.liveStrokes ?? {}, [layer.id]: [...list] }, ...thick ? { thick } : {} };
|
|
9425
10909
|
return this.withPaintEvent(next, { kind: "live-strokes", layerId: layer.id, data: { strokes: list.map((x) => JSON.parse(JSON.stringify(x))) } });
|
|
9426
10910
|
}
|
|
9427
10911
|
/** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
|
|
@@ -9439,23 +10923,27 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9439
10923
|
return out;
|
|
9440
10924
|
}
|
|
9441
10925
|
/** Draws one stroke onto `surface` of a plain copy of the layer; returns the result and the area it painted. */
|
|
9442
|
-
drawLiveStroke(doc, plain, surface, st) {
|
|
10926
|
+
drawLiveStroke(doc, plain, surface, st, thick) {
|
|
9443
10927
|
let out = surface;
|
|
10928
|
+
let outThick = thick;
|
|
9444
10929
|
const host = {
|
|
9445
10930
|
update: () => {
|
|
9446
10931
|
},
|
|
9447
|
-
commit: (
|
|
9448
|
-
if (!cancelled && drawn)
|
|
10932
|
+
commit: (s, drawn, cancelled) => {
|
|
10933
|
+
if (!cancelled && drawn) {
|
|
10934
|
+
out = drawn;
|
|
10935
|
+
outThick = s.thickChannels() ?? outThick;
|
|
10936
|
+
}
|
|
9449
10937
|
}
|
|
9450
10938
|
};
|
|
9451
10939
|
const paint = st.paint && this.liveEffectAt !== null && isLiveEffect(st) ? framePaint(st.paint, this.liveEffectAt) : st.paint;
|
|
9452
10940
|
const init = { layerId: plain.id, color: st.color, brush: st.brush, seed: st.seed, copies: st.copies ?? [], ...st.load ? { load: st.load } : {}, ...paint ? { paint } : {}, snapToPerspective: false, stencils: false, maskStencils: st.stencils ?? [] };
|
|
9453
|
-
const session = this.sessionFor({ ...doc, layers: mapLayer(doc.layers, plain.id, () => ({ ...plain, surface })) }, init, host);
|
|
10941
|
+
const session = this.sessionFor({ ...doc, layers: mapLayer(doc.layers, plain.id, () => ({ ...plain, surface })), ...outThick ? { thick: { ...doc.thick ?? {}, [plain.id]: outThick } } : {} }, init, host);
|
|
9454
10942
|
session.push(st.points);
|
|
9455
10943
|
const result = session.end();
|
|
9456
10944
|
const had = this.paintedBounds.get(st);
|
|
9457
10945
|
this.paintedBounds.set(st, had && result.bounds ? unionRect(had, result.bounds) : result.bounds ?? had ?? null);
|
|
9458
|
-
return { surface: out, bounds: result.bounds };
|
|
10946
|
+
return { surface: out, bounds: result.bounds, thick: outThick };
|
|
9459
10947
|
}
|
|
9460
10948
|
/**
|
|
9461
10949
|
* A stroke layer's pixels drawn from its strokes, bottom first. With `dirty` (the areas the old
|
|
@@ -9839,6 +11327,532 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9839
11327
|
});
|
|
9840
11328
|
}
|
|
9841
11329
|
}
|
|
11330
|
+
// --- Mutation paint --------------------------------------------------------------
|
|
11331
|
+
/**
|
|
11332
|
+
* Hears each mutation once it has settled (`settle` seconds after the user's last mutation
|
|
11333
|
+
* stroke). Registering a listener marks an agent as connected (`state.hasMutationAgent`).
|
|
11334
|
+
*/
|
|
11335
|
+
onMutationRequest(listener) {
|
|
11336
|
+
const had = this.mutationListeners.size > 0;
|
|
11337
|
+
this.mutationListeners.add(listener);
|
|
11338
|
+
if (!had) this.emit([]);
|
|
11339
|
+
return () => {
|
|
11340
|
+
this.mutationListeners.delete(listener);
|
|
11341
|
+
if (this.mutationListeners.size === 0) this.emit([]);
|
|
11342
|
+
};
|
|
11343
|
+
}
|
|
11344
|
+
/** Marks whether a mutation-answering agent is connected (for hosts that wake agents via tools). */
|
|
11345
|
+
setMutationAgentConnected(connected) {
|
|
11346
|
+
if (this.mutationAgentConnected === connected) return;
|
|
11347
|
+
this.mutationAgentConnected = connected;
|
|
11348
|
+
this.emit([]);
|
|
11349
|
+
}
|
|
11350
|
+
/** Attaches a `PaintDirector` so `answerMutation` and `readMutation` animate the agent's cursor. */
|
|
11351
|
+
setMutationDirector(director) {
|
|
11352
|
+
this.mutationDirector = director;
|
|
11353
|
+
}
|
|
11354
|
+
/** Lists pending mutations (or all mutations when `includeAll` is true). */
|
|
11355
|
+
listMutations(options = {}) {
|
|
11356
|
+
const list = options.includeAll ? this.mutations : this.mutations.filter((m) => m.state === "pending" || m.state === "settling");
|
|
11357
|
+
return list.map((m) => this.toMutationInfo(m));
|
|
11358
|
+
}
|
|
11359
|
+
/** One mutation by id (`undefined` when unknown). */
|
|
11360
|
+
getMutation(id) {
|
|
11361
|
+
const m = this.mutations.find((x) => x.id === id);
|
|
11362
|
+
return m ? this.toMutationInfo(m) : void 0;
|
|
11363
|
+
}
|
|
11364
|
+
/**
|
|
11365
|
+
* Claims a mutation (`id`) and starts its fade. Returns the strokes (points, colour, brush), a
|
|
11366
|
+
* PNG of the area around them (`cropPng`), and a preview of the whole painting (`canvasPng`) so
|
|
11367
|
+
* the agent can choose where to answer. Refused if already claimed, answered or declined.
|
|
11368
|
+
*/
|
|
11369
|
+
readMutation(id, options = {}) {
|
|
11370
|
+
const m = this.mutations.find((x) => x.id === id);
|
|
11371
|
+
if (!m) throw new PaintEditorError(`Unknown mutation "${id}". Call list_mutations for the pending ids.`);
|
|
11372
|
+
if (m.state === "claimed" || m.state === "answered") {
|
|
11373
|
+
throw new PaintEditorError(`Mutation "${id}" has already been claimed${m.claimedBy ? ` by ${m.claimedBy}` : ""}.`);
|
|
11374
|
+
}
|
|
11375
|
+
if (m.state === "declined") {
|
|
11376
|
+
throw new PaintEditorError(`Mutation "${id}" was already declined.`);
|
|
11377
|
+
}
|
|
11378
|
+
const { width, height } = this.doc.canvas;
|
|
11379
|
+
const pad = Math.max(0, Math.round(options.padding ?? 32));
|
|
11380
|
+
const baseRect = m.bounds ?? { x: 0, y: 0, width, height };
|
|
11381
|
+
const cropRect = clipRect(
|
|
11382
|
+
{ x: baseRect.x - pad, y: baseRect.y - pad, width: baseRect.width + pad * 2, height: baseRect.height + pad * 2 },
|
|
11383
|
+
width,
|
|
11384
|
+
height
|
|
11385
|
+
) ?? { x: 0, y: 0, width, height };
|
|
11386
|
+
const full2 = this.compositePixels({ wet: "display" });
|
|
11387
|
+
const cropData = new Uint8ClampedArray(cropRect.width * cropRect.height * 4);
|
|
11388
|
+
for (let y = 0; y < cropRect.height; y++) {
|
|
11389
|
+
const srcRow = ((cropRect.y + y) * width + cropRect.x) * 4;
|
|
11390
|
+
cropData.set(full2.data.subarray(srcRow, srcRow + cropRect.width * 4), y * cropRect.width * 4);
|
|
11391
|
+
}
|
|
11392
|
+
const cropPngBase64 = bytesToBase64(encodePng(cropRect.width, cropRect.height, cropData));
|
|
11393
|
+
const preview = this.renderPreview({ wet: "display", maxSide: options.maxSide ?? 512 });
|
|
11394
|
+
const canvasPngBase64 = bytesToBase64(encodePng(preview.width, preview.height, preview.data));
|
|
11395
|
+
const actorId = options.actor ?? (this.getOrigin() !== "user" ? this.getOrigin() : "agent");
|
|
11396
|
+
m.state = "claimed";
|
|
11397
|
+
m.claimedBy = actorId;
|
|
11398
|
+
m.fadeRemaining = m.fade;
|
|
11399
|
+
if (m.medium === "wet") {
|
|
11400
|
+
this.releaseMutationEvaporation(m.layerId);
|
|
11401
|
+
this.wet.releaseGhost(m.layerId, m.fade);
|
|
11402
|
+
this.recordWetSetting({ kind: "ghost-release", layerId: m.layerId, data: { linger: m.fade } });
|
|
11403
|
+
}
|
|
11404
|
+
if (this.mutationDirector && m.bounds) {
|
|
11405
|
+
const pid = actorId.replace(/^agent:/, "") || "mutation-agent";
|
|
11406
|
+
this.mutationDirector.presence.set({
|
|
11407
|
+
id: pid,
|
|
11408
|
+
name: "Agent",
|
|
11409
|
+
color: "#7c3aed",
|
|
11410
|
+
x: m.bounds.x + m.bounds.width / 2,
|
|
11411
|
+
y: m.bounds.y + m.bounds.height / 2,
|
|
11412
|
+
message: "Reading\u2026"
|
|
11413
|
+
});
|
|
11414
|
+
}
|
|
11415
|
+
this.emit([]);
|
|
11416
|
+
return {
|
|
11417
|
+
id: m.id,
|
|
11418
|
+
state: "claimed",
|
|
11419
|
+
layerId: m.layerId,
|
|
11420
|
+
medium: m.medium,
|
|
11421
|
+
bounds: m.bounds ? { ...m.bounds } : null,
|
|
11422
|
+
fade: m.fade,
|
|
11423
|
+
strokes: m.strokes.map((s) => ({
|
|
11424
|
+
...s,
|
|
11425
|
+
points: s.points.map((p) => ({ ...p })),
|
|
11426
|
+
bounds: s.bounds ? { ...s.bounds } : null
|
|
11427
|
+
})),
|
|
11428
|
+
cropPng: {
|
|
11429
|
+
width: cropRect.width,
|
|
11430
|
+
height: cropRect.height,
|
|
11431
|
+
rect: cropRect,
|
|
11432
|
+
data: cropPngBase64
|
|
11433
|
+
},
|
|
11434
|
+
canvasPng: {
|
|
11435
|
+
width: preview.width,
|
|
11436
|
+
height: preview.height,
|
|
11437
|
+
data: canvasPngBase64
|
|
11438
|
+
}
|
|
11439
|
+
};
|
|
11440
|
+
}
|
|
11441
|
+
/**
|
|
11442
|
+
* Paints the agent's answer to a claimed mutation (`id`). When a `PaintDirector` is attached (or
|
|
11443
|
+
* passed in `options`), the agent's cursor is seen painting live; all strokes commit as one undo
|
|
11444
|
+
* step with origin `agent:<id>`. A second answer to the same id is refused.
|
|
11445
|
+
*/
|
|
11446
|
+
async answerMutation(id, input, options = {}) {
|
|
11447
|
+
const m = this.mutations.find((x) => x.id === id);
|
|
11448
|
+
if (!m) throw new PaintEditorError(`Unknown mutation "${id}". Call list_mutations for the pending ids.`);
|
|
11449
|
+
if (m.state === "answered") throw new PaintEditorError(`Mutation "${id}" has already been answered.`);
|
|
11450
|
+
if (m.state === "declined") throw new PaintEditorError(`Mutation "${id}" was declined and cannot be answered.`);
|
|
11451
|
+
if (m.state !== "claimed") throw new PaintEditorError(`Mutation "${id}" has not been claimed yet; call read_mutation("${id}") first.`);
|
|
11452
|
+
if (!Array.isArray(input?.strokes) || !input.strokes.length) {
|
|
11453
|
+
throw new PaintEditorError("strokes is empty; give at least one stroke with points.");
|
|
11454
|
+
}
|
|
11455
|
+
const targetLayerId = input.layerId ?? m.layerId;
|
|
11456
|
+
this.paintTarget(targetLayerId, "paint on");
|
|
11457
|
+
const cleanedStrokes = input.strokes.map((s) => {
|
|
11458
|
+
const pts = this.checkPoints(s.points);
|
|
11459
|
+
const layerId = s.layerId ?? targetLayerId;
|
|
11460
|
+
this.paintTarget(layerId, "paint on");
|
|
11461
|
+
const baseBrush = s.brushId !== void 0 ? this.requireBrush(s.brushId) : this.brush;
|
|
11462
|
+
const stripAutoGhost = baseBrush.ghost?.trigger === "agent" && !s.brush?.ghost;
|
|
11463
|
+
const brushPatch = {
|
|
11464
|
+
...s.brush ?? {},
|
|
11465
|
+
mutation: null,
|
|
11466
|
+
...stripAutoGhost ? { ghost: null } : {}
|
|
11467
|
+
};
|
|
11468
|
+
return {
|
|
11469
|
+
...s,
|
|
11470
|
+
layerId,
|
|
11471
|
+
brush: brushPatch,
|
|
11472
|
+
points: pts
|
|
11473
|
+
};
|
|
11474
|
+
});
|
|
11475
|
+
m.state = "answered";
|
|
11476
|
+
try {
|
|
11477
|
+
for (const s of cleanedStrokes) {
|
|
11478
|
+
await this.prepareStrokePathEffect(s, [s.points]);
|
|
11479
|
+
}
|
|
11480
|
+
} catch (e) {
|
|
11481
|
+
m.state = "claimed";
|
|
11482
|
+
throw e;
|
|
11483
|
+
}
|
|
11484
|
+
const rawActorId = (options.actor?.id ?? m.claimedBy ?? "mutation-agent").replace(/^agent:/, "") || "mutation-agent";
|
|
11485
|
+
const actor = options.actor ?? {
|
|
11486
|
+
id: rawActorId,
|
|
11487
|
+
name: "Agent",
|
|
11488
|
+
color: "#7c3aed"
|
|
11489
|
+
};
|
|
11490
|
+
const origin = this.getOrigin() !== "user" ? this.getOrigin() : `agent:${actor.id}`;
|
|
11491
|
+
const director = options.director ?? this.mutationDirector;
|
|
11492
|
+
if (director) {
|
|
11493
|
+
const perf = await this.withOrigin(origin, async () => {
|
|
11494
|
+
this.answeringMutation = true;
|
|
11495
|
+
try {
|
|
11496
|
+
return await director.perform(
|
|
11497
|
+
actor,
|
|
11498
|
+
[{ kind: "strokes", strokes: cleanedStrokes, label: "Mutation answer", ...input.note ? { note: input.note } : {} }],
|
|
11499
|
+
{ key: m.id }
|
|
11500
|
+
);
|
|
11501
|
+
} finally {
|
|
11502
|
+
this.answeringMutation = false;
|
|
11503
|
+
}
|
|
11504
|
+
});
|
|
11505
|
+
const first = perf[0];
|
|
11506
|
+
if (!first || !first.ok) {
|
|
11507
|
+
m.state = "claimed";
|
|
11508
|
+
throw new PaintEditorError(first ? first.message : "The mutation answer failed.");
|
|
11509
|
+
}
|
|
11510
|
+
const res = first.result;
|
|
11511
|
+
m.answerStrokeIds = [...res.strokeIds];
|
|
11512
|
+
this.cached = null;
|
|
11513
|
+
this.emit([]);
|
|
11514
|
+
return {
|
|
11515
|
+
id: m.id,
|
|
11516
|
+
state: "answered",
|
|
11517
|
+
layerId: res.layerId,
|
|
11518
|
+
strokeIds: res.strokeIds,
|
|
11519
|
+
bounds: res.bounds,
|
|
11520
|
+
dabs: res.dabs,
|
|
11521
|
+
...res.wet !== void 0 ? { wet: res.wet } : {}
|
|
11522
|
+
};
|
|
11523
|
+
}
|
|
11524
|
+
return this.withOrigin(origin, () => {
|
|
11525
|
+
this.answeringMutation = true;
|
|
11526
|
+
try {
|
|
11527
|
+
return this.group("Mutation answer", () => {
|
|
11528
|
+
let bounds = null;
|
|
11529
|
+
let dabs = 0;
|
|
11530
|
+
const strokeIds = [];
|
|
11531
|
+
let wet;
|
|
11532
|
+
for (const s of cleanedStrokes) {
|
|
11533
|
+
const r = this.paintStroke(s);
|
|
11534
|
+
bounds = unionRect(bounds, r.bounds);
|
|
11535
|
+
dabs += r.dabs;
|
|
11536
|
+
if (r.strokeId) strokeIds.push(r.strokeId);
|
|
11537
|
+
if (r.wet !== void 0) wet = r.wet;
|
|
11538
|
+
}
|
|
11539
|
+
m.answerStrokeIds = [...strokeIds];
|
|
11540
|
+
this.cached = null;
|
|
11541
|
+
return {
|
|
11542
|
+
id: m.id,
|
|
11543
|
+
state: "answered",
|
|
11544
|
+
layerId: targetLayerId,
|
|
11545
|
+
strokeIds,
|
|
11546
|
+
bounds,
|
|
11547
|
+
dabs,
|
|
11548
|
+
...wet !== void 0 ? { wet } : {}
|
|
11549
|
+
};
|
|
11550
|
+
});
|
|
11551
|
+
} catch (e) {
|
|
11552
|
+
m.state = "claimed";
|
|
11553
|
+
throw e;
|
|
11554
|
+
} finally {
|
|
11555
|
+
this.answeringMutation = false;
|
|
11556
|
+
}
|
|
11557
|
+
});
|
|
11558
|
+
}
|
|
11559
|
+
/**
|
|
11560
|
+
* Gives up on a mutation (`id`) without answering. The user's paint stays as ordinary paint (wet
|
|
11561
|
+
* strokes stay in the wet layer, and dry temporary layers merge down into their target layer).
|
|
11562
|
+
*/
|
|
11563
|
+
declineMutation(id, reason) {
|
|
11564
|
+
const m = this.mutations.find((x) => x.id === id);
|
|
11565
|
+
if (!m) throw new PaintEditorError(`Unknown mutation "${id}". Call list_mutations for the pending ids.`);
|
|
11566
|
+
if (m.state === "answered") throw new PaintEditorError(`Mutation "${id}" has already been answered.`);
|
|
11567
|
+
if (m.state === "declined") return this.toMutationInfo(m);
|
|
11568
|
+
m.state = "declined";
|
|
11569
|
+
if (reason?.trim()) m.declineReason = reason.trim();
|
|
11570
|
+
this.keepMutationPaint(m);
|
|
11571
|
+
if (this.mutationDirector && m.claimedBy) {
|
|
11572
|
+
this.mutationDirector.presence.remove(m.claimedBy.replace(/^agent:/, "") || "mutation-agent");
|
|
11573
|
+
}
|
|
11574
|
+
this.emit();
|
|
11575
|
+
return this.toMutationInfo(m);
|
|
11576
|
+
}
|
|
11577
|
+
/**
|
|
11578
|
+
* Advances mutation timers (`settle`, `patience`, and dry temporary-layer `fade`) by `seconds`
|
|
11579
|
+
* of simulated time. Called automatically from `tickMutations` and `advanceWet`.
|
|
11580
|
+
*/
|
|
11581
|
+
advanceMutations(seconds) {
|
|
11582
|
+
if (!(seconds > 0) || !this.mutations.length) return;
|
|
11583
|
+
let changed = false;
|
|
11584
|
+
let dirty = [];
|
|
11585
|
+
for (const m of [...this.mutations]) {
|
|
11586
|
+
const wasState = m.state;
|
|
11587
|
+
if (wasState === "settling" || wasState === "pending" || wasState === "claimed") {
|
|
11588
|
+
m.age += seconds;
|
|
11589
|
+
changed = true;
|
|
11590
|
+
}
|
|
11591
|
+
if (wasState === "settling") {
|
|
11592
|
+
m.sinceLastStroke += seconds;
|
|
11593
|
+
if (m.sinceLastStroke >= m.settle - 1e-9) {
|
|
11594
|
+
m.state = "pending";
|
|
11595
|
+
m.sincePending = Math.max(0, m.sinceLastStroke - m.settle);
|
|
11596
|
+
const info = this.toMutationInfo(m);
|
|
11597
|
+
for (const listener of [...this.mutationListeners]) {
|
|
11598
|
+
listener(info);
|
|
11599
|
+
}
|
|
11600
|
+
if (m.state === "pending" && m.sincePending >= m.patience - 1e-9) {
|
|
11601
|
+
this.expireMutation(m);
|
|
11602
|
+
dirty = "all";
|
|
11603
|
+
}
|
|
11604
|
+
}
|
|
11605
|
+
} else if (wasState === "pending") {
|
|
11606
|
+
m.sincePending += seconds;
|
|
11607
|
+
if (m.sincePending >= m.patience - 1e-9) {
|
|
11608
|
+
this.expireMutation(m);
|
|
11609
|
+
dirty = "all";
|
|
11610
|
+
}
|
|
11611
|
+
} else if (wasState === "claimed" || wasState === "answered") {
|
|
11612
|
+
if (m.medium === "dry" && m.tempLayerId) {
|
|
11613
|
+
changed = true;
|
|
11614
|
+
m.fadeRemaining = Math.max(0, m.fadeRemaining - seconds);
|
|
11615
|
+
if (m.fadeRemaining <= 1e-9) {
|
|
11616
|
+
this.removeTempMutationLayer(m);
|
|
11617
|
+
dirty = "all";
|
|
11618
|
+
} else {
|
|
11619
|
+
const opacity = m.fade > 0 ? Math.max(0, Math.min(1, m.fadeRemaining / m.fade)) : 0;
|
|
11620
|
+
const tempId = m.tempLayerId;
|
|
11621
|
+
if (findLayer(this.doc.layers, tempId)) {
|
|
11622
|
+
this.doc = { ...this.doc, layers: mapLayer(this.doc.layers, tempId, (l) => ({ ...l, opacity })) };
|
|
11623
|
+
dirty = "all";
|
|
11624
|
+
}
|
|
11625
|
+
}
|
|
11626
|
+
}
|
|
11627
|
+
}
|
|
11628
|
+
}
|
|
11629
|
+
if (changed) {
|
|
11630
|
+
this.cached = null;
|
|
11631
|
+
this.emit(dirty);
|
|
11632
|
+
}
|
|
11633
|
+
}
|
|
11634
|
+
/** Steps mutation timers with the wall clock (`performance.now()`, in ms). */
|
|
11635
|
+
tickMutations(now) {
|
|
11636
|
+
if (!Number.isFinite(now)) return;
|
|
11637
|
+
if (this.lastMutationTick === null) {
|
|
11638
|
+
this.lastMutationTick = now;
|
|
11639
|
+
return;
|
|
11640
|
+
}
|
|
11641
|
+
const dt = Math.max(0, Math.min(1, (now - this.lastMutationTick) / 1e3));
|
|
11642
|
+
this.lastMutationTick = now;
|
|
11643
|
+
if (dt > 0 && !this.session) {
|
|
11644
|
+
this.advanceMutations(dt);
|
|
11645
|
+
}
|
|
11646
|
+
}
|
|
11647
|
+
toMutationInfo(m) {
|
|
11648
|
+
return {
|
|
11649
|
+
id: m.id,
|
|
11650
|
+
state: m.state,
|
|
11651
|
+
layerId: m.layerId,
|
|
11652
|
+
medium: m.medium,
|
|
11653
|
+
bounds: m.bounds ? { ...m.bounds } : null,
|
|
11654
|
+
strokeCount: m.strokes.length,
|
|
11655
|
+
age: Math.round(m.age * 100) / 100,
|
|
11656
|
+
settle: m.settle,
|
|
11657
|
+
patience: m.patience,
|
|
11658
|
+
fade: m.fade,
|
|
11659
|
+
...m.claimedBy ? { claimedBy: m.claimedBy } : {},
|
|
11660
|
+
...m.answerStrokeIds ? { answerStrokeIds: [...m.answerStrokeIds] } : {},
|
|
11661
|
+
...m.declineReason ? { declineReason: m.declineReason } : {}
|
|
11662
|
+
};
|
|
11663
|
+
}
|
|
11664
|
+
recordMutationStroke(stroke, bounds) {
|
|
11665
|
+
const mut = stroke.brush.mutation;
|
|
11666
|
+
if (!mut || this.answeringMutation || this.previewing || this.getOrigin().startsWith("agent:")) return;
|
|
11667
|
+
const medium = isWetBrush(stroke.brush) ? "wet" : "dry";
|
|
11668
|
+
const tempLayerId = medium === "dry" && this.tempLayerTargets.has(stroke.layerId) ? stroke.layerId : void 0;
|
|
11669
|
+
const targetLayerId = tempLayerId ? this.tempLayerTargets.get(tempLayerId) : stroke.layerId;
|
|
11670
|
+
let entry = this.mutations.find((m) => m.state === "settling" && m.layerId === targetLayerId && m.medium === medium);
|
|
11671
|
+
if (!entry) {
|
|
11672
|
+
entry = {
|
|
11673
|
+
id: `m${this.nextMutationId++}`,
|
|
11674
|
+
state: "settling",
|
|
11675
|
+
layerId: targetLayerId,
|
|
11676
|
+
...tempLayerId ? { tempLayerId } : {},
|
|
11677
|
+
medium,
|
|
11678
|
+
strokes: [],
|
|
11679
|
+
bounds: null,
|
|
11680
|
+
settle: mut.settle,
|
|
11681
|
+
patience: mut.patience,
|
|
11682
|
+
fade: mut.fade,
|
|
11683
|
+
age: 0,
|
|
11684
|
+
sinceLastStroke: 0,
|
|
11685
|
+
sincePending: 0,
|
|
11686
|
+
fadeRemaining: mut.fade
|
|
11687
|
+
};
|
|
11688
|
+
this.mutations.push(entry);
|
|
11689
|
+
if (medium === "wet") this.holdMutationEvaporation(targetLayerId);
|
|
11690
|
+
} else {
|
|
11691
|
+
entry.settle = mut.settle;
|
|
11692
|
+
entry.patience = mut.patience;
|
|
11693
|
+
entry.fade = mut.fade;
|
|
11694
|
+
entry.fadeRemaining = mut.fade;
|
|
11695
|
+
entry.sinceLastStroke = 0;
|
|
11696
|
+
if (tempLayerId && !entry.tempLayerId) entry.tempLayerId = tempLayerId;
|
|
11697
|
+
}
|
|
11698
|
+
const strokeRect = bounds ?? strokeBounds(stroke.points);
|
|
11699
|
+
entry.strokes.push({
|
|
11700
|
+
id: stroke.id,
|
|
11701
|
+
color: stroke.color,
|
|
11702
|
+
brushId: stroke.brush.id,
|
|
11703
|
+
brushName: stroke.brush.name,
|
|
11704
|
+
size: stroke.brush.tip.size,
|
|
11705
|
+
points: stroke.points.map((p) => ({ ...p })),
|
|
11706
|
+
bounds: strokeRect ? { ...strokeRect } : null
|
|
11707
|
+
});
|
|
11708
|
+
entry.bounds = unionRect(entry.bounds, strokeRect);
|
|
11709
|
+
this.cached = null;
|
|
11710
|
+
}
|
|
11711
|
+
holdMutationEvaporation(layerId) {
|
|
11712
|
+
if (this.heldEvaporation.has(layerId)) return;
|
|
11713
|
+
const currentEvap = (this.wet.settings.get(layerId)?.params ?? this.wet.info(layerId).params).evaporation;
|
|
11714
|
+
this.heldEvaporation.set(layerId, currentEvap);
|
|
11715
|
+
this.wet.setParams(layerId, { evaporation: 0 });
|
|
11716
|
+
}
|
|
11717
|
+
releaseMutationEvaporation(layerId) {
|
|
11718
|
+
const stillWaiting = this.mutations.some(
|
|
11719
|
+
(m) => m.medium === "wet" && m.layerId === layerId && (m.state === "settling" || m.state === "pending")
|
|
11720
|
+
);
|
|
11721
|
+
if (stillWaiting) return;
|
|
11722
|
+
const saved = this.heldEvaporation.get(layerId);
|
|
11723
|
+
if (saved === void 0) return;
|
|
11724
|
+
this.heldEvaporation.delete(layerId);
|
|
11725
|
+
if (findLayer(this.doc.layers, layerId)) {
|
|
11726
|
+
this.wet.setParams(layerId, { evaporation: saved });
|
|
11727
|
+
}
|
|
11728
|
+
}
|
|
11729
|
+
finishWetMutationsOnDry(layerId) {
|
|
11730
|
+
for (const m of this.mutations) {
|
|
11731
|
+
if (m.medium === "wet" && m.layerId === layerId && (m.state === "settling" || m.state === "pending")) {
|
|
11732
|
+
m.state = "declined";
|
|
11733
|
+
m.declineReason = "Dried before an agent claimed it.";
|
|
11734
|
+
}
|
|
11735
|
+
}
|
|
11736
|
+
this.releaseMutationEvaporation(layerId);
|
|
11737
|
+
}
|
|
11738
|
+
expireMutation(m) {
|
|
11739
|
+
m.state = "declined";
|
|
11740
|
+
m.declineReason = "No agent answered before patience ran out.";
|
|
11741
|
+
this.keepMutationPaint(m);
|
|
11742
|
+
}
|
|
11743
|
+
keepMutationPaint(m) {
|
|
11744
|
+
if (m.medium === "wet") {
|
|
11745
|
+
this.releaseMutationEvaporation(m.layerId);
|
|
11746
|
+
this.wet.keepGhost(m.layerId);
|
|
11747
|
+
this.recordWetSetting({ kind: "ghost-keep", layerId: m.layerId });
|
|
11748
|
+
return;
|
|
11749
|
+
}
|
|
11750
|
+
const tempId = m.tempLayerId;
|
|
11751
|
+
if (!tempId) return;
|
|
11752
|
+
m.tempLayerId = void 0;
|
|
11753
|
+
this.tempLayerTargets.delete(tempId);
|
|
11754
|
+
if (!findLayer(this.doc.layers, tempId)) return;
|
|
11755
|
+
this.doc = this.mergeTempLayerIntoTarget(this.doc, tempId, m.layerId);
|
|
11756
|
+
for (const e of this.undoStack) {
|
|
11757
|
+
if (findLayer(e.before.layers, tempId)) e.before = this.mergeTempLayerIntoTarget(e.before, tempId, m.layerId);
|
|
11758
|
+
if (findLayer(e.after.layers, tempId)) e.after = this.mergeTempLayerIntoTarget(e.after, tempId, m.layerId);
|
|
11759
|
+
}
|
|
11760
|
+
while (this.undoStack.length >= 2 && !findLayer(this.undoStack[this.undoStack.length - 2].before.layers, tempId) && this.undoStack[this.undoStack.length - 1].before.strokes.some((s) => m.strokes.some((ms) => ms.id === s.id)) && this.undoStack[this.undoStack.length - 2].after.strokes.some((s) => m.strokes.some((ms) => ms.id === s.id))) {
|
|
11761
|
+
const top = this.undoStack.pop();
|
|
11762
|
+
const prev = this.undoStack[this.undoStack.length - 1];
|
|
11763
|
+
prev.after = top.after;
|
|
11764
|
+
}
|
|
11765
|
+
}
|
|
11766
|
+
mergeTempLayerIntoTarget(doc, tempLayerId, targetLayerId) {
|
|
11767
|
+
const upper = findLayer(doc.layers, tempLayerId)?.layer;
|
|
11768
|
+
const below = findLayer(doc.layers, targetLayerId)?.layer;
|
|
11769
|
+
if (!isPixelLayer(upper)) return doc;
|
|
11770
|
+
if (!isPixelLayer(below)) {
|
|
11771
|
+
return {
|
|
11772
|
+
...doc,
|
|
11773
|
+
layers: mapLayer(doc.layers, tempLayerId, () => null),
|
|
11774
|
+
strokes: doc.strokes.filter((s) => s.layerId !== tempLayerId)
|
|
11775
|
+
};
|
|
11776
|
+
}
|
|
11777
|
+
const rect = unionRect(this.describeLayer(below).bounds, this.describeLayer(upper).bounds);
|
|
11778
|
+
let surface = below.surface;
|
|
11779
|
+
let offset = below.offset;
|
|
11780
|
+
if (rect) {
|
|
11781
|
+
const base = { ...below, opacity: 1, blend: "normal", visible: true, clipped: void 0, mask: void 0 };
|
|
11782
|
+
const top = { ...upper, opacity: 1, visible: true };
|
|
11783
|
+
const px = createPaintCompositor().composite([base, top], rect, this.sources());
|
|
11784
|
+
surface = surfaceFromPixels({ width: rect.width, height: rect.height, data: px }, 4, rect.x, rect.y);
|
|
11785
|
+
offset = { x: 0, y: 0 };
|
|
11786
|
+
}
|
|
11787
|
+
const merged = { ...below, surface, offset };
|
|
11788
|
+
const layers = mapLayer(mapLayer(doc.layers, upper.id, () => null), below.id, () => merged);
|
|
11789
|
+
const strokes = doc.strokes.map((s) => s.layerId === tempLayerId ? { ...s, layerId: targetLayerId } : s);
|
|
11790
|
+
return { ...doc, layers, strokes };
|
|
11791
|
+
}
|
|
11792
|
+
removeTempMutationLayer(m) {
|
|
11793
|
+
const tempId = m.tempLayerId;
|
|
11794
|
+
if (!tempId) return;
|
|
11795
|
+
m.tempLayerId = void 0;
|
|
11796
|
+
this.tempLayerTargets.delete(tempId);
|
|
11797
|
+
const strip = (d) => {
|
|
11798
|
+
if (!findLayer(d.layers, tempId) && !d.strokes.some((s) => s.layerId === tempId)) return d;
|
|
11799
|
+
return {
|
|
11800
|
+
...d,
|
|
11801
|
+
layers: mapLayer(d.layers, tempId, () => null),
|
|
11802
|
+
strokes: d.strokes.filter((s) => s.layerId !== tempId)
|
|
11803
|
+
};
|
|
11804
|
+
};
|
|
11805
|
+
this.doc = strip(this.doc);
|
|
11806
|
+
const mutStrokeIds = new Set(m.strokes.map((s) => s.id));
|
|
11807
|
+
for (const e of this.undoStack) {
|
|
11808
|
+
const isMutationCreationStep = !e.before.strokes.some((s) => mutStrokeIds.has(s.id)) && e.after.strokes.some((s) => mutStrokeIds.has(s.id));
|
|
11809
|
+
if (!isMutationCreationStep) {
|
|
11810
|
+
e.before = strip(e.before);
|
|
11811
|
+
e.after = strip(e.after);
|
|
11812
|
+
}
|
|
11813
|
+
}
|
|
11814
|
+
}
|
|
11815
|
+
reconcileMutationsAfterUndo() {
|
|
11816
|
+
if (!this.mutations.length) return;
|
|
11817
|
+
const remainingIds = new Set(this.doc.strokes.map((s) => s.id));
|
|
11818
|
+
const kept = [];
|
|
11819
|
+
for (const m of this.mutations) {
|
|
11820
|
+
m.strokes = m.strokes.filter((s) => remainingIds.has(s.id));
|
|
11821
|
+
if (m.strokes.length === 0) {
|
|
11822
|
+
this.mutationDirector?.cancel(m.id);
|
|
11823
|
+
if (m.claimedBy && this.mutationDirector) {
|
|
11824
|
+
this.mutationDirector.presence.remove(m.claimedBy.replace(/^agent:/, "") || "mutation-agent");
|
|
11825
|
+
}
|
|
11826
|
+
if (m.tempLayerId) {
|
|
11827
|
+
const tempId = m.tempLayerId;
|
|
11828
|
+
this.tempLayerTargets.delete(tempId);
|
|
11829
|
+
if (findLayer(this.doc.layers, tempId)) {
|
|
11830
|
+
this.doc = {
|
|
11831
|
+
...this.doc,
|
|
11832
|
+
layers: mapLayer(this.doc.layers, tempId, () => null),
|
|
11833
|
+
strokes: this.doc.strokes.filter((s) => s.layerId !== tempId)
|
|
11834
|
+
};
|
|
11835
|
+
}
|
|
11836
|
+
}
|
|
11837
|
+
if (m.medium === "wet") {
|
|
11838
|
+
this.releaseMutationEvaporation(m.layerId);
|
|
11839
|
+
}
|
|
11840
|
+
} else {
|
|
11841
|
+
m.bounds = m.strokes.reduce((acc, s) => unionRect(acc, s.bounds), null);
|
|
11842
|
+
kept.push(m);
|
|
11843
|
+
}
|
|
11844
|
+
}
|
|
11845
|
+
this.mutations = kept;
|
|
11846
|
+
}
|
|
11847
|
+
resetMutations() {
|
|
11848
|
+
for (const m of this.mutations) {
|
|
11849
|
+
this.mutationDirector?.cancel(m.id);
|
|
11850
|
+
}
|
|
11851
|
+
this.mutations = [];
|
|
11852
|
+
this.heldEvaporation.clear();
|
|
11853
|
+
this.tempLayerTargets.clear();
|
|
11854
|
+
this.lastMutationTick = null;
|
|
11855
|
+
}
|
|
9842
11856
|
/** A layer's tile surface (live while stroking), for thumbnails. */
|
|
9843
11857
|
layerSurface(id) {
|
|
9844
11858
|
const l = findLayer(this.currentLayers(), id)?.layer;
|
|
@@ -9989,6 +12003,7 @@ var MAX_IMAGE_SIDE = 8192;
|
|
|
9989
12003
|
var movePoint = (p, dx, dy) => ({ x: p.x + dx, y: p.y + dy });
|
|
9990
12004
|
var moveRect = (r, dx, dy) => ({ ...r, x: r.x + dx, y: r.y + dy });
|
|
9991
12005
|
var SPILL_SUFFIX = "::light";
|
|
12006
|
+
var UV_SPILL_SUFFIX = "::uv";
|
|
9992
12007
|
function withLayerLight(light, layerId, next) {
|
|
9993
12008
|
if (!next) return light ? { ...light } : void 0;
|
|
9994
12009
|
const out = { ...light ?? {} };
|
|
@@ -9996,6 +12011,13 @@ function withLayerLight(light, layerId, next) {
|
|
|
9996
12011
|
else delete out[layerId];
|
|
9997
12012
|
return out;
|
|
9998
12013
|
}
|
|
12014
|
+
function withLayerThick(thick, layerId, next) {
|
|
12015
|
+
if (!next) return thick ? { ...thick } : void 0;
|
|
12016
|
+
const out = { ...thick ?? {} };
|
|
12017
|
+
if (hasThickChannels(next)) out[layerId] = next;
|
|
12018
|
+
else delete out[layerId];
|
|
12019
|
+
return out;
|
|
12020
|
+
}
|
|
9999
12021
|
var BACKLIGHT_LAYER_ID = "__backlight";
|
|
10000
12022
|
function insertAfter(layers, id, node) {
|
|
10001
12023
|
const i = layers.findIndex((l) => l.id === id);
|
|
@@ -10207,16 +12229,42 @@ var PaintDirector = class {
|
|
|
10207
12229
|
options;
|
|
10208
12230
|
clock;
|
|
10209
12231
|
queue;
|
|
12232
|
+
cancelled = /* @__PURE__ */ new Set();
|
|
12233
|
+
currentRun = null;
|
|
12234
|
+
/** Cancels any queued or running performance with `key` (e.g. a mutation that was undone). */
|
|
12235
|
+
cancel(key) {
|
|
12236
|
+
this.cancelled.add(key);
|
|
12237
|
+
if (this.currentRun?.key === key) {
|
|
12238
|
+
const { actorId, cancelSession } = this.currentRun;
|
|
12239
|
+
this.currentRun = null;
|
|
12240
|
+
cancelSession?.();
|
|
12241
|
+
this.presence.remove(actorId);
|
|
12242
|
+
}
|
|
12243
|
+
}
|
|
10210
12244
|
/** Acts out `commands` in order for `actor`. Resolves with one result per command. */
|
|
10211
|
-
perform(actor, commands) {
|
|
12245
|
+
perform(actor, commands, options) {
|
|
12246
|
+
const key = options?.key;
|
|
12247
|
+
if (key) this.cancelled.delete(key);
|
|
10212
12248
|
return this.queue.run(actor, async () => {
|
|
12249
|
+
if (key && this.cancelled.has(key)) {
|
|
12250
|
+
return commands.map((command) => ({ ok: false, command, message: "The mutation was cancelled." }));
|
|
12251
|
+
}
|
|
12252
|
+
if (key) this.currentRun = { actorId: actor.id, key };
|
|
10213
12253
|
const results = [];
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
12254
|
+
try {
|
|
12255
|
+
for (const command of commands) {
|
|
12256
|
+
if (key && this.cancelled.has(key)) {
|
|
12257
|
+
results.push({ ok: false, command, message: "The mutation was cancelled." });
|
|
12258
|
+
continue;
|
|
12259
|
+
}
|
|
12260
|
+
try {
|
|
12261
|
+
results.push({ ok: true, command, result: await this.performOne(actor.id, command, key) });
|
|
12262
|
+
} catch (error) {
|
|
12263
|
+
results.push({ ok: false, command, message: error instanceof Error ? error.message : String(error) });
|
|
12264
|
+
}
|
|
10219
12265
|
}
|
|
12266
|
+
} finally {
|
|
12267
|
+
if (this.currentRun?.key === key) this.currentRun = null;
|
|
10220
12268
|
}
|
|
10221
12269
|
return results;
|
|
10222
12270
|
});
|
|
@@ -10242,7 +12290,7 @@ var PaintDirector = class {
|
|
|
10242
12290
|
this.presence.set({ id, message: text });
|
|
10243
12291
|
await this.clock.wait(this.ms(ms));
|
|
10244
12292
|
}
|
|
10245
|
-
async performOne(id, command) {
|
|
12293
|
+
async performOne(id, command, key) {
|
|
10246
12294
|
const c = this.controller;
|
|
10247
12295
|
switch (command.kind) {
|
|
10248
12296
|
case "say":
|
|
@@ -10257,10 +12305,10 @@ var PaintDirector = class {
|
|
|
10257
12305
|
return void 0;
|
|
10258
12306
|
case "stroke":
|
|
10259
12307
|
await this.say(id, command.note, 500);
|
|
10260
|
-
return this.stroke(id, command.stroke);
|
|
12308
|
+
return this.stroke(id, command.stroke, void 0, key);
|
|
10261
12309
|
case "strokes":
|
|
10262
12310
|
await this.say(id, command.note, 500);
|
|
10263
|
-
return this.strokes(id, command.strokes, command.label);
|
|
12311
|
+
return this.strokes(id, command.strokes, command.label, key);
|
|
10264
12312
|
case "fill": {
|
|
10265
12313
|
await this.say(id, command.note, 500);
|
|
10266
12314
|
const { width, height } = c.getState().document.canvas;
|
|
@@ -10297,19 +12345,26 @@ var PaintDirector = class {
|
|
|
10297
12345
|
}
|
|
10298
12346
|
}
|
|
10299
12347
|
/** Paints a stroke live: begins it, pushes its points frame by frame as the cursor reaches them, then ends it (one undo step). */
|
|
10300
|
-
async stroke(id, init, within = (step) => step()) {
|
|
12348
|
+
async stroke(id, init, within = (step) => step(), key) {
|
|
10301
12349
|
const c = this.controller;
|
|
10302
12350
|
const points = c.checkPoints(init.points);
|
|
10303
12351
|
const { points: _points, ...rest } = init;
|
|
10304
12352
|
const session = within(() => c.beginStroke({ snapToPerspective: false, start: points[0], ...rest }));
|
|
12353
|
+
if (key && this.currentRun?.key === key) {
|
|
12354
|
+
this.currentRun.cancelSession = () => {
|
|
12355
|
+
if (c.activeStroke === session) session.cancel();
|
|
12356
|
+
};
|
|
12357
|
+
}
|
|
10305
12358
|
try {
|
|
10306
12359
|
await this.glide(id, points[0]);
|
|
12360
|
+
if (key && this.cancelled.has(key)) throw new PaintEditorError("The mutation was cancelled.");
|
|
10307
12361
|
this.presence.set({ id, pressed: true });
|
|
10308
12362
|
const at = strokeSchedule(points);
|
|
10309
12363
|
const duration = this.ms(strokeDuration(points, this.options.maxStrokeMs));
|
|
10310
12364
|
let next = 0;
|
|
10311
12365
|
const frames = Math.max(1, Math.round(duration / FRAME));
|
|
10312
12366
|
for (let f = 1; f <= frames; f++) {
|
|
12367
|
+
if (key && this.cancelled.has(key)) throw new PaintEditorError("The mutation was cancelled.");
|
|
10313
12368
|
const t = f / frames;
|
|
10314
12369
|
const from = next;
|
|
10315
12370
|
while (next < points.length && at[next] <= t + 1e-9) next++;
|
|
@@ -10320,6 +12375,7 @@ var PaintDirector = class {
|
|
|
10320
12375
|
}
|
|
10321
12376
|
if (f < frames) await this.clock.wait(duration / frames);
|
|
10322
12377
|
}
|
|
12378
|
+
if (key && this.cancelled.has(key)) throw new PaintEditorError("The mutation was cancelled.");
|
|
10323
12379
|
if (next < points.length) session.push(points.slice(next));
|
|
10324
12380
|
this.presence.set({ id, pressed: false });
|
|
10325
12381
|
return session.end();
|
|
@@ -10327,10 +12383,12 @@ var PaintDirector = class {
|
|
|
10327
12383
|
if (c.activeStroke === session) session.cancel();
|
|
10328
12384
|
this.presence.set({ id, pressed: false });
|
|
10329
12385
|
throw error;
|
|
12386
|
+
} finally {
|
|
12387
|
+
if (key && this.currentRun?.key === key) delete this.currentRun.cancelSession;
|
|
10330
12388
|
}
|
|
10331
12389
|
}
|
|
10332
12390
|
/** Paints several strokes live in turn and commits them as one undo step (e.g. a path's subpaths). */
|
|
10333
|
-
async strokes(id, inits, label) {
|
|
12391
|
+
async strokes(id, inits, label, key) {
|
|
10334
12392
|
const c = this.controller;
|
|
10335
12393
|
if (!Array.isArray(inits) || !inits.length) throw new PaintEditorError("strokes is empty; give at least one stroke.");
|
|
10336
12394
|
const checked = inits.map((s) => ({ ...s, points: c.checkPoints(s.points) }));
|
|
@@ -10344,7 +12402,8 @@ var PaintDirector = class {
|
|
|
10344
12402
|
const strokeIds = [];
|
|
10345
12403
|
let last = null;
|
|
10346
12404
|
for (const s of checked) {
|
|
10347
|
-
|
|
12405
|
+
if (key && this.cancelled.has(key)) throw new PaintEditorError("The mutation was cancelled.");
|
|
12406
|
+
const r = await this.stroke(id, s, within, key);
|
|
10348
12407
|
layerId = r.layerId;
|
|
10349
12408
|
bounds = unionRect2(bounds, r.bounds);
|
|
10350
12409
|
dabs += r.dabs;
|
|
@@ -11290,7 +13349,7 @@ function createPaintThickTools(c) {
|
|
|
11290
13349
|
{
|
|
11291
13350
|
name: "set_lighting",
|
|
11292
13351
|
title: "Move the light",
|
|
11293
|
-
description: "Sets where the light comes from for thick paint (impasto relief) and metallic paper: azimuth (degrees clockwise from the top of the canvas; 315 is top left), elevation (5 grazing, deep shadows .. 90 straight on, no relief) and relief (0 flat .. 1 strong). Nothing is repainted: the relief
|
|
13352
|
+
description: "Sets where the light comes from for thick paint (impasto relief), thin-film iridescence and metallic paper: azimuth (degrees clockwise from the top of the canvas; 315 is top left), elevation (5 grazing, deep shadows .. 90 straight on, no relief) and relief (0 flat .. 1 strong). Nothing is repainted: the relief and iridescence are lit at render time. Saved with the document, not an undo step. Returns the lighting.",
|
|
11294
13353
|
inputSchema: {
|
|
11295
13354
|
type: "object",
|
|
11296
13355
|
properties: {
|
|
@@ -11303,6 +13362,29 @@ function createPaintThickTools(c) {
|
|
|
11303
13362
|
annotations: { title: "Move the light", ...SET3 },
|
|
11304
13363
|
handler: (a) => c.setLighting(a)
|
|
11305
13364
|
},
|
|
13365
|
+
{
|
|
13366
|
+
name: "set_view_tilt",
|
|
13367
|
+
title: "Tilt the view angle for iridescence",
|
|
13368
|
+
description: "Tilts the viewer\u2019s look angle for thin-film iridescence and specular highlights: x (-1 left .. 1 right, default 0) and y (-1 top .. 1 bottom, default 0). Moves the view vector, not the light, so iridescent paint (brushes with iridescence, such as scarab-lacquer, soap-film, pearl-glaze) shifts hue across the spectrum as the angle changes. Editor state (not saved with the document, not an undo step). Returns the view tilt.",
|
|
13369
|
+
inputSchema: {
|
|
13370
|
+
type: "object",
|
|
13371
|
+
properties: {
|
|
13372
|
+
x: { type: "number", minimum: -1, maximum: 1, description: "Horizontal view tilt, -1 (left) .. 1 (right); default 0." },
|
|
13373
|
+
y: { type: "number", minimum: -1, maximum: 1, description: "Vertical view tilt, -1 (top) .. 1 (bottom); default 0." }
|
|
13374
|
+
},
|
|
13375
|
+
additionalProperties: false
|
|
13376
|
+
},
|
|
13377
|
+
annotations: { title: "Tilt the view angle for iridescence", ...SET3 },
|
|
13378
|
+
handler: (a) => c.setViewTilt(a)
|
|
13379
|
+
},
|
|
13380
|
+
{
|
|
13381
|
+
name: "get_view_tilt",
|
|
13382
|
+
title: "Read the view tilt",
|
|
13383
|
+
description: "Reads the viewer\u2019s look angle for thin-film iridescence and specular highlights: x and y in -1..1 (0, 0 is straight on). Changes nothing.",
|
|
13384
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
13385
|
+
annotations: { title: "Read the view tilt", ...READ4 },
|
|
13386
|
+
handler: () => c.getViewTilt()
|
|
13387
|
+
},
|
|
11306
13388
|
{
|
|
11307
13389
|
name: "get_relief",
|
|
11308
13390
|
title: "Read a layer\u2019s relief",
|
|
@@ -11327,6 +13409,111 @@ function createPaintThickTools(c) {
|
|
|
11327
13409
|
inputSchema: { type: "object", properties: { on: { type: "boolean", description: "true dries every wet stroke as it ends." } }, required: ["on"], additionalProperties: false },
|
|
11328
13410
|
annotations: { title: "Dry wet strokes at once", ...SET3 },
|
|
11329
13411
|
handler: ({ on }) => ({ instantDry: c.setInstantDry(on) })
|
|
13412
|
+
},
|
|
13413
|
+
{
|
|
13414
|
+
name: "set_uv_light",
|
|
13415
|
+
title: "Set UV blacklight",
|
|
13416
|
+
description: "Switches the virtual UV blacklight (on) and sets its glow strength (0..2, default 1) and ambient daylight level (0..1, default 0.15). When on, the paper and daylight paint dim to ambient and fluorescent strokes (fluorescence.amount > 0, including invisible UV ink) glow and spill in their own colour. Saved with the document, not an undo step. Returns the UV light settings and fluorescentLayerIds.",
|
|
13417
|
+
inputSchema: {
|
|
13418
|
+
type: "object",
|
|
13419
|
+
properties: {
|
|
13420
|
+
on: { type: "boolean", description: "Whether the UV blacklight is switched on (default false)." },
|
|
13421
|
+
strength: { type: "number", minimum: 0, maximum: 2, description: "UV emission and spill strength, 0..2 (default 1)." },
|
|
13422
|
+
ambient: { type: "number", minimum: 0, maximum: 1, description: "How much normal daylight remains under UV, 0 (pitch dark) .. 1 (full daylight, default 0.15)." }
|
|
13423
|
+
},
|
|
13424
|
+
additionalProperties: false
|
|
13425
|
+
},
|
|
13426
|
+
annotations: { title: "Set UV blacklight", ...SET3 },
|
|
13427
|
+
handler: (a) => ({ ...c.setUvLight(a), fluorescentLayerIds: c.fluorescentLayerIds() })
|
|
13428
|
+
},
|
|
13429
|
+
{
|
|
13430
|
+
name: "get_uv_light",
|
|
13431
|
+
title: "Read UV blacklight",
|
|
13432
|
+
description: "Reads the virtual UV blacklight settings (on, strength, ambient) and the ids of layers that hold UV-fluorescent ink (fluorescentLayerIds). Changes nothing.",
|
|
13433
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
13434
|
+
annotations: { title: "Read UV blacklight", ...READ4 },
|
|
13435
|
+
handler: () => ({ ...c.getUvLight(), fluorescentLayerIds: c.fluorescentLayerIds() })
|
|
13436
|
+
},
|
|
13437
|
+
{
|
|
13438
|
+
name: "paint_heat",
|
|
13439
|
+
title: "Warm or cool the canvas",
|
|
13440
|
+
description: "Applies a heat brush along points on the document\u2019s coarse heat field (radius in document pixels, default 24; temperature 0 cools .. 1 warms, default 1). Thermochromic strokes (thermochromic.hotColor) shift smoothly toward their hot colour where local temperature crosses their threshold, and wet paint under hot tiles (temperature >= 0.5) dries in place. One undo step. Returns the heat field stats (max, mean, hotCells, bounds) and any dried wet layers.",
|
|
13441
|
+
inputSchema: {
|
|
13442
|
+
type: "object",
|
|
13443
|
+
properties: {
|
|
13444
|
+
points: {
|
|
13445
|
+
type: "array",
|
|
13446
|
+
minItems: 1,
|
|
13447
|
+
description: "Polyline of document positions to warm or cool along.",
|
|
13448
|
+
items: {
|
|
13449
|
+
type: "object",
|
|
13450
|
+
properties: {
|
|
13451
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
13452
|
+
y: { type: "number", description: "Document y in pixels." }
|
|
13453
|
+
},
|
|
13454
|
+
required: ["x", "y"],
|
|
13455
|
+
additionalProperties: false
|
|
13456
|
+
}
|
|
13457
|
+
},
|
|
13458
|
+
radius: { type: "number", minimum: 1, maximum: 512, description: "Heat falloff radius in document pixels, 1..512 (default 24)." },
|
|
13459
|
+
temperature: { type: "number", minimum: 0, maximum: 1, description: "Target temperature along the stroke, 0 (cool) .. 1 (hot, default 1)." }
|
|
13460
|
+
},
|
|
13461
|
+
required: ["points"],
|
|
13462
|
+
additionalProperties: false
|
|
13463
|
+
},
|
|
13464
|
+
annotations: { title: "Warm or cool the canvas", ...WRITE4 },
|
|
13465
|
+
coordinates: { in: ["points"], out: ["bounds", "dried[].bounds"] },
|
|
13466
|
+
handler: ({ points, radius, temperature }) => c.paintHeat({ points, radius, temperature })
|
|
13467
|
+
},
|
|
13468
|
+
{
|
|
13469
|
+
name: "advance_heat",
|
|
13470
|
+
title: "Spread and cool heat",
|
|
13471
|
+
description: "Advances the document\u2019s heat field by steps (1..200, default 10) of thermal diffusion and Newtonian cooling, so warm patches spread outward and fade back toward cold paint. Any wet paint under hot cells dries. Not an undo step (time passing never is). Returns steps, the updated heat stats (max, mean, hotCells, bounds) and any dried wet layers.",
|
|
13472
|
+
inputSchema: {
|
|
13473
|
+
type: "object",
|
|
13474
|
+
properties: {
|
|
13475
|
+
steps: { type: "integer", minimum: 1, maximum: 200, description: "Diffusion and cooling steps to run, 1..200 (default 10)." }
|
|
13476
|
+
},
|
|
13477
|
+
additionalProperties: false
|
|
13478
|
+
},
|
|
13479
|
+
annotations: { title: "Spread and cool heat", ...SET3 },
|
|
13480
|
+
coordinates: { out: ["bounds", "dried[].bounds"] },
|
|
13481
|
+
handler: ({ steps }) => c.advanceHeat(steps)
|
|
13482
|
+
},
|
|
13483
|
+
{
|
|
13484
|
+
name: "clear_heat",
|
|
13485
|
+
title: "Cool the whole canvas",
|
|
13486
|
+
description: "Cools the entire document heat field back to 0 at once so all thermochromic paint returns to its cold colour. One undo step when the canvas was warm. Returns { cleared }.",
|
|
13487
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
13488
|
+
annotations: { title: "Cool the whole canvas", ...WRITE4 },
|
|
13489
|
+
handler: () => c.clearHeat()
|
|
13490
|
+
},
|
|
13491
|
+
{
|
|
13492
|
+
name: "get_heat",
|
|
13493
|
+
title: "Read canvas heat",
|
|
13494
|
+
description: "Reads the document\u2019s heat field stats (max, mean, hotCells, bounds), optional temperature samples (0..1) at points in document pixels, and thermochromicLayerIds. Changes nothing.",
|
|
13495
|
+
inputSchema: {
|
|
13496
|
+
type: "object",
|
|
13497
|
+
properties: {
|
|
13498
|
+
points: {
|
|
13499
|
+
type: "array",
|
|
13500
|
+
description: "Optional document positions (px) at which to sample the local temperature (0..1).",
|
|
13501
|
+
items: {
|
|
13502
|
+
type: "object",
|
|
13503
|
+
properties: {
|
|
13504
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
13505
|
+
y: { type: "number", description: "Document y in pixels." }
|
|
13506
|
+
},
|
|
13507
|
+
required: ["x", "y"],
|
|
13508
|
+
additionalProperties: false
|
|
13509
|
+
}
|
|
13510
|
+
}
|
|
13511
|
+
},
|
|
13512
|
+
additionalProperties: false
|
|
13513
|
+
},
|
|
13514
|
+
annotations: { title: "Read canvas heat", ...READ4 },
|
|
13515
|
+
coordinates: { in: ["points"], out: ["bounds", "samples"] },
|
|
13516
|
+
handler: ({ points }) => ({ ...c.getHeat(points), thermochromicLayerIds: c.thermochromicLayerIds() })
|
|
11330
13517
|
}
|
|
11331
13518
|
];
|
|
11332
13519
|
}
|
|
@@ -11394,9 +13581,220 @@ function createPaintReplayTools(c) {
|
|
|
11394
13581
|
];
|
|
11395
13582
|
}
|
|
11396
13583
|
|
|
11397
|
-
// src/paint-editor/
|
|
13584
|
+
// src/paint-editor/mutation-tools.ts
|
|
11398
13585
|
var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11399
13586
|
var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
13587
|
+
var SET4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13588
|
+
var POINTS = {
|
|
13589
|
+
type: "array",
|
|
13590
|
+
minItems: 1,
|
|
13591
|
+
maxItems: 2e4,
|
|
13592
|
+
description: "The path in document pixels (raw samples; the brush smooths them). One point paints one dab.",
|
|
13593
|
+
items: {
|
|
13594
|
+
type: "object",
|
|
13595
|
+
properties: {
|
|
13596
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
13597
|
+
y: { type: "number", description: "Document y in pixels." },
|
|
13598
|
+
pressure: { type: "number", minimum: 0, maximum: 1, description: "Pen pressure 0..1 (default 1)." },
|
|
13599
|
+
t: { type: "number", description: "Time in milliseconds (optional; drives velocity dynamics)." },
|
|
13600
|
+
tiltX: { type: "number", minimum: -90, maximum: 90, description: "Pen tilt in degrees towards +x (Pointer Events tiltX)." },
|
|
13601
|
+
tiltY: { type: "number", minimum: -90, maximum: 90, description: "Pen tilt in degrees towards +y (Pointer Events tiltY)." },
|
|
13602
|
+
altitude: { type: "number", minimum: 0, maximum: 1.5708, description: "Pen altitude in radians: 1.5708 upright, 0 lying flat (overrides tiltX / tiltY)." },
|
|
13603
|
+
azimuth: { type: "number", minimum: 0, maximum: 6.2832, description: "Where the pen leans, radians clockwise from +x (with altitude)." },
|
|
13604
|
+
twist: { type: "number", minimum: 0, maximum: 359, description: "Barrel rotation in degrees (brushes with rotation barrel)." }
|
|
13605
|
+
},
|
|
13606
|
+
required: ["x", "y"],
|
|
13607
|
+
additionalProperties: false
|
|
13608
|
+
}
|
|
13609
|
+
};
|
|
13610
|
+
function createMutationTools(c, options = {}) {
|
|
13611
|
+
return [
|
|
13612
|
+
{
|
|
13613
|
+
name: "list_mutations",
|
|
13614
|
+
title: "List pending mutations",
|
|
13615
|
+
description: 'Lists mutation exchanges waiting for an agent (or all of them when includeAll is true): each with id, state ("settling" | "pending" | "claimed" | "answered" | "declined"), layerId, medium ("wet" | "dry"), bounds in document pixels, strokeCount, age in seconds, settle, patience and fade. Use an id with read_mutation or decline_mutation.',
|
|
13616
|
+
inputSchema: {
|
|
13617
|
+
type: "object",
|
|
13618
|
+
properties: {
|
|
13619
|
+
includeAll: {
|
|
13620
|
+
type: "boolean",
|
|
13621
|
+
description: "Include settling, claimed, answered and declined mutations as well as pending ones (default false)."
|
|
13622
|
+
}
|
|
13623
|
+
},
|
|
13624
|
+
additionalProperties: false
|
|
13625
|
+
},
|
|
13626
|
+
annotations: { title: "List pending mutations", ...READ6 },
|
|
13627
|
+
coordinates: { out: ["mutations[].bounds"] },
|
|
13628
|
+
handler: (a) => ({ mutations: c.listMutations({ includeAll: Boolean(a.includeAll) }) })
|
|
13629
|
+
},
|
|
13630
|
+
{
|
|
13631
|
+
name: "read_mutation",
|
|
13632
|
+
title: "Read and claim a mutation",
|
|
13633
|
+
description: 'Claims a mutation by id and starts its fade so the user\u2019s stroke sinks away as you read it. Returns the mutation\u2019s id, state ("claimed"), layerId, medium, bounds, fade duration in seconds, the strokes (id, color, brushId, brushName, size, points and bounds in document pixels), cropPng (base64 PNG and rect of the area around the strokes) and canvasPng (base64 PNG preview of the whole painting so you can choose where to answer). Refused if already claimed, answered or declined. Follow with answer_mutation or decline_mutation.',
|
|
13634
|
+
inputSchema: {
|
|
13635
|
+
type: "object",
|
|
13636
|
+
properties: {
|
|
13637
|
+
id: {
|
|
13638
|
+
type: "string",
|
|
13639
|
+
description: 'Mutation id from list_mutations (e.g. "m1").'
|
|
13640
|
+
},
|
|
13641
|
+
padding: {
|
|
13642
|
+
type: "integer",
|
|
13643
|
+
minimum: 0,
|
|
13644
|
+
maximum: 512,
|
|
13645
|
+
description: "Padding around the mutation bounds for cropPng, in document pixels (default 32)."
|
|
13646
|
+
},
|
|
13647
|
+
maxSide: {
|
|
13648
|
+
type: "integer",
|
|
13649
|
+
minimum: 64,
|
|
13650
|
+
maximum: 2048,
|
|
13651
|
+
description: "Longest side of the canvasPng preview in pixels (default 512)."
|
|
13652
|
+
}
|
|
13653
|
+
},
|
|
13654
|
+
required: ["id"],
|
|
13655
|
+
additionalProperties: false
|
|
13656
|
+
},
|
|
13657
|
+
annotations: { title: "Read and claim a mutation", ...WRITE5 },
|
|
13658
|
+
coordinates: { out: ["bounds", "strokes[].bounds", "strokes[].points[]", "cropPng.rect"] },
|
|
13659
|
+
handler: (a) => c.readMutation(a.id, {
|
|
13660
|
+
actor: options.actor?.id,
|
|
13661
|
+
padding: a.padding,
|
|
13662
|
+
maxSide: a.maxSide
|
|
13663
|
+
})
|
|
13664
|
+
},
|
|
13665
|
+
{
|
|
13666
|
+
name: "answer_mutation",
|
|
13667
|
+
title: "Answer a mutation with strokes",
|
|
13668
|
+
description: 'Paints your answer to a claimed mutation (from read_mutation). Pass one or more strokes (each with points in document pixels, and optional brushId, brush facets, color, paint, layerId and seed). When a director is attached, your cursor glides across the canvas and paints live; all strokes commit as one undo step with origin agent:<id>. A second answer to the same mutation id is refused. Returns the mutation id, state ("answered"), layerId, new strokeIds, painted bounds and dab count.',
|
|
13669
|
+
inputSchema: {
|
|
13670
|
+
type: "object",
|
|
13671
|
+
properties: {
|
|
13672
|
+
id: {
|
|
13673
|
+
type: "string",
|
|
13674
|
+
description: 'Claimed mutation id from read_mutation (e.g. "m1").'
|
|
13675
|
+
},
|
|
13676
|
+
strokes: {
|
|
13677
|
+
type: "array",
|
|
13678
|
+
minItems: 1,
|
|
13679
|
+
maxItems: 200,
|
|
13680
|
+
description: "The strokes that form the answer, painted in order as one undo step.",
|
|
13681
|
+
items: {
|
|
13682
|
+
type: "object",
|
|
13683
|
+
properties: {
|
|
13684
|
+
points: POINTS,
|
|
13685
|
+
layerId: {
|
|
13686
|
+
type: "string",
|
|
13687
|
+
description: "Layer id from list_layers for this stroke (default the answer layerId or the mutation layerId)."
|
|
13688
|
+
},
|
|
13689
|
+
brushId: {
|
|
13690
|
+
type: "string",
|
|
13691
|
+
description: "Brush id from list_brush_presets (default the active brush)."
|
|
13692
|
+
},
|
|
13693
|
+
brush: {
|
|
13694
|
+
type: "object",
|
|
13695
|
+
description: "Facets to change for this stroke only, laid over brushId or the active brush (as set_brush takes them).",
|
|
13696
|
+
properties: BRUSH_PATCH_PROPERTIES,
|
|
13697
|
+
additionalProperties: false
|
|
13698
|
+
},
|
|
13699
|
+
color: {
|
|
13700
|
+
type: "string",
|
|
13701
|
+
pattern: "^#[0-9a-fA-F]{6}$",
|
|
13702
|
+
description: "Paint colour #rrggbb (default the paint colour)."
|
|
13703
|
+
},
|
|
13704
|
+
paint: {
|
|
13705
|
+
...PAINT_SOURCE_JSON_SCHEMA,
|
|
13706
|
+
description: "Colour or gradient paint for this stroke."
|
|
13707
|
+
},
|
|
13708
|
+
seed: {
|
|
13709
|
+
type: "integer",
|
|
13710
|
+
minimum: 0,
|
|
13711
|
+
description: "Jitter seed for reproducible marks (default a new one)."
|
|
13712
|
+
},
|
|
13713
|
+
snapToPerspective: {
|
|
13714
|
+
type: "boolean",
|
|
13715
|
+
description: "Snap the points to the perspective guide (default false)."
|
|
13716
|
+
}
|
|
13717
|
+
},
|
|
13718
|
+
required: ["points"],
|
|
13719
|
+
additionalProperties: false
|
|
13720
|
+
}
|
|
13721
|
+
},
|
|
13722
|
+
layerId: {
|
|
13723
|
+
type: "string",
|
|
13724
|
+
description: "Default pixel layer id from list_layers for the answer strokes (default the mutation\u2019s layerId)."
|
|
13725
|
+
},
|
|
13726
|
+
note: {
|
|
13727
|
+
type: "string",
|
|
13728
|
+
description: "Short note shown next to the agent cursor while painting (optional)."
|
|
13729
|
+
}
|
|
13730
|
+
},
|
|
13731
|
+
required: ["id", "strokes"],
|
|
13732
|
+
additionalProperties: false
|
|
13733
|
+
},
|
|
13734
|
+
annotations: { title: "Answer a mutation with strokes", ...WRITE5 },
|
|
13735
|
+
coordinates: { in: ["strokes[].points[]"], out: ["bounds"] },
|
|
13736
|
+
handler: async (a) => {
|
|
13737
|
+
await c.wetIdle();
|
|
13738
|
+
const rawStrokes = a.strokes;
|
|
13739
|
+
const strokes = [];
|
|
13740
|
+
for (const s of rawStrokes) {
|
|
13741
|
+
const base = s.brushId ? c.getBrush(s.brushId) : c.getState().brush;
|
|
13742
|
+
const brush = s.brush ? c.brushChanges(s.brush, s.brushId) : void 0;
|
|
13743
|
+
if (base) {
|
|
13744
|
+
const refs = brushStickerRefs(normalizePaintBrush({ ...brush ?? {}, id: base.id }, base));
|
|
13745
|
+
if (refs.length) await c.prepareStickers(refs);
|
|
13746
|
+
}
|
|
13747
|
+
strokes.push({
|
|
13748
|
+
points: s.points,
|
|
13749
|
+
...s.layerId !== void 0 ? { layerId: s.layerId } : {},
|
|
13750
|
+
...s.brushId !== void 0 ? { brushId: s.brushId } : {},
|
|
13751
|
+
...brush !== void 0 ? { brush } : {},
|
|
13752
|
+
...s.color !== void 0 ? { color: s.color } : {},
|
|
13753
|
+
...s.paint !== void 0 ? { paint: s.paint } : {},
|
|
13754
|
+
...s.seed !== void 0 ? { seed: s.seed } : {},
|
|
13755
|
+
...s.snapToPerspective !== void 0 ? { snapToPerspective: s.snapToPerspective } : {}
|
|
13756
|
+
});
|
|
13757
|
+
}
|
|
13758
|
+
return c.answerMutation(
|
|
13759
|
+
a.id,
|
|
13760
|
+
{
|
|
13761
|
+
strokes,
|
|
13762
|
+
...a.layerId !== void 0 ? { layerId: a.layerId } : {},
|
|
13763
|
+
...a.note !== void 0 ? { note: a.note } : {}
|
|
13764
|
+
},
|
|
13765
|
+
{ director: options.director, actor: options.actor }
|
|
13766
|
+
);
|
|
13767
|
+
}
|
|
13768
|
+
},
|
|
13769
|
+
{
|
|
13770
|
+
name: "decline_mutation",
|
|
13771
|
+
title: "Decline a mutation",
|
|
13772
|
+
description: "Declines a mutation without answering so the user\u2019s paint stays as ordinary paint (wet strokes stay in the wet layer and dry temporary layers merge down into their target layer). Returns the updated mutation info.",
|
|
13773
|
+
inputSchema: {
|
|
13774
|
+
type: "object",
|
|
13775
|
+
properties: {
|
|
13776
|
+
id: {
|
|
13777
|
+
type: "string",
|
|
13778
|
+
description: 'Mutation id from list_mutations (e.g. "m1").'
|
|
13779
|
+
},
|
|
13780
|
+
reason: {
|
|
13781
|
+
type: "string",
|
|
13782
|
+
description: "Why the mutation was declined (optional)."
|
|
13783
|
+
}
|
|
13784
|
+
},
|
|
13785
|
+
required: ["id"],
|
|
13786
|
+
additionalProperties: false
|
|
13787
|
+
},
|
|
13788
|
+
annotations: { title: "Decline a mutation", ...SET4 },
|
|
13789
|
+
coordinates: { out: ["bounds"] },
|
|
13790
|
+
handler: (a) => c.declineMutation(a.id, a.reason)
|
|
13791
|
+
}
|
|
13792
|
+
];
|
|
13793
|
+
}
|
|
13794
|
+
|
|
13795
|
+
// src/paint-editor/evolution-tools.ts
|
|
13796
|
+
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13797
|
+
var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
11400
13798
|
var UNIT2 = (description) => ({ type: "number", minimum: 0, maximum: 1, description });
|
|
11401
13799
|
var EVOLUTION_MUTATION_SCHEMA = {
|
|
11402
13800
|
type: "object",
|
|
@@ -11441,7 +13839,7 @@ function createPaintEvolutionTools(c) {
|
|
|
11441
13839
|
required: ["strokeIds"],
|
|
11442
13840
|
additionalProperties: false
|
|
11443
13841
|
},
|
|
11444
|
-
annotations: { title: "Breed stroke variants", ...
|
|
13842
|
+
annotations: { title: "Breed stroke variants", ...WRITE6 },
|
|
11445
13843
|
coordinates: { out: ["proposals[].bounds"] },
|
|
11446
13844
|
handler: ({ strokeIds, count, mutation, seed }, context) => {
|
|
11447
13845
|
const actor = context?.actor?.id;
|
|
@@ -11484,7 +13882,7 @@ function createPaintEvolutionTools(c) {
|
|
|
11484
13882
|
required: ["proposalIds"],
|
|
11485
13883
|
additionalProperties: false
|
|
11486
13884
|
},
|
|
11487
|
-
annotations: { title: "Breed from paint proposals", ...
|
|
13885
|
+
annotations: { title: "Breed from paint proposals", ...WRITE6 },
|
|
11488
13886
|
coordinates: { out: ["proposals[].bounds"] },
|
|
11489
13887
|
handler: ({ proposalIds, count, mutation, seed }, context) => {
|
|
11490
13888
|
const actor = context?.actor?.id;
|
|
@@ -11512,7 +13910,7 @@ function createPaintEvolutionTools(c) {
|
|
|
11512
13910
|
required: ["proposalId"],
|
|
11513
13911
|
additionalProperties: false
|
|
11514
13912
|
},
|
|
11515
|
-
annotations: { title: "Get a proposal\u2019s stroke genome", ...
|
|
13913
|
+
annotations: { title: "Get a proposal\u2019s stroke genome", ...READ7 },
|
|
11516
13914
|
coordinates: { out: ["bounds", "strokes[].points[]"] },
|
|
11517
13915
|
handler: ({ proposalId }) => c.getProposalGenome(proposalId)
|
|
11518
13916
|
}
|
|
@@ -11531,15 +13929,15 @@ function createPaintEvolutionToolProvider(controller, options = {}) {
|
|
|
11531
13929
|
}
|
|
11532
13930
|
|
|
11533
13931
|
// src/paint-editor/tools.ts
|
|
11534
|
-
var
|
|
11535
|
-
var
|
|
11536
|
-
var
|
|
13932
|
+
var READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13933
|
+
var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
13934
|
+
var SET5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11537
13935
|
var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
11538
13936
|
var EMPTY3 = { type: "object", properties: {}, additionalProperties: false };
|
|
11539
13937
|
var COLOR3 = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
|
|
11540
13938
|
var LAYER_ID4 = { type: "string", description: "Layer id from list_layers (default the active layer)." };
|
|
11541
13939
|
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)." };
|
|
11542
|
-
var
|
|
13940
|
+
var POINTS2 = {
|
|
11543
13941
|
type: "array",
|
|
11544
13942
|
minItems: 1,
|
|
11545
13943
|
maxItems: 2e4,
|
|
@@ -11581,7 +13979,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11581
13979
|
title: "Get the canvas",
|
|
11582
13980
|
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.",
|
|
11583
13981
|
inputSchema: EMPTY3,
|
|
11584
|
-
annotations: { title: "Get the canvas", ...
|
|
13982
|
+
annotations: { title: "Get the canvas", ...READ8 },
|
|
11585
13983
|
coordinates: { out: ["bounds"] },
|
|
11586
13984
|
handler: () => {
|
|
11587
13985
|
const s = c.getState();
|
|
@@ -11642,7 +14040,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11642
14040
|
},
|
|
11643
14041
|
additionalProperties: false
|
|
11644
14042
|
},
|
|
11645
|
-
annotations: { title: "Resize the canvas", ...
|
|
14043
|
+
annotations: { title: "Resize the canvas", ...SET5 },
|
|
11646
14044
|
handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
|
|
11647
14045
|
});
|
|
11648
14046
|
const EXTEND_SIDE = (side) => ({ type: "integer", minimum: 0, maximum: MAX_CANVAS_SIDE - 1, description: `Pixels to add on the ${side} (default 0).` });
|
|
@@ -11655,7 +14053,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11655
14053
|
properties: { left: EXTEND_SIDE("left"), top: EXTEND_SIDE("top"), right: EXTEND_SIDE("right"), bottom: EXTEND_SIDE("bottom") },
|
|
11656
14054
|
additionalProperties: false
|
|
11657
14055
|
},
|
|
11658
|
-
annotations: { title: "Extend the canvas", ...
|
|
14056
|
+
annotations: { title: "Extend the canvas", ...WRITE7 },
|
|
11659
14057
|
handler: (a) => c.extendCanvas({ left: a.left, top: a.top, right: a.right, bottom: a.bottom })
|
|
11660
14058
|
});
|
|
11661
14059
|
add({
|
|
@@ -11663,7 +14061,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11663
14061
|
title: "List papers",
|
|
11664
14062
|
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.",
|
|
11665
14063
|
inputSchema: EMPTY3,
|
|
11666
|
-
annotations: { title: "List papers", ...
|
|
14064
|
+
annotations: { title: "List papers", ...READ8 },
|
|
11667
14065
|
handler: () => ({
|
|
11668
14066
|
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 })),
|
|
11669
14067
|
families: [...PAPER_FAMILIES],
|
|
@@ -11675,7 +14073,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11675
14073
|
title: "Get the paper",
|
|
11676
14074
|
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).",
|
|
11677
14075
|
inputSchema: EMPTY3,
|
|
11678
|
-
annotations: { title: "Get the paper", ...
|
|
14076
|
+
annotations: { title: "Get the paper", ...READ8 },
|
|
11679
14077
|
handler: () => JSON.parse(JSON.stringify(c.document.paper))
|
|
11680
14078
|
});
|
|
11681
14079
|
const tuning = {
|
|
@@ -11713,7 +14111,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11713
14111
|
},
|
|
11714
14112
|
additionalProperties: false
|
|
11715
14113
|
},
|
|
11716
|
-
annotations: { title: "Set the paper", ...
|
|
14114
|
+
annotations: { title: "Set the paper", ...SET5 },
|
|
11717
14115
|
handler: (a) => c.setPaper(a)
|
|
11718
14116
|
});
|
|
11719
14117
|
add({
|
|
@@ -11721,7 +14119,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11721
14119
|
title: "List layers",
|
|
11722
14120
|
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.",
|
|
11723
14121
|
inputSchema: EMPTY3,
|
|
11724
|
-
annotations: { title: "List layers", ...
|
|
14122
|
+
annotations: { title: "List layers", ...READ8 },
|
|
11725
14123
|
coordinates: { out: ["layers[].bounds"] },
|
|
11726
14124
|
handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId })
|
|
11727
14125
|
});
|
|
@@ -11739,7 +14137,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11739
14137
|
},
|
|
11740
14138
|
additionalProperties: false
|
|
11741
14139
|
},
|
|
11742
|
-
annotations: { title: "Add a layer", ...
|
|
14140
|
+
annotations: { title: "Add a layer", ...WRITE7 },
|
|
11743
14141
|
coordinates: { out: ["bounds"] },
|
|
11744
14142
|
handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId })
|
|
11745
14143
|
});
|
|
@@ -11775,7 +14173,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11775
14173
|
required: ["layerId"],
|
|
11776
14174
|
additionalProperties: false
|
|
11777
14175
|
},
|
|
11778
|
-
annotations: { title: "Change a layer", ...
|
|
14176
|
+
annotations: { title: "Change a layer", ...SET5 },
|
|
11779
14177
|
coordinates: { out: ["bounds"] },
|
|
11780
14178
|
handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
|
|
11781
14179
|
});
|
|
@@ -11784,7 +14182,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11784
14182
|
title: "List blend modes",
|
|
11785
14183
|
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.",
|
|
11786
14184
|
inputSchema: EMPTY3,
|
|
11787
|
-
annotations: { title: "List blend modes", ...
|
|
14185
|
+
annotations: { title: "List blend modes", ...READ8 },
|
|
11788
14186
|
handler: () => ({
|
|
11789
14187
|
modes: BLEND_MODES.map((m) => ({
|
|
11790
14188
|
id: m.id,
|
|
@@ -11800,7 +14198,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11800
14198
|
title: "Choose the active layer",
|
|
11801
14199
|
description: "Makes a layer active: strokes, fills and clears without a layerId go to it. Editor state, not an undo step. Returns the layer.",
|
|
11802
14200
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
11803
|
-
annotations: { title: "Choose the active layer", ...
|
|
14201
|
+
annotations: { title: "Choose the active layer", ...SET5 },
|
|
11804
14202
|
coordinates: { out: ["bounds"] },
|
|
11805
14203
|
handler: ({ layerId }) => {
|
|
11806
14204
|
c.setActiveLayer(layerId);
|
|
@@ -11820,7 +14218,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11820
14218
|
title: "Duplicate a layer",
|
|
11821
14219
|
description: "Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.",
|
|
11822
14220
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
11823
|
-
annotations: { title: "Duplicate a layer", ...
|
|
14221
|
+
annotations: { title: "Duplicate a layer", ...WRITE7 },
|
|
11824
14222
|
coordinates: { out: ["bounds"] },
|
|
11825
14223
|
handler: ({ layerId }) => c.duplicateLayer(layerId)
|
|
11826
14224
|
});
|
|
@@ -11838,7 +14236,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11838
14236
|
required: ["layerId", "index"],
|
|
11839
14237
|
additionalProperties: false
|
|
11840
14238
|
},
|
|
11841
|
-
annotations: { title: "Reorder a layer", ...
|
|
14239
|
+
annotations: { title: "Reorder a layer", ...WRITE7 },
|
|
11842
14240
|
coordinates: { out: ["bounds"] },
|
|
11843
14241
|
handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId)
|
|
11844
14242
|
});
|
|
@@ -11867,7 +14265,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11867
14265
|
},
|
|
11868
14266
|
additionalProperties: false
|
|
11869
14267
|
},
|
|
11870
|
-
annotations: { title: "Fill a layer", ...
|
|
14268
|
+
annotations: { title: "Fill a layer", ...WRITE7 },
|
|
11871
14269
|
coordinates: { out: ["bounds"] },
|
|
11872
14270
|
handler: (a) => act({ kind: "fill", fill: a }, () => c.fillLayer(a))
|
|
11873
14271
|
});
|
|
@@ -11914,7 +14312,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11914
14312
|
},
|
|
11915
14313
|
additionalProperties: false
|
|
11916
14314
|
},
|
|
11917
|
-
annotations: { title: "Fill a region (bucket / flats)", ...
|
|
14315
|
+
annotations: { title: "Fill a region (bucket / flats)", ...WRITE7 },
|
|
11918
14316
|
coordinates: { in: ["", "points[]"], out: ["bounds", "regions[].point", "regions[].bounds"] },
|
|
11919
14317
|
handler: (a) => act({ kind: "fill-region", fill: a }, () => c.fillRegion(a))
|
|
11920
14318
|
});
|
|
@@ -11938,7 +14336,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11938
14336
|
},
|
|
11939
14337
|
additionalProperties: false
|
|
11940
14338
|
},
|
|
11941
|
-
annotations: { title: "Find enclosed unfilled regions", ...
|
|
14339
|
+
annotations: { title: "Find enclosed unfilled regions", ...READ8 },
|
|
11942
14340
|
coordinates: { out: ["regions[].point", "regions[].centroid", "regions[].bounds"] },
|
|
11943
14341
|
handler: (a) => c.findUnfilledRegions(a)
|
|
11944
14342
|
});
|
|
@@ -11956,7 +14354,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11956
14354
|
title: "Get a brush",
|
|
11957
14355
|
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.",
|
|
11958
14356
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." } }, additionalProperties: false },
|
|
11959
|
-
annotations: { title: "Get a brush", ...
|
|
14357
|
+
annotations: { title: "Get a brush", ...READ8 },
|
|
11960
14358
|
handler: ({ id }) => {
|
|
11961
14359
|
const st = c.getState();
|
|
11962
14360
|
const b = id === void 0 ? st.brush : c.getBrush(id);
|
|
@@ -11970,7 +14368,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11970
14368
|
title: "Set the brush",
|
|
11971
14369
|
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).",
|
|
11972
14370
|
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 },
|
|
11973
|
-
annotations: { title: "Set the brush", ...
|
|
14371
|
+
annotations: { title: "Set the brush", ...SET5 },
|
|
11974
14372
|
handler: async ({ id, unset, ...patch }) => {
|
|
11975
14373
|
const stamps = patch.tip?.stamps;
|
|
11976
14374
|
if (stamps) await c.prepareStickers(stamps);
|
|
@@ -11991,7 +14389,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11991
14389
|
},
|
|
11992
14390
|
additionalProperties: false
|
|
11993
14391
|
},
|
|
11994
|
-
annotations: { title: "Set the paint colour", ...
|
|
14392
|
+
annotations: { title: "Set the paint colour", ...SET5 },
|
|
11995
14393
|
handler: ({ color, secondaryColor, locked }) => {
|
|
11996
14394
|
if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
|
|
11997
14395
|
if (color !== void 0) c.setColor(color);
|
|
@@ -12006,7 +14404,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12006
14404
|
title: "Set the paint (colour or gradient)",
|
|
12007
14405
|
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.',
|
|
12008
14406
|
inputSchema: { type: "object", properties: { paint: PAINT_SOURCE_JSON_SCHEMA }, required: ["paint"], additionalProperties: false },
|
|
12009
|
-
annotations: { title: "Set the paint (colour or gradient)", ...
|
|
14407
|
+
annotations: { title: "Set the paint (colour or gradient)", ...SET5 },
|
|
12010
14408
|
handler: ({ paint }) => {
|
|
12011
14409
|
const p = c.setPaint(paint);
|
|
12012
14410
|
const s = c.getState();
|
|
@@ -12027,7 +14425,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12027
14425
|
required: ["x", "y"],
|
|
12028
14426
|
additionalProperties: false
|
|
12029
14427
|
},
|
|
12030
|
-
annotations: { title: "Pick a colour", ...
|
|
14428
|
+
annotations: { title: "Pick a colour", ...READ8 },
|
|
12031
14429
|
coordinates: { in: [""] },
|
|
12032
14430
|
handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId })
|
|
12033
14431
|
});
|
|
@@ -12036,7 +14434,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12036
14434
|
title: "Get the Mixer Brush load",
|
|
12037
14435
|
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.",
|
|
12038
14436
|
inputSchema: EMPTY3,
|
|
12039
|
-
annotations: { title: "Get the Mixer Brush load", ...
|
|
14437
|
+
annotations: { title: "Get the Mixer Brush load", ...READ8 },
|
|
12040
14438
|
handler: () => loadResult(c.getState().mixerLoad)
|
|
12041
14439
|
});
|
|
12042
14440
|
add({
|
|
@@ -12044,7 +14442,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12044
14442
|
title: "Load the Mixer Brush",
|
|
12045
14443
|
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.",
|
|
12046
14444
|
inputSchema: { type: "object", properties: { color: { ...COLOR3, description: "Colour #rrggbb (default the paint colour)." } }, additionalProperties: false },
|
|
12047
|
-
annotations: { title: "Load the Mixer Brush", ...
|
|
14445
|
+
annotations: { title: "Load the Mixer Brush", ...SET5 },
|
|
12048
14446
|
handler: ({ color }) => loadResult(c.loadMixerBrush(color))
|
|
12049
14447
|
});
|
|
12050
14448
|
add({
|
|
@@ -12052,7 +14450,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12052
14450
|
title: "Clean the Mixer Brush",
|
|
12053
14451
|
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.",
|
|
12054
14452
|
inputSchema: { type: "object", properties: { emptyReservoir: { type: "boolean", description: "Also drop the loaded paint (default false)." } }, additionalProperties: false },
|
|
12055
|
-
annotations: { title: "Clean the Mixer Brush", ...
|
|
14453
|
+
annotations: { title: "Clean the Mixer Brush", ...SET5 },
|
|
12056
14454
|
handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
|
|
12057
14455
|
});
|
|
12058
14456
|
add({
|
|
@@ -12062,7 +14460,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12062
14460
|
inputSchema: {
|
|
12063
14461
|
type: "object",
|
|
12064
14462
|
properties: {
|
|
12065
|
-
points:
|
|
14463
|
+
points: POINTS2,
|
|
12066
14464
|
layerId: LAYER_ID4,
|
|
12067
14465
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
12068
14466
|
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 },
|
|
@@ -12074,7 +14472,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12074
14472
|
required: ["points"],
|
|
12075
14473
|
additionalProperties: false
|
|
12076
14474
|
},
|
|
12077
|
-
annotations: { title: "Paint a brush stroke", ...
|
|
14475
|
+
annotations: { title: "Paint a brush stroke", ...WRITE7 },
|
|
12078
14476
|
coordinates: { in: ["points[]"], out: ["bounds"] },
|
|
12079
14477
|
handler: async (a) => {
|
|
12080
14478
|
await c.wetIdle();
|
|
@@ -12111,7 +14509,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12111
14509
|
required: ["path"],
|
|
12112
14510
|
additionalProperties: false
|
|
12113
14511
|
},
|
|
12114
|
-
annotations: { title: "Brush along a path", ...
|
|
14512
|
+
annotations: { title: "Brush along a path", ...WRITE7 },
|
|
12115
14513
|
coordinates: { out: ["bounds"] },
|
|
12116
14514
|
handler: async (a) => {
|
|
12117
14515
|
await c.wetIdle();
|
|
@@ -12165,7 +14563,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12165
14563
|
properties: { limit: { type: "integer", minimum: 1, maximum: 5e3, description: "Most recent samples to return (default 500)." } },
|
|
12166
14564
|
additionalProperties: false
|
|
12167
14565
|
},
|
|
12168
|
-
annotations: { title: "Get the stroke being painted", ...
|
|
14566
|
+
annotations: { title: "Get the stroke being painted", ...READ8 },
|
|
12169
14567
|
handler: (args) => {
|
|
12170
14568
|
const live = c.getLiveStroke();
|
|
12171
14569
|
if (!live) return { live: false };
|
|
@@ -12187,7 +14585,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12187
14585
|
},
|
|
12188
14586
|
additionalProperties: false
|
|
12189
14587
|
},
|
|
12190
|
-
annotations: { title: "List recorded strokes", ...
|
|
14588
|
+
annotations: { title: "List recorded strokes", ...READ8 },
|
|
12191
14589
|
coordinates: { out: ["strokes[].bounds", "strokes[].points[]"] },
|
|
12192
14590
|
handler: ({ ids, last, live, layerId }) => {
|
|
12193
14591
|
if (layerId !== void 0 && !c.getLayer(layerId)) throw new Error(`Unknown layer "${layerId}". Call list_layers for the ids.`);
|
|
@@ -12216,7 +14614,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12216
14614
|
required: ["x", "y"],
|
|
12217
14615
|
additionalProperties: false
|
|
12218
14616
|
},
|
|
12219
|
-
annotations: { title: "Find the stroke at a point", ...
|
|
14617
|
+
annotations: { title: "Find the stroke at a point", ...READ8 },
|
|
12220
14618
|
coordinates: { in: [""], out: ["stroke.bounds"] },
|
|
12221
14619
|
handler: ({ layerId, x, y }) => {
|
|
12222
14620
|
const s = c.liveStrokeAt(layerId, x, y);
|
|
@@ -12240,7 +14638,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12240
14638
|
required: ["id"],
|
|
12241
14639
|
additionalProperties: false
|
|
12242
14640
|
},
|
|
12243
|
-
annotations: { title: "Change a stroke", ...
|
|
14641
|
+
annotations: { title: "Change a stroke", ...WRITE7 },
|
|
12244
14642
|
coordinates: { out: ["bounds", "stroke.bounds"] },
|
|
12245
14643
|
handler: ({ id, ...update }) => {
|
|
12246
14644
|
if (!Object.keys(update).length) throw new Error("Pass at least one change: brushId, brush, color, paint or seed.");
|
|
@@ -12261,7 +14659,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12261
14659
|
type: "object",
|
|
12262
14660
|
properties: {
|
|
12263
14661
|
id: { type: "string", description: "Stroke id from list_strokes {live: true}." },
|
|
12264
|
-
points:
|
|
14662
|
+
points: POINTS2,
|
|
12265
14663
|
transform: {
|
|
12266
14664
|
type: "object",
|
|
12267
14665
|
description: "A move, scale, rotation or flip of the whole stroke.",
|
|
@@ -12279,7 +14677,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12279
14677
|
required: ["id"],
|
|
12280
14678
|
additionalProperties: false
|
|
12281
14679
|
},
|
|
12282
|
-
annotations: { title: "Move or reshape a stroke", ...
|
|
14680
|
+
annotations: { title: "Move or reshape a stroke", ...WRITE7 },
|
|
12283
14681
|
coordinates: { in: ["points[]", "transform.origin"], out: ["bounds", "stroke.bounds", "stroke.points[]"] },
|
|
12284
14682
|
handler: async ({ id, points, transform }) => {
|
|
12285
14683
|
await c.prepareStrokeEdit(id, { points, transform });
|
|
@@ -12297,7 +14695,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12297
14695
|
required: ["id", "index"],
|
|
12298
14696
|
additionalProperties: false
|
|
12299
14697
|
},
|
|
12300
|
-
annotations: { title: "Move a stroke up or down", ...
|
|
14698
|
+
annotations: { title: "Move a stroke up or down", ...WRITE7 },
|
|
12301
14699
|
handler: ({ id, index }) => ({ order: c.reorderStroke(id, index) })
|
|
12302
14700
|
});
|
|
12303
14701
|
add({
|
|
@@ -12319,7 +14717,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12319
14717
|
title: "Make a layer a stroke layer",
|
|
12320
14718
|
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.',
|
|
12321
14719
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers (default the active layer)." } }, additionalProperties: false },
|
|
12322
|
-
annotations: { title: "Make a layer a stroke layer", ...
|
|
14720
|
+
annotations: { title: "Make a layer a stroke layer", ...WRITE7 },
|
|
12323
14721
|
coordinates: { out: ["bounds"] },
|
|
12324
14722
|
handler: ({ layerId }) => c.convertToLiveStrokes(layerId)
|
|
12325
14723
|
});
|
|
@@ -12361,7 +14759,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12361
14759
|
required: ["strokeIds"],
|
|
12362
14760
|
additionalProperties: false
|
|
12363
14761
|
},
|
|
12364
|
-
annotations: { title: "Replay strokes, varied", ...
|
|
14762
|
+
annotations: { title: "Replay strokes, varied", ...WRITE7 },
|
|
12365
14763
|
coordinates: { in: ["origin"], out: ["bounds"] },
|
|
12366
14764
|
handler: async (a) => {
|
|
12367
14765
|
const replay = a;
|
|
@@ -12383,7 +14781,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12383
14781
|
},
|
|
12384
14782
|
additionalProperties: false
|
|
12385
14783
|
},
|
|
12386
|
-
annotations: { title: "Rotate, mirror or zoom the view", ...
|
|
14784
|
+
annotations: { title: "Rotate, mirror or zoom the view", ...SET5 },
|
|
12387
14785
|
handler: ({ reset, ...v }) => {
|
|
12388
14786
|
if (reset) c.resetView();
|
|
12389
14787
|
return c.setView(v);
|
|
@@ -12394,7 +14792,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12394
14792
|
title: "Set the drawing hand",
|
|
12395
14793
|
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.",
|
|
12396
14794
|
inputSchema: { type: "object", properties: { hand: { type: "string", enum: ["right", "left"], description: "right or left." } }, required: ["hand"], additionalProperties: false },
|
|
12397
|
-
annotations: { title: "Set the drawing hand", ...
|
|
14795
|
+
annotations: { title: "Set the drawing hand", ...SET5 },
|
|
12398
14796
|
handler: ({ hand }) => ({ hand: c.setHand(hand) })
|
|
12399
14797
|
});
|
|
12400
14798
|
add({
|
|
@@ -12402,7 +14800,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12402
14800
|
title: "Undo",
|
|
12403
14801
|
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.",
|
|
12404
14802
|
inputSchema: EMPTY3,
|
|
12405
|
-
annotations: { title: "Undo", ...
|
|
14803
|
+
annotations: { title: "Undo", ...WRITE7 },
|
|
12406
14804
|
handler: () => {
|
|
12407
14805
|
const undone = c.undo();
|
|
12408
14806
|
const s = c.getState();
|
|
@@ -12414,7 +14812,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12414
14812
|
title: "Redo",
|
|
12415
14813
|
description: "Redoes the last undone step. Returns the label redone (null when there was nothing) and what can be undone and redone next.",
|
|
12416
14814
|
inputSchema: EMPTY3,
|
|
12417
|
-
annotations: { title: "Redo", ...
|
|
14815
|
+
annotations: { title: "Redo", ...WRITE7 },
|
|
12418
14816
|
handler: () => {
|
|
12419
14817
|
const redone = c.redo();
|
|
12420
14818
|
const s = c.getState();
|
|
@@ -12434,7 +14832,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12434
14832
|
},
|
|
12435
14833
|
additionalProperties: false
|
|
12436
14834
|
},
|
|
12437
|
-
annotations: { title: "Render a preview", ...
|
|
14835
|
+
annotations: { title: "Render a preview", ...READ8 },
|
|
12438
14836
|
handler: ({ maxSide, layerIds, paper }) => {
|
|
12439
14837
|
if (layerIds) {
|
|
12440
14838
|
for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer "${id}". Call list_layers for the ids.`);
|
|
@@ -12458,7 +14856,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12458
14856
|
},
|
|
12459
14857
|
additionalProperties: false
|
|
12460
14858
|
},
|
|
12461
|
-
annotations: { title: "Export the painting", ...
|
|
14859
|
+
annotations: { title: "Export the painting", ...READ8 },
|
|
12462
14860
|
handler: async ({ format, quality, scale, paper, time }) => {
|
|
12463
14861
|
const run = () => c.exportImage({ format, quality, scale, paper });
|
|
12464
14862
|
const bytes = time === void 0 ? await run() : await c.withEffectTime(time, run);
|
|
@@ -12470,7 +14868,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12470
14868
|
title: "Export as PSD",
|
|
12471
14869
|
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).',
|
|
12472
14870
|
inputSchema: { type: "object", properties: { time: TIME }, additionalProperties: false },
|
|
12473
|
-
annotations: { title: "Export as PSD", ...
|
|
14871
|
+
annotations: { title: "Export as PSD", ...READ8 },
|
|
12474
14872
|
handler: async ({ time }) => {
|
|
12475
14873
|
const r = time === void 0 ? c.exportPsd() : await c.withEffectTime(time, () => c.exportPsd());
|
|
12476
14874
|
return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
|
|
@@ -12482,6 +14880,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
12482
14880
|
tools.push(...createPaintReplayTools(c));
|
|
12483
14881
|
tools.push(...createPaintEvolutionTools(c));
|
|
12484
14882
|
tools.push(...createPaintStickerTools(c));
|
|
14883
|
+
tools.push(...createMutationTools(c, { director: options.director, actor: options.actor }));
|
|
12485
14884
|
tools.push(...createPigmentTools());
|
|
12486
14885
|
tools.push(...createStickerTools({ provider: () => c.stickersProvider }));
|
|
12487
14886
|
tools.push(
|
|
@@ -13097,6 +15496,53 @@ var LIGHT_SLIDERS = [
|
|
|
13097
15496
|
{ path: "elevation", label: "Light height", min: 5, max: 90, step: 1 },
|
|
13098
15497
|
{ path: "relief", label: "Relief", min: 0, max: 1, step: 0.01 }
|
|
13099
15498
|
];
|
|
15499
|
+
var UV_LIGHT_SLIDERS = [
|
|
15500
|
+
{ path: "strength", label: "UV strength", min: 0, max: 2, step: 0.05 },
|
|
15501
|
+
{ path: "ambient", label: "Ambient light", min: 0, max: 1, step: 0.01 }
|
|
15502
|
+
];
|
|
15503
|
+
var VIEW_TILT_SLIDERS = [
|
|
15504
|
+
{ path: "x", label: "View tilt X", min: -1, max: 1, step: 0.01 },
|
|
15505
|
+
{ path: "y", label: "View tilt Y", min: -1, max: 1, step: 0.01 }
|
|
15506
|
+
];
|
|
15507
|
+
function deviceOrientationToViewTilt(betaOrEvent, gammaOrRest, rest = { beta: 45, gamma: 0 }) {
|
|
15508
|
+
const isObj = typeof betaOrEvent === "object" && betaOrEvent !== null;
|
|
15509
|
+
const r = isObj && typeof gammaOrRest === "object" && gammaOrRest !== null ? gammaOrRest : rest;
|
|
15510
|
+
const rawBeta = isObj ? betaOrEvent.beta : betaOrEvent;
|
|
15511
|
+
const rawGamma = isObj ? betaOrEvent.gamma : typeof gammaOrRest === "number" ? gammaOrRest : void 0;
|
|
15512
|
+
const b = typeof rawBeta === "number" && Number.isFinite(rawBeta) ? rawBeta : r.beta;
|
|
15513
|
+
const g = typeof rawGamma === "number" && Number.isFinite(rawGamma) ? rawGamma : r.gamma;
|
|
15514
|
+
const clamp2 = (v) => Math.round(Math.max(-1, Math.min(1, v)) * 100) / 100;
|
|
15515
|
+
return { x: clamp2((g - r.gamma) / 35), y: clamp2((b - r.beta) / 35) };
|
|
15516
|
+
}
|
|
15517
|
+
async function requestDeviceTiltPermission() {
|
|
15518
|
+
const g = globalThis;
|
|
15519
|
+
if (!g.DeviceOrientationEvent) return "unsupported";
|
|
15520
|
+
if (typeof g.DeviceOrientationEvent.requestPermission === "function") {
|
|
15521
|
+
try {
|
|
15522
|
+
return await g.DeviceOrientationEvent.requestPermission() === "granted" ? "granted" : "denied";
|
|
15523
|
+
} catch {
|
|
15524
|
+
return "denied";
|
|
15525
|
+
}
|
|
15526
|
+
}
|
|
15527
|
+
return "granted";
|
|
15528
|
+
}
|
|
15529
|
+
function startDeviceTilt(controller) {
|
|
15530
|
+
const g = globalThis;
|
|
15531
|
+
if (typeof g.addEventListener !== "function") return () => {
|
|
15532
|
+
};
|
|
15533
|
+
let rest = null;
|
|
15534
|
+
const onOrientation = (e) => {
|
|
15535
|
+
if (typeof e.beta !== "number" || typeof e.gamma !== "number") return;
|
|
15536
|
+
if (!rest) rest = { beta: e.beta, gamma: e.gamma };
|
|
15537
|
+
const next = deviceOrientationToViewTilt(e.beta, e.gamma, rest);
|
|
15538
|
+
const cur = controller.getViewTilt();
|
|
15539
|
+
if (next.x !== cur.x || next.y !== cur.y) controller.setViewTilt(next);
|
|
15540
|
+
};
|
|
15541
|
+
g.addEventListener("deviceorientation", onOrientation);
|
|
15542
|
+
return () => {
|
|
15543
|
+
g.removeEventListener?.("deviceorientation", onOrientation);
|
|
15544
|
+
};
|
|
15545
|
+
}
|
|
13100
15546
|
var PARTICLE_KIND_LABELS = { string: "Strings", spring: "Springs", orbit: "Orbits", flow: "Flow", elastic: "Elastic", fuzzy: "Fuzzy" };
|
|
13101
15547
|
var PARTICLE_RENDER_LABELS = { dot: "Dots", line: "Lines", sequence: "Beads", ribbon: "Ribbon" };
|
|
13102
15548
|
var RIBBON_PAIR_LABELS = { adjacent: "Neighbours", "first-last": "Outermost" };
|
|
@@ -13427,8 +15873,8 @@ function createPaintEditorDocument(controller, options = {}) {
|
|
|
13427
15873
|
}
|
|
13428
15874
|
|
|
13429
15875
|
// src/paint-editor/collaboration-tools.ts
|
|
13430
|
-
var
|
|
13431
|
-
var
|
|
15876
|
+
var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15877
|
+
var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
13432
15878
|
var proposalIdArg = {
|
|
13433
15879
|
type: "object",
|
|
13434
15880
|
properties: { proposalId: { type: "string", minLength: 1, description: "Id of a pending paint proposal, from propose_paint or list_paint_proposals." } },
|
|
@@ -13506,7 +15952,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
13506
15952
|
required: ["title", "ops"],
|
|
13507
15953
|
additionalProperties: false
|
|
13508
15954
|
},
|
|
13509
|
-
annotations: { title: "Propose paint", ...
|
|
15955
|
+
annotations: { title: "Propose paint", ...WRITE8 },
|
|
13510
15956
|
handler: (args, context) => {
|
|
13511
15957
|
const origin = context.actor?.id ? `agent:${context.actor.id}` : options.fallbackOrigin;
|
|
13512
15958
|
const proposal = proposals.propose({ ...args, ...origin ? { origin } : {} });
|
|
@@ -13519,7 +15965,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
13519
15965
|
title: "List paint proposals",
|
|
13520
15966
|
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.",
|
|
13521
15967
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
13522
|
-
annotations: { title: "List paint proposals", ...
|
|
15968
|
+
annotations: { title: "List paint proposals", ...READ9 },
|
|
13523
15969
|
handler: () => ({ proposals: proposals.getState().proposals.map(describe), readOnly: readOnlyReason() })
|
|
13524
15970
|
};
|
|
13525
15971
|
const acceptTool = {
|
|
@@ -13527,7 +15973,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
13527
15973
|
title: "Accept a paint proposal",
|
|
13528
15974
|
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.",
|
|
13529
15975
|
inputSchema: proposalIdArg,
|
|
13530
|
-
annotations: { title: "Accept a paint proposal", ...
|
|
15976
|
+
annotations: { title: "Accept a paint proposal", ...WRITE8 },
|
|
13531
15977
|
handler: (args) => {
|
|
13532
15978
|
const reason = readOnlyReason();
|
|
13533
15979
|
if (reason) throw new Error(reason);
|
|
@@ -13539,7 +15985,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
13539
15985
|
title: "Reject a paint proposal",
|
|
13540
15986
|
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.",
|
|
13541
15987
|
inputSchema: proposalIdArg,
|
|
13542
|
-
annotations: { title: "Reject a paint proposal", ...
|
|
15988
|
+
annotations: { title: "Reject a paint proposal", ...WRITE8 },
|
|
13543
15989
|
handler: (args) => {
|
|
13544
15990
|
const p = proposals.reject(args.proposalId);
|
|
13545
15991
|
return { proposalId: p.id, title: p.title, rejected: true };
|
|
@@ -13611,6 +16057,35 @@ var PAINT_DOCUMENT_JSON_SCHEMA = {
|
|
|
13611
16057
|
},
|
|
13612
16058
|
required: ["id", "layerId", "points", "brush"]
|
|
13613
16059
|
}
|
|
16060
|
+
},
|
|
16061
|
+
fluor: {
|
|
16062
|
+
type: "object",
|
|
16063
|
+
description: "Per-layer UV-fluorescent ink channels (layerId \u2192 bounds + PNG data URL).",
|
|
16064
|
+
additionalProperties: { type: "object" }
|
|
16065
|
+
},
|
|
16066
|
+
thermo: {
|
|
16067
|
+
type: "object",
|
|
16068
|
+
description: "Per-layer thermochromic ink channels (layerId \u2192 bounds + PNG data URL).",
|
|
16069
|
+
additionalProperties: { type: "object" }
|
|
16070
|
+
},
|
|
16071
|
+
heat: {
|
|
16072
|
+
type: "object",
|
|
16073
|
+
description: "Coarse document heat field (width, height, png) when the canvas is warm.",
|
|
16074
|
+
properties: {
|
|
16075
|
+
width: { type: "integer", minimum: 1, description: "Heat grid width in cells (1/4 canvas resolution)." },
|
|
16076
|
+
height: { type: "integer", minimum: 1, description: "Heat grid height in cells (1/4 canvas resolution)." },
|
|
16077
|
+
png: { type: "string", description: "Heat grid encoded as an RGBA PNG data URL." }
|
|
16078
|
+
},
|
|
16079
|
+
required: ["width", "height", "png"]
|
|
16080
|
+
},
|
|
16081
|
+
uvLight: {
|
|
16082
|
+
type: "object",
|
|
16083
|
+
description: "Virtual UV blacklight settings when non-default.",
|
|
16084
|
+
properties: {
|
|
16085
|
+
on: { type: "boolean", description: "Whether the UV blacklight is switched on." },
|
|
16086
|
+
strength: { type: "number", minimum: 0, maximum: 2, description: "UV emission and spill strength (0..2)." },
|
|
16087
|
+
ambient: { type: "number", minimum: 0, maximum: 1, description: "Remaining daylight ambient level (0..1)." }
|
|
16088
|
+
}
|
|
13614
16089
|
}
|
|
13615
16090
|
},
|
|
13616
16091
|
required: ["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId"]
|
|
@@ -13633,7 +16108,7 @@ function uiTables() {
|
|
|
13633
16108
|
brushOptions: BRUSH_OPTION_CONTROLS,
|
|
13634
16109
|
blendMenu: BLEND_MENU_GROUPS,
|
|
13635
16110
|
layerEffects: LAYER_EFFECT_MENU.map(({ kind, label }) => ({ kind, label, controls: layerEffectControls(defaultLayerEffect(kind, kind)) })),
|
|
13636
|
-
sliders: { paper: PAPER_SLIDERS, wet: WET_PARAM_SLIDERS, wetTilt: WET_TILT_SLIDERS, wetSpeed: WET_SPEED_SLIDER, light: LIGHT_SLIDERS, ghost: GHOST_SLIDERS, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS },
|
|
16111
|
+
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, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS },
|
|
13637
16112
|
pathStrokeTapers: PATH_STROKE_TAPERS,
|
|
13638
16113
|
padSampleSizes: PAD_SAMPLE_SIZES,
|
|
13639
16114
|
wetGpu: WET_GPU_OPTIONS,
|
|
@@ -13710,11 +16185,14 @@ export {
|
|
|
13710
16185
|
DEFAULT_DPI,
|
|
13711
16186
|
DEFAULT_DYNAMICS,
|
|
13712
16187
|
DEFAULT_FLOW,
|
|
16188
|
+
DEFAULT_FLUORESCENCE,
|
|
13713
16189
|
DEFAULT_GHOST,
|
|
13714
16190
|
DEFAULT_IMPASTO,
|
|
16191
|
+
DEFAULT_IRIDESCENCE,
|
|
13715
16192
|
DEFAULT_LIGHT,
|
|
13716
16193
|
DEFAULT_LIGHTING,
|
|
13717
16194
|
DEFAULT_MULTI_COLOR,
|
|
16195
|
+
DEFAULT_MUTATION,
|
|
13718
16196
|
DEFAULT_MUTATION_RATES,
|
|
13719
16197
|
DEFAULT_PAINT_GRADIENT,
|
|
13720
16198
|
DEFAULT_PAINT_GRADIENT_MAPPING,
|
|
@@ -13723,8 +16201,11 @@ export {
|
|
|
13723
16201
|
DEFAULT_SPRAY,
|
|
13724
16202
|
DEFAULT_STICKER,
|
|
13725
16203
|
DEFAULT_SWATCHES,
|
|
16204
|
+
DEFAULT_THERMOCHROMIC,
|
|
13726
16205
|
DEFAULT_TIP_GRAIN,
|
|
16206
|
+
DEFAULT_UV_LIGHT,
|
|
13727
16207
|
DEFAULT_VIEW,
|
|
16208
|
+
DEFAULT_VIEW_TILT,
|
|
13728
16209
|
DEFAULT_WET_BRUSH,
|
|
13729
16210
|
DEFAULT_WET_PARAMS,
|
|
13730
16211
|
DOT_ROTATIONS,
|
|
@@ -13737,15 +16218,22 @@ export {
|
|
|
13737
16218
|
EVOLUTION_BRUSH_FACETS,
|
|
13738
16219
|
EVOLUTION_MUTATION_SCHEMA,
|
|
13739
16220
|
EVOLUTION_SLIDERS,
|
|
16221
|
+
FilmBuffer,
|
|
13740
16222
|
GHOST_SLIDERS,
|
|
16223
|
+
GHOST_TRIGGERS,
|
|
13741
16224
|
GPU_LEAD,
|
|
13742
16225
|
GRAIN_PATTERNS,
|
|
13743
16226
|
GRAIN_TILE,
|
|
13744
16227
|
HARMONY_KINDS,
|
|
16228
|
+
HEAT_COOLING,
|
|
16229
|
+
HEAT_DIFFUSION,
|
|
16230
|
+
HEAT_DRY_THRESHOLD,
|
|
16231
|
+
HEAT_SCALE,
|
|
13745
16232
|
IDENTITY_AFFINE,
|
|
13746
16233
|
IDENTITY_COPY,
|
|
13747
16234
|
IMPASTO_TOOLS,
|
|
13748
16235
|
IMPASTO_TOOL_LABELS,
|
|
16236
|
+
IRIDESCENCE_SLIDERS,
|
|
13749
16237
|
LIGHT_SLIDERS,
|
|
13750
16238
|
LIVE_STROKE_ENGINES,
|
|
13751
16239
|
LitCache,
|
|
@@ -13764,6 +16252,7 @@ export {
|
|
|
13764
16252
|
MIXING_PAD_HEIGHT,
|
|
13765
16253
|
MIXING_PAD_WIDTH,
|
|
13766
16254
|
MULTI_COLOR_MODES,
|
|
16255
|
+
MUTATION_SLIDERS,
|
|
13767
16256
|
MixingPad,
|
|
13768
16257
|
PAD_SAMPLE_SIZES,
|
|
13769
16258
|
PAINTERLY_BLEND_MODES,
|
|
@@ -13836,9 +16325,13 @@ export {
|
|
|
13836
16325
|
StickerSpray,
|
|
13837
16326
|
TIP_GRAIN_MODES,
|
|
13838
16327
|
TRACING_NODE_ID,
|
|
16328
|
+
ThermoCache,
|
|
13839
16329
|
ThickStrokeDriver,
|
|
13840
16330
|
TileableNoise,
|
|
13841
16331
|
TipGrain,
|
|
16332
|
+
UV_LIGHT_SLIDERS,
|
|
16333
|
+
UvCache,
|
|
16334
|
+
VIEW_TILT_SLIDERS,
|
|
13842
16335
|
WET_FLOW_KINDS,
|
|
13843
16336
|
WET_GPU_MODES,
|
|
13844
16337
|
WET_GPU_OPTIONS,
|
|
@@ -13855,6 +16348,7 @@ export {
|
|
|
13855
16348
|
WetStrokeDriver,
|
|
13856
16349
|
addPaintGradientStop,
|
|
13857
16350
|
addPolygonArea,
|
|
16351
|
+
advanceHeatField,
|
|
13858
16352
|
affineCopy,
|
|
13859
16353
|
applyAffine,
|
|
13860
16354
|
backlightOf,
|
|
@@ -13866,11 +16360,16 @@ export {
|
|
|
13866
16360
|
buildTimeLapse,
|
|
13867
16361
|
checkEvolutionStrokes,
|
|
13868
16362
|
checkPaintEffectPlace,
|
|
16363
|
+
clearHeatField,
|
|
16364
|
+
cloneHeatField,
|
|
13869
16365
|
clonePaper,
|
|
13870
16366
|
colorHarmony2 as colorHarmony,
|
|
13871
16367
|
composeAffine,
|
|
16368
|
+
computeUvSpill,
|
|
13872
16369
|
contactSheet,
|
|
13873
16370
|
createEvolutionTools,
|
|
16371
|
+
createHeatField,
|
|
16372
|
+
createMutationTools,
|
|
13874
16373
|
createPaintAgentStrokes,
|
|
13875
16374
|
createPaintBrushLibraryToolProvider,
|
|
13876
16375
|
createPaintChangeFeed,
|
|
@@ -13899,6 +16398,7 @@ export {
|
|
|
13899
16398
|
describeGenomeDiff,
|
|
13900
16399
|
deserializePaintDocument,
|
|
13901
16400
|
deserializePaintDocumentAsync,
|
|
16401
|
+
deviceOrientationToViewTilt,
|
|
13902
16402
|
documentExtensions,
|
|
13903
16403
|
documentToScreen,
|
|
13904
16404
|
downloadBytes,
|
|
@@ -13912,19 +16412,30 @@ export {
|
|
|
13912
16412
|
effectPaintKey,
|
|
13913
16413
|
effectPaintSize,
|
|
13914
16414
|
emptiedLayers,
|
|
16415
|
+
emptyFluor,
|
|
16416
|
+
emptyThermo,
|
|
13915
16417
|
emptyThickChannels,
|
|
13916
16418
|
engineBrushSourceIds,
|
|
13917
16419
|
exportProposalContactSheetPng,
|
|
13918
16420
|
fillRegionMask,
|
|
16421
|
+
finalizeThermoStroke,
|
|
13919
16422
|
findUnfilledRegionsMask,
|
|
13920
16423
|
fitZoom,
|
|
16424
|
+
fluorFromJSON,
|
|
16425
|
+
fluorToJSON,
|
|
16426
|
+
fluorTwinBrush,
|
|
13921
16427
|
grainCatch,
|
|
13922
16428
|
grainHeights,
|
|
13923
16429
|
guideOverlay,
|
|
13924
16430
|
handlePaintShortcut,
|
|
16431
|
+
hasThickChannels,
|
|
13925
16432
|
hash2,
|
|
16433
|
+
heatFromJSON,
|
|
16434
|
+
heatStats,
|
|
16435
|
+
heatToJSON,
|
|
13926
16436
|
hexToPaintRgb,
|
|
13927
16437
|
hexToRgb8,
|
|
16438
|
+
hotCellsBounds,
|
|
13928
16439
|
hslToRgb,
|
|
13929
16440
|
hsvToRgb,
|
|
13930
16441
|
hueToWheel,
|
|
@@ -13943,6 +16454,7 @@ export {
|
|
|
13943
16454
|
liveStrokeBounds,
|
|
13944
16455
|
liveStrokeRefusal,
|
|
13945
16456
|
mapPenPoint,
|
|
16457
|
+
mergeFourChannel,
|
|
13946
16458
|
mix32,
|
|
13947
16459
|
mixColorOklch,
|
|
13948
16460
|
mixPixels,
|
|
@@ -13955,6 +16467,7 @@ export {
|
|
|
13955
16467
|
multiColorToPaint,
|
|
13956
16468
|
mutateGenome,
|
|
13957
16469
|
mutateStroke,
|
|
16470
|
+
mutationBrushRefusal,
|
|
13958
16471
|
newPaintLayerId,
|
|
13959
16472
|
nextPaperEffectVersion,
|
|
13960
16473
|
normalizeBehavior,
|
|
@@ -13963,12 +16476,14 @@ export {
|
|
|
13963
16476
|
normalizeCanvas,
|
|
13964
16477
|
normalizeDynamics,
|
|
13965
16478
|
normalizeFlow,
|
|
16479
|
+
normalizeFluorescence,
|
|
13966
16480
|
normalizeGhost,
|
|
13967
16481
|
normalizeHexColor,
|
|
13968
16482
|
normalizeImpasto,
|
|
13969
16483
|
normalizeLight,
|
|
13970
16484
|
normalizeLighting,
|
|
13971
16485
|
normalizeMultiColor,
|
|
16486
|
+
normalizeMutation,
|
|
13972
16487
|
normalizeMutationRates,
|
|
13973
16488
|
normalizePaintBrush,
|
|
13974
16489
|
normalizePaper,
|
|
@@ -13979,7 +16494,10 @@ export {
|
|
|
13979
16494
|
normalizeStickerSettings,
|
|
13980
16495
|
normalizeStickerUpdate,
|
|
13981
16496
|
normalizeSymmetry,
|
|
16497
|
+
normalizeThermochromic,
|
|
16498
|
+
normalizeUvLight,
|
|
13982
16499
|
normalizeView,
|
|
16500
|
+
normalizeViewTilt,
|
|
13983
16501
|
normalizeWetBrush,
|
|
13984
16502
|
normalizeWetParams,
|
|
13985
16503
|
overPixels,
|
|
@@ -13988,6 +16506,7 @@ export {
|
|
|
13988
16506
|
paintBrushPreviewRenderer,
|
|
13989
16507
|
paintContracts,
|
|
13990
16508
|
paintGradientMappingFor,
|
|
16509
|
+
paintHeatField,
|
|
13991
16510
|
paintPlaybackPanel,
|
|
13992
16511
|
paintProposalGhost,
|
|
13993
16512
|
paintShortcutFor,
|
|
@@ -14030,6 +16549,7 @@ export {
|
|
|
14030
16549
|
replayFrame,
|
|
14031
16550
|
replayItems,
|
|
14032
16551
|
replayPainting,
|
|
16552
|
+
requestDeviceTiltPermission,
|
|
14033
16553
|
rerollStrokeSeed,
|
|
14034
16554
|
resampleStrokePoints,
|
|
14035
16555
|
resolvePaintGradient,
|
|
@@ -14040,8 +16560,10 @@ export {
|
|
|
14040
16560
|
sampleArt,
|
|
14041
16561
|
sampleEffectAt,
|
|
14042
16562
|
sampleEffectField,
|
|
16563
|
+
sampleHeatAt,
|
|
14043
16564
|
samplePath,
|
|
14044
16565
|
samplePressure,
|
|
16566
|
+
scaleFluor,
|
|
14045
16567
|
scalePaintGradient,
|
|
14046
16568
|
scaleStencil,
|
|
14047
16569
|
screenToDocument,
|
|
@@ -14049,10 +16571,12 @@ export {
|
|
|
14049
16571
|
seededStrokeVariation,
|
|
14050
16572
|
serializePaintDocument,
|
|
14051
16573
|
serializePaintDocumentExternal,
|
|
16574
|
+
shiftHeatField,
|
|
14052
16575
|
solidPaint,
|
|
14053
16576
|
squaredDistanceTransform,
|
|
14054
16577
|
stampCoverage,
|
|
14055
16578
|
standardBlendMode,
|
|
16579
|
+
startDeviceTilt,
|
|
14056
16580
|
startWetClock,
|
|
14057
16581
|
stencilMask,
|
|
14058
16582
|
stencilOutline,
|
|
@@ -14070,8 +16594,12 @@ export {
|
|
|
14070
16594
|
symmetryAxisAngles,
|
|
14071
16595
|
symmetryAxisSegments,
|
|
14072
16596
|
symmetryCopies,
|
|
16597
|
+
thermoFromJSON,
|
|
16598
|
+
thermoToJSON,
|
|
16599
|
+
thermoTwinBrush,
|
|
14073
16600
|
thickFromJSON,
|
|
14074
16601
|
thickToJSON,
|
|
16602
|
+
thinFilmRgb,
|
|
14075
16603
|
thumbnailStroke,
|
|
14076
16604
|
tiltToOrientation,
|
|
14077
16605
|
timeLapseVideo,
|
|
@@ -14083,11 +16611,14 @@ export {
|
|
|
14083
16611
|
translateStencil,
|
|
14084
16612
|
updatePaintGradientStop,
|
|
14085
16613
|
usesPaintExchange,
|
|
16614
|
+
uvLightKey,
|
|
16615
|
+
uvPaper,
|
|
14086
16616
|
variationAffine,
|
|
14087
16617
|
varyStroke,
|
|
14088
16618
|
viewCssTransform,
|
|
14089
16619
|
viewMatrix,
|
|
14090
16620
|
viewPenToDocument,
|
|
16621
|
+
viewTiltKey,
|
|
14091
16622
|
wetEngineSummary,
|
|
14092
16623
|
wetPaperInput,
|
|
14093
16624
|
wetPigmentOf,
|