@pixerate/editor 0.251.0 → 0.252.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/brushes/index.cjs +687 -6
- package/dist/brushes/index.d.cts +6 -1
- package/dist/brushes/index.d.ts +6 -1
- package/dist/brushes/index.js +684 -3
- package/dist/brushes/library.d.cts +104 -0
- package/dist/brushes/library.d.ts +104 -0
- package/dist/brushes/profiles.d.cts +85 -0
- package/dist/brushes/profiles.d.ts +85 -0
- package/dist/brushes/tools.d.cts +37 -0
- package/dist/brushes/tools.d.ts +37 -0
- package/dist/tools/skill-evals.d.cts +8 -1
- package/dist/tools/skill-evals.d.ts +8 -1
- package/package.json +1 -1
- package/skills/README.md +2 -0
- package/skills/pixerate-brushes/SKILL.md +104 -0
- package/skills/pixerate-brushes-design/SKILL.md +88 -0
- package/skills/pixerate-brushes-design/evals/canvas-chalk.json +15 -0
- package/skills/pixerate-brushes-design/evals/pink-highlighter.json +15 -0
- package/skills/pixerate-brushes-design/evals/port-to-vector.json +17 -0
package/dist/brushes/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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; }
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await 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
2
|
|
|
3
3
|
|
|
4
4
|
|
|
@@ -417,9 +417,9 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
417
417
|
if (c.palette !== void 0) {
|
|
418
418
|
if (!Array.isArray(c.palette)) problems.push("brush.colour.palette must be a list; dropped.");
|
|
419
419
|
else {
|
|
420
|
-
const
|
|
421
|
-
if (
|
|
422
|
-
colour.palette =
|
|
420
|
+
const list2 = c.palette.map((p, i) => readPaint(p, `brush.colour.palette[${i}]`, problems)).filter((p) => p !== void 0);
|
|
421
|
+
if (list2.length > MAX_BRUSH_PALETTE) problems.push(`brush.colour.palette has more than ${MAX_BRUSH_PALETTE} colours; the rest were dropped.`);
|
|
422
|
+
colour.palette = list2.slice(0, MAX_BRUSH_PALETTE);
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
if (typeof c.own === "boolean") colour.own = c.own;
|
|
@@ -516,7 +516,7 @@ var EDITOR_STAMPS = {
|
|
|
516
516
|
try {
|
|
517
517
|
const stamp = _chunkJSDND7DFcjs.editorStampOf.call(void 0, ref);
|
|
518
518
|
return stamp ? [stamp] : [];
|
|
519
|
-
} catch (
|
|
519
|
+
} catch (e2) {
|
|
520
520
|
return [];
|
|
521
521
|
}
|
|
522
522
|
}),
|
|
@@ -1385,6 +1385,687 @@ var BRUSH_PRESETS = [
|
|
|
1385
1385
|
}
|
|
1386
1386
|
];
|
|
1387
1387
|
|
|
1388
|
+
// src/brushes/profiles.ts
|
|
1389
|
+
var ALL_COLOURS = ["solid", "dual", "gradient", "effect"];
|
|
1390
|
+
var ELLIPSES = ["round", "flat", "chisel"];
|
|
1391
|
+
var PAINT_PROFILE = {
|
|
1392
|
+
id: "paint",
|
|
1393
|
+
name: "Paint editor",
|
|
1394
|
+
kinds: ["mark", "tool"],
|
|
1395
|
+
tools: BRUSH_TOOLS,
|
|
1396
|
+
tipShapes: BRUSH_TIP_SHAPES,
|
|
1397
|
+
marks: BRUSH_MARKS_KINDS,
|
|
1398
|
+
surfaces: ["dry", "wet", "thick"],
|
|
1399
|
+
exchange: "full",
|
|
1400
|
+
reservoir: "full",
|
|
1401
|
+
grain: "full",
|
|
1402
|
+
light: "full",
|
|
1403
|
+
motion: "full",
|
|
1404
|
+
colours: ALL_COLOURS,
|
|
1405
|
+
velocity: "full",
|
|
1406
|
+
tilt: "full"
|
|
1407
|
+
};
|
|
1408
|
+
var IMAGE_PROFILE = {
|
|
1409
|
+
id: "image",
|
|
1410
|
+
name: "Image editor",
|
|
1411
|
+
kinds: ["mark", "tool"],
|
|
1412
|
+
tools: ["eraser"],
|
|
1413
|
+
tipShapes: ELLIPSES,
|
|
1414
|
+
marks: ["continuous"],
|
|
1415
|
+
surfaces: ["dry"],
|
|
1416
|
+
exchange: "ignored",
|
|
1417
|
+
reservoir: "full",
|
|
1418
|
+
grain: "ignored",
|
|
1419
|
+
light: "none",
|
|
1420
|
+
motion: "none",
|
|
1421
|
+
colours: ["solid", "dual", "gradient"],
|
|
1422
|
+
velocity: "ignored",
|
|
1423
|
+
tilt: "ignored"
|
|
1424
|
+
};
|
|
1425
|
+
var CANVAS_PROFILE = {
|
|
1426
|
+
id: "canvas",
|
|
1427
|
+
name: "Canvas",
|
|
1428
|
+
kinds: ["mark"],
|
|
1429
|
+
tools: [],
|
|
1430
|
+
tipShapes: ELLIPSES,
|
|
1431
|
+
marks: ["continuous"],
|
|
1432
|
+
surfaces: ["dry"],
|
|
1433
|
+
exchange: "ignored",
|
|
1434
|
+
reservoir: "none",
|
|
1435
|
+
grain: "full",
|
|
1436
|
+
light: "none",
|
|
1437
|
+
motion: "none",
|
|
1438
|
+
colours: ["solid", "dual", "gradient"],
|
|
1439
|
+
velocity: "ignored",
|
|
1440
|
+
tilt: "full"
|
|
1441
|
+
};
|
|
1442
|
+
var VECTOR_PROFILE = { ...CANVAS_PROFILE, id: "vector", name: "Vector editor", light: "ignored" };
|
|
1443
|
+
var BRUSH_HOST_PROFILES = { paint: PAINT_PROFILE, image: IMAGE_PROFILE, canvas: CANVAS_PROFILE, vector: VECTOR_PROFILE };
|
|
1444
|
+
function brushColourKind(paint) {
|
|
1445
|
+
if (typeof paint === "string") return "solid";
|
|
1446
|
+
if ("type" in paint) return paint.type === "gradient" ? "gradient" : "effect";
|
|
1447
|
+
return "dual";
|
|
1448
|
+
}
|
|
1449
|
+
var list = (values) => values.join(", ");
|
|
1450
|
+
var nonZero = (v) => v !== void 0 && v !== 0;
|
|
1451
|
+
function checkBrush(brush, profile) {
|
|
1452
|
+
const issues = [];
|
|
1453
|
+
const host = profile.name;
|
|
1454
|
+
const add = (facet, severity, reason, fix) => issues.push({ facet, severity, reason, fix });
|
|
1455
|
+
const feature = (support, used, facet, what, fix) => {
|
|
1456
|
+
if (!used || support === "full") return;
|
|
1457
|
+
if (support === "none") add(facet, "error", `${host} can't paint ${what}.`, fix);
|
|
1458
|
+
else add(facet, "warning", `${host} paints this brush without ${what}.`, `Nothing to do; or ${fix.charAt(0).toLowerCase()}${fix.slice(1)}`);
|
|
1459
|
+
};
|
|
1460
|
+
if (!profile.kinds.includes(brush.kind)) add("kind", "error", `${host} has no ${brush.kind} brushes.`, `Use a ${list(profile.kinds)} brush.`);
|
|
1461
|
+
else if (brush.kind === "tool" && brush.tool && !profile.tools.includes(brush.tool)) {
|
|
1462
|
+
add("tool", "error", `${host} has no ${brush.tool} tool.`, profile.tools.length ? `Use one of: ${list(profile.tools)}.` : `${host} has no tools; use a mark brush.`);
|
|
1463
|
+
}
|
|
1464
|
+
if (!profile.tipShapes.includes(brush.tip.shape)) add("tip.shape", "error", `${host} can't lay a ${brush.tip.shape} tip.`, `Set tip.shape to one of: ${list(profile.tipShapes)}.`);
|
|
1465
|
+
const marks = _nullishCoalesce(_optionalChain([brush, 'access', _27 => _27.marks, 'optionalAccess', _28 => _28.kind]), () => ( "continuous"));
|
|
1466
|
+
if (!profile.marks.includes(marks)) add("marks", "error", `${host} can't lay ${marks} marks.`, `Use marks ${list(profile.marks)} (remove marks for continuous dabs).`);
|
|
1467
|
+
const surface = _nullishCoalesce(_optionalChain([brush, 'access', _29 => _29.behaviour, 'optionalAccess', _30 => _30.surface]), () => ( "dry"));
|
|
1468
|
+
if (brush.kind === "mark" && !profile.surfaces.includes(surface)) add("behaviour.surface", "error", `${host} has no ${surface} surface.`, `Set behaviour.surface to ${list(profile.surfaces)}.`);
|
|
1469
|
+
const ex = _optionalChain([brush, 'access', _31 => _31.behaviour, 'optionalAccess', _32 => _32.exchange]);
|
|
1470
|
+
feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess', _33 => _33.reservoir]), "behaviour.exchange.reservoir", "a Mixer Brush reservoir", "Remove behaviour.exchange.reservoir.");
|
|
1471
|
+
feature(profile.exchange, !_optionalChain([ex, 'optionalAccess', _34 => _34.reservoir]) && (nonZero(_optionalChain([ex, 'optionalAccess', _35 => _35.pickup])) || !!_optionalChain([ex, 'optionalAccess', _36 => _36.dirty])), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
|
|
1472
|
+
feature(profile.grain, nonZero(_optionalChain([brush, 'access', _37 => _37.behaviour, 'optionalAccess', _38 => _38.grain])), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
|
|
1473
|
+
feature(profile.light, nonZero(_optionalChain([brush, 'access', _39 => _39.light, 'optionalAccess', _40 => _40.amount])), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
|
|
1474
|
+
const m = brush.motion;
|
|
1475
|
+
feature(profile.motion, !!m && (!!m.live || nonZero(m.waveSpeed) || m.fps !== void 0), "motion", "animation", "Remove motion.");
|
|
1476
|
+
const d = _nullishCoalesce(brush.dynamics, () => ( {}));
|
|
1477
|
+
feature(profile.velocity, nonZero(d.velocitySize) || nonZero(d.velocityOpacity), "dynamics.velocity", "velocity dynamics (its strokes carry no timing)", "Set dynamics.velocitySize and velocityOpacity to 0.");
|
|
1478
|
+
feature(profile.tilt, nonZero(d.tiltSize) || nonZero(d.tiltRoundness) || nonZero(d.tiltOpacity), "dynamics.tilt", "tilt dynamics", "Set dynamics.tiltSize, tiltRoundness and tiltOpacity to 0.");
|
|
1479
|
+
const colour = brush.colour;
|
|
1480
|
+
if (_optionalChain([colour, 'optionalAccess', _41 => _41.default]) !== void 0) {
|
|
1481
|
+
const kind = brushColourKind(colour.default);
|
|
1482
|
+
if (!profile.colours.includes(kind)) add("colour.default", "error", `${host} can't paint ${kind} colours.`, `Use a ${list(profile.colours)} colour, or remove colour.default.`);
|
|
1483
|
+
}
|
|
1484
|
+
const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _42 => _42.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
|
|
1485
|
+
if (foreign.length) add("colour.palette", "warning", `${host} skips the palette's ${list(foreign)} colours.`, `Nothing to do; or keep to ${list(profile.colours)} colours.`);
|
|
1486
|
+
return { ok: !issues.some((i) => i.severity === "error"), issues };
|
|
1487
|
+
}
|
|
1488
|
+
function listPresets(profile, presets = BRUSH_PRESETS) {
|
|
1489
|
+
return presets.filter((b) => checkBrush(b, profile).ok);
|
|
1490
|
+
}
|
|
1491
|
+
function degrade(brush, profile) {
|
|
1492
|
+
const before = checkBrush(brush, profile).issues.filter((i) => i.severity === "error");
|
|
1493
|
+
const b = JSON.parse(JSON.stringify(brush));
|
|
1494
|
+
const dropped = [];
|
|
1495
|
+
for (const issue of before) {
|
|
1496
|
+
switch (issue.facet) {
|
|
1497
|
+
case "tip.shape":
|
|
1498
|
+
b.tip = { ...b.tip, shape: profile.tipShapes.includes("round") ? "round" : profile.tipShapes[0] };
|
|
1499
|
+
delete b.tip.bristle;
|
|
1500
|
+
delete b.tip.stamps;
|
|
1501
|
+
break;
|
|
1502
|
+
case "marks":
|
|
1503
|
+
delete b.marks;
|
|
1504
|
+
if (b.tip.shape === "stamp") {
|
|
1505
|
+
b.tip.shape = "round";
|
|
1506
|
+
delete b.tip.stamps;
|
|
1507
|
+
}
|
|
1508
|
+
break;
|
|
1509
|
+
case "behaviour.surface":
|
|
1510
|
+
delete b.behaviour.surface;
|
|
1511
|
+
delete b.behaviour.wet;
|
|
1512
|
+
delete b.behaviour.thick;
|
|
1513
|
+
break;
|
|
1514
|
+
case "behaviour.exchange.reservoir":
|
|
1515
|
+
delete b.behaviour.exchange.reservoir;
|
|
1516
|
+
break;
|
|
1517
|
+
case "behaviour.exchange":
|
|
1518
|
+
delete b.behaviour.exchange;
|
|
1519
|
+
break;
|
|
1520
|
+
case "behaviour.grain":
|
|
1521
|
+
delete b.behaviour.grain;
|
|
1522
|
+
break;
|
|
1523
|
+
case "light":
|
|
1524
|
+
delete b.light;
|
|
1525
|
+
break;
|
|
1526
|
+
case "motion":
|
|
1527
|
+
delete b.motion;
|
|
1528
|
+
break;
|
|
1529
|
+
case "dynamics.velocity":
|
|
1530
|
+
delete b.dynamics.velocitySize;
|
|
1531
|
+
delete b.dynamics.velocityOpacity;
|
|
1532
|
+
break;
|
|
1533
|
+
case "dynamics.tilt":
|
|
1534
|
+
delete b.dynamics.tiltSize;
|
|
1535
|
+
delete b.dynamics.tiltRoundness;
|
|
1536
|
+
delete b.dynamics.tiltOpacity;
|
|
1537
|
+
break;
|
|
1538
|
+
case "colour.default":
|
|
1539
|
+
delete b.colour.default;
|
|
1540
|
+
break;
|
|
1541
|
+
default:
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
dropped.push(issue);
|
|
1545
|
+
}
|
|
1546
|
+
if (_optionalChain([b, 'access', _43 => _43.behaviour, 'optionalAccess', _44 => _44.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
|
|
1547
|
+
const after = checkBrush(b, profile).issues.filter((i) => i.severity === "error");
|
|
1548
|
+
return { brush: b, dropped, issues: after };
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/brushes/library.ts
|
|
1552
|
+
var BrushLibraryError = class extends Error {
|
|
1553
|
+
constructor(code, message) {
|
|
1554
|
+
super(message);
|
|
1555
|
+
this.code = code;
|
|
1556
|
+
this.name = "BrushLibraryError";
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
};
|
|
1560
|
+
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1561
|
+
var isV1 = (item) => "engine" in item || "category" in item || !("kind" in item);
|
|
1562
|
+
function parseBrushLibrary(raw, problems = []) {
|
|
1563
|
+
if (typeof raw === "string") {
|
|
1564
|
+
try {
|
|
1565
|
+
raw = JSON.parse(raw);
|
|
1566
|
+
} catch (e3) {
|
|
1567
|
+
throw new BrushLibraryError("invalid", "Not a brush library: the text is not JSON.");
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const file = isObject2(raw) ? raw : null;
|
|
1571
|
+
const items = Array.isArray(raw) ? raw : file && Array.isArray(file.brushes) ? file.brushes : null;
|
|
1572
|
+
if (!items) throw new BrushLibraryError("invalid", 'Not a brush library: expected {"format":"pixerate-brushes","version":2,"brushes":[...]} or an array of brushes.');
|
|
1573
|
+
if (file && file.version !== void 0 && file.version !== 1 && file.version !== 2) {
|
|
1574
|
+
throw new BrushLibraryError("invalid", `Brush library version ${JSON.stringify(file.version)} is not supported; this library reads versions 1 and 2.`);
|
|
1575
|
+
}
|
|
1576
|
+
const v1File = _optionalChain([file, 'optionalAccess', _45 => _45.version]) === 1;
|
|
1577
|
+
const out = [];
|
|
1578
|
+
items.forEach((item, i) => {
|
|
1579
|
+
const at = `brushes[${i}]`;
|
|
1580
|
+
if (!isObject2(item)) return void problems.push(`${at} is not an object; skipped.`);
|
|
1581
|
+
if (typeof item.id !== "string" || !_chunkHS6QDWDFcjs.BRUSH_ID.test(item.id)) return void problems.push(`${at} has no valid id (lowercase letters, digits and dashes); skipped.`);
|
|
1582
|
+
if (typeof item.name !== "string" || !item.name.trim()) return void problems.push(`${at} ("${item.id}") has no name; skipped.`);
|
|
1583
|
+
const { builtin: _b, ...rest } = item;
|
|
1584
|
+
const own = [];
|
|
1585
|
+
try {
|
|
1586
|
+
out.push(normalizeBrushSpec(v1File || isV1(rest) ? migrateBrushV1(rest) : rest, own));
|
|
1587
|
+
} catch (e) {
|
|
1588
|
+
own.push(`${e.message} Skipped.`);
|
|
1589
|
+
}
|
|
1590
|
+
problems.push(...own.map((p) => `${at} ("${item.id}"): ${p}`));
|
|
1591
|
+
});
|
|
1592
|
+
return out;
|
|
1593
|
+
}
|
|
1594
|
+
var words = (s) => s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
1595
|
+
function matches(entry, f2) {
|
|
1596
|
+
const b = entry.brush;
|
|
1597
|
+
if (f2.source && f2.source === "builtin" !== entry.builtin) return false;
|
|
1598
|
+
if (f2.kind && b.kind !== f2.kind) return false;
|
|
1599
|
+
if (f2.medium && b.medium !== f2.medium) return false;
|
|
1600
|
+
if (f2.variety && b.variety !== f2.variety) return false;
|
|
1601
|
+
if (f2.tool && b.tool !== f2.tool) return false;
|
|
1602
|
+
if (f2.tag && !(_nullishCoalesce(b.tags, () => ( []))).includes(f2.tag)) return false;
|
|
1603
|
+
if (f2.text) {
|
|
1604
|
+
const text = words([b.id, b.name, _nullishCoalesce(b.description, () => ( "")), _nullishCoalesce(b.variety, () => ( "")), _nullishCoalesce(b.medium, () => ( "")), _nullishCoalesce(b.tool, () => ( "")), ..._nullishCoalesce(b.tags, () => ( []))].join(" "));
|
|
1605
|
+
if (!words(f2.text).every((w) => text.some((t) => t.startsWith(w)))) return false;
|
|
1606
|
+
}
|
|
1607
|
+
if (f2.profile && !checkBrush(b, f2.profile).ok) return false;
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
var copy3 = (v) => JSON.parse(JSON.stringify(v));
|
|
1611
|
+
function createBrushLibrary(options = {}) {
|
|
1612
|
+
const presets = (_nullishCoalesce(options.presets, () => ( BRUSH_PRESETS))).map(copy3);
|
|
1613
|
+
const builtinIds = new Set(presets.map((b) => b.id));
|
|
1614
|
+
const key = `${_nullishCoalesce(options.namespace, () => ( "pixerate.brushes"))}:user`;
|
|
1615
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1616
|
+
let user = null;
|
|
1617
|
+
let queue = Promise.resolve();
|
|
1618
|
+
const run = (task) => {
|
|
1619
|
+
const result = queue.then(task, task);
|
|
1620
|
+
queue = result.catch(() => void 0);
|
|
1621
|
+
return result;
|
|
1622
|
+
};
|
|
1623
|
+
const notify = () => listeners.forEach((listener) => listener());
|
|
1624
|
+
const load = async () => {
|
|
1625
|
+
if (user) return user;
|
|
1626
|
+
const raw = options.storage ? await options.storage.get(key) : null;
|
|
1627
|
+
let brushes = [];
|
|
1628
|
+
if (raw) {
|
|
1629
|
+
try {
|
|
1630
|
+
brushes = parseBrushLibrary(raw).filter((b) => !builtinIds.has(b.id));
|
|
1631
|
+
} catch (e4) {
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
user = brushes;
|
|
1635
|
+
return user;
|
|
1636
|
+
};
|
|
1637
|
+
const store = async (brushes) => {
|
|
1638
|
+
const file = { format: "pixerate-brushes", version: 2, brushes };
|
|
1639
|
+
if (options.storage) await options.storage.set(key, JSON.stringify(file));
|
|
1640
|
+
user = brushes;
|
|
1641
|
+
};
|
|
1642
|
+
const entries = async () => [...presets.map((brush) => ({ brush, builtin: true })), ...(await load()).map((brush) => ({ brush, builtin: false }))];
|
|
1643
|
+
const find = async (id) => {
|
|
1644
|
+
const entry = (await entries()).find((e) => e.brush.id === id);
|
|
1645
|
+
if (!entry) throw new BrushLibraryError("unknown_brush", `No brush "${id}". ${closest(id, (await entries()).map((e) => e.brush.id))}list_brush_presets lists them.`);
|
|
1646
|
+
return entry;
|
|
1647
|
+
};
|
|
1648
|
+
const refuseBuiltin = (id, what) => {
|
|
1649
|
+
if (builtinIds.has(id)) throw new BrushLibraryError("builtin", `"${id}" is a built-in brush and can't be ${what}; duplicate it first (duplicate_brush) and change the copy.`);
|
|
1650
|
+
};
|
|
1651
|
+
const freeId = async (base) => {
|
|
1652
|
+
const taken = new Set((await entries()).map((e) => e.brush.id));
|
|
1653
|
+
const stem = base.slice(0, 58);
|
|
1654
|
+
for (let n = 1; ; n++) {
|
|
1655
|
+
const id = n === 1 ? `${stem}-copy` : `${stem}-copy-${n}`;
|
|
1656
|
+
if (!taken.has(id)) return id;
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
const saveLocked = async (raw, overwrite) => {
|
|
1660
|
+
const problems = [];
|
|
1661
|
+
let brush;
|
|
1662
|
+
try {
|
|
1663
|
+
brush = normalizeBrushSpec(raw, problems);
|
|
1664
|
+
} catch (e) {
|
|
1665
|
+
throw new BrushLibraryError("invalid", e.message);
|
|
1666
|
+
}
|
|
1667
|
+
refuseBuiltin(brush.id, "overwritten");
|
|
1668
|
+
const brushes = await load();
|
|
1669
|
+
const at = brushes.findIndex((b) => b.id === brush.id);
|
|
1670
|
+
if (at !== -1 && !overwrite) throw new BrushLibraryError("exists", `A brush "${brush.id}" exists already; pick another id, or update it (update_brush).`);
|
|
1671
|
+
await store(at === -1 ? [...brushes, brush] : brushes.map((b, i) => i === at ? brush : b));
|
|
1672
|
+
return { brush: copy3(brush), problems, replaced: at !== -1 };
|
|
1673
|
+
};
|
|
1674
|
+
return {
|
|
1675
|
+
list: (filter = {}) => run(async () => (await entries()).filter((e) => matches(e, filter)).map(copy3)),
|
|
1676
|
+
get: (id) => run(async () => {
|
|
1677
|
+
const entry = (await entries()).find((e) => e.brush.id === id);
|
|
1678
|
+
return entry ? copy3(entry) : null;
|
|
1679
|
+
}),
|
|
1680
|
+
isBuiltin: (id) => builtinIds.has(id),
|
|
1681
|
+
save: (brush, o = {}) => run(async () => {
|
|
1682
|
+
const result = await saveLocked(brush, !!o.overwrite);
|
|
1683
|
+
notify();
|
|
1684
|
+
return result;
|
|
1685
|
+
}),
|
|
1686
|
+
remove: (id) => run(async () => {
|
|
1687
|
+
refuseBuiltin(id, "removed");
|
|
1688
|
+
const brushes = await load();
|
|
1689
|
+
if (!brushes.some((b) => b.id === id)) return false;
|
|
1690
|
+
await store(brushes.filter((b) => b.id !== id));
|
|
1691
|
+
notify();
|
|
1692
|
+
return true;
|
|
1693
|
+
}),
|
|
1694
|
+
duplicate: (id, o = {}) => run(async () => {
|
|
1695
|
+
const { brush } = await find(id);
|
|
1696
|
+
const newId = await _asyncNullishCoalesce(o.id, async () => ( await freeId(id)));
|
|
1697
|
+
const { brush: saved } = await saveLocked({ ...copy3(brush), id: newId, name: _nullishCoalesce(o.name, () => ( `${brush.name} copy`.slice(0, 80))) }, false);
|
|
1698
|
+
notify();
|
|
1699
|
+
return saved;
|
|
1700
|
+
}),
|
|
1701
|
+
exportFile: (ids) => run(async () => {
|
|
1702
|
+
const brushes = ids ? await Promise.all(ids.map(async (id) => (await find(id)).brush)) : await load();
|
|
1703
|
+
return { format: "pixerate-brushes", version: 2, brushes: brushes.map(copy3) };
|
|
1704
|
+
}),
|
|
1705
|
+
importFile: (raw, o = {}) => run(async () => {
|
|
1706
|
+
const problems = [];
|
|
1707
|
+
const incoming = parseBrushLibrary(raw, problems);
|
|
1708
|
+
const result = { added: [], replaced: [], skipped: [], problems };
|
|
1709
|
+
let brushes = [...await load()];
|
|
1710
|
+
for (const brush of incoming) {
|
|
1711
|
+
if (builtinIds.has(brush.id)) {
|
|
1712
|
+
result.skipped.push(brush.id);
|
|
1713
|
+
problems.push(`"${brush.id}" is a built-in brush id; skipped (rename it to import it).`);
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
const at = brushes.findIndex((b) => b.id === brush.id);
|
|
1717
|
+
if (at === -1) {
|
|
1718
|
+
brushes.push(brush);
|
|
1719
|
+
result.added.push(brush.id);
|
|
1720
|
+
} else if (o.overwrite) {
|
|
1721
|
+
brushes = brushes.map((b, i) => i === at ? brush : b);
|
|
1722
|
+
result.replaced.push(brush.id);
|
|
1723
|
+
} else {
|
|
1724
|
+
result.skipped.push(brush.id);
|
|
1725
|
+
problems.push(`"${brush.id}" exists already; skipped (pass overwrite to replace it).`);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (result.added.length || result.replaced.length) {
|
|
1729
|
+
await store(brushes);
|
|
1730
|
+
notify();
|
|
1731
|
+
}
|
|
1732
|
+
return result;
|
|
1733
|
+
}),
|
|
1734
|
+
subscribe(listener) {
|
|
1735
|
+
listeners.add(listener);
|
|
1736
|
+
return () => {
|
|
1737
|
+
listeners.delete(listener);
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function closest(id, ids, max = 3) {
|
|
1743
|
+
const stem = id.toLowerCase();
|
|
1744
|
+
const score = (c) => {
|
|
1745
|
+
let n = 0;
|
|
1746
|
+
while (n < Math.min(c.length, stem.length) && c[n] === stem[n]) n++;
|
|
1747
|
+
return n + (c.includes(stem) || stem.includes(c) ? 4 : 0) + words(c).filter((w) => words(stem).includes(w)).length * 3;
|
|
1748
|
+
};
|
|
1749
|
+
const near = ids.map((c) => [c, score(c)]).filter(([, s]) => s >= 3).sort((a, b) => b[1] - a[1]).slice(0, max).map(([c]) => c);
|
|
1750
|
+
return near.length ? `Closest: ${near.join(", ")}. ` : "";
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/brushes/tools.ts
|
|
1754
|
+
var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
1755
|
+
var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
1756
|
+
var FACETS = BRUSH_JSON_SCHEMA.properties;
|
|
1757
|
+
var ID = { type: "string", pattern: String(FACETS.id.pattern), description: "Brush id from list_brush_presets (lowercase letters, digits and dashes)." };
|
|
1758
|
+
var FACET_PROPS = (({ id: _id, name: _name, ...rest }) => rest)(FACETS);
|
|
1759
|
+
FACET_PROPS.tip = (({ required: _r, ...tip }) => ({ ...tip, description: `${tip.description} In a patch, shape may be left out to keep it.` }))(FACET_PROPS.tip);
|
|
1760
|
+
var UNSETTABLE = ["description", "tags", "medium", "variety", "marks", "dynamics", "behaviour", "light", "motion", "colour"];
|
|
1761
|
+
var HOSTS = Object.keys(BRUSH_HOST_PROFILES);
|
|
1762
|
+
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1763
|
+
var copy4 = (v) => JSON.parse(JSON.stringify(v));
|
|
1764
|
+
function merge(base, patch, path = "") {
|
|
1765
|
+
const out = { ...base };
|
|
1766
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
1767
|
+
if (v === void 0) continue;
|
|
1768
|
+
const old = out[k];
|
|
1769
|
+
const replaceMarks = path === "" && k === "marks" && isObject3(old) && isObject3(v) && old.kind !== v.kind;
|
|
1770
|
+
out[k] = isObject3(v) && isObject3(old) && !replaceMarks ? merge(old, v, `${path}${k}.`) : copy4(v);
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1774
|
+
function changedPaths(a, b, path = "") {
|
|
1775
|
+
if (isObject3(a) && isObject3(b)) return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].flatMap((k) => changedPaths(a[k], b[k], path ? `${path}.${k}` : k));
|
|
1776
|
+
return JSON.stringify(a) === JSON.stringify(b) ? [] : [path];
|
|
1777
|
+
}
|
|
1778
|
+
function summary(entry, profile, withCheck) {
|
|
1779
|
+
const b = entry.brush;
|
|
1780
|
+
const engine = resolveEngine(b);
|
|
1781
|
+
const check = withCheck ? checkBrush(b, profile) : null;
|
|
1782
|
+
return {
|
|
1783
|
+
id: b.id,
|
|
1784
|
+
name: b.name,
|
|
1785
|
+
builtin: entry.builtin,
|
|
1786
|
+
kind: b.kind,
|
|
1787
|
+
...b.medium ? { medium: b.medium } : {},
|
|
1788
|
+
...b.variety ? { variety: b.variety } : {},
|
|
1789
|
+
...b.tool ? { tool: b.tool } : {},
|
|
1790
|
+
category: resolveCategory(b, engine),
|
|
1791
|
+
engine,
|
|
1792
|
+
tip: b.tip.shape,
|
|
1793
|
+
...b.tip.size !== void 0 ? { size: b.tip.size } : {},
|
|
1794
|
+
marks: _nullishCoalesce(_optionalChain([b, 'access', _46 => _46.marks, 'optionalAccess', _47 => _47.kind]), () => ( "continuous")),
|
|
1795
|
+
surface: _nullishCoalesce(_optionalChain([b, 'access', _48 => _48.behaviour, 'optionalAccess', _49 => _49.surface]), () => ( "dry")),
|
|
1796
|
+
..._optionalChain([b, 'access', _50 => _50.light, 'optionalAccess', _51 => _51.amount]) ? { light: b.light.amount } : {},
|
|
1797
|
+
..._optionalChain([b, 'access', _52 => _52.tags, 'optionalAccess', _53 => _53.length]) ? { tags: b.tags } : {},
|
|
1798
|
+
...check ? { renderable: check.ok } : {}
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
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.)`;
|
|
1802
|
+
var warnings = (check) => check.issues.filter((i) => i.severity === "warning");
|
|
1803
|
+
function createBrushLibraryTools(library, options = {}) {
|
|
1804
|
+
const profile = _nullishCoalesce(options.profile, () => ( PAINT_PROFILE));
|
|
1805
|
+
const hostOf = (id) => {
|
|
1806
|
+
if (id === void 0) return profile;
|
|
1807
|
+
const p = BRUSH_HOST_PROFILES[id];
|
|
1808
|
+
if (!p) throw new BrushLibraryError("invalid", `No host "${id}"; use one of: ${HOSTS.join(", ")}.`);
|
|
1809
|
+
return p;
|
|
1810
|
+
};
|
|
1811
|
+
const entry = async (id) => {
|
|
1812
|
+
const found = await library.get(id);
|
|
1813
|
+
if (found) return found;
|
|
1814
|
+
const ids = (await library.list()).map((e) => e.brush.id);
|
|
1815
|
+
throw new BrushLibraryError("unknown_brush", `No brush "${id}". ${closest(id, ids)}list_brush_presets lists them.`);
|
|
1816
|
+
};
|
|
1817
|
+
const store = async (raw, overwrite) => {
|
|
1818
|
+
const problems = [];
|
|
1819
|
+
let brush;
|
|
1820
|
+
try {
|
|
1821
|
+
brush = normalizeBrushSpec(raw, problems);
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
throw new BrushLibraryError("invalid", e.message);
|
|
1824
|
+
}
|
|
1825
|
+
const check2 = checkBrush(brush, profile);
|
|
1826
|
+
if (!check2.ok) throw new BrushLibraryError("invalid", refusal(check2, profile));
|
|
1827
|
+
const saved = await library.save(brush, { overwrite });
|
|
1828
|
+
return { ...saved, problems, check: check2 };
|
|
1829
|
+
};
|
|
1830
|
+
const list2 = {
|
|
1831
|
+
name: "list_brush_presets",
|
|
1832
|
+
title: "List brushes",
|
|
1833
|
+
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.`,
|
|
1834
|
+
inputSchema: {
|
|
1835
|
+
type: "object",
|
|
1836
|
+
properties: {
|
|
1837
|
+
kind: { type: "string", enum: [...BRUSH_KINDS], description: "mark (lays a material) or tool (works on paint already down)." },
|
|
1838
|
+
medium: { type: "string", enum: [...BRUSH_MEDIA], description: "Marks laying this medium." },
|
|
1839
|
+
variety: { type: "string", maxLength: 40, description: "This variety (oil, watercolor, pencil, highlighter\u2026)." },
|
|
1840
|
+
tool: { type: "string", enum: [...BRUSH_TOOLS], description: "Tools that do this." },
|
|
1841
|
+
tag: { type: "string", maxLength: 40, description: "Brushes with this tag." },
|
|
1842
|
+
text: { type: "string", maxLength: 200, description: "Words in the id, name, description, variety or tags (each word must match the start of one)." },
|
|
1843
|
+
source: { type: "string", enum: ["all", "builtin", "user"], description: "all (default), builtin presets only, or user (saved) brushes only." },
|
|
1844
|
+
all: { type: "boolean", description: "Also list brushes this host can't render, each with renderable false (default false)." },
|
|
1845
|
+
offset: { type: "integer", minimum: 0, description: "Skip this many (default 0)." },
|
|
1846
|
+
limit: { type: "integer", minimum: 1, maximum: 200, description: "At most this many (default 100)." }
|
|
1847
|
+
},
|
|
1848
|
+
additionalProperties: false
|
|
1849
|
+
},
|
|
1850
|
+
annotations: { title: "List brushes", ...READ },
|
|
1851
|
+
handler: async (a) => {
|
|
1852
|
+
const source = a.source === "all" ? void 0 : a.source;
|
|
1853
|
+
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 } });
|
|
1854
|
+
const offset = _nullishCoalesce(a.offset, () => ( 0));
|
|
1855
|
+
const page = found.slice(offset, offset + (_nullishCoalesce(a.limit, () => ( 100))));
|
|
1856
|
+
return { host: profile.id, total: found.length, brushes: page.map((e) => summary(e, profile, !!a.all)) };
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
const describe = {
|
|
1860
|
+
name: "describe_brush",
|
|
1861
|
+
title: "Describe a brush",
|
|
1862
|
+
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.`,
|
|
1863
|
+
inputSchema: { type: "object", properties: { id: ID }, required: ["id"], additionalProperties: false },
|
|
1864
|
+
annotations: { title: "Describe a brush", ...READ },
|
|
1865
|
+
handler: async ({ id }) => {
|
|
1866
|
+
const e = await entry(id);
|
|
1867
|
+
const engine = resolveEngine(e.brush);
|
|
1868
|
+
return { brush: e.brush, builtin: e.builtin, engine, category: resolveCategory(e.brush, engine), check: checkBrush(e.brush, profile) };
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
const check = {
|
|
1872
|
+
name: "check_brush",
|
|
1873
|
+
title: "Check a brush for a host",
|
|
1874
|
+
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.`,
|
|
1875
|
+
inputSchema: {
|
|
1876
|
+
type: "object",
|
|
1877
|
+
properties: {
|
|
1878
|
+
id: { ...ID, description: "A brush in the library (give id or brush, not both)." },
|
|
1879
|
+
brush: { type: "object", description: "A facet brush spec (the create_brush fields, with id and name)." },
|
|
1880
|
+
host: { type: "string", enum: HOSTS, description: `The host profile (default ${profile.id}).` }
|
|
1881
|
+
},
|
|
1882
|
+
additionalProperties: false
|
|
1883
|
+
},
|
|
1884
|
+
annotations: { title: "Check a brush for a host", ...READ },
|
|
1885
|
+
handler: async ({ id, brush, host }) => {
|
|
1886
|
+
if (id === void 0 === (brush === void 0)) throw new BrushLibraryError("invalid", "Give either id (a library brush) or brush (a spec), not both or neither.");
|
|
1887
|
+
const p = hostOf(host);
|
|
1888
|
+
let b;
|
|
1889
|
+
const problems = [];
|
|
1890
|
+
if (id !== void 0) b = (await entry(id)).brush;
|
|
1891
|
+
else {
|
|
1892
|
+
try {
|
|
1893
|
+
b = normalizeBrushSpec(brush, problems);
|
|
1894
|
+
} catch (e) {
|
|
1895
|
+
throw new BrushLibraryError("invalid", e.message);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
const result = checkBrush(b, p);
|
|
1899
|
+
return { host: p.id, ...result, ...problems.length ? { problems } : {}, ...!result.ok ? { degraded: degrade(b, p) } : {} };
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
const exportTool = {
|
|
1903
|
+
name: "export_brushes",
|
|
1904
|
+
title: "Export brushes",
|
|
1905
|
+
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.",
|
|
1906
|
+
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 },
|
|
1907
|
+
annotations: { title: "Export brushes", ...READ },
|
|
1908
|
+
handler: async ({ ids }) => ({ file: await library.exportFile(ids) })
|
|
1909
|
+
};
|
|
1910
|
+
const tools = [list2, describe, check, exportTool];
|
|
1911
|
+
if (options.renderPreview) {
|
|
1912
|
+
const render = options.renderPreview;
|
|
1913
|
+
tools.push({
|
|
1914
|
+
name: "preview_brush",
|
|
1915
|
+
title: "Preview a brush",
|
|
1916
|
+
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.",
|
|
1917
|
+
inputSchema: {
|
|
1918
|
+
type: "object",
|
|
1919
|
+
properties: {
|
|
1920
|
+
id: ID,
|
|
1921
|
+
width: { type: "integer", minimum: 32, maximum: 1024, description: "Image width in px (default 256)." },
|
|
1922
|
+
height: { type: "integer", minimum: 16, maximum: 512, description: "Image height in px (default 96)." },
|
|
1923
|
+
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Paint colour #rrggbb (default the brush's own default colour, else #1f2937)." }
|
|
1924
|
+
},
|
|
1925
|
+
required: ["id"],
|
|
1926
|
+
additionalProperties: false
|
|
1927
|
+
},
|
|
1928
|
+
annotations: { title: "Preview a brush", ...READ },
|
|
1929
|
+
handler: async ({ id, width, height, color }) => {
|
|
1930
|
+
const { brush } = await entry(id);
|
|
1931
|
+
const own = typeof _optionalChain([brush, 'access', _54 => _54.colour, 'optionalAccess', _55 => _55.default]) === "string" && /^#[0-9a-f]{6}/i.test(brush.colour.default) ? brush.colour.default.slice(0, 7) : void 0;
|
|
1932
|
+
const w = _nullishCoalesce(width, () => ( 256));
|
|
1933
|
+
const h = _nullishCoalesce(height, () => ( 96));
|
|
1934
|
+
const image = await render(brush, { width: w, height: h, color: _nullishCoalesce(_nullishCoalesce(color, () => ( own)), () => ( "#1f2937")) });
|
|
1935
|
+
return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }], structuredContent: { id, width: w, height: h } };
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
if (options.readOnly) return tools;
|
|
1940
|
+
tools.push(
|
|
1941
|
+
{
|
|
1942
|
+
name: "create_brush",
|
|
1943
|
+
title: "Create a brush",
|
|
1944
|
+
description: `Saves a new brush made of facets, 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.`,
|
|
1945
|
+
inputSchema: {
|
|
1946
|
+
type: "object",
|
|
1947
|
+
properties: {
|
|
1948
|
+
id: { ...FACETS.id, description: "The new brush id: lowercase letters, digits and dashes, not taken (list_brush_presets)." },
|
|
1949
|
+
name: FACETS.name,
|
|
1950
|
+
basedOn: { ...ID, description: "Copy this brush's facets first (a built-in preset or a saved brush)." },
|
|
1951
|
+
...FACET_PROPS
|
|
1952
|
+
},
|
|
1953
|
+
required: ["id", "name"],
|
|
1954
|
+
additionalProperties: false
|
|
1955
|
+
},
|
|
1956
|
+
annotations: { title: "Create a brush", ...WRITE },
|
|
1957
|
+
handler: async ({ basedOn, ...spec }) => {
|
|
1958
|
+
if (library.isBuiltin(spec.id)) throw new BrushLibraryError("builtin", `"${spec.id}" is a built-in brush id; pick another id (or basedOn "${spec.id}" with a new id).`);
|
|
1959
|
+
if (await library.get(spec.id)) throw new BrushLibraryError("exists", `A brush "${spec.id}" exists already; pick another id, or change it with update_brush.`);
|
|
1960
|
+
const base = basedOn !== void 0 ? (await entry(basedOn)).brush : void 0;
|
|
1961
|
+
const raw = base ? merge(base, spec) : spec;
|
|
1962
|
+
const saved = await store(raw, false);
|
|
1963
|
+
return { id: saved.brush.id, brush: saved.brush, engine: resolveEngine(saved.brush), problems: saved.problems, warnings: warnings(saved.check) };
|
|
1964
|
+
}
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
name: "update_brush",
|
|
1968
|
+
title: "Update a brush",
|
|
1969
|
+
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.`,
|
|
1970
|
+
inputSchema: {
|
|
1971
|
+
type: "object",
|
|
1972
|
+
properties: {
|
|
1973
|
+
id: { ...ID, description: "The saved brush to change (not a built-in)." },
|
|
1974
|
+
name: FACETS.name,
|
|
1975
|
+
...FACET_PROPS,
|
|
1976
|
+
unset: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", enum: [...UNSETTABLE], description: "A facet to remove." }, description: "Facets to remove (applied before the facets given)." }
|
|
1977
|
+
},
|
|
1978
|
+
required: ["id"],
|
|
1979
|
+
additionalProperties: false
|
|
1980
|
+
},
|
|
1981
|
+
annotations: { title: "Update a brush", ...WRITE, idempotentHint: true },
|
|
1982
|
+
handler: async ({ id, unset, ...patch }) => {
|
|
1983
|
+
const e = await entry(id);
|
|
1984
|
+
if (e.builtin) throw new BrushLibraryError("builtin", `"${id}" is a built-in brush and can't be changed; duplicate_brush makes a copy you can change.`);
|
|
1985
|
+
const base = copy4(e.brush);
|
|
1986
|
+
for (const k of _nullishCoalesce(unset, () => ( []))) delete base[k];
|
|
1987
|
+
const saved = await store(merge(base, { ...patch, id }), true);
|
|
1988
|
+
return { id, brush: saved.brush, changed: changedPaths(e.brush, saved.brush), problems: saved.problems, warnings: warnings(saved.check) };
|
|
1989
|
+
}
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
name: "duplicate_brush",
|
|
1993
|
+
title: "Duplicate a brush",
|
|
1994
|
+
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.",
|
|
1995
|
+
inputSchema: {
|
|
1996
|
+
type: "object",
|
|
1997
|
+
properties: {
|
|
1998
|
+
id: ID,
|
|
1999
|
+
newId: { ...FACETS.id, description: "Id for the copy (default <id>-copy, -copy-2\u2026); must not be taken." },
|
|
2000
|
+
name: { ...FACETS.name, description: 'Name for the copy (default "<name> copy").' }
|
|
2001
|
+
},
|
|
2002
|
+
required: ["id"],
|
|
2003
|
+
additionalProperties: false
|
|
2004
|
+
},
|
|
2005
|
+
annotations: { title: "Duplicate a brush", ...WRITE },
|
|
2006
|
+
handler: async ({ id, newId, name }) => {
|
|
2007
|
+
const brush = await library.duplicate(id, { ...newId !== void 0 ? { id: newId } : {}, ...name !== void 0 ? { name } : {} });
|
|
2008
|
+
return { id: brush.id, brush };
|
|
2009
|
+
}
|
|
2010
|
+
},
|
|
2011
|
+
{
|
|
2012
|
+
name: "remove_brush",
|
|
2013
|
+
title: "Remove a brush",
|
|
2014
|
+
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.",
|
|
2015
|
+
inputSchema: { type: "object", properties: { id: { ...ID, description: "The saved brush to remove (not a built-in)." } }, required: ["id"], additionalProperties: false },
|
|
2016
|
+
annotations: { title: "Remove a brush", ...WRITE, destructiveHint: true },
|
|
2017
|
+
handler: async ({ id }) => {
|
|
2018
|
+
await entry(id);
|
|
2019
|
+
await library.remove(id);
|
|
2020
|
+
return { removed: id };
|
|
2021
|
+
}
|
|
2022
|
+
},
|
|
2023
|
+
{
|
|
2024
|
+
name: "import_brushes",
|
|
2025
|
+
title: "Import brushes",
|
|
2026
|
+
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).",
|
|
2027
|
+
inputSchema: {
|
|
2028
|
+
type: "object",
|
|
2029
|
+
properties: {
|
|
2030
|
+
file: { type: ["object", "array"], description: "The library file object, or an array of brushes." },
|
|
2031
|
+
overwrite: { type: "boolean", description: "Replace saved brushes with the same id (default false: skip them)." }
|
|
2032
|
+
},
|
|
2033
|
+
required: ["file"],
|
|
2034
|
+
additionalProperties: false
|
|
2035
|
+
},
|
|
2036
|
+
annotations: { title: "Import brushes", ...WRITE, destructiveHint: true },
|
|
2037
|
+
handler: async ({ file, overwrite }) => library.importFile(file, { overwrite: !!overwrite })
|
|
2038
|
+
}
|
|
2039
|
+
);
|
|
2040
|
+
return tools;
|
|
2041
|
+
}
|
|
2042
|
+
function createBrushLibraryToolProvider(library, options = {}) {
|
|
2043
|
+
const profile = _nullishCoalesce(options.profile, () => ( PAINT_PROFILE));
|
|
2044
|
+
const tools = createBrushLibraryTools(library, options);
|
|
2045
|
+
return {
|
|
2046
|
+
id: _nullishCoalesce(options.id, () => ( "brushes")),
|
|
2047
|
+
namespace: options.namespace,
|
|
2048
|
+
title: "Brush library",
|
|
2049
|
+
description: `The shared brush library: find, describe, create, change, check and share brushes made of facets, for ${profile.name}.`,
|
|
2050
|
+
skill: "pixerate-brushes",
|
|
2051
|
+
tools
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
|
|
2057
|
+
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
|
|
2064
|
+
|
|
2065
|
+
|
|
2066
|
+
|
|
2067
|
+
|
|
2068
|
+
|
|
1388
2069
|
|
|
1389
2070
|
|
|
1390
2071
|
|
|
@@ -1403,4 +2084,4 @@ var BRUSH_PRESETS = [
|
|
|
1403
2084
|
|
|
1404
2085
|
|
|
1405
2086
|
|
|
1406
|
-
exports.BRUSH_FACET_SCHEMAS = BRUSH_FACET_SCHEMAS; exports.BRUSH_JSON_SCHEMA = BRUSH_JSON_SCHEMA; exports.BRUSH_KINDS = BRUSH_KINDS; exports.BRUSH_MARKS_KINDS = BRUSH_MARKS_KINDS; exports.BRUSH_MEDIA = BRUSH_MEDIA; exports.BRUSH_PRESETS = BRUSH_PRESETS; exports.BRUSH_SURFACES = BRUSH_SURFACES; exports.BRUSH_TIP_SHAPES = BRUSH_TIP_SHAPES; exports.BRUSH_TOOLS = BRUSH_TOOLS; exports.BRUSH_WORD = BRUSH_WORD; exports.MAX_BRUSH_PALETTE = MAX_BRUSH_PALETTE; exports.WET_BRUSH_TOOLS = WET_BRUSH_TOOLS; exports.migrateBrushV1 = migrateBrushV1; exports.normalizeBrushSpec = normalizeBrushSpec; exports.resolveBrush = resolveBrush; exports.resolveCategory = resolveCategory; exports.resolveEngine = resolveEngine; exports.resolveWetMedium = resolveWetMedium;
|
|
2087
|
+
exports.BRUSH_FACET_SCHEMAS = BRUSH_FACET_SCHEMAS; exports.BRUSH_HOST_PROFILES = BRUSH_HOST_PROFILES; exports.BRUSH_JSON_SCHEMA = BRUSH_JSON_SCHEMA; exports.BRUSH_KINDS = BRUSH_KINDS; exports.BRUSH_MARKS_KINDS = BRUSH_MARKS_KINDS; exports.BRUSH_MEDIA = BRUSH_MEDIA; exports.BRUSH_PRESETS = BRUSH_PRESETS; exports.BRUSH_SURFACES = BRUSH_SURFACES; exports.BRUSH_TIP_SHAPES = BRUSH_TIP_SHAPES; exports.BRUSH_TOOLS = BRUSH_TOOLS; exports.BRUSH_WORD = BRUSH_WORD; exports.BrushLibraryError = BrushLibraryError; exports.CANVAS_PROFILE = CANVAS_PROFILE; exports.IMAGE_PROFILE = IMAGE_PROFILE; exports.MAX_BRUSH_PALETTE = MAX_BRUSH_PALETTE; exports.PAINT_PROFILE = PAINT_PROFILE; exports.VECTOR_PROFILE = VECTOR_PROFILE; exports.WET_BRUSH_TOOLS = WET_BRUSH_TOOLS; exports.brushColourKind = brushColourKind; exports.checkBrush = checkBrush; exports.createBrushLibrary = createBrushLibrary; exports.createBrushLibraryToolProvider = createBrushLibraryToolProvider; exports.createBrushLibraryTools = createBrushLibraryTools; exports.degrade = degrade; exports.listPresets = listPresets; exports.migrateBrushV1 = migrateBrushV1; exports.normalizeBrushSpec = normalizeBrushSpec; exports.parseBrushLibrary = parseBrushLibrary; exports.resolveBrush = resolveBrush; exports.resolveCategory = resolveCategory; exports.resolveEngine = resolveEngine; exports.resolveWetMedium = resolveWetMedium;
|