// Compose one cast figure from atoms, per the rig table in cast.md ("Build from ATOMS"). // usage: yarn node - < compose-peep.cjs // kind = standing | sitting | bust // // Composing from Separate Atoms/ rather than Templates/ is the whole point: a template has to // be REJECTED for printed text, sunglasses or a Rage face, whereas a composed triple can // never contain one. Atom names are the curated local set, not upstream. // // Every head is 473x567 and every face 289x293, so one set of offsets serves any combination. // Layer order is body/pose first, then the head group with the face nested inside it. const fs = require('node:fs'); const path = require('node:path'); const ATOMS = '/home/ubuntu/generated-images/character-moodboard/cast/atoms'; const RIG = { standing: { viewBox: '0 0 1179 3291', base: 'translate(-121,634)', head: 'translate(404,180)', dir: 'pose-standing' }, sitting: { viewBox: '0 0 1647 2500', base: 'translate(-81,637)', head: 'translate(345,180)', dir: 'pose-sitting' }, bust: { viewBox: '0 0 1136 1533', base: 'translate(147,639)', head: 'translate(372,180)', dir: 'body' }, }; const FACE_OFFSET = 'translate(159,186)'; // The atom files are Sketch exports; everything between the svg tags is the drawing. const inner = (file) => { const s = fs.readFileSync(file, 'utf8'); const open = s.indexOf('>', s.indexOf('')).trim(); }; function compose({ kind, base, head, face }) { const rig = RIG[kind]; if (!rig) throw new Error(`kind must be one of ${Object.keys(RIG).join(', ')}`); const baseFile = path.join(ATOMS, rig.dir, base + '.svg'); const headFile = path.join(ATOMS, 'head', head + '.svg'); const faceFile = path.join(ATOMS, 'face', face + '.svg'); for (const f of [baseFile, headFile, faceFile]) { if (!fs.existsSync(f)) throw new Error(`no such atom: ${f}`); } return [ // xmlns:xlink is required, not decorative: some atoms use and rendering // fails with a namespace error without it. ``, ``, inner(baseFile), ``, ``, inner(headFile), ` `, inner(faceFile), ` `, ``, ``, '', ].join('\n'); } module.exports = { compose, RIG }; // Checked on argv, not `require.main === module`: these scripts are piped in on stdin so Yarn // PnP can resolve from the workspace, and under stdin `require.main` is not this module, so a // require.main guard silently does nothing. { const [kind, base, head, face, out] = process.argv.slice(2); if (!out) { console.error('usage: compose-peep.cjs '); process.exit(1); } fs.mkdirSync(path.dirname(out), { recursive: true }); fs.writeFileSync(out, compose({ kind, base, head, face })); console.log(` ${path.basename(out).padEnd(34)} ${kind}/${base} + ${head} + ${face}`); }