@pixerate/editor 0.255.0 → 0.256.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/paint-editor/collaboration-tools.d.cts +19 -0
- package/dist/paint-editor/collaboration-tools.d.ts +19 -0
- package/dist/paint-editor/collaboration.d.cts +180 -0
- package/dist/paint-editor/collaboration.d.ts +180 -0
- package/dist/paint-editor/controller.d.cts +54 -0
- package/dist/paint-editor/controller.d.ts +54 -0
- package/dist/paint-editor/index.cjs +738 -205
- package/dist/paint-editor/index.d.cts +2 -0
- package/dist/paint-editor/index.d.ts +2 -0
- package/dist/paint-editor/index.js +554 -21
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +31 -46
- package/skills/pixerate-paint-editor/references/painting-alongside.md +19 -0
|
@@ -370,8 +370,8 @@ function normalizePaper(raw = {}, base) {
|
|
|
370
370
|
...readOffset(r.offset, b.offset)
|
|
371
371
|
};
|
|
372
372
|
}
|
|
373
|
-
function hexToRgb8(
|
|
374
|
-
const n = parseInt(
|
|
373
|
+
function hexToRgb8(hex2) {
|
|
374
|
+
const n = parseInt(hex2.slice(1, 7), 16);
|
|
375
375
|
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
376
376
|
}
|
|
377
377
|
function renderPaper(paper, rect, grain = new PaperGrain(paper), light) {
|
|
@@ -1929,8 +1929,8 @@ function sampleArt(art, x, y, out) {
|
|
|
1929
1929
|
}
|
|
1930
1930
|
|
|
1931
1931
|
// src/paint-editor/stickers.ts
|
|
1932
|
-
function hexRgb(
|
|
1933
|
-
const n = parseInt(
|
|
1932
|
+
function hexRgb(hex2) {
|
|
1933
|
+
const n = parseInt(hex2.slice(1, 7), 16);
|
|
1934
1934
|
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
1935
1935
|
}
|
|
1936
1936
|
function stickerRect(s) {
|
|
@@ -2014,8 +2014,8 @@ function drawArtSticker(medium, s, art) {
|
|
|
2014
2014
|
medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
|
|
2015
2015
|
return rect;
|
|
2016
2016
|
}
|
|
2017
|
-
function jitterColor(
|
|
2018
|
-
const [r, g, b] = hexRgb(
|
|
2017
|
+
function jitterColor(hex2, amount, seed, i) {
|
|
2018
|
+
const [r, g, b] = hexRgb(hex2);
|
|
2019
2019
|
const lch = oklabToOklch(rgbToOklab({ r, g, b, a: 1 }));
|
|
2020
2020
|
const l = Math.min(1, Math.max(0, lch.l + (rand01(seed, i, 61) - 0.5) * amount * 0.3));
|
|
2021
2021
|
const h = lch.h + (rand01(seed, i, 63) - 0.5) * amount * 80;
|
|
@@ -2897,7 +2897,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2897
2897
|
const init = this.init;
|
|
2898
2898
|
const grey = Math.round(init.brush.light.amount * 255);
|
|
2899
2899
|
const g = grey / 255;
|
|
2900
|
-
const
|
|
2900
|
+
const hex2 = "#" + grey.toString(16).padStart(2, "0").repeat(3);
|
|
2901
2901
|
const host = {
|
|
2902
2902
|
update: () => {
|
|
2903
2903
|
},
|
|
@@ -2906,14 +2906,14 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2906
2906
|
}
|
|
2907
2907
|
};
|
|
2908
2908
|
const effect = init.effect ? { source: init.effect.source, pixels: { ...init.effect.pixels, data: greyPixels(init.effect.pixels.data, grey) } } : null;
|
|
2909
|
-
const paint = init.paint ? { ...init.paint, stops: init.paint.stops.map((st) => ({ ...st, color:
|
|
2909
|
+
const paint = init.paint ? { ...init.paint, stops: init.paint.stops.map((st) => ({ ...st, color: hex2 })) } : null;
|
|
2910
2910
|
return new _PaintStrokeSession(host, {
|
|
2911
2911
|
...init,
|
|
2912
2912
|
surface: lightToShadow(light),
|
|
2913
2913
|
lockTransparency: false,
|
|
2914
2914
|
brush: lightTwinBrush(init.brush),
|
|
2915
2915
|
color: [grey, grey, grey, 255],
|
|
2916
|
-
colorHex:
|
|
2916
|
+
colorHex: hex2,
|
|
2917
2917
|
secondary: [grey, grey, grey, 255],
|
|
2918
2918
|
inputFilter: void 0,
|
|
2919
2919
|
mixerLoad: { reservoir: init.mixerLoad.reservoir ? [g, g, g] : null, amount: init.mixerLoad.amount, pickup: null },
|
|
@@ -4461,8 +4461,8 @@ var PaintWetLayers = class {
|
|
|
4461
4461
|
this.gpu = null;
|
|
4462
4462
|
}
|
|
4463
4463
|
};
|
|
4464
|
-
function wetPigmentOf(
|
|
4465
|
-
const [r, g, b] = hexToRgb8(
|
|
4464
|
+
function wetPigmentOf(hex2, body) {
|
|
4465
|
+
const [r, g, b] = hexToRgb8(hex2);
|
|
4466
4466
|
return Array.from(pigmentOf([r / 255, g / 255, b / 255], body));
|
|
4467
4467
|
}
|
|
4468
4468
|
|
|
@@ -4934,8 +4934,8 @@ var MixingPad = class {
|
|
|
4934
4934
|
A += a;
|
|
4935
4935
|
}
|
|
4936
4936
|
if (A === 0) return null;
|
|
4937
|
-
const
|
|
4938
|
-
return `#${
|
|
4937
|
+
const hex2 = (v) => Math.round(v / A).toString(16).padStart(2, "0");
|
|
4938
|
+
return `#${hex2(R)}${hex2(G)}${hex2(B)}`;
|
|
4939
4939
|
}
|
|
4940
4940
|
clear() {
|
|
4941
4941
|
this.surface = emptySurface(4);
|
|
@@ -4996,8 +4996,8 @@ var MixingPad = class {
|
|
|
4996
4996
|
paints2.map((p) => p.parts)
|
|
4997
4997
|
);
|
|
4998
4998
|
const core = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: R * 1.1, hardness: 0.55 } }, PAD_BLOB);
|
|
4999
|
-
const
|
|
5000
|
-
this.run(core, [{ x: cx, y: cy }],
|
|
4999
|
+
const hex2 = "#" + mixed.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("");
|
|
5000
|
+
this.run(core, [{ x: cx, y: cy }], hex2);
|
|
5001
5001
|
return { color: this.pick(cx, cy, 2), bounds };
|
|
5002
5002
|
}
|
|
5003
5003
|
/** The pad's pixels as a PNG data URL (with alpha), for saving. */
|
|
@@ -5699,8 +5699,8 @@ function parseColor(value, what = "color") {
|
|
|
5699
5699
|
if (typeof value !== "string" || !HEX3.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
5700
5700
|
return value.trim().toLowerCase();
|
|
5701
5701
|
}
|
|
5702
|
-
function rgbaOf(
|
|
5703
|
-
const [r, g, b] = hexToRgb8(
|
|
5702
|
+
function rgbaOf(hex2) {
|
|
5703
|
+
const [r, g, b] = hexToRgb8(hex2);
|
|
5704
5704
|
return [r, g, b, 255];
|
|
5705
5705
|
}
|
|
5706
5706
|
function rgbToHex(r, g, b) {
|
|
@@ -5753,6 +5753,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5753
5753
|
stickerPreview = null;
|
|
5754
5754
|
encodeImage;
|
|
5755
5755
|
listeners = /* @__PURE__ */ new Set();
|
|
5756
|
+
commitListeners = /* @__PURE__ */ new Set();
|
|
5757
|
+
/** Origins set by `withOrigin`, innermost last. */
|
|
5758
|
+
origins = [];
|
|
5759
|
+
/** `group` in progress: commits fold into one step, recorded when it ends. */
|
|
5760
|
+
grouping = null;
|
|
5761
|
+
/** `previewPaint` in progress: nothing is emitted or recorded. */
|
|
5762
|
+
previewing = false;
|
|
5756
5763
|
version = 0;
|
|
5757
5764
|
cached = null;
|
|
5758
5765
|
session = null;
|
|
@@ -5860,7 +5867,125 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5860
5867
|
this.listeners.add(listener);
|
|
5861
5868
|
return () => this.listeners.delete(listener);
|
|
5862
5869
|
}
|
|
5870
|
+
/** Hears each undo step recorded, merged, undone or redone, with who made it. Returns an unsubscribe function. */
|
|
5871
|
+
onCommit(listener) {
|
|
5872
|
+
this.commitListeners.add(listener);
|
|
5873
|
+
return () => {
|
|
5874
|
+
this.commitListeners.delete(listener);
|
|
5875
|
+
};
|
|
5876
|
+
}
|
|
5877
|
+
notifyCommit(event) {
|
|
5878
|
+
if (this.previewing) return;
|
|
5879
|
+
for (const l of [...this.commitListeners]) l(event);
|
|
5880
|
+
}
|
|
5881
|
+
/** The origin steps are recorded with now: the innermost `withOrigin`, else `user`. */
|
|
5882
|
+
getOrigin() {
|
|
5883
|
+
return this.origins[this.origins.length - 1] ?? "user";
|
|
5884
|
+
}
|
|
5885
|
+
/**
|
|
5886
|
+
* Records the steps `fn` makes as made by `origin` (`agent:<id>` for an agent). When `fn` returns
|
|
5887
|
+
* a promise, the origin holds until it settles. Satisfies `createOriginMiddleware`'s history target,
|
|
5888
|
+
* so agent tool calls are recorded as the calling agent's.
|
|
5889
|
+
*/
|
|
5890
|
+
withOrigin(origin, fn) {
|
|
5891
|
+
this.origins.push(origin);
|
|
5892
|
+
let pending = false;
|
|
5893
|
+
const pop = () => {
|
|
5894
|
+
const i = this.origins.lastIndexOf(origin);
|
|
5895
|
+
if (i >= 0) this.origins.splice(i, 1);
|
|
5896
|
+
};
|
|
5897
|
+
try {
|
|
5898
|
+
const result = fn();
|
|
5899
|
+
if (result && typeof result.then === "function") {
|
|
5900
|
+
pending = true;
|
|
5901
|
+
return Promise.resolve(result).finally(pop);
|
|
5902
|
+
}
|
|
5903
|
+
return result;
|
|
5904
|
+
} finally {
|
|
5905
|
+
if (!pending) pop();
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
/**
|
|
5909
|
+
* Runs `fn` as one undo step named `label`: every change it makes (strokes, fills, new layers) is
|
|
5910
|
+
* undone together. Wet paint can't be grouped. Nested groups join the outer one. If `fn` throws,
|
|
5911
|
+
* its changes are put back and nothing is recorded.
|
|
5912
|
+
*/
|
|
5913
|
+
group(label, fn) {
|
|
5914
|
+
if (this.grouping) return fn();
|
|
5915
|
+
this.requireIdle("grouping changes");
|
|
5916
|
+
const before = this.doc;
|
|
5917
|
+
const revision = this.docRevision;
|
|
5918
|
+
this.grouping = { label, before, changed: false };
|
|
5919
|
+
let result;
|
|
5920
|
+
try {
|
|
5921
|
+
result = fn();
|
|
5922
|
+
} catch (e) {
|
|
5923
|
+
this.grouping = null;
|
|
5924
|
+
if (this.doc !== before) {
|
|
5925
|
+
this.doc = before;
|
|
5926
|
+
this.docRevision = revision + 1;
|
|
5927
|
+
this.emit();
|
|
5928
|
+
}
|
|
5929
|
+
throw e;
|
|
5930
|
+
}
|
|
5931
|
+
const g = this.grouping;
|
|
5932
|
+
this.grouping = null;
|
|
5933
|
+
if (g.changed && this.doc !== before) {
|
|
5934
|
+
this.undoStack.push({ label, origin: this.getOrigin(), before, after: this.doc });
|
|
5935
|
+
if (this.undoStack.length > this.historyLimit) this.undoStack.shift();
|
|
5936
|
+
this.redoStack = [];
|
|
5937
|
+
this.notifyCommit({ kind: "execute", label, origin: this.getOrigin() });
|
|
5938
|
+
this.emit();
|
|
5939
|
+
}
|
|
5940
|
+
return result;
|
|
5941
|
+
}
|
|
5942
|
+
/**
|
|
5943
|
+
* What `fn` would paint, without changing anything: it runs on a new empty layer (above the
|
|
5944
|
+
* active layer, another one, or at the bottom), and the layer's pixels and the picture with them
|
|
5945
|
+
* come back while the document, history and listeners stay as they were. `fn` gets that layer's id and must paint on it with dry brushes (strokes, replays and
|
|
5946
|
+
* fills); wet paint can't be previewed. For ghost paint an agent suggests (`createPaintProposals`).
|
|
5947
|
+
*/
|
|
5948
|
+
previewPaint(fn, placement = {}) {
|
|
5949
|
+
this.requireIdle("previewing");
|
|
5950
|
+
if (this.previewing) throw new PaintEditorError("A preview is already running.");
|
|
5951
|
+
const saved = {
|
|
5952
|
+
doc: this.doc,
|
|
5953
|
+
undo: [...this.undoStack],
|
|
5954
|
+
redo: this.redoStack,
|
|
5955
|
+
revision: this.docRevision,
|
|
5956
|
+
strokeCounter: this.strokeCounter,
|
|
5957
|
+
seedCounter: this.seedCounter,
|
|
5958
|
+
mixerLoad: this.mixerLoad
|
|
5959
|
+
};
|
|
5960
|
+
this.previewing = true;
|
|
5961
|
+
try {
|
|
5962
|
+
const layer = this.addLayer({ name: "Preview", ...placement.aboveId ? { aboveId: placement.aboveId } : {} });
|
|
5963
|
+
if (placement.bottom) this.moveLayer(layer.id, 0, null);
|
|
5964
|
+
fn(layer.id);
|
|
5965
|
+
if (this.wetOp || this.pendingWetEvents.length) throw new PaintEditorError("Wet paint can't be previewed; suggest it with a dry brush.");
|
|
5966
|
+
const found = findLayer(this.doc.layers, layer.id)?.layer;
|
|
5967
|
+
const surface = found && isPixelLayer(found) ? found.surface : null;
|
|
5968
|
+
const bounds = surface ? contentBounds(surface) : null;
|
|
5969
|
+
const box = bounds ? clipToCanvas(bounds, this.doc.canvas) : null;
|
|
5970
|
+
if (!surface || !box) return { bounds: null, pixels: new Uint8ClampedArray(0), composite: new Uint8ClampedArray(0) };
|
|
5971
|
+
const composite = this.exportCompositor.composite(this.renderLayers({ wet: "display" }), box, this.sources());
|
|
5972
|
+
return { bounds: box, pixels: readRegion(surface, box), composite };
|
|
5973
|
+
} finally {
|
|
5974
|
+
this.wetOp = null;
|
|
5975
|
+
this.pendingWetEvents = [];
|
|
5976
|
+
this.doc = saved.doc;
|
|
5977
|
+
this.undoStack = saved.undo;
|
|
5978
|
+
this.redoStack = saved.redo;
|
|
5979
|
+
this.docRevision = saved.revision;
|
|
5980
|
+
this.strokeCounter = saved.strokeCounter;
|
|
5981
|
+
this.seedCounter = saved.seedCounter;
|
|
5982
|
+
this.mixerLoad = saved.mixerLoad;
|
|
5983
|
+
this.cached = null;
|
|
5984
|
+
this.previewing = false;
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5863
5987
|
emit(dirty = "all") {
|
|
5988
|
+
if (this.previewing) return;
|
|
5864
5989
|
if (dirty === "all" || this.dirty === "all") this.dirty = "all";
|
|
5865
5990
|
else this.dirty.push(...dirty);
|
|
5866
5991
|
this.version++;
|
|
@@ -5943,7 +6068,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5943
6068
|
if (this.session) throw new PaintEditorError(`A stroke is in progress; end or cancel it before ${what}.`);
|
|
5944
6069
|
}
|
|
5945
6070
|
commit(label, next, before = this.doc) {
|
|
5946
|
-
|
|
6071
|
+
if (this.grouping) {
|
|
6072
|
+
this.grouping.changed = true;
|
|
6073
|
+
this.doc = next;
|
|
6074
|
+
this.docRevision++;
|
|
6075
|
+
return;
|
|
6076
|
+
}
|
|
6077
|
+
const entry = { label, origin: this.getOrigin(), before, after: next };
|
|
5947
6078
|
if (this.wetOp) {
|
|
5948
6079
|
entry.wet = { before: this.wetOp.before, after: this.wet.snapshot(), lost: false };
|
|
5949
6080
|
this.wetOp = null;
|
|
@@ -5954,6 +6085,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5954
6085
|
this.doc = next;
|
|
5955
6086
|
this.docRevision++;
|
|
5956
6087
|
this.trimWetHistory();
|
|
6088
|
+
this.notifyCommit({ kind: "execute", label, origin: entry.origin });
|
|
5957
6089
|
this.emit();
|
|
5958
6090
|
}
|
|
5959
6091
|
/**
|
|
@@ -5962,7 +6094,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5962
6094
|
*/
|
|
5963
6095
|
commitOrAmend(label, next, commitToHistory) {
|
|
5964
6096
|
const last = this.undoStack[this.undoStack.length - 1];
|
|
5965
|
-
if (commitToHistory || !last || last.label !== label || last.wet || this.wetOp) {
|
|
6097
|
+
if (commitToHistory || !last || last.label !== label || last.wet || this.wetOp || this.grouping) {
|
|
5966
6098
|
this.commit(label, next);
|
|
5967
6099
|
return;
|
|
5968
6100
|
}
|
|
@@ -5970,6 +6102,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5970
6102
|
this.redoStack = [];
|
|
5971
6103
|
this.doc = next;
|
|
5972
6104
|
this.docRevision++;
|
|
6105
|
+
this.notifyCommit({ kind: "merge", label, origin: last.origin });
|
|
5973
6106
|
this.emit();
|
|
5974
6107
|
}
|
|
5975
6108
|
/** Keeps the wet snapshots in history within the wet undo memory, dropping the oldest. */
|
|
@@ -6039,6 +6172,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6039
6172
|
this.wet.restore(e.wet.lost ? null : e.wet.before);
|
|
6040
6173
|
if (paper !== this.doc.paper) this.wet.paperChanged();
|
|
6041
6174
|
}
|
|
6175
|
+
this.notifyCommit({ kind: "undo", label: e.label, origin: this.getOrigin() });
|
|
6042
6176
|
this.emit();
|
|
6043
6177
|
return e.label;
|
|
6044
6178
|
}
|
|
@@ -6057,6 +6191,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6057
6191
|
this.wet.restore(e.wet.lost ? null : e.wet.after);
|
|
6058
6192
|
if (paper !== this.doc.paper) this.wet.paperChanged();
|
|
6059
6193
|
}
|
|
6194
|
+
this.notifyCommit({ kind: "redo", label: e.label, origin: this.getOrigin() });
|
|
6060
6195
|
this.emit();
|
|
6061
6196
|
return e.label;
|
|
6062
6197
|
}
|
|
@@ -7263,6 +7398,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7263
7398
|
return l;
|
|
7264
7399
|
})();
|
|
7265
7400
|
const brush = this.strokeBrush(init);
|
|
7401
|
+
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.`);
|
|
7266
7402
|
const effect = this.strokeEffect(init, brush);
|
|
7267
7403
|
if (brush.light.amount > 0) {
|
|
7268
7404
|
if (brush.engine === "sticker") throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take light yet; paint light with a dab or particle light brush (glow-gel, neon-marker; list_brush_presets lists them), or set light.amount to 0 (set_brush).`);
|
|
@@ -9566,6 +9702,13 @@ function resampleInto(src, rect, width, height) {
|
|
|
9566
9702
|
}
|
|
9567
9703
|
return { width, height, data: out };
|
|
9568
9704
|
}
|
|
9705
|
+
function clipToCanvas(rect, canvas) {
|
|
9706
|
+
const x0 = Math.max(0, rect.x);
|
|
9707
|
+
const y0 = Math.max(0, rect.y);
|
|
9708
|
+
const x1 = Math.min(canvas.width, rect.x + rect.width);
|
|
9709
|
+
const y1 = Math.min(canvas.height, rect.y + rect.height);
|
|
9710
|
+
return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : null;
|
|
9711
|
+
}
|
|
9569
9712
|
|
|
9570
9713
|
// src/paint-editor/presence.ts
|
|
9571
9714
|
var PaintPresence = class extends PresenceStore {
|
|
@@ -10786,8 +10929,8 @@ var POINTS = {
|
|
|
10786
10929
|
}
|
|
10787
10930
|
};
|
|
10788
10931
|
function loadResult(load) {
|
|
10789
|
-
const
|
|
10790
|
-
return { reservoir:
|
|
10932
|
+
const hex2 = (c) => c ? "#" + c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("") : null;
|
|
10933
|
+
return { reservoir: hex2(load.reservoir), amount: Math.round(load.amount * 1e3) / 1e3, pickup: hex2(load.pickup) };
|
|
10791
10934
|
}
|
|
10792
10935
|
function createPaintEditorToolProvider(controller, options = {}) {
|
|
10793
10936
|
const c = typeof controller === "function" ? lazyController(controller) : controller;
|
|
@@ -12405,6 +12548,392 @@ function createPaintEditorDocument(controller, options = {}) {
|
|
|
12405
12548
|
readOnly: () => controller.busyReason() ?? false
|
|
12406
12549
|
});
|
|
12407
12550
|
}
|
|
12551
|
+
|
|
12552
|
+
// src/paint-editor/collaboration.ts
|
|
12553
|
+
var LAYER_FIELDS = ["name", "visible", "opacity", "blend"];
|
|
12554
|
+
function createPaintChangeFeed(controller, options = {}) {
|
|
12555
|
+
const now = options.now ?? (() => Date.now());
|
|
12556
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
12557
|
+
let seq = 0;
|
|
12558
|
+
let pending;
|
|
12559
|
+
const take = () => {
|
|
12560
|
+
const state = controller.getState();
|
|
12561
|
+
const doc = state.document;
|
|
12562
|
+
const layers = /* @__PURE__ */ new Map();
|
|
12563
|
+
for (const l of controller.listLayers()) layers.set(l.id, { name: l.name, visible: l.visible, opacity: l.opacity, blend: l.blend });
|
|
12564
|
+
return { doc, strokes: new Map(doc.strokes.map((s) => [s.id, s])), events: doc.paintEvents?.length ?? 0, layers, stroking: state.stroking };
|
|
12565
|
+
};
|
|
12566
|
+
let last = take();
|
|
12567
|
+
const check = () => {
|
|
12568
|
+
const state = controller.getState();
|
|
12569
|
+
if (state.document === last.doc && state.stroking === last.stroking) return null;
|
|
12570
|
+
const next = take();
|
|
12571
|
+
const addedStrokes = [...next.strokes.values()].filter((s) => !last.strokes.has(s.id));
|
|
12572
|
+
const removedStrokes = [...last.strokes.values()].filter((s) => !next.strokes.has(s.id));
|
|
12573
|
+
const allEvents = state.document.paintEvents ?? [];
|
|
12574
|
+
const events = next.events > last.events ? allEvents.slice(last.events) : [];
|
|
12575
|
+
const addedLayers = [];
|
|
12576
|
+
const changedLayers = [];
|
|
12577
|
+
for (const [id, l] of next.layers) {
|
|
12578
|
+
const before = last.layers.get(id);
|
|
12579
|
+
if (!before) addedLayers.push({ id, name: String(l.name) });
|
|
12580
|
+
else {
|
|
12581
|
+
const fields = LAYER_FIELDS.filter((f) => before[f] !== l[f]);
|
|
12582
|
+
if (fields.length) changedLayers.push({ id, name: String(l.name), fields });
|
|
12583
|
+
}
|
|
12584
|
+
}
|
|
12585
|
+
const removedLayers = [...last.layers].filter(([id]) => !next.layers.has(id)).map(([id, l]) => ({ id, name: String(l.name) }));
|
|
12586
|
+
const gestureStarted = next.stroking && !last.stroking;
|
|
12587
|
+
const gestureEnded = !next.stroking && last.stroking;
|
|
12588
|
+
last = next;
|
|
12589
|
+
const edited = addedStrokes.length + removedStrokes.length + events.length + addedLayers.length + removedLayers.length + changedLayers.length > 0;
|
|
12590
|
+
if (!edited && !gestureStarted && !gestureEnded) return null;
|
|
12591
|
+
const commit = edited ? pending : void 0;
|
|
12592
|
+
if (edited) pending = void 0;
|
|
12593
|
+
const change = {
|
|
12594
|
+
seq: ++seq,
|
|
12595
|
+
origin: commit?.origin ?? "user",
|
|
12596
|
+
...commit ? { commit } : {},
|
|
12597
|
+
addedStrokes,
|
|
12598
|
+
removedStrokes,
|
|
12599
|
+
events,
|
|
12600
|
+
addedLayers,
|
|
12601
|
+
removedLayers,
|
|
12602
|
+
changedLayers,
|
|
12603
|
+
inGesture: next.stroking,
|
|
12604
|
+
...gestureStarted ? { gestureStarted: true } : {},
|
|
12605
|
+
...gestureEnded ? { gestureEnded: true } : {},
|
|
12606
|
+
at: now()
|
|
12607
|
+
};
|
|
12608
|
+
listeners.forEach((l) => l(change));
|
|
12609
|
+
return change;
|
|
12610
|
+
};
|
|
12611
|
+
const offCommit = controller.onCommit((event) => {
|
|
12612
|
+
pending = event;
|
|
12613
|
+
});
|
|
12614
|
+
const offState = controller.subscribe(() => {
|
|
12615
|
+
check();
|
|
12616
|
+
});
|
|
12617
|
+
return {
|
|
12618
|
+
check,
|
|
12619
|
+
subscribe(listener) {
|
|
12620
|
+
listeners.add(listener);
|
|
12621
|
+
return () => {
|
|
12622
|
+
listeners.delete(listener);
|
|
12623
|
+
};
|
|
12624
|
+
},
|
|
12625
|
+
reset() {
|
|
12626
|
+
last = take();
|
|
12627
|
+
pending = void 0;
|
|
12628
|
+
},
|
|
12629
|
+
dispose() {
|
|
12630
|
+
offCommit();
|
|
12631
|
+
offState();
|
|
12632
|
+
listeners.clear();
|
|
12633
|
+
}
|
|
12634
|
+
};
|
|
12635
|
+
}
|
|
12636
|
+
var HEX4 = /^#[0-9a-f]{6}$/i;
|
|
12637
|
+
var DRY_ENGINES = /* @__PURE__ */ new Set(["dab", "particle", "bristle", "mixer"]);
|
|
12638
|
+
function suggestionBrushRefusal(brush) {
|
|
12639
|
+
if (!DRY_ENGINES.has(brush.engine)) return `The ${brush.name} brush (${brush.engine}) can't paint a suggestion: suggestions are dry paint on a layer of their own. Use a dab, particle, bristle or mixer brush.`;
|
|
12640
|
+
if (brush.behavior.pickup > 0) return `The ${brush.name} brush picks up paint, which a suggestion on its own layer can't; choose a brush without pickup.`;
|
|
12641
|
+
return null;
|
|
12642
|
+
}
|
|
12643
|
+
function createPaintProposals(options) {
|
|
12644
|
+
const { controller } = options;
|
|
12645
|
+
const now = options.now ?? (() => Date.now());
|
|
12646
|
+
const maxPoints = options.maxPoints ?? 4e3;
|
|
12647
|
+
let seq = 0;
|
|
12648
|
+
const makeId = options.idGenerator ?? (() => `paint-proposal-${++seq}`);
|
|
12649
|
+
let state = { proposals: [] };
|
|
12650
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
12651
|
+
const set = (proposals) => {
|
|
12652
|
+
state = { proposals };
|
|
12653
|
+
listeners.forEach((l) => l(state));
|
|
12654
|
+
};
|
|
12655
|
+
const find = (id) => {
|
|
12656
|
+
const p = state.proposals.find((x) => x.id === id);
|
|
12657
|
+
if (!p) throw new PaintEditorError(`No pending paint proposal "${id}". Call list_paint_proposals for the ids.`);
|
|
12658
|
+
return p;
|
|
12659
|
+
};
|
|
12660
|
+
const strokeIds = () => new Set(controller.getStrokes().map((s) => s.id));
|
|
12661
|
+
const validate = (input) => {
|
|
12662
|
+
if (!input.title?.trim()) throw new PaintEditorError("A proposal needs a title: what it adds, in a few words.");
|
|
12663
|
+
if (!Array.isArray(input.ops) || !input.ops.length) throw new PaintEditorError("A proposal needs at least one op (stroke, wash or echo).");
|
|
12664
|
+
const place = input.place ?? "top";
|
|
12665
|
+
if (place === "above") {
|
|
12666
|
+
if (!input.layerId) throw new PaintEditorError('place "above" needs layerId: the layer to put the suggestion above (list_layers).');
|
|
12667
|
+
if (!controller.listLayers().some((l) => l.id === input.layerId)) throw new PaintEditorError(`Unknown layer "${input.layerId}". Call list_layers for the ids.`);
|
|
12668
|
+
}
|
|
12669
|
+
const known = strokeIds();
|
|
12670
|
+
let points = 0;
|
|
12671
|
+
input.ops.forEach((op, i) => {
|
|
12672
|
+
const at = `ops[${i}]`;
|
|
12673
|
+
if (op.kind === "stroke") {
|
|
12674
|
+
if (!op.points?.length) throw new PaintEditorError(`${at}: a stroke needs points in document pixels.`);
|
|
12675
|
+
points += op.points.length;
|
|
12676
|
+
if (op.color !== void 0 && !HEX4.test(op.color)) throw new PaintEditorError(`${at}: color "${op.color}" is not #rrggbb.`);
|
|
12677
|
+
const brush = op.brushId ? controller.getBrush(op.brushId) : controller.getState().brush;
|
|
12678
|
+
if (!brush) throw new PaintEditorError(`${at}: unknown brush "${op.brushId}". Call list_brush_presets for the ids.`);
|
|
12679
|
+
const refusal = suggestionBrushRefusal(brush);
|
|
12680
|
+
if (refusal) throw new PaintEditorError(`${at}: ${refusal}`);
|
|
12681
|
+
if (op.size !== void 0 && !(op.size > 0)) throw new PaintEditorError(`${at}: size must be above 0 (document pixels).`);
|
|
12682
|
+
} else if (op.kind === "wash") {
|
|
12683
|
+
if (!HEX4.test(op.color ?? "")) throw new PaintEditorError(`${at}: a wash needs color as #rrggbb.`);
|
|
12684
|
+
if (op.opacity !== void 0 && !(op.opacity > 0 && op.opacity <= 1)) throw new PaintEditorError(`${at}: opacity must be in (0, 1].`);
|
|
12685
|
+
} else if (op.kind === "echo") {
|
|
12686
|
+
if (!op.strokeIds?.length) throw new PaintEditorError(`${at}: an echo needs strokeIds (list_strokes).`);
|
|
12687
|
+
const unknown = op.strokeIds.filter((id) => !known.has(id));
|
|
12688
|
+
if (unknown.length) throw new PaintEditorError(`${at}: unknown stroke ${unknown.map((u) => `"${u}"`).join(", ")}. Call list_strokes for the ids.`);
|
|
12689
|
+
if (op.color !== void 0 && !HEX4.test(op.color)) throw new PaintEditorError(`${at}: color "${op.color}" is not #rrggbb.`);
|
|
12690
|
+
if (op.repeat !== void 0 && !(Number.isInteger(op.repeat) && op.repeat >= 1 && op.repeat <= 50)) throw new PaintEditorError(`${at}: repeat must be a whole number from 1 to 50.`);
|
|
12691
|
+
for (const id of op.strokeIds) {
|
|
12692
|
+
const stroke = controller.getStrokes().find((s) => s.id === id);
|
|
12693
|
+
const refusal = suggestionBrushRefusal(stroke.brush);
|
|
12694
|
+
if (refusal) throw new PaintEditorError(`${at}: stroke "${id}": ${refusal}`);
|
|
12695
|
+
points += stroke.points.length * (op.repeat ?? 1);
|
|
12696
|
+
}
|
|
12697
|
+
} else {
|
|
12698
|
+
throw new PaintEditorError(`${at}: unknown kind "${op.kind}"; use stroke, wash or echo.`);
|
|
12699
|
+
}
|
|
12700
|
+
});
|
|
12701
|
+
if (points > maxPoints) throw new PaintEditorError(`The proposal paints ${points} points; the most is ${maxPoints}. Suggest less, or echo fewer strokes.`);
|
|
12702
|
+
};
|
|
12703
|
+
const paint = (ops, layerId) => {
|
|
12704
|
+
const before = strokeIds();
|
|
12705
|
+
for (const op of ops) {
|
|
12706
|
+
if (op.kind === "stroke") {
|
|
12707
|
+
controller.paintStroke({
|
|
12708
|
+
layerId,
|
|
12709
|
+
points: op.points,
|
|
12710
|
+
...op.brushId ? { brushId: op.brushId } : {},
|
|
12711
|
+
...op.color ? { color: op.color } : {},
|
|
12712
|
+
...op.size ? { brush: { tip: { size: op.size } } } : {},
|
|
12713
|
+
stencils: false
|
|
12714
|
+
});
|
|
12715
|
+
} else if (op.kind === "wash") {
|
|
12716
|
+
controller.fillLayer({ layerId, color: op.color, opacity: op.opacity ?? 0.35, grain: op.grain ?? 0.4 });
|
|
12717
|
+
} else {
|
|
12718
|
+
const { kind: _kind, strokeIds: ids, color, repeat, ...variation } = op;
|
|
12719
|
+
controller.replayStrokes({ strokeIds: ids, layerId, ...color ? { color } : {}, ...repeat ? { repeat } : {}, ...variation });
|
|
12720
|
+
}
|
|
12721
|
+
}
|
|
12722
|
+
return controller.getStrokes().map((s) => s.id).filter((id) => !before.has(id));
|
|
12723
|
+
};
|
|
12724
|
+
const missing = (id) => {
|
|
12725
|
+
const p = state.proposals.find((x) => x.id === id);
|
|
12726
|
+
if (!p) return [];
|
|
12727
|
+
const known = strokeIds();
|
|
12728
|
+
return p.anchors.filter((a) => !known.has(a));
|
|
12729
|
+
};
|
|
12730
|
+
const placementOf = (input) => ({
|
|
12731
|
+
place: input.place ?? "top",
|
|
12732
|
+
...input.layerId ? { layerId: input.layerId } : {},
|
|
12733
|
+
...input.layerName ? { layerName: input.layerName } : {}
|
|
12734
|
+
});
|
|
12735
|
+
const topLayerId = () => controller.listLayers().find((l) => l.parentId === null)?.id;
|
|
12736
|
+
return {
|
|
12737
|
+
getState: () => state,
|
|
12738
|
+
subscribe(listener) {
|
|
12739
|
+
listeners.add(listener);
|
|
12740
|
+
return () => {
|
|
12741
|
+
listeners.delete(listener);
|
|
12742
|
+
};
|
|
12743
|
+
},
|
|
12744
|
+
propose(input) {
|
|
12745
|
+
validate(input);
|
|
12746
|
+
const placement = placementOf(input);
|
|
12747
|
+
const preview = controller.previewPaint(
|
|
12748
|
+
(layerId) => {
|
|
12749
|
+
paint(input.ops, layerId);
|
|
12750
|
+
},
|
|
12751
|
+
placement.place === "bottom" ? { bottom: true } : { aboveId: placement.place === "above" ? placement.layerId : topLayerId() }
|
|
12752
|
+
);
|
|
12753
|
+
const proposal = {
|
|
12754
|
+
id: makeId(),
|
|
12755
|
+
title: input.title.trim().slice(0, 120),
|
|
12756
|
+
...input.rationale ? { rationale: input.rationale } : {},
|
|
12757
|
+
origin: input.origin ?? options.origin ?? "agent:anonymous",
|
|
12758
|
+
ops: input.ops.map((op) => ({ ...op })),
|
|
12759
|
+
placement,
|
|
12760
|
+
bounds: preview.bounds,
|
|
12761
|
+
ghost: preview.composite,
|
|
12762
|
+
anchors: [...new Set(input.ops.flatMap((op) => op.kind === "echo" ? op.strokeIds : []))],
|
|
12763
|
+
createdAt: now()
|
|
12764
|
+
};
|
|
12765
|
+
set([...state.proposals, proposal]);
|
|
12766
|
+
return proposal;
|
|
12767
|
+
},
|
|
12768
|
+
accept(id) {
|
|
12769
|
+
const p = find(id);
|
|
12770
|
+
const gone = missing(id);
|
|
12771
|
+
if (gone.length) throw new PaintEditorError(`Paint proposal "${p.title}" echoes strokes that are no longer on the canvas (${gone.join(", ")}); dismiss it or propose it again.`);
|
|
12772
|
+
const placement = p.placement;
|
|
12773
|
+
if (placement.place === "above" && !controller.listLayers().some((l) => l.id === placement.layerId)) {
|
|
12774
|
+
throw new PaintEditorError(`Paint proposal "${p.title}" goes above layer "${placement.layerId}", which is gone; dismiss it or propose it again.`);
|
|
12775
|
+
}
|
|
12776
|
+
const active = controller.getState().document.activeLayerId;
|
|
12777
|
+
const result = controller.withOrigin(
|
|
12778
|
+
p.origin,
|
|
12779
|
+
() => controller.group(p.title, () => {
|
|
12780
|
+
const layer = controller.addLayer({ name: placement.layerName ?? p.title, ...placement.place === "above" ? { aboveId: placement.layerId } : placement.place === "top" ? { aboveId: topLayerId() } : {} });
|
|
12781
|
+
if (placement.place === "bottom") controller.moveLayer(layer.id, 0, null);
|
|
12782
|
+
return { layerId: layer.id, strokeIds: paint(p.ops, layer.id) };
|
|
12783
|
+
})
|
|
12784
|
+
);
|
|
12785
|
+
if (active && controller.listLayers().some((l) => l.id === active)) controller.setActiveLayer(active);
|
|
12786
|
+
set(state.proposals.filter((x) => x.id !== id));
|
|
12787
|
+
return { proposalId: id, layerId: result.layerId, strokeIds: result.strokeIds, label: p.title };
|
|
12788
|
+
},
|
|
12789
|
+
reject(id) {
|
|
12790
|
+
const p = find(id);
|
|
12791
|
+
set(state.proposals.filter((x) => x.id !== id));
|
|
12792
|
+
return p;
|
|
12793
|
+
},
|
|
12794
|
+
clear(origin) {
|
|
12795
|
+
const dropped = state.proposals.filter((p) => !origin || p.origin === origin);
|
|
12796
|
+
if (dropped.length) set(state.proposals.filter((p) => !dropped.includes(p)));
|
|
12797
|
+
return dropped.map((p) => p.id);
|
|
12798
|
+
},
|
|
12799
|
+
get: (id) => state.proposals.find((p) => p.id === id),
|
|
12800
|
+
missing
|
|
12801
|
+
};
|
|
12802
|
+
}
|
|
12803
|
+
function paintProposalGhost(proposal) {
|
|
12804
|
+
const b = proposal.bounds;
|
|
12805
|
+
if (!b || !b.width || !b.height || proposal.ghost.length !== b.width * b.height * 4) return null;
|
|
12806
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height, data: proposal.ghost };
|
|
12807
|
+
}
|
|
12808
|
+
|
|
12809
|
+
// src/paint-editor/collaboration-tools.ts
|
|
12810
|
+
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
12811
|
+
var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
12812
|
+
var proposalIdArg = {
|
|
12813
|
+
type: "object",
|
|
12814
|
+
properties: { proposalId: { type: "string", minLength: 1, description: "Id of a pending paint proposal, from propose_paint or list_paint_proposals." } },
|
|
12815
|
+
required: ["proposalId"],
|
|
12816
|
+
additionalProperties: false
|
|
12817
|
+
};
|
|
12818
|
+
var hex = { type: "string", pattern: "^#[0-9a-fA-F]{6}$" };
|
|
12819
|
+
var opSchema = {
|
|
12820
|
+
type: "object",
|
|
12821
|
+
description: `One thing to paint. kind "stroke": a stroke along points with a dry brush. kind "wash": an even wash over the suggestion's layer (a ground, a glaze). kind "echo": strokes already on the canvas painted again, moved, turned, scaled or mirrored (grass along a horizon, rain, hatching).`,
|
|
12822
|
+
properties: {
|
|
12823
|
+
kind: { type: "string", enum: ["stroke", "wash", "echo"], description: "What this op paints (see above)." },
|
|
12824
|
+
points: {
|
|
12825
|
+
type: "array",
|
|
12826
|
+
minItems: 1,
|
|
12827
|
+
description: "stroke: pen samples in document pixels, in drawing order.",
|
|
12828
|
+
items: {
|
|
12829
|
+
type: "object",
|
|
12830
|
+
properties: {
|
|
12831
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
12832
|
+
y: { type: "number", description: "Document y in pixels." },
|
|
12833
|
+
pressure: { type: "number", minimum: 0, maximum: 1, description: "Pen pressure 0..1 (default 0.5)." }
|
|
12834
|
+
},
|
|
12835
|
+
required: ["x", "y"],
|
|
12836
|
+
additionalProperties: false
|
|
12837
|
+
}
|
|
12838
|
+
},
|
|
12839
|
+
brushId: { type: "string", description: "stroke: a dry brush id from list_brush_presets (dab, particle, bristle or mixer without pickup). Default the editor brush." },
|
|
12840
|
+
size: { type: "number", exclusiveMinimum: 0, description: "stroke: brush size in document pixels. Default the brush's." },
|
|
12841
|
+
color: { ...hex, description: "stroke and echo: #rrggbb (stroke default the editor colour; echo default each stroke's colour). wash: required." },
|
|
12842
|
+
opacity: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "wash: 0..1 (default 0.35)." },
|
|
12843
|
+
grain: { type: "number", minimum: 0, maximum: 1, description: "wash: how much it catches the paper grain, 0..1 (default 0.4)." },
|
|
12844
|
+
strokeIds: { type: "array", minItems: 1, items: { type: "string" }, description: "echo: ids of strokes on the canvas (list_strokes)." },
|
|
12845
|
+
dx: { type: "number", description: "echo: move right by this many document pixels (default 0)." },
|
|
12846
|
+
dy: { type: "number", description: "echo: move down by this many document pixels (default 0)." },
|
|
12847
|
+
rotate: { type: "number", description: "echo: turn this many degrees clockwise about the strokes' centre (default 0)." },
|
|
12848
|
+
scale: { type: "number", exclusiveMinimum: 0, description: "echo: scale about the centre; brush size scales too (default 1)." },
|
|
12849
|
+
flipX: { type: "boolean", description: "echo: mirror horizontally about the centre (default false)." },
|
|
12850
|
+
repeat: { type: "integer", minimum: 1, maximum: 50, description: "echo: repeat the move this many times, each on top of the last (default 1)." }
|
|
12851
|
+
},
|
|
12852
|
+
required: ["kind"],
|
|
12853
|
+
additionalProperties: false
|
|
12854
|
+
};
|
|
12855
|
+
function createPaintProposalToolProvider(proposals, options = {}) {
|
|
12856
|
+
const readOnlyReason = () => {
|
|
12857
|
+
const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
|
|
12858
|
+
return ro ? typeof ro === "string" ? ro : "The painting is read-only: paint proposals can be made and rejected but not accepted." : null;
|
|
12859
|
+
};
|
|
12860
|
+
const describe = (p) => ({
|
|
12861
|
+
id: p.id,
|
|
12862
|
+
title: p.title,
|
|
12863
|
+
...p.rationale ? { rationale: p.rationale } : {},
|
|
12864
|
+
origin: p.origin,
|
|
12865
|
+
ops: p.ops.map((op) => op.kind),
|
|
12866
|
+
placement: p.placement,
|
|
12867
|
+
bounds: p.bounds,
|
|
12868
|
+
anchors: p.anchors,
|
|
12869
|
+
missing: proposals.missing(p.id),
|
|
12870
|
+
createdAt: new Date(p.createdAt).toISOString()
|
|
12871
|
+
});
|
|
12872
|
+
const proposeTool = {
|
|
12873
|
+
name: "propose_paint",
|
|
12874
|
+
title: "Propose paint",
|
|
12875
|
+
description: "Suggests paint without painting it: the user sees it as a ghost over the canvas and accepts or dismisses it, so use it for contributions they did not ask for. Accepted, it is painted on a new layer of its own (on top by default, at the bottom for a ground, or above a layer) as one undo step recorded as yours. Prefer what agents do well: a wash or ground, an echo of the user's strokes, a few deliberate strokes. Dry brushes only. Returns the proposal id, where the ghost sits (bounds, document pixels) and the strokes it builds on. Follow with accept_paint_proposal only when the user asked for it.",
|
|
12876
|
+
inputSchema: {
|
|
12877
|
+
type: "object",
|
|
12878
|
+
properties: {
|
|
12879
|
+
title: { type: "string", minLength: 1, maxLength: 120, description: `What the suggestion adds, in a few words, e.g. "Warm ground behind the figure". The undo step's name once accepted.` },
|
|
12880
|
+
rationale: { type: "string", description: "Why it fits what the user is painting, in one sentence. Shown beside the ghost." },
|
|
12881
|
+
ops: { type: "array", minItems: 1, items: opSchema, description: "What to paint, in order, onto the suggestion's layer." },
|
|
12882
|
+
place: { type: "string", enum: ["top", "bottom", "above"], description: "Where the new layer goes: top (default), bottom (behind everything: grounds and washes), or above layerId." },
|
|
12883
|
+
layerId: { type: "string", description: 'With place "above": the layer to put it above (list_layers).' },
|
|
12884
|
+
layerName: { type: "string", description: "The new layer's name (default the title)." }
|
|
12885
|
+
},
|
|
12886
|
+
required: ["title", "ops"],
|
|
12887
|
+
additionalProperties: false
|
|
12888
|
+
},
|
|
12889
|
+
annotations: { title: "Propose paint", ...WRITE6 },
|
|
12890
|
+
handler: (args, context) => {
|
|
12891
|
+
const origin = context.actor?.id ? `agent:${context.actor.id}` : options.fallbackOrigin;
|
|
12892
|
+
const proposal = proposals.propose({ ...args, ...origin ? { origin } : {} });
|
|
12893
|
+
const { id: _id, missing: _missing, createdAt: _createdAt, ...rest } = describe(proposal);
|
|
12894
|
+
return { proposalId: proposal.id, ...rest };
|
|
12895
|
+
}
|
|
12896
|
+
};
|
|
12897
|
+
const listTool = {
|
|
12898
|
+
name: "list_paint_proposals",
|
|
12899
|
+
title: "List paint proposals",
|
|
12900
|
+
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.",
|
|
12901
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
12902
|
+
annotations: { title: "List paint proposals", ...READ7 },
|
|
12903
|
+
handler: () => ({ proposals: proposals.getState().proposals.map(describe), readOnly: readOnlyReason() })
|
|
12904
|
+
};
|
|
12905
|
+
const acceptTool = {
|
|
12906
|
+
name: "accept_paint_proposal",
|
|
12907
|
+
title: "Accept a paint proposal",
|
|
12908
|
+
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.",
|
|
12909
|
+
inputSchema: proposalIdArg,
|
|
12910
|
+
annotations: { title: "Accept a paint proposal", ...WRITE6 },
|
|
12911
|
+
handler: (args) => {
|
|
12912
|
+
const reason = readOnlyReason();
|
|
12913
|
+
if (reason) throw new Error(reason);
|
|
12914
|
+
return proposals.accept(args.proposalId);
|
|
12915
|
+
}
|
|
12916
|
+
};
|
|
12917
|
+
const rejectTool = {
|
|
12918
|
+
name: "reject_paint_proposal",
|
|
12919
|
+
title: "Reject a paint proposal",
|
|
12920
|
+
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.",
|
|
12921
|
+
inputSchema: proposalIdArg,
|
|
12922
|
+
annotations: { title: "Reject a paint proposal", ...WRITE6 },
|
|
12923
|
+
handler: (args) => {
|
|
12924
|
+
const p = proposals.reject(args.proposalId);
|
|
12925
|
+
return { proposalId: p.id, title: p.title, rejected: true };
|
|
12926
|
+
}
|
|
12927
|
+
};
|
|
12928
|
+
return {
|
|
12929
|
+
id: options.id ?? "paint-proposals",
|
|
12930
|
+
...options.namespace !== void 0 ? { namespace: options.namespace } : {},
|
|
12931
|
+
title: "Paint proposals",
|
|
12932
|
+
description: "Suggest paint as a ghost the user accepts or dismisses, and accept or reject pending suggestions.",
|
|
12933
|
+
skill: "pixerate-paint-editor",
|
|
12934
|
+
tools: [proposeTool, listTool, acceptTool, rejectTool]
|
|
12935
|
+
};
|
|
12936
|
+
}
|
|
12408
12937
|
export {
|
|
12409
12938
|
BACKLIGHT_SPILL,
|
|
12410
12939
|
BLEND_MENU_GROUPS,
|
|
@@ -12565,12 +13094,15 @@ export {
|
|
|
12565
13094
|
composeAffine,
|
|
12566
13095
|
contactSheet,
|
|
12567
13096
|
createPaintBrushLibraryToolProvider,
|
|
13097
|
+
createPaintChangeFeed,
|
|
12568
13098
|
createPaintCompositor,
|
|
12569
13099
|
createPaintDocument,
|
|
12570
13100
|
createPaintEditorDocument,
|
|
12571
13101
|
createPaintEditorToolProvider,
|
|
12572
13102
|
createPaintEffectsToolProvider,
|
|
12573
13103
|
createPaintLayer,
|
|
13104
|
+
createPaintProposalToolProvider,
|
|
13105
|
+
createPaintProposals,
|
|
12574
13106
|
createPaintReplayTools,
|
|
12575
13107
|
createPaintStickerTools,
|
|
12576
13108
|
createPaintThickTools,
|
|
@@ -12655,6 +13187,7 @@ export {
|
|
|
12655
13187
|
padSampleRadius,
|
|
12656
13188
|
paintGradientMappingFor,
|
|
12657
13189
|
paintPlaybackPanel,
|
|
13190
|
+
paintProposalGhost,
|
|
12658
13191
|
paintSourceColor,
|
|
12659
13192
|
paintSourceCss,
|
|
12660
13193
|
paintToMultiColor,
|