@pixerate/editor 0.254.1 → 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.
Files changed (81) hide show
  1. package/dist/agent/index.cjs +2 -2
  2. package/dist/agent/index.js +1 -1
  3. package/dist/animation-editor/index.cjs +59 -59
  4. package/dist/animation-editor/index.js +3 -3
  5. package/dist/brushes/index.cjs +31 -29
  6. package/dist/brushes/index.d.cts +1 -1
  7. package/dist/brushes/index.d.ts +1 -1
  8. package/dist/brushes/index.js +27 -25
  9. package/dist/brushes/library.d.cts +17 -0
  10. package/dist/brushes/library.d.ts +17 -0
  11. package/dist/{brushstrokeRender-5PYSKZIJ.js → brushstrokeRender-HCOQUSWW.js} +53 -18
  12. package/dist/{brushstrokeRender-7FLLSQEC.cjs → brushstrokeRender-ZZOXI5O5.cjs} +55 -20
  13. package/dist/canvas/brushTypes.d.cts +22 -3
  14. package/dist/canvas/brushTypes.d.ts +22 -3
  15. package/dist/canvas/brushstroke.d.cts +34 -12
  16. package/dist/canvas/brushstroke.d.ts +34 -12
  17. package/dist/canvas/brushstrokeRender.d.cts +14 -8
  18. package/dist/canvas/brushstrokeRender.d.ts +14 -8
  19. package/dist/canvas/drawController.d.cts +11 -3
  20. package/dist/canvas/drawController.d.ts +11 -3
  21. package/dist/canvas/freehand.d.cts +6 -1
  22. package/dist/canvas/freehand.d.ts +6 -1
  23. package/dist/canvas/index.cjs +114 -102
  24. package/dist/canvas/index.js +16 -4
  25. package/dist/{chunk-XVIK73LS.js → chunk-5P4JEOIV.js} +4 -4
  26. package/dist/{chunk-JPEHXIB5.cjs → chunk-5YWWQ36H.cjs} +66 -21
  27. package/dist/{chunk-CHAWTXBE.cjs → chunk-6WCQ35GI.cjs} +4 -4
  28. package/dist/{chunk-GYMMNCFT.js → chunk-AF23QK5F.js} +56 -11
  29. package/dist/{chunk-JFHBXNR2.cjs → chunk-B7V34GIH.cjs} +86 -86
  30. package/dist/{chunk-QC3OXRE4.js → chunk-DMTEQ6SH.js} +1 -1
  31. package/dist/{chunk-TLOGFIIV.cjs → chunk-GCTUOTCW.cjs} +35 -35
  32. package/dist/{chunk-34KLWUH4.cjs → chunk-HIMGBZAM.cjs} +1133 -597
  33. package/dist/{chunk-7P6VF3FG.js → chunk-HYWRU5VX.js} +1 -1
  34. package/dist/{chunk-QPHNVZHO.js → chunk-JVRLGV3C.js} +1118 -582
  35. package/dist/{chunk-LFMKWGFL.cjs → chunk-LRNUZKWV.cjs} +22 -8
  36. package/dist/chunk-PGPFWC35.cjs +517 -0
  37. package/dist/{chunk-QLSGTCYT.cjs → chunk-TBAVCLEQ.cjs} +23 -23
  38. package/dist/{chunk-2ZJ4DT3M.js → chunk-VUKUTT2W.js} +1 -1
  39. package/dist/{chunk-5JNA6LLY.js → chunk-WYGSR3IJ.js} +22 -8
  40. package/dist/chunk-YHUMGBZD.js +517 -0
  41. package/dist/commands/builtins.d.cts +1 -1
  42. package/dist/commands/builtins.d.ts +1 -1
  43. package/dist/commands/index.cjs +2 -2
  44. package/dist/commands/index.js +1 -1
  45. package/dist/image-editor/brush-presets.d.cts +32 -0
  46. package/dist/image-editor/brush-presets.d.ts +32 -0
  47. package/dist/image-editor/controller.d.cts +15 -0
  48. package/dist/image-editor/controller.d.ts +15 -0
  49. package/dist/image-editor/index.cjs +848 -775
  50. package/dist/image-editor/index.d.cts +1 -0
  51. package/dist/image-editor/index.d.ts +1 -0
  52. package/dist/image-editor/index.js +81 -8
  53. package/dist/image-editor/types.d.cts +7 -0
  54. package/dist/image-editor/types.d.ts +7 -0
  55. package/dist/paint-editor/collaboration-tools.d.cts +19 -0
  56. package/dist/paint-editor/collaboration-tools.d.ts +19 -0
  57. package/dist/paint-editor/collaboration.d.cts +180 -0
  58. package/dist/paint-editor/collaboration.d.ts +180 -0
  59. package/dist/paint-editor/controller.d.cts +54 -0
  60. package/dist/paint-editor/controller.d.ts +54 -0
  61. package/dist/paint-editor/index.cjs +816 -283
  62. package/dist/paint-editor/index.d.cts +2 -0
  63. package/dist/paint-editor/index.d.ts +2 -0
  64. package/dist/paint-editor/index.js +567 -34
  65. package/dist/vector/brushstroke.d.cts +21 -5
  66. package/dist/vector/brushstroke.d.ts +21 -5
  67. package/dist/vector/controller.d.cts +14 -1
  68. package/dist/vector/controller.d.ts +14 -1
  69. package/dist/vector/index.cjs +1220 -1199
  70. package/dist/vector/index.js +34 -13
  71. package/dist/vector/types.d.cts +7 -0
  72. package/dist/vector/types.d.ts +7 -0
  73. package/package.json +1 -1
  74. package/skills/pixerate-brushes/SKILL.md +9 -3
  75. package/skills/pixerate-canvas/SKILL.md +10 -5
  76. package/skills/pixerate-image-editor/SKILL.md +2 -1
  77. package/skills/pixerate-paint-editor/SKILL.md +31 -46
  78. package/skills/pixerate-paint-editor/references/painting-alongside.md +19 -0
  79. package/skills/pixerate-vector/SKILL.md +1 -1
  80. package/dist/chunk-HS6QDWDF.cjs +0 -1029
  81. package/dist/chunk-MTKY6K3Q.js +0 -1029
