makeables 0.6.0 → 0.7.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/{chunk-4AJQBAFB.js → chunk-QJHEOZGN.js} +1954 -236
- package/dist/cli.js +383 -33
- package/dist/{mcp-LXTO7XOO.js → mcp-AOJ6UJSC.js} +2 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
cardStyles,
|
|
9
9
|
checkSubmittedSvg,
|
|
10
10
|
connectAccount,
|
|
11
|
+
createStickerSheet,
|
|
11
12
|
defaultStickerMaterial,
|
|
12
13
|
designCatalog,
|
|
13
14
|
designProfile,
|
|
@@ -22,16 +23,27 @@ import {
|
|
|
22
23
|
newBadge,
|
|
23
24
|
newCard,
|
|
24
25
|
openBrowser,
|
|
26
|
+
packSheet,
|
|
25
27
|
packageJson,
|
|
28
|
+
placeNear,
|
|
29
|
+
prepareSheetPrint,
|
|
26
30
|
readBadge,
|
|
27
31
|
readPackageJson,
|
|
28
32
|
removeBadgeLayer,
|
|
29
33
|
renderBadge,
|
|
30
34
|
setBadgeLayer,
|
|
35
|
+
sheetPrintPdf,
|
|
36
|
+
sheetSizes,
|
|
37
|
+
sheetSpec,
|
|
38
|
+
sheetTypeIds,
|
|
39
|
+
sheetTypes,
|
|
31
40
|
side,
|
|
32
41
|
skills,
|
|
33
42
|
startStudio,
|
|
43
|
+
stickerCut,
|
|
44
|
+
stickerCutSvg,
|
|
34
45
|
stickerMaterialSchema,
|
|
46
|
+
stickerSheetSchema,
|
|
35
47
|
stickerStyles,
|
|
36
48
|
studioRequest,
|
|
37
49
|
submitDesign,
|
|
@@ -40,15 +52,15 @@ import {
|
|
|
40
52
|
svgNodeXml,
|
|
41
53
|
version,
|
|
42
54
|
writeAtomic
|
|
43
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-QJHEOZGN.js";
|
|
44
56
|
|
|
45
57
|
// src/cli.ts
|
|
46
58
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
47
59
|
import { constants as constants2 } from "fs";
|
|
48
|
-
import { access, lstat as lstat2, open as open2, readFile as
|
|
49
|
-
import { dirname as dirname3, extname as extname4, resolve as
|
|
60
|
+
import { access, lstat as lstat2, open as open2, readFile as readFile7 } from "fs/promises";
|
|
61
|
+
import { dirname as dirname3, extname as extname4, resolve as resolve6 } from "path";
|
|
50
62
|
import { parseArgs } from "util";
|
|
51
|
-
import
|
|
63
|
+
import sharp8 from "sharp";
|
|
52
64
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
53
65
|
|
|
54
66
|
// src/card-import.ts
|
|
@@ -908,10 +920,195 @@ async function referenceImage(command, file, options, out, reference) {
|
|
|
908
920
|
}
|
|
909
921
|
}
|
|
910
922
|
|
|
923
|
+
// src/sheet.ts
|
|
924
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
925
|
+
import { resolve as resolve4 } from "path";
|
|
926
|
+
import { deflateSync } from "zlib";
|
|
927
|
+
import { createCanvas } from "@napi-rs/canvas";
|
|
928
|
+
import sharp4 from "sharp";
|
|
929
|
+
async function readSheet(file) {
|
|
930
|
+
const value = JSON.parse(await readFile4(resolve4(file), "utf8"));
|
|
931
|
+
const parsed = stickerSheetSchema.safeParse(value);
|
|
932
|
+
if (!parsed.success)
|
|
933
|
+
throw new InputError(
|
|
934
|
+
`Invalid sheet: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`
|
|
935
|
+
);
|
|
936
|
+
return parsed.data;
|
|
937
|
+
}
|
|
938
|
+
function newSheet(type, size, name) {
|
|
939
|
+
if (typeof type !== "string" || !sheetTypeIds.includes(type))
|
|
940
|
+
throw new InputError(`--type must be one of ${sheetTypeIds.join(", ")}.`);
|
|
941
|
+
const sheet = createStickerSheet(
|
|
942
|
+
type,
|
|
943
|
+
typeof name === "string" ? name : void 0
|
|
944
|
+
);
|
|
945
|
+
if (size !== void 0) {
|
|
946
|
+
if (typeof size !== "string" || !(size in sheetSizes))
|
|
947
|
+
throw new InputError(`--size must be one of ${Object.keys(sheetSizes).join(", ")}.`);
|
|
948
|
+
sheet.size = size;
|
|
949
|
+
}
|
|
950
|
+
return parse(sheet);
|
|
951
|
+
}
|
|
952
|
+
function parse(sheet) {
|
|
953
|
+
const parsed = stickerSheetSchema.safeParse(sheet);
|
|
954
|
+
if (!parsed.success)
|
|
955
|
+
throw new InputError(parsed.error.issues.map((issue) => issue.message).join("; "));
|
|
956
|
+
return parsed.data;
|
|
957
|
+
}
|
|
958
|
+
async function readSticker(source) {
|
|
959
|
+
const style2 = stickerStyles.find((entry) => entry.source === source);
|
|
960
|
+
if (style2) return badgeDesignSchema.parse(structuredClone(style2));
|
|
961
|
+
const design = badgeDesignSchema.parse(JSON.parse(await readFile4(resolve4(source), "utf8")));
|
|
962
|
+
if (design.product !== "sticker") throw new InputError(`${source} is not a sticker design.`);
|
|
963
|
+
return design;
|
|
964
|
+
}
|
|
965
|
+
var artCache = /* @__PURE__ */ new Map();
|
|
966
|
+
function flatArt(design) {
|
|
967
|
+
const key = JSON.stringify(design);
|
|
968
|
+
let art = artCache.get(key);
|
|
969
|
+
if (!art) {
|
|
970
|
+
art = renderBadge(design, { face: "front", person: {} }).then(async ({ png }) => {
|
|
971
|
+
const { data, info } = await sharp4(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
972
|
+
return {
|
|
973
|
+
width: info.width,
|
|
974
|
+
height: info.height,
|
|
975
|
+
rgba: new Uint8ClampedArray(data.buffer, data.byteOffset, data.length)
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
artCache.set(key, art);
|
|
979
|
+
}
|
|
980
|
+
return art;
|
|
981
|
+
}
|
|
982
|
+
async function sheetArt(sheet) {
|
|
983
|
+
return new Map(
|
|
984
|
+
await Promise.all(
|
|
985
|
+
sheet.items.map(async (item) => [item.id, await flatArt(item.design)])
|
|
986
|
+
)
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
function bounds(sheet) {
|
|
990
|
+
const spec = sheetSpec(sheet);
|
|
991
|
+
return { widthMm: spec.widthMm ?? 0, heightMm: spec.heightMm ?? 0 };
|
|
992
|
+
}
|
|
993
|
+
async function addToSheet(sheet, source, widthMm) {
|
|
994
|
+
const type = sheetTypes[sheet.type];
|
|
995
|
+
if (type.cut === "die-cut" && sheet.items.length)
|
|
996
|
+
throw new InputError("A single sticker file holds one sticker. Start another sheet.");
|
|
997
|
+
const width = widthMm === void 0 ? type.stickerWidthMm : Number(widthMm);
|
|
998
|
+
if (!Number.isFinite(width) || width < 5 || width > 300)
|
|
999
|
+
throw new InputError("--width-mm must be a number from 5 to 300.");
|
|
1000
|
+
const design = await readSticker(source);
|
|
1001
|
+
const base = (design.source ?? "sticker").replace(/[^a-z0-9-]/g, "").slice(0, 40) || "sticker";
|
|
1002
|
+
let id = base;
|
|
1003
|
+
for (let n = 2; sheet.items.some((item2) => item2.id === id); n++) id = `${base}-${n}`;
|
|
1004
|
+
const spec = sheetSpec(sheet);
|
|
1005
|
+
const item = {
|
|
1006
|
+
id,
|
|
1007
|
+
design,
|
|
1008
|
+
x: (spec.widthMm ?? width) / 2,
|
|
1009
|
+
y: (spec.heightMm ?? width) / 2,
|
|
1010
|
+
rotation: 0,
|
|
1011
|
+
widthMm: width
|
|
1012
|
+
};
|
|
1013
|
+
const next = parse({ ...sheet, items: [...sheet.items, item] });
|
|
1014
|
+
if (type.cut === "die-cut") return { sheet: next, item, placed: true };
|
|
1015
|
+
const print = prepareSheetPrint(next, await sheetArt(next));
|
|
1016
|
+
const moved = placeNear(
|
|
1017
|
+
spec,
|
|
1018
|
+
bounds(next),
|
|
1019
|
+
next.items.map((entry) => ({ item: entry, shape: print.shapes.get(entry.id) })),
|
|
1020
|
+
id,
|
|
1021
|
+
[item.x, item.y]
|
|
1022
|
+
);
|
|
1023
|
+
if (moved) next.items[next.items.length - 1] = moved;
|
|
1024
|
+
return { sheet: next, item: moved ?? item, placed: Boolean(moved) };
|
|
1025
|
+
}
|
|
1026
|
+
async function packStickerSheet(sheet) {
|
|
1027
|
+
if (sheetTypes[sheet.type].cut === "die-cut") return { sheet, missing: [] };
|
|
1028
|
+
const print = prepareSheetPrint(sheet, await sheetArt(sheet));
|
|
1029
|
+
const packed = packSheet(
|
|
1030
|
+
sheetSpec(sheet),
|
|
1031
|
+
bounds(sheet),
|
|
1032
|
+
sheet.items.map((item) => ({ item, shape: print.shapes.get(item.id) })),
|
|
1033
|
+
sheet.hero
|
|
1034
|
+
);
|
|
1035
|
+
return { sheet: parse({ ...sheet, items: packed.items }), missing: packed.missing };
|
|
1036
|
+
}
|
|
1037
|
+
async function checkSheet(sheet) {
|
|
1038
|
+
return prepareSheetPrint(sheet, await sheetArt(sheet));
|
|
1039
|
+
}
|
|
1040
|
+
function sheetReport(print) {
|
|
1041
|
+
return {
|
|
1042
|
+
printable: print.printable,
|
|
1043
|
+
widthMm: Number(print.widthMm.toFixed(2)),
|
|
1044
|
+
heightMm: Number(print.heightMm.toFixed(2)),
|
|
1045
|
+
stickers: print.items.length,
|
|
1046
|
+
violations: print.violations,
|
|
1047
|
+
issues: print.issues.map(({ id, issue }) => ({ id, ...issue }))
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
async function exportSheetPdf(sheet, print) {
|
|
1051
|
+
return sheetPrintPdf(sheet, print, async (bytes) => new Uint8Array(deflateSync(bytes)));
|
|
1052
|
+
}
|
|
1053
|
+
function sheetPreviewPng(print, dpi = 150) {
|
|
1054
|
+
const scale = dpi / 25.4;
|
|
1055
|
+
const canvas = createCanvas(
|
|
1056
|
+
Math.round(print.widthMm * scale),
|
|
1057
|
+
Math.round(print.heightMm * scale)
|
|
1058
|
+
);
|
|
1059
|
+
const context = canvas.getContext("2d");
|
|
1060
|
+
context.fillStyle = "#f4f3ef";
|
|
1061
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
1062
|
+
for (const item of print.pdfItems) {
|
|
1063
|
+
const art = createCanvas(item.art.width, item.art.height);
|
|
1064
|
+
const data = art.getContext("2d").createImageData(item.art.width, item.art.height);
|
|
1065
|
+
data.data.set(item.art.rgba);
|
|
1066
|
+
art.getContext("2d").putImageData(data, 0, 0);
|
|
1067
|
+
context.save();
|
|
1068
|
+
context.translate(item.x * scale, item.y * scale);
|
|
1069
|
+
context.rotate(item.rotation * Math.PI / 180);
|
|
1070
|
+
context.drawImage(
|
|
1071
|
+
art,
|
|
1072
|
+
-item.widthMm / 2 * scale,
|
|
1073
|
+
-item.heightMm / 2 * scale,
|
|
1074
|
+
item.widthMm * scale,
|
|
1075
|
+
item.heightMm * scale
|
|
1076
|
+
);
|
|
1077
|
+
context.restore();
|
|
1078
|
+
context.strokeStyle = "#ff00ff";
|
|
1079
|
+
context.lineWidth = Math.max(1, 0.2 * scale);
|
|
1080
|
+
context.beginPath();
|
|
1081
|
+
for (const loop of item.cut) {
|
|
1082
|
+
for (const [i, [x, y]] of loop.entries())
|
|
1083
|
+
if (i) context.lineTo(x * scale, y * scale);
|
|
1084
|
+
else context.moveTo(x * scale, y * scale);
|
|
1085
|
+
context.closePath();
|
|
1086
|
+
}
|
|
1087
|
+
context.stroke();
|
|
1088
|
+
}
|
|
1089
|
+
return canvas.toBuffer("image/png");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/sticker-cut.ts
|
|
1093
|
+
import sharp5 from "sharp";
|
|
1094
|
+
async function stickerPrintCut(design) {
|
|
1095
|
+
const { png } = await renderBadge(design, { face: "front", person: {} });
|
|
1096
|
+
const { data, info } = await sharp5(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
1097
|
+
const material = design.sticker ?? defaultStickerMaterial;
|
|
1098
|
+
const cut = stickerCut(
|
|
1099
|
+
new Uint8ClampedArray(data.buffer, data.byteOffset, data.length),
|
|
1100
|
+
info.width,
|
|
1101
|
+
info.height,
|
|
1102
|
+
material.border,
|
|
1103
|
+
material.widthMm
|
|
1104
|
+
);
|
|
1105
|
+
return { cut, svg: stickerCutSvg(cut) };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
911
1108
|
// src/sticker-material.ts
|
|
912
|
-
import { readFile as
|
|
1109
|
+
import { readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
913
1110
|
import { basename as basename2 } from "path";
|
|
914
|
-
import
|
|
1111
|
+
import sharp6 from "sharp";
|
|
915
1112
|
async function bindStickerMap(design, layerId, path) {
|
|
916
1113
|
if (design.product !== "sticker") throw new InputError("Optical maps require a sticker.");
|
|
917
1114
|
const layer = design.front.layers.find((layer2) => layer2.id === layerId);
|
|
@@ -919,8 +1116,8 @@ async function bindStickerMap(design, layerId, path) {
|
|
|
919
1116
|
const info = await stat3(path);
|
|
920
1117
|
if (!info.isFile() || info.size > 4e6)
|
|
921
1118
|
throw new InputError("Use an optical image below 4 MB.");
|
|
922
|
-
const bytes = await
|
|
923
|
-
const meta = await
|
|
1119
|
+
const bytes = await readFile5(path);
|
|
1120
|
+
const meta = await sharp6(bytes, { limitInputPixels: 16777216 }).metadata();
|
|
924
1121
|
if (!meta.width || !meta.height || meta.width > 4096 || meta.height > 4096 || !["png", "webp"].includes(meta.format ?? ""))
|
|
925
1122
|
throw new InputError("Use an RGB PNG or WebP optical map up to 4096 \xD7 4096.");
|
|
926
1123
|
const asset = design.sticker?.effectMap?.asset ?? "optical-map";
|
|
@@ -949,10 +1146,10 @@ async function bindStickerMap(design, layerId, path) {
|
|
|
949
1146
|
import { spawn } from "child_process";
|
|
950
1147
|
import { createHash as createHash3 } from "crypto";
|
|
951
1148
|
import { existsSync as existsSync2 } from "fs";
|
|
952
|
-
import { chmod, lstat, mkdir as mkdir2, mkdtemp, readFile as
|
|
1149
|
+
import { chmod, lstat, mkdir as mkdir2, mkdtemp, readFile as readFile6, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
953
1150
|
import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
|
|
954
|
-
import { dirname as dirname2, extname as extname3, join as join3, resolve as
|
|
955
|
-
import
|
|
1151
|
+
import { dirname as dirname2, extname as extname3, join as join3, resolve as resolve5 } from "path";
|
|
1152
|
+
import sharp7 from "sharp";
|
|
956
1153
|
|
|
957
1154
|
// src/wallet-release.json
|
|
958
1155
|
var wallet_release_default = {
|
|
@@ -982,7 +1179,7 @@ var WalletError = class extends Error {
|
|
|
982
1179
|
function installation() {
|
|
983
1180
|
const platform = `${process.platform}-${process.arch}`;
|
|
984
1181
|
const cache = process.env.MAKEABLES_WALLET_CACHE ?? join3(homedir2(), "Library", "Caches", "Makeables", "wallet");
|
|
985
|
-
const root =
|
|
1182
|
+
const root = resolve5(cache, `${wallet_release_default.version}-${platform}`);
|
|
986
1183
|
return { platform, root, executable: join3(root, "makeables-wallet", "makeables-wallet") };
|
|
987
1184
|
}
|
|
988
1185
|
async function execute(command, args, progress) {
|
|
@@ -1133,7 +1330,7 @@ async function companion() {
|
|
|
1133
1330
|
return info.installed;
|
|
1134
1331
|
}
|
|
1135
1332
|
async function walletArtwork(file, side2 = "front") {
|
|
1136
|
-
const input =
|
|
1333
|
+
const input = resolve5(file);
|
|
1137
1334
|
const info = await lstat(input);
|
|
1138
1335
|
if (!info.isFile() || info.size > 12e6)
|
|
1139
1336
|
throw new InputError("Expected a local design or artwork file below 12 MB.");
|
|
@@ -1143,13 +1340,13 @@ async function walletArtwork(file, side2 = "front") {
|
|
|
1143
1340
|
if (design.product !== "card") throw new InputError("Wallet artwork requires a card document.");
|
|
1144
1341
|
source = (await renderBadge(design, { face: side2, person: {} })).png;
|
|
1145
1342
|
} else {
|
|
1146
|
-
source = await
|
|
1343
|
+
source = await readFile6(input);
|
|
1147
1344
|
if (extname3(input).toLowerCase() === ".svg") {
|
|
1148
1345
|
const result = checkSubmittedSvg(source.toString("utf8"));
|
|
1149
1346
|
if (!result.ok) throw new InputError(result.error);
|
|
1150
1347
|
}
|
|
1151
1348
|
}
|
|
1152
|
-
return
|
|
1349
|
+
return sharp7(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
|
|
1153
1350
|
}
|
|
1154
1351
|
async function walletCommand(command, values, progress) {
|
|
1155
1352
|
const need = (flag) => {
|
|
@@ -1200,7 +1397,7 @@ async function walletCommand(command, values, progress) {
|
|
|
1200
1397
|
try {
|
|
1201
1398
|
const raster = join3(work, "artwork.png");
|
|
1202
1399
|
await writeFile2(raster, png, { mode: 384 });
|
|
1203
|
-
const target = command === "wallet prep" ?
|
|
1400
|
+
const target = command === "wallet prep" ? resolve5(need("out")) : join3(work, "assets");
|
|
1204
1401
|
const prepared = await invoke(
|
|
1205
1402
|
executable,
|
|
1206
1403
|
["prepare", "--file", raster, "--out", target],
|
|
@@ -1231,6 +1428,12 @@ var commands = [
|
|
|
1231
1428
|
"layer add --file <design.json> --sticker <petdex:slug|path> [--side front|back]",
|
|
1232
1429
|
"layer set --file <design.json> --id <layer> [--side front|back] [--x --y --rotation] [--params <json|@file>]",
|
|
1233
1430
|
"layer remove --file <design.json> --id <layer> [--side front|back]",
|
|
1431
|
+
"cut --file <sticker.json> [--out <cut.svg>] [--overwrite]",
|
|
1432
|
+
"sheet new --type classic|small|single|clear|holographic --out <new-file> [--size a5|a6|10x15] [--name <title>]",
|
|
1433
|
+
"sheet add --file <sheet.json> --sticker <sticker.json|style-id> [--width-mm <mm>]",
|
|
1434
|
+
"sheet pack --file <sheet.json>",
|
|
1435
|
+
"sheet check --file <sheet.json>",
|
|
1436
|
+
"sheet export --file <sheet.json> --out <sheet.pdf> [--preview <proof.png>] [--overwrite]",
|
|
1234
1437
|
"render --file <design.json|design.makeables.json> --out <png> [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
|
|
1235
1438
|
"validate --file <design.json|artwork.svg> [--out <preview.png>]",
|
|
1236
1439
|
"new card --file <artwork.svg|image.png> --out <card.json> [--name <title>]",
|
|
@@ -1278,7 +1481,7 @@ function emit(data, next, human, json) {
|
|
|
1278
1481
|
);
|
|
1279
1482
|
}
|
|
1280
1483
|
async function newPath(file) {
|
|
1281
|
-
const path =
|
|
1484
|
+
const path = resolve6(file);
|
|
1282
1485
|
try {
|
|
1283
1486
|
await lstat2(path);
|
|
1284
1487
|
throw new InputError(`File already exists: ${path}. Choose a new --out path.`);
|
|
@@ -1332,7 +1535,11 @@ async function run() {
|
|
|
1332
1535
|
card: { type: "string" },
|
|
1333
1536
|
backup: { type: "string" },
|
|
1334
1537
|
device: { type: "string" },
|
|
1335
|
-
seconds: { type: "string" }
|
|
1538
|
+
seconds: { type: "string" },
|
|
1539
|
+
type: { type: "string" },
|
|
1540
|
+
size: { type: "string" },
|
|
1541
|
+
"width-mm": { type: "string" },
|
|
1542
|
+
preview: { type: "string" }
|
|
1336
1543
|
}
|
|
1337
1544
|
});
|
|
1338
1545
|
const json = machineOutput(Boolean(values.json));
|
|
@@ -1423,6 +1630,12 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
1423
1630
|
"layer add": ["file", "sticker", "side"],
|
|
1424
1631
|
"layer set": ["file", "id", "x", "y", "rotation", "side", "params"],
|
|
1425
1632
|
"layer remove": ["file", "id", "side"],
|
|
1633
|
+
cut: ["file", "out", "overwrite"],
|
|
1634
|
+
"sheet new": ["type", "size", "name", "out"],
|
|
1635
|
+
"sheet add": ["file", "sticker", "width-mm"],
|
|
1636
|
+
"sheet pack": ["file"],
|
|
1637
|
+
"sheet check": ["file"],
|
|
1638
|
+
"sheet export": ["file", "out", "preview", "overwrite"],
|
|
1426
1639
|
render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number"],
|
|
1427
1640
|
validate: ["file", "out"]
|
|
1428
1641
|
};
|
|
@@ -1591,7 +1804,7 @@ Run makeables skills get core for setup.`,
|
|
|
1591
1804
|
}
|
|
1592
1805
|
if (command === "mcp") {
|
|
1593
1806
|
try {
|
|
1594
|
-
const { runMcp } = await import("./mcp-
|
|
1807
|
+
const { runMcp } = await import("./mcp-AOJ6UJSC.js");
|
|
1595
1808
|
await runMcp(values.root ?? process.cwd());
|
|
1596
1809
|
} catch (error) {
|
|
1597
1810
|
process.stderr.write(
|
|
@@ -1650,7 +1863,7 @@ Keep this process running while you design.`,
|
|
|
1650
1863
|
const info = await lstat2(path2);
|
|
1651
1864
|
if (!info.isFile() || info.size > 12e6)
|
|
1652
1865
|
throw new InputError("Expected a params file below 12 MB.");
|
|
1653
|
-
params = JSON.parse(await
|
|
1866
|
+
params = JSON.parse(await readFile7(path2, "utf8"));
|
|
1654
1867
|
} else if (values.params) params = JSON.parse(values.params);
|
|
1655
1868
|
}
|
|
1656
1869
|
const result2 = await studioRequest(
|
|
@@ -1756,14 +1969,14 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
|
|
|
1756
1969
|
return;
|
|
1757
1970
|
}
|
|
1758
1971
|
if (!values.file) throw new InputError("Supply --file to validate a document.");
|
|
1759
|
-
const path =
|
|
1972
|
+
const path = resolve6(values.file);
|
|
1760
1973
|
const handle = await open2(path, "r");
|
|
1761
1974
|
let input;
|
|
1762
1975
|
try {
|
|
1763
1976
|
const info = await handle.stat();
|
|
1764
1977
|
if (!info.isFile() || info.size > 2 * 1024 * 1024)
|
|
1765
1978
|
throw new InputError("Expected a JSON file smaller than 2 MB.");
|
|
1766
|
-
input = JSON.parse(await
|
|
1979
|
+
input = JSON.parse(await readFile7(handle, "utf8"));
|
|
1767
1980
|
} finally {
|
|
1768
1981
|
await handle.close();
|
|
1769
1982
|
}
|
|
@@ -1782,7 +1995,7 @@ Schema and layout constraints passed.`,
|
|
|
1782
1995
|
}
|
|
1783
1996
|
async function readParams(value) {
|
|
1784
1997
|
return JSON.parse(
|
|
1785
|
-
value.startsWith("@") ? await
|
|
1998
|
+
value.startsWith("@") ? await readFile7(resolve6(value.slice(1)), "utf8") : value
|
|
1786
1999
|
);
|
|
1787
2000
|
}
|
|
1788
2001
|
async function cardCommand(command, values, json) {
|
|
@@ -1844,7 +2057,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
1844
2057
|
return true;
|
|
1845
2058
|
}
|
|
1846
2059
|
if (command === "material map") {
|
|
1847
|
-
const path =
|
|
2060
|
+
const path = resolve6(need(file, "file"));
|
|
1848
2061
|
const next = await bindStickerMap(
|
|
1849
2062
|
await readBadge(path),
|
|
1850
2063
|
need(values.id, "id"),
|
|
@@ -1860,10 +2073,10 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
1860
2073
|
return true;
|
|
1861
2074
|
}
|
|
1862
2075
|
if (command === "material set") {
|
|
1863
|
-
const path =
|
|
2076
|
+
const path = resolve6(need(file, "file"));
|
|
1864
2077
|
const design = await readBadge(path);
|
|
1865
2078
|
const raw = need(values.params, "params");
|
|
1866
|
-
const input = JSON.parse(raw.startsWith("@") ? await
|
|
2079
|
+
const input = JSON.parse(raw.startsWith("@") ? await readFile7(raw.slice(1), "utf8") : raw);
|
|
1867
2080
|
if (design.product === "sticker") {
|
|
1868
2081
|
const patch2 = stickerMaterialSchema.partial().strict().safeParse(input);
|
|
1869
2082
|
if (!patch2.success)
|
|
@@ -1947,7 +2160,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
1947
2160
|
await readBadge(path),
|
|
1948
2161
|
face,
|
|
1949
2162
|
need(values.sticker, "sticker"),
|
|
1950
|
-
dirname3(
|
|
2163
|
+
dirname3(resolve6(path))
|
|
1951
2164
|
);
|
|
1952
2165
|
await writeAtomic(path, `${JSON.stringify(design, null, 2)}
|
|
1953
2166
|
`);
|
|
@@ -1960,16 +2173,153 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
1960
2173
|
);
|
|
1961
2174
|
return true;
|
|
1962
2175
|
}
|
|
2176
|
+
if (command.startsWith("sheet ")) {
|
|
2177
|
+
const write = async (target, bytes) => {
|
|
2178
|
+
const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
|
|
2179
|
+
try {
|
|
2180
|
+
await handle.writeFile(bytes);
|
|
2181
|
+
} finally {
|
|
2182
|
+
await handle.close();
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
const save = (path2, sheet2) => writeAtomic(path2, `${JSON.stringify(sheet2, null, 2)}
|
|
2186
|
+
`);
|
|
2187
|
+
const checkNext = (path2) => [
|
|
2188
|
+
`makeables sheet check --file ${JSON.stringify(path2)}`,
|
|
2189
|
+
`makeables sheet export --file ${JSON.stringify(path2)} --out sheet.pdf --preview proof.png`
|
|
2190
|
+
];
|
|
2191
|
+
if (command === "sheet new") {
|
|
2192
|
+
const sheet2 = newSheet(values.type, values.size, values.name);
|
|
2193
|
+
const path2 = await newPath(need(values.out, "out"));
|
|
2194
|
+
await writeAtomic(path2, `${JSON.stringify(sheet2, null, 2)}
|
|
2195
|
+
`, true);
|
|
2196
|
+
emit(
|
|
2197
|
+
{ path: path2, sheet: sheet2 },
|
|
2198
|
+
[`makeables sheet add --file ${JSON.stringify(path2)} --sticker <sticker.json|style-id>`],
|
|
2199
|
+
`${style("Sheet ready", "1", !json)} ${sheet2.name} ${path2}`,
|
|
2200
|
+
json
|
|
2201
|
+
);
|
|
2202
|
+
return true;
|
|
2203
|
+
}
|
|
2204
|
+
const path = need(file, "file");
|
|
2205
|
+
const sheet = await readSheet(path);
|
|
2206
|
+
if (command === "sheet add") {
|
|
2207
|
+
const result = await addToSheet(sheet, need(values.sticker, "sticker"), values["width-mm"]);
|
|
2208
|
+
await save(path, result.sheet);
|
|
2209
|
+
emit(
|
|
2210
|
+
{
|
|
2211
|
+
item: { ...result.item, design: void 0 },
|
|
2212
|
+
placed: result.placed,
|
|
2213
|
+
stickers: result.sheet.items.length
|
|
2214
|
+
},
|
|
2215
|
+
result.placed ? checkNext(path) : [
|
|
2216
|
+
`No free spot. Run makeables sheet pack --file ${JSON.stringify(path)} or choose a smaller --width-mm.`
|
|
2217
|
+
],
|
|
2218
|
+
`${style(result.placed ? "Added" : "Added, overlapping", "1", !json)} ${result.item.id} (${result.item.widthMm} mm)`,
|
|
2219
|
+
json
|
|
2220
|
+
);
|
|
2221
|
+
return true;
|
|
2222
|
+
}
|
|
2223
|
+
if (command === "sheet pack") {
|
|
2224
|
+
const result = await packStickerSheet(sheet);
|
|
2225
|
+
await save(path, result.sheet);
|
|
2226
|
+
emit(
|
|
2227
|
+
{ stickers: result.sheet.items.length, missing: result.missing },
|
|
2228
|
+
result.missing.length ? ["Remove stickers or make them smaller, then pack again.", ...checkNext(path)] : checkNext(path),
|
|
2229
|
+
`${style("Packed", "1", !json)} ${result.sheet.items.length - result.missing.length}/${result.sheet.items.length}${result.missing.length ? `, no room for ${result.missing.join(", ")}` : ""}`,
|
|
2230
|
+
json
|
|
2231
|
+
);
|
|
2232
|
+
return true;
|
|
2233
|
+
}
|
|
2234
|
+
const print = await checkSheet(sheet);
|
|
2235
|
+
const report = sheetReport(print);
|
|
2236
|
+
const lines = [
|
|
2237
|
+
...report.violations.map(
|
|
2238
|
+
(v) => v.code === "too-close" ? ` error: ${v.ids.join(" and ")} are ${v.distanceMm.toFixed(1)} mm apart; the gap is ${v.gapMm} mm.` : ` error: ${v.ids[0]} crosses the ${v.marginMm} mm margin.`
|
|
2239
|
+
),
|
|
2240
|
+
...report.issues.map((issue) => ` ${issue.severity}: ${issue.id}: ${issue.message}`)
|
|
2241
|
+
];
|
|
2242
|
+
const summary = `${style(report.printable ? "Printable" : "Not printable", "1", !json)} ${report.widthMm} \xD7 ${report.heightMm} mm, ${report.stickers} sticker${report.stickers === 1 ? "" : "s"}`;
|
|
2243
|
+
if (command === "sheet check") {
|
|
2244
|
+
emit(
|
|
2245
|
+
report,
|
|
2246
|
+
report.printable ? checkNext(path).slice(1) : ["Fix the errors, then check again."],
|
|
2247
|
+
[summary, ...lines].join("\n"),
|
|
2248
|
+
json
|
|
2249
|
+
);
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
2252
|
+
if (command === "sheet export") {
|
|
2253
|
+
if (!report.printable)
|
|
2254
|
+
throw new InputError(
|
|
2255
|
+
`The sheet is not printable. Run makeables sheet check --file ${JSON.stringify(path)}.`
|
|
2256
|
+
);
|
|
2257
|
+
const out = resolve6(need(values.out, "out"));
|
|
2258
|
+
if (!out.endsWith(".pdf")) throw new InputError("--out must end in .pdf.");
|
|
2259
|
+
const preview = typeof values.preview === "string" ? resolve6(values.preview) : void 0;
|
|
2260
|
+
if (preview && !preview.endsWith(".png")) throw new InputError("--preview must end in .png.");
|
|
2261
|
+
const targets = [out, ...preview ? [preview] : []];
|
|
2262
|
+
if (!values.overwrite) for (const target of targets) await newPath(target);
|
|
2263
|
+
await write(out, await exportSheetPdf(sheet, print));
|
|
2264
|
+
if (preview) await write(preview, sheetPreviewPng(print));
|
|
2265
|
+
emit(
|
|
2266
|
+
{ ...report, path: out, ...preview ? { preview } : {} },
|
|
2267
|
+
["Send the PDF to the printer. Confirm its spot names and template before the first run."],
|
|
2268
|
+
[`${summary} ${out}${preview ? ` ${preview}` : ""}`, ...lines].join("\n"),
|
|
2269
|
+
json
|
|
2270
|
+
);
|
|
2271
|
+
return true;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
if (command === "cut") {
|
|
2275
|
+
const design = await readBadge(need(file, "file"));
|
|
2276
|
+
if (design.product !== "sticker") throw new InputError("cut applies to sticker designs.");
|
|
2277
|
+
const out = typeof values.out === "string" ? resolve6(values.out) : void 0;
|
|
2278
|
+
if (out && !out.endsWith(".svg")) throw new InputError("--out must end in .svg.");
|
|
2279
|
+
const target = out && (values.overwrite ? out : await newPath(out));
|
|
2280
|
+
const { cut, svg } = await stickerPrintCut(design);
|
|
2281
|
+
if (target) {
|
|
2282
|
+
const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
|
|
2283
|
+
try {
|
|
2284
|
+
await handle.writeFile(svg);
|
|
2285
|
+
} finally {
|
|
2286
|
+
await handle.close();
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
const printable = !cut.issues.some((issue) => issue.severity === "error");
|
|
2290
|
+
const size = `${cut.widthMm.toFixed(1)} \xD7 ${cut.heightMm.toFixed(1)} mm, border ${cut.borderMm.toFixed(2)} mm`;
|
|
2291
|
+
emit(
|
|
2292
|
+
{
|
|
2293
|
+
...target ? { path: target } : {},
|
|
2294
|
+
printable,
|
|
2295
|
+
widthMm: cut.widthMm,
|
|
2296
|
+
heightMm: cut.heightMm,
|
|
2297
|
+
borderMm: cut.borderMm,
|
|
2298
|
+
paths: cut.paths.length,
|
|
2299
|
+
issues: cut.issues
|
|
2300
|
+
},
|
|
2301
|
+
[
|
|
2302
|
+
...cut.issues.map((issue) => issue.message),
|
|
2303
|
+
`Set the printed size with material set --params '{"widthMm":75}'.`
|
|
2304
|
+
],
|
|
2305
|
+
[
|
|
2306
|
+
`${style(printable ? "Printable" : "Not printable", "1", !json)} ${size}, ${cut.paths.length} cut path${cut.paths.length === 1 ? "" : "s"}${target ? ` ${target}` : ""}`,
|
|
2307
|
+
...cut.issues.map((issue) => ` ${issue.severity}: ${issue.message}`)
|
|
2308
|
+
].join("\n"),
|
|
2309
|
+
json
|
|
2310
|
+
);
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
1963
2313
|
if (command === "render") {
|
|
1964
2314
|
const path = need(file, "file");
|
|
1965
|
-
const out =
|
|
2315
|
+
const out = resolve6(need(values.out, "out"));
|
|
1966
2316
|
if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
|
|
1967
2317
|
const target = values.overwrite ? out : await newPath(out);
|
|
1968
2318
|
const number = values.number === void 0 ? void 0 : Number(values.number);
|
|
1969
2319
|
if (number !== void 0 && !Number.isInteger(number))
|
|
1970
2320
|
throw new InputError("--number must be an integer.");
|
|
1971
2321
|
const face = side(values.side);
|
|
1972
|
-
const value = JSON.parse(await
|
|
2322
|
+
const value = JSON.parse(await readFile7(resolve6(path), "utf8"));
|
|
1973
2323
|
const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
|
|
1974
2324
|
if (pkg && ["photo", "name", "role", "number"].some((key) => values[key] !== void 0))
|
|
1975
2325
|
throw new InputError(
|
|
@@ -2031,13 +2381,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
2031
2381
|
return true;
|
|
2032
2382
|
}
|
|
2033
2383
|
if (command === "validate") {
|
|
2034
|
-
const path =
|
|
2384
|
+
const path = resolve6(need(file, "file"));
|
|
2035
2385
|
if (extname4(path).toLowerCase() === ".svg") {
|
|
2036
|
-
const source = await
|
|
2386
|
+
const source = await readFile7(path, "utf8");
|
|
2037
2387
|
const result2 = checkSubmittedSvg(source);
|
|
2038
2388
|
if (!result2.ok) throw new InputError(result2.error);
|
|
2039
2389
|
const preview = values.out ? await newPath(need(values.out, "out")) : void 0;
|
|
2040
|
-
if (preview) await
|
|
2390
|
+
if (preview) await sharp8(Buffer.from(result2.svg)).png().toFile(preview);
|
|
2041
2391
|
emit(
|
|
2042
2392
|
{
|
|
2043
2393
|
valid: true,
|
|
@@ -2054,7 +2404,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
2054
2404
|
return true;
|
|
2055
2405
|
}
|
|
2056
2406
|
if (values.out) throw new InputError("--out is only used to preview SVG validation.");
|
|
2057
|
-
const input = JSON.parse(await
|
|
2407
|
+
const input = JSON.parse(await readFile7(path, "utf8"));
|
|
2058
2408
|
const result = badgeDesignSchema.safeParse(input);
|
|
2059
2409
|
if (!result.success)
|
|
2060
2410
|
throw new InputError(
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
stickerStyles,
|
|
16
16
|
version,
|
|
17
17
|
writeAtomic
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-QJHEOZGN.js";
|
|
19
19
|
|
|
20
20
|
// src/mcp.ts
|
|
21
21
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -239,6 +239,7 @@ async function createMakeablesMcp(root, html) {
|
|
|
239
239
|
guide: z.enum([
|
|
240
240
|
"core",
|
|
241
241
|
"cards",
|
|
242
|
+
"card-art-direction",
|
|
242
243
|
"badges",
|
|
243
244
|
"stickers",
|
|
244
245
|
"sticker-art-direction",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makeables",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Create editable cards, badges and stickers with layered designs, live materials and your coding agent.",
|
|
5
5
|
"homepage": "https://makeables.dev",
|
|
6
6
|
"repository": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"@types/proper-lockfile": "4.1.4",
|
|
52
52
|
"tsup": "^8.4.0",
|
|
53
53
|
"typescript": "^5.8.3",
|
|
54
|
-
"@makeables/
|
|
54
|
+
"@makeables/design": "0.0.1",
|
|
55
55
|
"@makeables/renderer": "0.0.1",
|
|
56
|
-
"@makeables/
|
|
56
|
+
"@makeables/testing": "0.0.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=22"
|