makeables 0.5.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.
Files changed (31) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/stickers/community/brew-fx.webp +0 -0
  3. package/dist/assets/stickers/community/brew.webp +0 -0
  4. package/dist/assets/stickers/community/crafter-fx.webp +0 -0
  5. package/dist/assets/stickers/community/crafter.webp +0 -0
  6. package/dist/assets/stickers/community/friday-fx.webp +0 -0
  7. package/dist/assets/stickers/community/friday.webp +0 -0
  8. package/dist/assets/stickers/community/provenance.json +43 -0
  9. package/dist/assets/stickers/typingmind/absolutely-right-fx.webp +0 -0
  10. package/dist/assets/stickers/typingmind/absolutely-right.webp +0 -0
  11. package/dist/assets/stickers/typingmind/agi-disco-fx.webp +0 -0
  12. package/dist/assets/stickers/typingmind/agi-disco.webp +0 -0
  13. package/dist/assets/stickers/typingmind/agi-is-coming-fx.webp +0 -0
  14. package/dist/assets/stickers/typingmind/agi-is-coming.webp +0 -0
  15. package/dist/assets/stickers/typingmind/cmd-enter-fx.webp +0 -0
  16. package/dist/assets/stickers/typingmind/cmd-enter.webp +0 -0
  17. package/dist/assets/stickers/typingmind/emotional-support-fx.webp +0 -0
  18. package/dist/assets/stickers/typingmind/emotional-support.webp +0 -0
  19. package/dist/assets/stickers/typingmind/energy-fx.webp +0 -0
  20. package/dist/assets/stickers/typingmind/energy.webp +0 -0
  21. package/dist/assets/stickers/typingmind/motion-fx.webp +0 -0
  22. package/dist/assets/stickers/typingmind/motion.webp +0 -0
  23. package/dist/assets/stickers/typingmind/provenance.json +97 -0
  24. package/dist/assets/stickers/typingmind/typingmind-fx.webp +0 -0
  25. package/dist/assets/stickers/typingmind/typingmind.webp +0 -0
  26. package/dist/assets/stickers/typingmind/youre-cooked-fx.webp +0 -0
  27. package/dist/assets/stickers/typingmind/youre-cooked.webp +0 -0
  28. package/dist/{chunk-WVXA65H6.js → chunk-QJHEOZGN.js} +4633 -451
  29. package/dist/cli.js +437 -30
  30. package/dist/{mcp-LKLESPJC.js → mcp-AOJ6UJSC.js} +12 -2
  31. 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-WVXA65H6.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 readFile5 } 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 sharp5 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,14 +920,236 @@ 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
+
1108
+ // src/sticker-material.ts
1109
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
1110
+ import { basename as basename2 } from "path";
1111
+ import sharp6 from "sharp";
1112
+ async function bindStickerMap(design, layerId, path) {
1113
+ if (design.product !== "sticker") throw new InputError("Optical maps require a sticker.");
1114
+ const layer = design.front.layers.find((layer2) => layer2.id === layerId);
1115
+ if (layer?.kind !== "image") throw new InputError("Choose a front image layer with --id.");
1116
+ const info = await stat3(path);
1117
+ if (!info.isFile() || info.size > 4e6)
1118
+ throw new InputError("Use an optical image below 4 MB.");
1119
+ const bytes = await readFile5(path);
1120
+ const meta = await sharp6(bytes, { limitInputPixels: 16777216 }).metadata();
1121
+ if (!meta.width || !meta.height || meta.width > 4096 || meta.height > 4096 || !["png", "webp"].includes(meta.format ?? ""))
1122
+ throw new InputError("Use an RGB PNG or WebP optical map up to 4096 \xD7 4096.");
1123
+ const asset = design.sticker?.effectMap?.asset ?? "optical-map";
1124
+ if (design.front.layers.some((item) => item.kind === "image" && item.asset === asset))
1125
+ throw new InputError(
1126
+ "The optical-map asset is used as artwork. Rename it before binding a map."
1127
+ );
1128
+ return badgeDesignSchema.parse({
1129
+ ...design,
1130
+ assets: {
1131
+ ...design.assets,
1132
+ [asset]: {
1133
+ source: "image",
1134
+ name: basename2(path).slice(0, 80),
1135
+ data: `data:image/${meta.format};base64,${bytes.toString("base64")}`
1136
+ }
1137
+ },
1138
+ sticker: {
1139
+ ...design.sticker ?? defaultStickerMaterial,
1140
+ effectMap: { asset, layer: layerId }
1141
+ }
1142
+ });
1143
+ }
1144
+
911
1145
  // src/wallet.ts
912
1146
  import { spawn } from "child_process";
913
1147
  import { createHash as createHash3 } from "crypto";