@@ -74,17 +74,25 @@ function profileAlongPolyline(points, values, count = 32) {
74
74
  var round3 = (v) => Math.round(v * 1e3) / 1e3;
75
75
  var loading = null;
76
76
  function loadCanvasBrushstrokeRenderer() {
77
- loading ??= Promise.resolve().then(() => _interopRequireWildcard(require("./brushstrokeRender-7FLLSQEC.cjs"))).catch((error) => {
77
+ loading ??= Promise.resolve().then(() => _interopRequireWildcard(require("./brushstrokeRender-ZZOXI5O5.cjs"))).catch((error) => {
78
78
  loading = null;
79
79
  throw error;
80
80
  });
81
81
  return loading;
82
82
  }
83
- async function listCanvasBrushes() {
84
- return (await loadCanvasBrushstrokeRenderer()).listCanvasBrushes();
83
+ async function listCanvasBrushes(library) {
84
+ const user = library ? (await library.list({ source: "user" })).map((e) => e.brush) : [];
85
+ return (await loadCanvasBrushstrokeRenderer()).listCanvasBrushes(user);
85
86
  }
86
- async function validateCanvasBrush(brush) {
87
- (await loadCanvasBrushstrokeRenderer()).resolveCanvasBrush(brush, 1);
87
+ async function resolveCanvasBrushSpec(brush, library) {
88
+ if (typeof brush !== "string" || !library || library.isBuiltin(brush)) return void 0;
89
+ const entry = await library.get(brush);
90
+ if (!entry) return void 0;
91
+ (await loadCanvasBrushstrokeRenderer()).readCanvasBrushSpec(entry.brush);
92
+ return entry.brush;
93
+ }
94
+ async function validateCanvasBrush(brush, spec) {
95
+ (await loadCanvasBrushstrokeRenderer()).resolveCanvasBrush(brush, 1, spec);
88
96
  }
89
97
  async function validateCanvasBrushPaint(paint) {
90
98
  (await loadCanvasBrushstrokeRenderer()).resolveCanvasPaint(paint);
@@ -105,9 +113,15 @@ async function getBrushedShapeSvg(shape, size, options = {}) {
105
113
  if (!isBrushedShape(shape)) return null;
106
114
  return (await loadCanvasBrushstrokeRenderer()).brushedShapeSvgNow(shape, size, _nullishCoalesce(options.scale, () => ( 2)), options);
107
115
  }
108
- async function renderCanvasBrushSwatch(brush, options = {}) {
109
- return (await loadCanvasBrushstrokeRenderer()).brushSwatchNow(brush, options);
116
+ async function renderCanvasBrushSwatch(brush, options = {}, spec) {
117
+ return (await loadCanvasBrushstrokeRenderer()).brushSwatchNow(brush, options, spec);
110
118
  }
119
+ var renderCanvasBrushPreview = async (brush, { width, height, color }) => {
120
+ const url = await renderCanvasBrushSwatch(brush.id, { width, height, color, scale: 1 }, brush);
121
+ return { data: url.replace(/^data:image\/png;base64,/, ""), mimeType: "image/png" };
122
+ };
123
+
124
+
111
125
 
112
126
 
113
127
 
@@ -121,4 +135,4 @@ async function renderCanvasBrushSwatch(brush, options = {}) {
121
135
 
122
136
 
123
137
 
124
- exports.isBrushedShape = isBrushedShape; exports.pathDataToPolyline = pathDataToPolyline; exports.profileAlongPolyline = profileAlongPolyline; exports.loadCanvasBrushstrokeRenderer = loadCanvasBrushstrokeRenderer; exports.listCanvasBrushes = listCanvasBrushes; exports.validateCanvasBrush = validateCanvasBrush; exports.validateCanvasBrushPaint = validateCanvasBrushPaint; exports.brushstrokeSeed = brushstrokeSeed; exports.renderShapeBrushstroke = renderShapeBrushstroke; exports.getBrushedShapeSvg = getBrushedShapeSvg; exports.renderCanvasBrushSwatch = renderCanvasBrushSwatch;
138
+ exports.isBrushedShape = isBrushedShape; exports.pathDataToPolyline = pathDataToPolyline; exports.profileAlongPolyline = profileAlongPolyline; exports.loadCanvasBrushstrokeRenderer = loadCanvasBrushstrokeRenderer; exports.listCanvasBrushes = listCanvasBrushes; exports.resolveCanvasBrushSpec = resolveCanvasBrushSpec; exports.validateCanvasBrush = validateCanvasBrush; exports.validateCanvasBrushPaint = validateCanvasBrushPaint; exports.brushstrokeSeed = brushstrokeSeed; exports.renderShapeBrushstroke = renderShapeBrushstroke; exports.getBrushedShapeSvg = getBrushedShapeSvg; exports.renderCanvasBrushSwatch = renderCanvasBrushSwatch; exports.renderCanvasBrushPreview = renderCanvasBrushPreview;
@@ -0,0 +1,517 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+ var _chunkHIMGBZAMcjs = require('./chunk-HIMGBZAM.cjs');
16
+
17
+ // src/brushes/patch.ts
18
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19
+ var copy = (v) => JSON.parse(JSON.stringify(v));
20
+ var FACETS = _chunkHIMGBZAMcjs.BRUSH_JSON_SCHEMA.properties;
21
+ var BRUSH_PATCH_PROPERTIES = (() => {
22
+ const { id: _id, name: _name, ...rest } = FACETS;
23
+ const { required: _r, ...tip } = rest.tip;
24
+ return { ...rest, tip: { ...tip, description: `${tip.description} In a patch, shape may be left out to keep it.` } };
25
+ })();
26
+ var UNSETTABLE_BRUSH_FACETS = ["description", "tags", "medium", "variety", "marks", "dynamics", "behaviour", "light", "motion", "colour"];
27
+ var BRUSH_UNSET_PROPERTY = {
28
+ type: "array",
29
+ minItems: 1,
30
+ uniqueItems: true,
31
+ items: { type: "string", enum: [...UNSETTABLE_BRUSH_FACETS], description: "A facet to remove." },
32
+ description: "Facets to remove, back to their defaults (applied before the facets given)."
33
+ };
34
+ function mergeBrushPatch(base, patch, path = "") {
35
+ const out = { ...base };
36
+ for (const [k, v] of Object.entries(patch)) {
37
+ if (v === void 0) continue;
38
+ const old = out[k];
39
+ const replaceMarks = path === "" && k === "marks" && isObject(old) && isObject(v) && old.kind !== v.kind;
40
+ out[k] = isObject(v) && isObject(old) && !replaceMarks ? mergeBrushPatch(old, v, `${path}${k}.`) : copy(v);
41
+ }
42
+ return out;
43
+ }
44
+ function changedBrushPaths(a, b, path = "") {
45
+ if (isObject(a) && isObject(b)) return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].flatMap((k) => changedBrushPaths(a[k], b[k], path ? `${path}.${k}` : k));
46
+ return JSON.stringify(a) === JSON.stringify(b) ? [] : [path];
47
+ }
48
+
49
+ // src/brushes/tools.ts
50
+ var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
51
+ var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
52
+ var FACETS2 = _chunkHIMGBZAMcjs.BRUSH_JSON_SCHEMA.properties;
53
+ var ID = { type: "string", pattern: String(FACETS2.id.pattern), description: "Brush id from list_brush_presets (lowercase letters, digits and dashes)." };
54
+ var FACET_PROPS = BRUSH_PATCH_PROPERTIES;
55
+ var HOSTS = Object.keys(_chunkHIMGBZAMcjs.BRUSH_HOST_PROFILES);
56
+ var copy2 = (v) => JSON.parse(JSON.stringify(v));
57
+ var merge = mergeBrushPatch;
58
+ var changedPaths = changedBrushPaths;
59
+ function summary(entry, profile, withCheck) {
60
+ const b = entry.brush;
61
+ const engine = _chunkHIMGBZAMcjs.resolveEngine.call(void 0, b);
62
+ const check = withCheck ? _chunkHIMGBZAMcjs.checkBrush.call(void 0, b, profile) : null;
63
+ return {
64
+ id: b.id,
65
+ name: b.name,
66
+ builtin: entry.builtin,
67
+ kind: b.kind,
68
+ ...b.medium ? { medium: b.medium } : {},
69
+ ...b.variety ? { variety: b.variety } : {},
70
+ ...b.tool ? { tool: b.tool } : {},
71
+ category: _chunkHIMGBZAMcjs.resolveCategory.call(void 0, b, engine),
72
+ engine,
73
+ tip: b.tip.shape,
74
+ ...b.tip.size !== void 0 ? { size: b.tip.size } : {},
75
+ marks: _nullishCoalesce(_optionalChain([b, 'access', _ => _.marks, 'optionalAccess', _2 => _2.kind]), () => ( "continuous")),
76
+ surface: _nullishCoalesce(_optionalChain([b, 'access', _3 => _3.behaviour, 'optionalAccess', _4 => _4.surface]), () => ( "dry")),
77
+ ..._optionalChain([b, 'access', _5 => _5.light, 'optionalAccess', _6 => _6.amount]) ? { light: b.light.amount } : {},
78
+ ..._optionalChain([b, 'access', _7 => _7.tags, 'optionalAccess', _8 => _8.length]) ? { tags: b.tags } : {},
79
+ ...check ? { renderable: check.ok } : {}
80
+ };
81
+ }
82
+ var refusal = (check, host) => `${host.name} can't use this brush: ${check.issues.filter((i) => i.severity === "error").map((i) => `${i.facet}: ${i.reason} ${i.fix}`).join(" ")} (check_brush lists every issue; nothing was saved.)`;
83
+ var warnings = (check) => check.issues.filter((i) => i.severity === "warning");
84
+ function createBrushLibraryTools(library, options = {}) {
85
+ const profile = _nullishCoalesce(options.profile, () => ( _chunkHIMGBZAMcjs.PAINT_PROFILE));
86
+ const hostOf = (id) => {
87
+ if (id === void 0) return profile;
88
+ const p = _chunkHIMGBZAMcjs.BRUSH_HOST_PROFILES[id];
89
+ if (!p) throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("invalid", `No host "${id}"; use one of: ${HOSTS.join(", ")}.`);
90
+ return p;
91
+ };
92
+ const entry = async (id) => {
93
+ const found = await library.get(id);
94
+ if (found) return found;
95
+ const ids = (await library.list()).map((e) => e.brush.id);
96
+ throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("unknown_brush", `No brush "${id}". ${_chunkHIMGBZAMcjs.closest.call(void 0, id, ids)}list_brush_presets lists them.`);
97
+ };
98
+ const store = async (raw, overwrite) => {
99
+ const problems = [];
100
+ let brush;
101
+ try {
102
+ brush = _chunkHIMGBZAMcjs.normalizeBrushSpec.call(void 0, raw, problems);
103
+ } catch (e) {
104
+ throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("invalid", e.message);
105
+ }
106
+ const check2 = _chunkHIMGBZAMcjs.checkBrush.call(void 0, brush, profile);
107
+ if (!check2.ok) throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("invalid", refusal(check2, profile));
108
+ const saved = await library.save(brush, { overwrite });
109
+ return { ...saved, problems, check: check2 };
110
+ };
111
+ const list = {
112
+ name: "list_brush_presets",
113
+ title: "List brushes",
114
+ description: `Lists the brush library (built-in presets and saved brushes) that this host (${profile.name}) can render, filtered by kind, medium, variety, tool, tag or words. Returns each brush's id (for describe_brush, create_brush basedOn, duplicate_brush and the editor's set_brush), name, builtin, kind, medium, variety or tool, category, engine, tip shape and size, marks, surface and light, plus total. Call it first to find the nearest brush to start from. Changes nothing.`,
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ kind: { type: "string", enum: [..._chunkHIMGBZAMcjs.BRUSH_KINDS], description: "mark (lays a material) or tool (works on paint already down)." },
119
+ medium: { type: "string", enum: [..._chunkHIMGBZAMcjs.BRUSH_MEDIA], description: "Marks laying this medium." },
120
+ variety: { type: "string", maxLength: 40, description: "This variety (oil, watercolor, pencil, highlighter\u2026)." },
121
+ tool: { type: "string", enum: [..._chunkHIMGBZAMcjs.BRUSH_TOOLS], description: "Tools that do this." },
122
+ tag: { type: "string", maxLength: 40, description: "Brushes with this tag." },
123
+ text: { type: "string", maxLength: 200, description: "Words in the id, name, description, variety or tags (each word must match the start of one)." },
124
+ source: { type: "string", enum: ["all", "builtin", "user"], description: "all (default), builtin presets only, or user (saved) brushes only." },
125
+ all: { type: "boolean", description: "Also list brushes this host can't render, each with renderable false (default false)." },
126
+ offset: { type: "integer", minimum: 0, description: "Skip this many (default 0)." },
127
+ limit: { type: "integer", minimum: 1, maximum: 200, description: "At most this many (default 100)." }
128
+ },
129
+ additionalProperties: false
130
+ },
131
+ annotations: { title: "List brushes", ...READ },
132
+ handler: async (a) => {
133
+ const source = a.source === "all" ? void 0 : a.source;
134
+ const found = await library.list({ kind: a.kind, medium: a.medium, variety: a.variety, tool: a.tool, tag: a.tag, text: a.text, source, ...a.all ? {} : { profile } });
135
+ const offset = _nullishCoalesce(a.offset, () => ( 0));
136
+ const page = found.slice(offset, offset + (_nullishCoalesce(a.limit, () => ( 100))));
137
+ return { host: profile.id, total: found.length, brushes: page.map((e) => summary(e, profile, !!a.all)) };
138
+ }
139
+ };
140
+ const describe = {
141
+ name: "describe_brush",
142
+ title: "Describe a brush",
143
+ description: `Reads one brush by id: its full facet spec (kind, medium, variety or tool, tip, marks, dynamics, behaviour, light, motion, colour), whether it is built in, the engine and category it resolves to, and check: what this host (${profile.name}) refuses or paints without, each with a fix. Unknown ids are refused with the closest ones. Changes nothing.`,
144
+ inputSchema: { type: "object", properties: { id: ID }, required: ["id"], additionalProperties: false },
145
+ annotations: { title: "Describe a brush", ...READ },
146
+ handler: async ({ id }) => {
147
+ const e = await entry(id);
148
+ const engine = _chunkHIMGBZAMcjs.resolveEngine.call(void 0, e.brush);
149
+ return { brush: e.brush, builtin: e.builtin, engine, category: _chunkHIMGBZAMcjs.resolveCategory.call(void 0, e.brush, engine), check: _chunkHIMGBZAMcjs.checkBrush.call(void 0, e.brush, profile) };
150
+ }
151
+ };
152
+ const check = {
153
+ name: "check_brush",
154
+ title: "Check a brush for a host",
155
+ description: `Checks a brush (by id, or a facet spec not yet saved) against a host profile: ${HOSTS.join(", ")} (default this host, ${profile.id}). Returns ok (no errors), issues (facet, severity error or warning, reason, fix) and, when it is refused, degraded: the brush with the refused facets stripped and what was dropped. Use it before create_brush, or to see whether a brush works in another editor. Changes nothing.`,
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ id: { ...ID, description: "A brush in the library (give id or brush, not both)." },
160
+ brush: { type: "object", description: "A facet brush spec (the create_brush fields, with id and name)." },
161
+ host: { type: "string", enum: HOSTS, description: `The host profile (default ${profile.id}).` }
162
+ },
163
+ additionalProperties: false
164
+ },
165
+ annotations: { title: "Check a brush for a host", ...READ },
166
+ handler: async ({ id, brush, host }) => {
167
+ if (id === void 0 === (brush === void 0)) throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("invalid", "Give either id (a library brush) or brush (a spec), not both or neither.");
168
+ const p = hostOf(host);
169
+ let b;
170
+ const problems = [];
171
+ if (id !== void 0) b = (await entry(id)).brush;
172
+ else {
173
+ try {
174
+ b = _chunkHIMGBZAMcjs.normalizeBrushSpec.call(void 0, brush, problems);
175
+ } catch (e) {
176
+ throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("invalid", e.message);
177
+ }
178
+ }
179
+ const result = _chunkHIMGBZAMcjs.checkBrush.call(void 0, b, p);
180
+ return { host: p.id, ...result, ...problems.length ? { problems } : {}, ...!result.ok ? { degraded: _chunkHIMGBZAMcjs.degrade.call(void 0, b, p) } : {} };
181
+ }
182
+ };
183
+ const exportTool = {
184
+ name: "export_brushes",
185
+ title: "Export brushes",
186
+ description: "Writes a brush library file (format pixerate-brushes, version 2) of the saved brushes, or of the ids given (built-ins included). Returns the file as JSON, for import_brushes in another library. Changes nothing.",
187
+ inputSchema: { type: "object", properties: { ids: { type: "array", minItems: 1, maxItems: 500, uniqueItems: true, items: ID, description: "Only these brushes (default every saved brush)." } }, additionalProperties: false },
188
+ annotations: { title: "Export brushes", ...READ },
189
+ handler: async ({ ids }) => ({ file: await library.exportFile(ids) })
190
+ };
191
+ const tools = [list, describe, check, exportTool];
192
+ if (options.renderPreview) {
193
+ const render = options.renderPreview;
194
+ tools.push({
195
+ name: "preview_brush",
196
+ title: "Preview a brush",
197
+ description: "Draws a sample S-stroke with a brush (by id) on a plain background and returns it as an image, to see a brush before painting with it or after create_brush / update_brush. Returns the image plus id, width and height. Changes nothing.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ id: ID,
202
+ width: { type: "integer", minimum: 32, maximum: 1024, description: "Image width in px (default 256)." },
203
+ height: { type: "integer", minimum: 16, maximum: 512, description: "Image height in px (default 96)." },
204
+ color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Paint colour #rrggbb (default the brush's own default colour, else #1f2937)." }
205
+ },
206
+ required: ["id"],
207
+ additionalProperties: false
208
+ },
209
+ annotations: { title: "Preview a brush", ...READ },
210
+ handler: async ({ id, width, height, color }) => {
211
+ const { brush } = await entry(id);
212
+ const own = typeof _optionalChain([brush, 'access', _9 => _9.colour, 'optionalAccess', _10 => _10.default]) === "string" && /^#[0-9a-f]{6}/i.test(brush.colour.default) ? brush.colour.default.slice(0, 7) : void 0;
213
+ const w = _nullishCoalesce(width, () => ( 256));
214
+ const h = _nullishCoalesce(height, () => ( 96));
215
+ const image = await render(brush, { width: w, height: h, color: _nullishCoalesce(_nullishCoalesce(color, () => ( own)), () => ( "#1f2937")) });
216
+ return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }], structuredContent: { id, width: w, height: h } };
217
+ }
218
+ });
219
+ }
220
+ if (options.readOnly) return tools;
221
+ tools.push(
222
+ {
223
+ name: "create_brush",
224
+ title: "Save a new brush",
225
+ description: `Saves a new brush made of facets (to save an editor's current brush settings, pass the brush get_brush returns), optionally basedOn an existing brush (its facets are copied and the ones given here laid over them: objects merge, lists and values replace). Values are normalised (out-of-range numbers clamped, unused facets dropped, each reported in problems) and checked against this host (${profile.name}): a brush it can't render is refused with the facet and fix, and nothing is saved. Returns the new id, the stored brush, the engine it resolves to, problems and warnings (settings this host paints without). Start from the nearest preset (list_brush_presets) rather than from nothing.`,
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ id: { ...FACETS2.id, description: "The new brush id: lowercase letters, digits and dashes, not taken (list_brush_presets)." },
230
+ name: FACETS2.name,
231
+ basedOn: { ...ID, description: "Copy this brush's facets first (a built-in preset or a saved brush)." },
232
+ ...FACET_PROPS
233
+ },
234
+ required: ["id", "name"],
235
+ additionalProperties: false
236
+ },
237
+ annotations: { title: "Save a new brush", ...WRITE },
238
+ handler: async ({ basedOn, ...spec }) => {
239
+ if (library.isBuiltin(spec.id)) throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("builtin", `"${spec.id}" is a built-in brush id; pick another id (or basedOn "${spec.id}" with a new id).`);
240
+ if (await library.get(spec.id)) throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("exists", `A brush "${spec.id}" exists already; pick another id, or change it with update_brush.`);
241
+ const base = basedOn !== void 0 ? (await entry(basedOn)).brush : void 0;
242
+ const raw = base ? merge(base, spec) : spec;
243
+ const saved = await store(raw, false);
244
+ return { id: saved.brush.id, brush: saved.brush, engine: _chunkHIMGBZAMcjs.resolveEngine.call(void 0, saved.brush), problems: saved.problems, warnings: warnings(saved.check) };
245
+ }
246
+ },
247
+ {
248
+ name: "update_brush",
249
+ title: "Update a brush",
250
+ description: `Changes a saved brush: the facets given are laid over its own (objects merge, lists and values replace; marks of another kind replace the marks), and unset removes whole facets back to their defaults. Built-in presets are refused (duplicate_brush first). Checked against this host like create_brush; a refused change saves nothing. One change. Returns the brush, the leaf paths changed, problems and warnings.`,
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ id: { ...ID, description: "The saved brush to change (not a built-in)." },
255
+ name: FACETS2.name,
256
+ ...FACET_PROPS,
257
+ unset: BRUSH_UNSET_PROPERTY
258
+ },
259
+ required: ["id"],
260
+ additionalProperties: false
261
+ },
262
+ annotations: { title: "Update a brush", ...WRITE, idempotentHint: true },
263
+ handler: async ({ id, unset, ...patch }) => {
264
+ const e = await entry(id);
265
+ if (e.builtin) throw new (0, _chunkHIMGBZAMcjs.BrushLibraryError)("builtin", `"${id}" is a built-in brush and can't be changed; duplicate_brush makes a copy you can change.`);
266
+ const base = copy2(e.brush);
267
+ for (const k of _nullishCoalesce(unset, () => ( []))) delete base[k];
268
+ const saved = await store(merge(base, { ...patch, id }), true);
269
+ return { id, brush: saved.brush, changed: changedPaths(e.brush, saved.brush), problems: saved.problems, warnings: warnings(saved.check) };
270
+ }
271
+ },
272
+ {
273
+ name: "duplicate_brush",
274
+ title: "Duplicate a brush",
275
+ description: "Copies any brush (built-in presets included) to a new saved brush you can change with update_brush. Returns the new id (default <id>-copy, -copy-2\u2026) and the brush.",
276
+ inputSchema: {
277
+ type: "object",
278
+ properties: {
279
+ id: ID,
280
+ newId: { ...FACETS2.id, description: "Id for the copy (default <id>-copy, -copy-2\u2026); must not be taken." },
281
+ name: { ...FACETS2.name, description: 'Name for the copy (default "<name> copy").' }
282
+ },
283
+ required: ["id"],
284
+ additionalProperties: false
285
+ },
286
+ annotations: { title: "Duplicate a brush", ...WRITE },
287
+ handler: async ({ id, newId, name }) => {
288
+ const brush = await library.duplicate(id, { ...newId !== void 0 ? { id: newId } : {}, ...name !== void 0 ? { name } : {} });
289
+ return { id: brush.id, brush };
290
+ }
291
+ },
292
+ {
293
+ name: "remove_brush",
294
+ title: "Remove a brush",
295
+ description: "Deletes a saved brush from the library for good (export_brushes first to keep a copy). Built-in presets are refused. Returns the id removed.",
296
+ inputSchema: { type: "object", properties: { id: { ...ID, description: "The saved brush to remove (not a built-in)." } }, required: ["id"], additionalProperties: false },
297
+ annotations: { title: "Remove a brush", ...WRITE, destructiveHint: true },
298
+ handler: async ({ id }) => {
299
+ await entry(id);
300
+ await library.remove(id);
301
+ return { removed: id };
302
+ }
303
+ },
304
+ {
305
+ name: "import_brushes",
306
+ title: "Import brushes",
307
+ description: "Adds the brushes of a library file (format pixerate-brushes version 2 from export_brushes, a version 1 file of the paint editor's brushes, which are migrated, or a bare list of brushes). Brushes with a built-in id are skipped, and so are ids already saved unless overwrite. Returns added, replaced and skipped ids and problems (each fix or skip, per brush).",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ file: { type: ["object", "array"], description: "The library file object, or an array of brushes." },
312
+ overwrite: { type: "boolean", description: "Replace saved brushes with the same id (default false: skip them)." }
313
+ },
314
+ required: ["file"],
315
+ additionalProperties: false
316
+ },
317
+ annotations: { title: "Import brushes", ...WRITE, destructiveHint: true },
318
+ handler: async ({ file, overwrite }) => library.importFile(file, { overwrite: !!overwrite })
319
+ }
320
+ );
321
+ return tools;
322
+ }
323
+ function createBrushLibraryToolProvider(library, options = {}) {
324
+ const profile = _nullishCoalesce(options.profile, () => ( _chunkHIMGBZAMcjs.PAINT_PROFILE));
325
+ const tools = createBrushLibraryTools(library, options);
326
+ return {
327
+ id: _nullishCoalesce(options.id, () => ( "brushes")),
328
+ namespace: options.namespace,
329
+ title: "Brush library",
330
+ description: `The shared brush library: find, describe, create, change, check and share brushes made of facets, for ${profile.name}.`,
331
+ skill: "pixerate-brushes",
332
+ tools
333
+ };
334
+ }
335
+
336
+ // src/paint-editor/brush-controls.ts
337
+ var BRUSH_SLIDERS = [
338
+ { path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
339
+ { path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
340
+ { path: "tip.flow", label: "Flow", min: 0, max: 1, step: 0.01 },
341
+ { path: "tip.hardness", label: "Hardness", min: 0, max: 1, step: 0.01 },
342
+ { path: "tip.spacing", label: "Spacing", min: 0.01, max: 1, step: 0.01 },
343
+ { path: "tip.roundness", label: "Roundness", min: 0.05, max: 1, step: 0.01 },
344
+ { path: "tip.angle", label: "Angle", min: -180, max: 180, step: 1 },
345
+ { path: "grain", label: "Grain", min: 0, max: 1, step: 0.01 },
346
+ { path: "tip.smoothing", label: "Smoothing", min: 0, max: 0.95, step: 0.01 },
347
+ { path: "pressureSmoothing", label: "Pressure smoothing", min: 0, max: 0.95, step: 0.01 },
348
+ { path: "dynamics.minSize", label: "Min size", min: 0, max: 1, step: 0.01 },
349
+ { path: "dynamics.tiltSize", label: "Tilt size", min: 0, max: 1, step: 0.01 },
350
+ { path: "dynamics.velocitySize", label: "Speed size", min: -1, max: 1, step: 0.01 }
351
+ ];
352
+ var LIGHT_SLIDER = { path: "light.amount", label: "Light", min: 0, max: 1, step: 0.01 };
353
+ var PICKUP_SLIDERS = [
354
+ { path: "behavior.pickup", label: "Pick-up", min: 0, max: 1, step: 0.01 },
355
+ { path: "behavior.mix", label: "Mix", min: 0, max: 1, step: 0.01 }
356
+ ];
357
+ function brushEngineControls(brush) {
358
+ const c = engineControls(brush);
359
+ const wetPaint = brush.engine === "wet" && _optionalChain([brush, 'access', _11 => _11.wet, 'optionalAccess', _12 => _12.tool]) === "paint";
360
+ if (!c || !(wetPaint || ["dab", "mixer", "particle", "bristle", "impasto"].includes(brush.engine))) return c;
361
+ const extra = brush.engine === "impasto" ? [{ path: "impasto.rim", label: "Rim", min: 0, max: 1, step: 0.01 }, { path: "impasto.clarity", label: "Clarity", min: 0, max: 1, step: 0.01 }] : [];
362
+ return { ...c, sliders: [...c.sliders, LIGHT_SLIDER, ...extra], light: true };
363
+ }
364
+ function engineControls(brush) {
365
+ switch (brush.engine) {
366
+ case "particle":
367
+ return {
368
+ title: "Particles",
369
+ sliders: [
370
+ { path: "particle.count", label: "Particles", min: 1, max: 64, step: 1 },
371
+ { path: "particle.size", label: "Mark size", min: 0, max: 1, step: 0.01 },
372
+ { path: "particle.spread", label: "Spread", min: 0, max: 1, step: 0.01 },
373
+ { path: "particle.stiffness", label: "Stiffness", min: 0, max: 1, step: 0.01 },
374
+ { path: "particle.damping", label: "Damping", min: 0, max: 1, step: 0.01 },
375
+ { path: "particle.speed", label: "Speed", min: 0, max: 1, step: 0.01 },
376
+ { path: "particle.gravity", label: "Gravity", min: -1, max: 1, step: 0.01 },
377
+ { path: "particle.life", label: "Life", min: 0, max: 1, step: 0.01 },
378
+ { path: "particle.interval", label: "Bead spacing", min: 0, max: 1, step: 0.01 },
379
+ { path: "particle.ribbonFill", label: "Ribbon fill", min: 0, max: 1, step: 0.01 },
380
+ { path: "particle.waveAmplitude", label: "Wave", min: 0, max: 1, step: 0.01 },
381
+ { path: "particle.waveLength", label: "Wavelength", min: 4, max: 400, step: 1 },
382
+ { path: "particle.waveSpeed", label: "Wave speed", min: 0, max: 1, step: 0.01 },
383
+ { path: "particle.wavePhaseJitter", label: "Wave scatter", min: 0, max: 1, step: 0.01 }
384
+ ],
385
+ mixingPath: null,
386
+ dirty: false,
387
+ multicolor: false,
388
+ load: false,
389
+ particleMenus: true
390
+ };
391
+ case "bristle":
392
+ return {
393
+ title: "Bristles",
394
+ sliders: [
395
+ { path: "bristle.count", label: "Bristles", min: 3, max: 128, step: 1 },
396
+ { path: "bristle.stiffness", label: "Stiffness", min: 0, max: 1, step: 0.01 },
397
+ { path: "bristle.splay", label: "Splay", min: 0, max: 1, step: 0.01 },
398
+ { path: "bristle.thickness", label: "Bristle width", min: 0, max: 1, step: 0.01 },
399
+ { path: "bristle.load", label: "Load", min: 0, max: 1, step: 0.01 },
400
+ { path: "bristle.jitter", label: "Variation", min: 0, max: 1, step: 0.01 }
401
+ ],
402
+ mixingPath: null,
403
+ dirty: false,
404
+ multicolor: false,
405
+ load: false
406
+ };
407
+ case "sticker":
408
+ return {
409
+ title: "Stickers",
410
+ sliders: [
411
+ { path: "sticker.density", label: "Density", min: 0, max: 1, step: 0.01 },
412
+ { path: "sticker.sizeJitter", label: "Size jitter", min: 0, max: 1, step: 0.01 },
413
+ { path: "sticker.rotationJitter", label: "Turn jitter", min: 0, max: 1, step: 0.01 },
414
+ { path: "sticker.scatter", label: "Scatter", min: 0, max: 2, step: 0.01 },
415
+ { path: "sticker.colorJitter", label: "Colour jitter", min: 0, max: 1, step: 0.01 }
416
+ ],
417
+ mixingPath: null,
418
+ dirty: false,
419
+ multicolor: false,
420
+ load: false,
421
+ stickerStamps: true
422
+ };
423
+ case "impasto":
424
+ return {
425
+ title: "Thick paint",
426
+ sliders: [
427
+ { path: "impasto.thickness", label: "Thickness", min: 0, max: 1, step: 0.01 },
428
+ { path: "impasto.gloss", label: "Gloss", min: 0, max: 1, step: 0.01 },
429
+ { path: "impasto.metallic", label: "Metallic", min: 0, max: 1, step: 0.01 },
430
+ { path: "impasto.load", label: "Load", min: 0, max: 1, step: 0.01 },
431
+ { path: "impasto.thinner", label: "Thinner", min: 0, max: 1, step: 0.01 },
432
+ { path: "impasto.drag", label: "Drag", min: 0, max: 1, step: 0.01 },
433
+ ...PICKUP_SLIDERS
434
+ ],
435
+ mixingPath: "behavior.mixing",
436
+ dirty: true,
437
+ multicolor: false,
438
+ load: true,
439
+ impastoTool: true
440
+ };
441
+ case "wet": {
442
+ const tool = _nullishCoalesce(_optionalChain([brush, 'access', _13 => _13.wet, 'optionalAccess', _14 => _14.tool]), () => ( "paint"));
443
+ const sliders = tool === "paint" ? [
444
+ { path: "wet.water", label: "Water", min: 0, max: 1, step: 0.01 },
445
+ { path: "wet.pigment", label: "Pigment", min: 0, max: 1, step: 0.01 },
446
+ { path: "wet.body", label: "Body (opacity)", min: 0, max: 1, step: 0.01 },
447
+ { path: "wet.rewet", label: "Re-wet", min: 0, max: 1, step: 0.01 },
448
+ { path: "wet.load", label: "Load", min: 0, max: 1, step: 0.01 },
449
+ { path: "wet.thinner", label: "Thinner", min: 0, max: 1, step: 0.01 },
450
+ ...PICKUP_SLIDERS
451
+ ] : tool === "water" ? [
452
+ { path: "wet.water", label: "Water", min: 0, max: 1, step: 0.01 },
453
+ { path: "wet.rewet", label: "Re-wet", min: 0, max: 1, step: 0.01 }
454
+ ] : [{ path: "wet.strength", label: "Strength", min: 0, max: 1, step: 0.01 }];
455
+ return { title: "Wet brush", sliders, mixingPath: tool === "paint" ? "behavior.mixing" : null, dirty: tool === "paint", multicolor: false, load: tool === "paint", wetTool: true };
456
+ }
457
+ case "blender":
458
+ return {
459
+ title: "Blender",
460
+ sliders: [
461
+ { path: "blender.strength", label: "Strength", min: 0, max: 1, step: 0.01 },
462
+ { path: "blender.distance", label: "Blend distance", min: 0, max: 1, step: 0.01 }
463
+ ],
464
+ mixingPath: "blender.mixing",
465
+ dirty: false,
466
+ multicolor: false,
467
+ load: false
468
+ };
469
+ case "smudge":
470
+ return {
471
+ title: "Smudge",
472
+ sliders: [
473
+ { path: "smudge.strength", label: "Strength", min: 0, max: 1, step: 0.01 },
474
+ { path: "smudge.length", label: "Length", min: 0, max: 1, step: 0.01 }
475
+ ],
476
+ mixingPath: "smudge.mixing",
477
+ dirty: false,
478
+ multicolor: false,
479
+ load: false
480
+ };
481
+ case "mixer":
482
+ return { title: "Mixer Brush", sliders: [...PICKUP_SLIDERS, { path: "mixer.load", label: "Load", min: 0, max: 1, step: 0.01 }], mixingPath: "behavior.mixing", dirty: true, multicolor: true, load: true };
483
+ case "dab":
484
+ return { title: "Paint behaviour", sliders: PICKUP_SLIDERS, mixingPath: "behavior.mixing", dirty: true, multicolor: true, load: true };
485
+ default:
486
+ return null;
487
+ }
488
+ }
489
+ function multiColorSliders(mode) {
490
+ if (mode === "off") return [];
491
+ const soft = { path: "behavior.multicolor.softness", label: "Colour softness", min: 0, max: 1, step: 0.01 };
492
+ return mode === "dab" ? [soft, { path: "behavior.multicolor.length", label: "Dabs per colour", min: 1, max: 100, step: 1 }] : [soft];
493
+ }
494
+ function patchBrushValue(brush, path, value) {
495
+ const set = (obj, keys) => {
496
+ const [head2, ...rest2] = keys;
497
+ if (!rest2.length) return { ...obj, [head2]: value };
498
+ return { ...obj, [head2]: set(_nullishCoalesce(obj[head2], () => ( {})), rest2) };
499
+ };
500
+ const [head, ...rest] = path.split(".");
501
+ if (!rest.length) return { [head]: value };
502
+ return { [head]: set({ ..._nullishCoalesce(brush[head], () => ( {})) }, rest) };
503
+ }
504
+
505
+
506
+
507
+
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+ exports.BRUSH_PATCH_PROPERTIES = BRUSH_PATCH_PROPERTIES; exports.UNSETTABLE_BRUSH_FACETS = UNSETTABLE_BRUSH_FACETS; exports.BRUSH_UNSET_PROPERTY = BRUSH_UNSET_PROPERTY; exports.mergeBrushPatch = mergeBrushPatch; exports.changedBrushPaths = changedBrushPaths; exports.createBrushLibraryTools = createBrushLibraryTools; exports.createBrushLibraryToolProvider = createBrushLibraryToolProvider; exports.BRUSH_SLIDERS = BRUSH_SLIDERS; exports.brushEngineControls = brushEngineControls; exports.multiColorSliders = multiColorSliders; exports.patchBrushValue = patchBrushValue;