From 9c50fb033e213c22a2b8136213fca602eceda000 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sat, 24 Oct 2026 19:53:09 +0800 Subject: [PATCH] feat(06-07): committed 8-glyph PNG set rasterized from the app's SVG sources (D-12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - routing/scripts/build-glyphs.mjs: maintainer shell (Usage/Exit codes/ collect-then-write — a missing/invalid source exits 1 with the kind named and nothing written); recolors currentColor → #EB8008 in-memory and injects a full-bleed #1A1A1A plate (stroke="none" — it must not inherit the root's orange stroke); the committed SVGs are THE single source and stay untouched - sharp 0.35.5 as a routing devDependency (maintainer-only, zero runtime deps); 'glyphs' script added - 8× 256×256 PNGs committed (web/public/glyphs/) — the chat turn-card's sendPhoto URL rides the Worker's static handler; two render bugs fixed en route: density multiplied the explicit size attrs into 2048² (dropped), and the plate inherited the root stroke (orange frame — stroke="none") - SVG sources verified unchanged after the run --- package-lock.json | 1 + routing/package.json | 4 +- routing/scripts/build-glyphs.mjs | 115 +++++++++++++++++++++++++++++ web/public/glyphs/arrive.png | Bin 0 -> 2846 bytes web/public/glyphs/depart.png | Bin 0 -> 2350 bytes web/public/glyphs/left.png | Bin 0 -> 2482 bytes web/public/glyphs/right.png | Bin 0 -> 2593 bytes web/public/glyphs/slight-left.png | Bin 0 -> 2293 bytes web/public/glyphs/slight-right.png | Bin 0 -> 2251 bytes web/public/glyphs/straight.png | Bin 0 -> 2717 bytes web/public/glyphs/uturn.png | Bin 0 -> 2306 bytes 11 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 routing/scripts/build-glyphs.mjs create mode 100644 web/public/glyphs/arrive.png create mode 100644 web/public/glyphs/depart.png create mode 100644 web/public/glyphs/left.png create mode 100644 web/public/glyphs/right.png create mode 100644 web/public/glyphs/slight-left.png create mode 100644 web/public/glyphs/slight-right.png create mode 100644 web/public/glyphs/straight.png create mode 100644 web/public/glyphs/uturn.png diff --git a/package-lock.json b/package-lock.json index 91cf7c3..0a10b0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3959,6 +3959,7 @@ "routing": { "name": "@araharah/routing", "devDependencies": { + "sharp": "^0.35.5", "typescript": "^5.9.3" } }, diff --git a/routing/package.json b/routing/package.json index 766779b..5df593a 100644 --- a/routing/package.json +++ b/routing/package.json @@ -6,9 +6,11 @@ "test": "node test/runner.mjs", "typecheck": "tsc --noEmit", "fixtures": "node scripts/fetch-route-fixtures.mjs", - "corpus": "node scripts/route-corpus.mjs" + "corpus": "node scripts/route-corpus.mjs", + "glyphs": "node scripts/build-glyphs.mjs" }, "devDependencies": { + "sharp": "^0.35.5", "typescript": "^5.9.3" } } diff --git a/routing/scripts/build-glyphs.mjs b/routing/scripts/build-glyphs.mjs new file mode 100644 index 0000000..8316202 --- /dev/null +++ b/routing/scripts/build-glyphs.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +/* + * build-glyphs.mjs — rasterize the 8 turn-card glyphs from the app's SVG sources (06-07, D-12). + * ------------------------------------------------------------------------------------------------ + * A MAINTAINER-TIME REPO TOOL in the fetch-route-fixtures.mjs / upload-road-graph.mjs pattern: + * it is NOT a runtime dependency. Nothing in bot/src/, web/src/, or routing/src/ imports it; + * no deploy, test, CI run, or vite build ever runs it. sharp is a devDependency of the routing + * workspace only (RESEARCH Installation — zero runtime deps added). + * + * D-12 BOUNDARY + SINGLE-SOURCE LAW: the committed web/src/guidance/glyphs/*.svg are THE + * source — the SAME paths the app renders inline (banner 32×32, list rows 24×24). The PNGs + * are their rasterization: committed once, reused everywhere (the chat turn-card's sendPhoto + * fetches web/public/glyphs/.png from the Worker's static handler). The recolor is + * IN-MEMORY ONLY — the committed SVGs keep their currentColor law (theme-following in-app + * rendering) and are never rewritten by this tool. + * + * Colors are DSGN-01 build constants copied verbatim from theme/src/tokens.css (--color-signal-orange, + * --color-matte-black) — the script is not runtime CSS, hex literals are its interface. + * + * Mechanically, per kind: read the SVG → substitute currentColor → STROKE → inject a full-bleed + * 32×32 PLATE rect as the FIRST child (glyph strokes paint over it) → set width/height SIZE + * attributes → render 256×256 PNG via sharp. COLLECT-THEN-WRITE: every source is read and + * rendered BEFORE any file is written — a missing/invalid source or a failed render exits 1 + * with the kind named and NO partial writes (the fixture-precompute precedent). + * + * Usage: + * npm run glyphs -w routing # rebuild all 8 web/public/glyphs/*.png + * node scripts/build-glyphs.mjs # same, direct + * + * Exit codes: 0 all 8 PNGs rendered and written; 1 missing/invalid source SVG, failed render, + * or a size/plate sanity-check failure (no file written for a failed batch). + */ +import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); // routing/scripts +const SRC_DIR = resolve(here, "..", "..", "web", "src", "guidance", "glyphs"); +const OUT_DIR = resolve(here, "..", "..", "web", "public", "glyphs"); + +/** The 8 turn-card kinds — the GlyphKindName union in bot/src/guidance/cron.ts + * (structurally RouteStep.kind, one authority with engine A). */ +const GLYPH_KINDS = ["depart", "straight", "left", "right", "slight-left", "slight-right", "uturn", "arrive"]; + +/** DSGN-01 tokens, verbatim: signal-orange stroke on a matte-black full-bleed plate. */ +const STROKE = "#EB8008"; +const PLATE = "#1A1A1A"; +const SIZE = 256; // output pixels — chat renders it ~48–96px; the oversize keeps edges crisp +const SOURCE_GRID = 32; // the committed sources' viewBox (3px stroke, square caps) + +const MIN_BYTES = 500; // a flat-color 256×256 PNG is ≫ 500 B; smaller means a broken render + +function fail(message) { + console.error(`build-glyphs: ${message}`); + process.exit(1); +} + +/* ————— collect: read + transform + render EVERYTHING before any write ————— */ + +let sharp; +try { + sharp = (await import("sharp")).default; +} catch { + fail("sharp is not importable — run `npm install -D -w routing sharp` (maintainer devDependency only)"); +} + +const rendered = []; +for (const kind of GLYPH_KINDS) { + const srcPath = resolve(SRC_DIR, `${kind}.svg`); + let source; + try { + source = readFileSync(srcPath, "utf8"); + } catch { + fail(`missing source SVG for kind "${kind}": ${srcPath} — nothing written`); + } + if (!/]*>/.test(source)) { + fail(`invalid source SVG (no root) for kind "${kind}": ${srcPath} — nothing written`); + } + if (!source.includes("currentColor")) { + fail(`source SVG for kind "${kind}" lost its currentColor law (theme-following rendering) — refusing to rasterize; nothing written`); + } + + // Recolor + plate + size — in-memory only; the committed source is untouched. + const svg = source + .replaceAll("currentColor", STROKE) + .replace(/]*>/, (m) => `${m}`); + if (svg.includes("currentColor")) { + fail(`recolor incomplete for kind "${kind}" — nothing written`); + } + + let png; + try { + // The width/height SIZE attributes already govern the raster size — an + // explicit density here would MULTIPLY with them (256 × 576/72 = 2048), + // so none is set. + png = await sharp(Buffer.from(svg)).png().toBuffer(); + } catch (err) { + fail(`sharp render failed for kind "${kind}": ${err?.message ?? err} — nothing written`); + } + if (png.length < MIN_BYTES) { + fail(`render for kind "${kind}" is ${png.length} bytes (< ${MIN_BYTES}) — a broken/empty plate; nothing written`); + } + rendered.push({ kind, png, srcPath }); +} + +/* ————— write: all 8 rendered — now commit the batch ————— */ + +mkdirSync(OUT_DIR, { recursive: true }); +for (const { kind, png } of rendered) { + const outPath = resolve(OUT_DIR, `${kind}.png`); + writeFileSync(outPath, png); + console.log(`build-glyphs: wrote ${outPath} (${statSync(outPath).size} bytes)`); +} +console.log(`build-glyphs: ${rendered.length}/${GLYPH_KINDS.length} glyphs rasterized from the committed SVG sources`); diff --git a/web/public/glyphs/arrive.png b/web/public/glyphs/arrive.png new file mode 100644 index 0000000000000000000000000000000000000000..e655b49c1e0cab761289c4b921e69e4f9721a77b GIT binary patch literal 2846 zcmeH}Yfuwc6vxlahD~?`qE?B~N@%N9M*#~$tzg1SVDN#;jHRf_pnxJ0Mq0}&ut>*O zox)TrR4Gtjz*I6;M9}gG7F%0T&;nARlImzE4)QX9u!LT+x1T%x&@ZhYc6RTc^Z(ud zIkP+W91YnJAhMEL0e~nd&~GyU!jA}qG`?-yUmS&RmhpjM2>{l!rY{7i^CSRP%|U+N zp^15YU0&MfAI-is7(TSXC-JB4QmgZC#PkHZg}ydZx;%ByWmvkb{usUT!}rkZj4cQX zsbN>-&zV7t&W33OnqD!l5!)gtAW&I126rL_r*dKM(Bk4?{isr!~ z5H>2@z!&&YvV1tCedk7C}#Rb|xPvA7E#)qDXQx|v)as)+p#j@F8BSbJ>6IlW(g%FxYmj_$CLjBiz z)wvYL*6q=(*2F;7Urkj;f zBAHaTC&pQKpnSOL_aQzo<1;A*6t4FX71lON6>iKgEkyam62Q2SWi z{&a`+Lt_5#U4i8EPaHpF=sTt$`w+Fo}Vx_1t9PpX!RYKaPZPZ z!`N?j1y40v3rCSYB6-vp-ERZ^Ri@a<;Z2|ury9knQ(xFWw}I}ICl(J4ABgXR(?xaK zn|5m^dPbCd)9|@~TDGFIAPXNg`dYiX?zF-BJ80m+Xn{UOE0Brw2ge^b#ZCF^9nyNO zbvC!>0G;c;-Jxo*5oa}cv7S%Ku>lowxJGtT07nOh4C8m+)_BV*lr!M6PXR>}2MTFZ1{E0;$Sk6FXcKM>F#3$iox+)sU`v(kn-5h`L8@Kj> z2W)Pd0kovt=+4Z85`z$^;~mNKbRwoDfzP0|`>I8HTG6|Tr-O6X9@6oY%s3yZm`(H4 zR3*wzJ_Ihh^Q9H23j)35AW_|K>x4KspYH%RE_CU|R%-$mT>G5%YEM?f{UbMA+M zK~cng62`j{yeG!bCGO{7{1Ji|V0=BnQy3pX+#}qazwm&Y(_Y+PlR6b#KIL%ov|uz> z4R;i6$vEI6Tfhe!B+kv)&5dE6$h;b})rlcv?h$7snhuUFVH|d|U$9<1SWI~H;FSP6&frnoHo49@O)1!rV-yV*#s1sBfZ!t7ze6fI?!EZ?%O R#1;<*5ahqXPwS&P_$NP=Hk<$e literal 0 HcmV?d00001 diff --git a/web/public/glyphs/depart.png b/web/public/glyphs/depart.png new file mode 100644 index 0000000000000000000000000000000000000000..fee70476592dd8a48dee2bd3fd8a2114eba41755 GIT binary patch literal 2350 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6%U;1OBOz`!jG!i)^F=14Fw za6IsIaSW-L^Y+fcz-&$dW(RrM=0&CZ_?Yxk90H{l+r9V~KI<*hZ(BLfeJ<>I3X^4R zZvAJt*eK1xAj8OU$Keml0}h7=Y>&bj+!>h61?DqaFbV8v{2_2bu;Bpjk?jn93@mvH z``AiY6bhI>$}}i5G)i}{H-fd=aJ~R*4NL%Qea{5eS|ND?to3#SSZf@68H+*(-yd6` z`At0K5BQs8Bpgcw7$DYg9uWNVjN{(==kwWueum5b5PdM8mEkY%Cr8Euje-mp-s>(N zWzaPca!na5444>x?M*cqWnc+}wQLORZVV3lxA%-PMgw6qCD0%j$j|%!I@0&|{&=Q_ zr^*Zt_02T}e^?3;M%txQv2>>EdW>C zrVGFo_j5>{Z^Q8fTyc9kfGh5PCTmcgUm*_BdfEY8ao>a1`Bod0862NkfB4QIbMVi9 z7Rh~#3}`htsG+bP(onEthS;YFu`izy+)$|Dg)|f_Ax`CIvIN^F1ZiU|J^--~>Qti% lV5e?plm#_U>a2dSNr}kTF7^E82W$W_c)I$ztaD0e0sv4H4gmlF literal 0 HcmV?d00001 diff --git a/web/public/glyphs/left.png b/web/public/glyphs/left.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2082e1eef2efe7b2ceeb02952f962e99400189 GIT binary patch literal 2482 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 literal 0 HcmV?d00001 diff --git a/web/public/glyphs/slight-left.png b/web/public/glyphs/slight-left.png new file mode 100644 index 0000000000000000000000000000000000000000..908bcfc2c10ea91db56d923d9095d49af83221d3 GIT binary patch literal 2293 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 literal 0 HcmV?d00001 diff --git a/web/public/glyphs/straight.png b/web/public/glyphs/straight.png new file mode 100644 index 0000000000000000000000000000000000000000..5dcbb644e01ad35d86d5532c232035019678e917 GIT binary patch literal 2717 zcmeHJX;2eq82)zMl_Xp-2}P?GtP_bKq#hKkKmvgx(2{r*4-gnHaBL-r)G!9Js8#W( z2S})*3EtyHRmvqLD^fh5GCHj~g98E0P&{CXAgmzUFB@js>7P#jcKosP9nbqb@4N5L z_wAFXij8#UeaHg_8{O$@pu4u%Bb+r z#hT(*#i1K!+;YQy>WfQk$aCrndlPoG=Y>LB;>;QA#Ib4*>dc=JY&NR*d|tGKRS=4ZAK=c5oRZIqufsaM!wT*Qc(L%@A_Lx zWlZX{mv`?!%H64F+L*VEy1}!8!NP6@BHDhXayXjMc<_v#yDXL^Xj6w+9?ErYEX8&W zoCG{{?lDx;rI1Q|m)qI}Oh~WpC_bm{?Xr8BG;z|<)NhFcc!{snBrto|U=m4t8N0az z;O*=tF9;D8sGnPXkd6!^;Dse5=7`Dr8WcwM*&;i`VC_{ku=c6Fm`YXWYpb|950!8- zv*^PhvYl%*Zf_|};_?j>aTUMY3YESmh4Sg9;Y}k}=HBayQ?ErLa=Jo&)63+=LZRFW zDbq9PJ+qfy2y2u2nFO98Nw8qb)|lVgZHPcTP6WKc# z5s`w%TO+c%Qchme2|e)BX<@{1@W604$J&XTqks~0iTfXyVv-_vDdJbRK1YcbbOZyv zEa&CGMt~pqw8St>e>ZU4Mv8fHMP}-29bX?q?vgE?Ao`TK{5qb?aVWu zu;}!lK%SHCCJ(G^u;|W$0Q2(rH3M;8|5X46VH+v2KlHoA1_NIp&( zG0jlo7o6lZw9*|S=1bSpe+Gqx%vdByg0z-pQ8t=dIkTXqyRdD{XgTPbk{Unak35!n-0RUG zr*;_AX?HxC(MqoSU{v>%)D4(MQK%I(FZsGIadj-v&A}=PMp-yz zQm7Y=LUF*gd2>xgcJl>R@(iw?%JXbP97_JWDMSTEV6B?D8y@sfC{|D>#g3P2{a&Z%M)UN2{T+2AC? zZoK%n?Z%7y2dmRQL**DaEcK_OejY#|tDLb7F?QG-S*#WAxjE>_(t#?9f736(El^Yx5{ykKopsji)~^!@C}jY z5&L&mjvPOe42&5&o0N6w~3QEf8Ks z8HVRGVcZ}UrqzWCZ(w zc0?H_4hh?yh1k%na&|57O%6pUhh`>L9bTA!oVu%#y|@2f6^9}CdI=;*zwFaWjy1Vh z57bX9FJ0a;`Qxbp$_A5PW?;_xUZDlg)z`sxVESv09zPxY>g^pd{-?-3c>MbqTm{ZL zn6a|fXBoN`=S9QEk3u>=qj(ejbNM8)0b{q8Y`qe~cdM$A9?w)HicJ6N?(F|E=@;m` zm+{oXjj8sQmb~{Z09TH8g2(q6fT3nL<%T|UOzwi(-ivrir>5@D$bwJwIEw(TuS`e! zuKjF)@z6c!;m&I0V0&5<#n|Ly&>=P$^f)-_hxtZ93uveH6@{dDc!{?&AkC#Gid}9O zF7>Y2rxh9dvaH^j53or^w%N|xM}{;aZP2JzP!Mn=D7-T(^pNS-@hb0$+i?@Cc+Dqp zjZ}kxQ?%mb78~q!l8y5_FJ$0Rk(4U0znu}G!N3jNxt-=^?OEEYXihnpzyz&WHlzvE z3Ry2jdFfpy+GdPb1rEjbAU-OfONrJ;L_R#w$c-}|Q!g@urKi=?5^ESEbyUZqM@@2_ zHjhf_0QX-w0-ljV+D`e}u3zO9mvb${1lFt*JGRFWDdWSPaMV6g6 zi9}?fN}vW3HDj}{N!{!d5yU4QAF;^@?1OH`-;}JyNj9~c_5ZLVW_qzZZUZc-+DMVC zH~l2PiJrbK???aE4P>r)HKU8-GH<1M39g&vQT2hiy7?u6a$3M^bjVU~{ z?ZtbHeV~<2+N=oxkJ^gCSKBSu729C#5)1Bf_Ijhn@tu{;R7j^NpX}1dY+>Y-0o8u4 zKw6s`k8+<&=gXadG>lA>6cyQ=zLd&y2KhSD!9VTw25%sB?h_FfPz5UW{ZR)8Py6N0 zCF*hK!E9+ntSpbZ+@^N1{-&p{nW~VTfBM-t|6T5aGIUXy&*c9AY5)KL literal 0 HcmV?d00001