wand-decks-kit 0.8.2 → 0.8.4
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/SKILL.md +14 -0
- package/package.json +1 -1
- package/tools/sheet.py +106 -0
- package/wand.js +12 -2
- package/wand_kit.js +1 -1
package/SKILL.md
CHANGED
|
@@ -99,6 +99,20 @@ Decks built here carry their own spec inside the file. Change only what was
|
|
|
99
99
|
asked, then `ship`. If the deck carries no spec it was not built here — treat it
|
|
100
100
|
as a rebrand.
|
|
101
101
|
|
|
102
|
+
## Spend tool calls like they are metered — they are
|
|
103
|
+
|
|
104
|
+
Chat turns cap the number of tool invocations, and a rebuild is command-heavy.
|
|
105
|
+
Batch aggressively:
|
|
106
|
+
|
|
107
|
+
- Setup is ONE call: `npm install ... && node wand.js doctor`
|
|
108
|
+
- Gates are ONE call: `node wand.js validate spec && node wand.js check spec`
|
|
109
|
+
- Write the whole spec in one write, never slide by slide
|
|
110
|
+
- `ship` is already the whole pipeline — one call, at the end
|
|
111
|
+
- The visual pass is ONE image: `slides/sheet.png`
|
|
112
|
+
|
|
113
|
+
A smooth job fits in about ten calls. If a turn is cut off mid-way, continuing
|
|
114
|
+
is safe: the sandbox and its installed kit survive within the same chat.
|
|
115
|
+
|
|
102
116
|
## Work in fast loops, ship once
|
|
103
117
|
|
|
104
118
|
`node wand.js validate` and `node wand.js check` run in seconds; `ship` runs the
|
package/package.json
CHANGED
package/tools/sheet.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
sheet.py — stitch rendered slides into one contact sheet.
|
|
4
|
+
|
|
5
|
+
python3 tools/sheet.py <dir-of-pngs> <out.png> [cols]
|
|
6
|
+
|
|
7
|
+
Why: the visual gate on a 24-slide deck used to mean viewing 24 images — the
|
|
8
|
+
single largest token cost of a rebuild, bigger than the instructions and the
|
|
9
|
+
spec combined. One sheet gives the whole-deck pass for the price of one image;
|
|
10
|
+
the full-size renders stay on disk for zooming into anything that looks off.
|
|
11
|
+
|
|
12
|
+
Pure standard library, like everything else in this kit.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import glob, os, struct, sys, zlib
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_png(path):
|
|
19
|
+
d = open(path, "rb").read()
|
|
20
|
+
pos, idat = 8, b""
|
|
21
|
+
w = h = ct = None
|
|
22
|
+
while pos < len(d):
|
|
23
|
+
ln = struct.unpack(">I", d[pos:pos + 4])[0]
|
|
24
|
+
typ = d[pos + 4:pos + 8]
|
|
25
|
+
if typ == b"IHDR":
|
|
26
|
+
w, h, bd, ct = struct.unpack(">IIBB", d[pos + 8:pos + 18])
|
|
27
|
+
elif typ == b"IDAT":
|
|
28
|
+
idat += d[pos + 8:pos + 8 + ln]
|
|
29
|
+
pos += 12 + ln
|
|
30
|
+
ch = {0: 1, 2: 3, 6: 4}[ct]
|
|
31
|
+
raw = zlib.decompress(idat)
|
|
32
|
+
stride = w * ch
|
|
33
|
+
out = bytearray(h * stride)
|
|
34
|
+
prev = bytearray(stride)
|
|
35
|
+
i = 0
|
|
36
|
+
for y in range(h):
|
|
37
|
+
f = raw[i]; i += 1
|
|
38
|
+
line = bytearray(raw[i:i + stride]); i += stride
|
|
39
|
+
for x in range(stride):
|
|
40
|
+
a = line[x - ch] if x >= ch else 0
|
|
41
|
+
b = prev[x]
|
|
42
|
+
c = prev[x - ch] if x >= ch else 0
|
|
43
|
+
v = line[x]
|
|
44
|
+
if f == 1: v += a
|
|
45
|
+
elif f == 2: v += b
|
|
46
|
+
elif f == 3: v += (a + b) // 2
|
|
47
|
+
elif f == 4:
|
|
48
|
+
p = a + b - c
|
|
49
|
+
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
|
|
50
|
+
v += a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
|
|
51
|
+
line[x] = v & 255
|
|
52
|
+
out[y * stride:(y + 1) * stride] = line
|
|
53
|
+
prev = line
|
|
54
|
+
# normalise to RGB
|
|
55
|
+
if ch == 3:
|
|
56
|
+
return w, h, bytes(out)
|
|
57
|
+
rgb = bytearray(w * h * 3)
|
|
58
|
+
for px in range(w * h):
|
|
59
|
+
if ch == 1:
|
|
60
|
+
rgb[px * 3:px * 3 + 3] = bytes([out[px]] * 3)
|
|
61
|
+
else:
|
|
62
|
+
rgb[px * 3:px * 3 + 3] = out[px * 4:px * 4 + 3]
|
|
63
|
+
return w, h, bytes(rgb)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def write_png(path, w, h, rgb):
|
|
67
|
+
raw = bytearray()
|
|
68
|
+
for y in range(h):
|
|
69
|
+
raw.append(0)
|
|
70
|
+
raw += rgb[y * w * 3:(y + 1) * w * 3]
|
|
71
|
+
def chunk(t, d):
|
|
72
|
+
return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
|
|
73
|
+
open(path, "wb").write(
|
|
74
|
+
b"\x89PNG\r\n\x1a\n"
|
|
75
|
+
+ chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
|
|
76
|
+
+ chunk(b"IDAT", zlib.compress(bytes(raw), 9))
|
|
77
|
+
+ chunk(b"IEND", b""))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
src, out = sys.argv[1], sys.argv[2]
|
|
82
|
+
cols = int(sys.argv[3]) if len(sys.argv) > 3 else 4
|
|
83
|
+
files = sorted(glob.glob(os.path.join(src, "*.png")))
|
|
84
|
+
files = [f for f in files if os.path.abspath(f) != os.path.abspath(out)]
|
|
85
|
+
if not files:
|
|
86
|
+
raise SystemExit(f"no PNGs in {src}")
|
|
87
|
+
tiles = [read_png(f) for f in files]
|
|
88
|
+
tw = max(t[0] for t in tiles)
|
|
89
|
+
th = max(t[1] for t in tiles)
|
|
90
|
+
gap = 6
|
|
91
|
+
rows = (len(tiles) + cols - 1) // cols
|
|
92
|
+
W = cols * tw + (cols + 1) * gap
|
|
93
|
+
H = rows * th + (rows + 1) * gap
|
|
94
|
+
sheet = bytearray(b"\x1a\x16\x38" * (W * H)) # dark ground between tiles
|
|
95
|
+
for i, (w, h, rgb) in enumerate(tiles):
|
|
96
|
+
ox = gap + (i % cols) * (tw + gap)
|
|
97
|
+
oy = gap + (i // cols) * (th + gap)
|
|
98
|
+
for y in range(h):
|
|
99
|
+
dst = ((oy + y) * W + ox) * 3
|
|
100
|
+
sheet[dst:dst + w * 3] = rgb[y * w * 3:(y + 1) * w * 3]
|
|
101
|
+
write_png(out, W, H, bytes(sheet))
|
|
102
|
+
print(f"{out} ({len(tiles)} slides, {W}x{H})")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
package/wand.js
CHANGED
|
@@ -210,8 +210,18 @@ function ship(specPath, outArg) {
|
|
|
210
210
|
const dir = path.join(work, "slides");
|
|
211
211
|
fs.mkdirSync(dir, { recursive: true });
|
|
212
212
|
spawnSync("pdftoppm", ["-r", "96", "-png", pdfOut, path.join(dir, "slide")], { stdio: "inherit" });
|
|
213
|
-
shots = fs.readdirSync(dir).filter((f) => f.
|
|
214
|
-
|
|
213
|
+
shots = fs.readdirSync(dir).filter((f) => f.startsWith("slide")).length;
|
|
214
|
+
// One contact sheet is the whole-deck pass for the price of one image.
|
|
215
|
+
// Viewing 24 renders one by one was the biggest token line of a rebuild —
|
|
216
|
+
// more than the instructions and the spec combined.
|
|
217
|
+
const small = fs.mkdtempSync(path.join(require("os").tmpdir(), "wand-sheet-"));
|
|
218
|
+
spawnSync("pdftoppm", ["-r", "26", "-png", pdfOut, path.join(small, "s")], { stdio: "ignore" });
|
|
219
|
+
const sheet = path.join(dir, "sheet.png");
|
|
220
|
+
runPython([path.join(KIT, "tools", "sheet.py"), small, sheet]);
|
|
221
|
+
fs.rmSync(small, { recursive: true, force: true });
|
|
222
|
+
console.log(`rendered : ${shots} slide(s) to ${rel(dir)}/`);
|
|
223
|
+
console.log(`visual pass : open ${rel(sheet)} — one image, every slide. Open an`);
|
|
224
|
+
console.log(` individual slide-NN.png only where something looks off.`);
|
|
215
225
|
} else {
|
|
216
226
|
console.log(`open ${rel(path.join(KIT, "preview", "preview.html"))} and look at every slide.`);
|
|
217
227
|
}
|
package/wand_kit.js
CHANGED
|
@@ -999,7 +999,7 @@ function productMap(pres, { tag, titleText, subtitleText, bandRow, cards, split,
|
|
|
999
999
|
|
|
1000
1000
|
|
|
1001
1001
|
module.exports = {
|
|
1002
|
-
VERSION: "0.8.
|
|
1002
|
+
VERSION: "0.8.4",
|
|
1003
1003
|
T, F, COLS, ASSETS, radius, theme, autoSize, measure: M,
|
|
1004
1004
|
background, chrome, footer, title, subtitle, kicker, contentTop,
|
|
1005
1005
|
card, iconTile, pill,
|