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/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-4AJQBAFB.js";
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 readFile6 } from "fs/promises";
49
- import { dirname as dirname3, extname as extname4, resolve as resolve5 } from "path";
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 sharp6 from "sharp";
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 readFile4, stat as stat3 } from "fs/promises";
1109
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
913
1110
  import { basename as basename2 } from "path";
914
- import sharp4 from "sharp";
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 readFile4(path);
923
- const meta = await sharp4(bytes, { limitInputPixels: 16777216 }).metadata();
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 readFile5, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
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 resolve4 } from "path";
955
- import sharp5 from "sharp";
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 = resolve4(cache, `${wallet_release_default.version}-${platform}`);
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 = resolve4(file);
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 readFile5(input);
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 sharp5(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
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" ? resolve4(need("out")) : join3(work, "assets");
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 = resolve5(file);
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-LXTO7XOO.js");
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 readFile6(path2, "utf8"));
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 = resolve5(values.file);
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 readFile6(handle, "utf8"));
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 readFile6(resolve5(value.slice(1)), "utf8") : value
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 = resolve5(need(file, "file"));
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 = resolve5(need(file, "file"));
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 readFile6(raw.slice(1), "utf8") : raw);
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(resolve5(path))
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 = resolve5(need(values.out, "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 readFile6(resolve5(path), "utf8"));
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 = resolve5(need(file, "file"));
2384
+ const path = resolve6(need(file, "file"));
2035
2385
  if (extname4(path).toLowerCase() === ".svg") {
2036
- const source = await readFile6(path, "utf8");
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 sharp6(Buffer.from(result2.svg)).png().toFile(preview);
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 readFile6(path, "utf8"));
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-4AJQBAFB.js";
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.6.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/testing": "0.0.0",
54
+ "@makeables/design": "0.0.1",
55
55
  "@makeables/renderer": "0.0.1",
56
- "@makeables/design": "0.0.1"
56
+ "@makeables/testing": "0.0.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=22"