914
1148
  import { existsSync as existsSync2 } from "fs";
915
- import { chmod, lstat, mkdir as mkdir2, mkdtemp, readFile as readFile4, 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";
916
1150
  import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
917
- import { dirname as dirname2, extname as extname3, join as join3, resolve as resolve4 } from "path";
918
- import sharp4 from "sharp";
1151
+ import { dirname as dirname2, extname as extname3, join as join3, resolve as resolve5 } from "path";
1152
+ import sharp7 from "sharp";
919
1153
 
920
1154
  // src/wallet-release.json
921
1155
  var wallet_release_default = {
@@ -945,7 +1179,7 @@ var WalletError = class extends Error {
945
1179
  function installation() {
946
1180
  const platform = `${process.platform}-${process.arch}`;
947
1181
  const cache = process.env.MAKEABLES_WALLET_CACHE ?? join3(homedir2(), "Library", "Caches", "Makeables", "wallet");
948
- const root = resolve4(cache, `${wallet_release_default.version}-${platform}`);
1182
+ const root = resolve5(cache, `${wallet_release_default.version}-${platform}`);
949
1183
  return { platform, root, executable: join3(root, "makeables-wallet", "makeables-wallet") };
950
1184
  }
951
1185
  async function execute(command, args, progress) {
@@ -1096,7 +1330,7 @@ async function companion() {
1096
1330
  return info.installed;
1097
1331
  }
1098
1332
  async function walletArtwork(file, side2 = "front") {
1099
- const input = resolve4(file);
1333
+ const input = resolve5(file);
1100
1334
  const info = await lstat(input);
1101
1335
  if (!info.isFile() || info.size > 12e6)
1102
1336
  throw new InputError("Expected a local design or artwork file below 12 MB.");
@@ -1106,13 +1340,13 @@ async function walletArtwork(file, side2 = "front") {
1106
1340
  if (design.product !== "card") throw new InputError("Wallet artwork requires a card document.");
1107
1341
  source = (await renderBadge(design, { face: side2, person: {} })).png;
1108
1342
  } else {
1109
- source = await readFile4(input);
1343
+ source = await readFile6(input);
1110
1344
  if (extname3(input).toLowerCase() === ".svg") {
1111
1345
  const result = checkSubmittedSvg(source.toString("utf8"));
1112
1346
  if (!result.ok) throw new InputError(result.error);
1113
1347
  }
1114
1348
  }
1115
- return sharp4(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();
1116
1350
  }
1117
1351
  async function walletCommand(command, values, progress) {
1118
1352
  const need = (flag) => {
@@ -1163,7 +1397,7 @@ async function walletCommand(command, values, progress) {
1163
1397
  try {
1164
1398
  const raster = join3(work, "artwork.png");
1165
1399
  await writeFile2(raster, png, { mode: 384 });
1166
- const target = command === "wallet prep" ? resolve4(need("out")) : join3(work, "assets");
1400
+ const target = command === "wallet prep" ? resolve5(need("out")) : join3(work, "assets");
1167
1401
  const prepared = await invoke(
1168
1402
  executable,
1169
1403
  ["prepare", "--file", raster, "--out", target],
@@ -1187,12 +1421,19 @@ var commands = [
1187
1421
  "designs list",
1188
1422
  "new card --design <slug> | --style <id> --out <new-file> [--dry-run]",
1189
1423
  "material set --file <design.json> --params <json|@file>",
1424
+ "material map --file <sticker.json> --id <image-layer> --ref <optical.png|optical.webp>",
1190
1425
  "new badge --style <id> --out <new-file> [--dry-run]",
1191
1426
  "new sticker --style <id> --out <new-file> [--dry-run]",
1192
1427
  "layer list --file <design.json>",
1193
1428
  "layer add --file <design.json> --sticker <petdex:slug|path> [--side front|back]",
1194
1429
  "layer set --file <design.json> --id <layer> [--side front|back] [--x --y --rotation] [--params <json|@file>]",
1195
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]",
1196
1437
  "render --file <design.json|design.makeables.json> --out <png> [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
1197
1438
  "validate --file <design.json|artwork.svg> [--out <preview.png>]",
1198
1439
  "new card --file <artwork.svg|image.png> --out <card.json> [--name <title>]",
@@ -1240,7 +1481,7 @@ function emit(data, next, human, json) {
1240
1481
  );
1241
1482
  }
1242
1483
  async function newPath(file) {
1243
- const path = resolve5(file);
1484
+ const path = resolve6(file);
1244
1485
  try {
1245
1486
  await lstat2(path);
1246
1487
  throw new InputError(`File already exists: ${path}. Choose a new --out path.`);
@@ -1294,7 +1535,11 @@ async function run() {
1294
1535
  card: { type: "string" },
1295
1536
  backup: { type: "string" },
1296
1537
  device: { type: "string" },
1297
- seconds: { type: "string" }
1538
+ seconds: { type: "string" },
1539
+ type: { type: "string" },
1540
+ size: { type: "string" },
1541
+ "width-mm": { type: "string" },
1542
+ preview: { type: "string" }
1298
1543
  }
1299
1544
  });
1300
1545
  const json = machineOutput(Boolean(values.json));
@@ -1379,11 +1624,18 @@ From a built checkout: npm run studio -- <command>`,
1379
1624
  "new card": ["design", "style", "file", "name", "out", "dry-run"],
1380
1625
  "new sticker": ["style", "out", "dry-run"],
1381
1626
  "material set": ["file", "params"],
1627
+ "material map": ["file", "id", "ref"],
1382
1628
  "new badge": ["style", "out", "dry-run"],
1383
1629
  "layer list": ["file"],
1384
1630
  "layer add": ["file", "sticker", "side"],
1385
1631
  "layer set": ["file", "id", "x", "y", "rotation", "side", "params"],
1386
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"],
1387
1639
  render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number"],
1388
1640
  validate: ["file", "out"]
1389
1641
  };
@@ -1552,7 +1804,7 @@ Run makeables skills get core for setup.`,
1552
1804
  }
1553
1805
  if (command === "mcp") {
1554
1806
  try {
1555
- const { runMcp } = await import("./mcp-LKLESPJC.js");
1807
+ const { runMcp } = await import("./mcp-AOJ6UJSC.js");
1556
1808
  await runMcp(values.root ?? process.cwd());
1557
1809
  } catch (error) {
1558
1810
  process.stderr.write(
@@ -1611,7 +1863,7 @@ Keep this process running while you design.`,
1611
1863
  const info = await lstat2(path2);
1612
1864
  if (!info.isFile() || info.size > 12e6)
1613
1865
  throw new InputError("Expected a params file below 12 MB.");
1614
- params = JSON.parse(await readFile5(path2, "utf8"));
1866
+ params = JSON.parse(await readFile7(path2, "utf8"));
1615
1867
  } else if (values.params) params = JSON.parse(values.params);
1616
1868
  }
1617
1869
  const result2 = await studioRequest(
@@ -1653,7 +1905,7 @@ Keep this process running while you design.`,
1653
1905
  emit(
1654
1906
  { styles },
1655
1907
  [
1656
- values.product === "sticker" ? "npx makeables new sticker --style good-energy --out sticker.json" : values.product === "card" ? "npx makeables new card --style pearl --out card.json" : "npx makeables design create --style opalo-lunar --out badge.json"
1908
+ values.product === "sticker" ? "npx makeables new sticker --style typingmind-absolutely-right --out sticker.json" : values.product === "card" ? "npx makeables new card --style pearl --out card.json" : "npx makeables design create --style opalo-lunar --out badge.json"
1657
1909
  ],
1658
1910
  `${style(`${styles.length} starting points`, "1", !json)}
1659
1911
 
@@ -1717,14 +1969,14 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
1717
1969
  return;
1718
1970
  }
1719
1971
  if (!values.file) throw new InputError("Supply --file to validate a document.");
1720
- const path = resolve5(values.file);
1972
+ const path = resolve6(values.file);
1721
1973
  const handle = await open2(path, "r");
1722
1974
  let input;
1723
1975
  try {
1724
1976
  const info = await handle.stat();
1725
1977
  if (!info.isFile() || info.size > 2 * 1024 * 1024)
1726
1978
  throw new InputError("Expected a JSON file smaller than 2 MB.");
1727
- input = JSON.parse(await readFile5(handle, "utf8"));
1979
+ input = JSON.parse(await readFile7(handle, "utf8"));
1728
1980
  } finally {
1729
1981
  await handle.close();
1730
1982
  }
@@ -1743,7 +1995,7 @@ Schema and layout constraints passed.`,
1743
1995
  }
1744
1996
  async function readParams(value) {
1745
1997
  return JSON.parse(
1746
- value.startsWith("@") ? await readFile5(resolve5(value.slice(1)), "utf8") : value
1998
+ value.startsWith("@") ? await readFile7(resolve6(value.slice(1)), "utf8") : value
1747
1999
  );
1748
2000
  }
1749
2001
  async function cardCommand(command, values, json) {
@@ -1754,7 +2006,9 @@ async function cardCommand(command, values, json) {
1754
2006
  };
1755
2007
  const renderHint = (path, face, design) => `npx makeables render --file ${JSON.stringify(path)} --side ${face} --out ${design.product ?? "badge"}.png --overwrite`;
1756
2008
  if (command === "new sticker") {
1757
- const preset = stickerStyles.find((entry) => entry.source === (values.style ?? "good-energy"));
2009
+ const preset = stickerStyles.find(
2010
+ (entry) => entry.source === (values.style ?? "typingmind-absolutely-right")
2011
+ );
1758
2012
  if (!preset)
1759
2013
  throw new InputError("Unknown sticker style. Run makeables styles list --product sticker.");
1760
2014
  const design = badgeDesignSchema.parse(structuredClone(preset));
@@ -1802,11 +2056,27 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1802
2056
  );
1803
2057
  return true;
1804
2058
  }
2059
+ if (command === "material map") {
2060
+ const path = resolve6(need(file, "file"));
2061
+ const next = await bindStickerMap(
2062
+ await readBadge(path),
2063
+ need(values.id, "id"),
2064
+ need(values.ref, "ref")
2065
+ );
2066
+ await writeAtomic(path, JSON.stringify(next, null, 2));
2067
+ emit(
2068
+ { material: next.sticker },
2069
+ ["Preview the foil, glitter and relief in the sticker studio."],
2070
+ "Optical map attached.",
2071
+ json
2072
+ );
2073
+ return true;
2074
+ }
1805
2075
  if (command === "material set") {
1806
- const path = resolve5(need(file, "file"));
2076
+ const path = resolve6(need(file, "file"));
1807
2077
  const design = await readBadge(path);
1808
2078
  const raw = need(values.params, "params");
1809
- const input = JSON.parse(raw.startsWith("@") ? await readFile5(raw.slice(1), "utf8") : raw);
2079
+ const input = JSON.parse(raw.startsWith("@") ? await readFile7(raw.slice(1), "utf8") : raw);
1810
2080
  if (design.product === "sticker") {
1811
2081
  const patch2 = stickerMaterialSchema.partial().strict().safeParse(input);
1812
2082
  if (!patch2.success)
@@ -1890,7 +2160,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1890
2160
  await readBadge(path),
1891
2161
  face,
1892
2162
  need(values.sticker, "sticker"),
1893
- dirname3(resolve5(path))
2163
+ dirname3(resolve6(path))
1894
2164
  );
1895
2165
  await writeAtomic(path, `${JSON.stringify(design, null, 2)}
1896
2166
  `);
@@ -1903,16 +2173,153 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1903
2173
  );
1904
2174
  return true;
1905
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
+ }
1906
2313
  if (command === "render") {
1907
2314
  const path = need(file, "file");
1908
- const out = resolve5(need(values.out, "out"));
2315
+ const out = resolve6(need(values.out, "out"));
1909
2316
  if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
1910
2317
  const target = values.overwrite ? out : await newPath(out);
1911
2318
  const number = values.number === void 0 ? void 0 : Number(values.number);
1912
2319
  if (number !== void 0 && !Number.isInteger(number))
1913
2320
  throw new InputError("--number must be an integer.");
1914
2321
  const face = side(values.side);
1915
- const value = JSON.parse(await readFile5(resolve5(path), "utf8"));
2322
+ const value = JSON.parse(await readFile7(resolve6(path), "utf8"));
1916
2323
  const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
1917
2324
  if (pkg && ["photo", "name", "role", "number"].some((key) => values[key] !== void 0))
1918
2325
  throw new InputError(
@@ -1974,13 +2381,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1974
2381
  return true;
1975
2382
  }
1976
2383
  if (command === "validate") {
1977
- const path = resolve5(need(file, "file"));
2384
+ const path = resolve6(need(file, "file"));
1978
2385
  if (extname4(path).toLowerCase() === ".svg") {
1979
- const source = await readFile5(path, "utf8");
2386
+ const source = await readFile7(path, "utf8");
1980
2387
  const result2 = checkSubmittedSvg(source);
1981
2388
  if (!result2.ok) throw new InputError(result2.error);
1982
2389
  const preview = values.out ? await newPath(need(values.out, "out")) : void 0;
1983
- if (preview) await sharp5(Buffer.from(result2.svg)).png().toFile(preview);
2390
+ if (preview) await sharp8(Buffer.from(result2.svg)).png().toFile(preview);
1984
2391
  emit(
1985
2392
  {
1986
2393
  valid: true,
@@ -1997,7 +2404,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1997
2404
  return true;
1998
2405
  }
1999
2406
  if (values.out) throw new InputError("--out is only used to preview SVG validation.");
2000
- const input = JSON.parse(await readFile5(path, "utf8"));
2407
+ const input = JSON.parse(await readFile7(path, "utf8"));
2001
2408
  const result = badgeDesignSchema.safeParse(input);
2002
2409
  if (!result.success)
2003
2410
  throw new InputError(