Files
ycode/scripts/rea/recover-runtime-icons.mjs
T

41 lines
3.1 KiB
JavaScript

// Recover inert geometric SVG attributes already retained by REA; no target access.
import fs from 'node:fs/promises'
import crypto from 'node:crypto'
const reference = JSON.parse(await fs.readFile('.tmp/rea-visual-reference/layout.json', 'utf8'))
const evidence = JSON.parse(await fs.readFile(reference.archive, 'utf8'))
if (evidence.evidence_id !== reference.evidenceId || JSON.stringify(evidence.normalized_result.visuals.layout) !== JSON.stringify(reference.layout)) throw new Error('Visual export does not match archived REA Evidence')
const document = reference.layout.documents[0]
const nodes = new Map([...document.nodes, ...document.svg_nodes].map(n => [n.node_index, n]))
function ancestor(node, matcher) {
for (let p = node.parent_index; nodes.has(p); p = nodes.get(p).parent_index) if (matcher(nodes.get(p).attributes?.class ?? '')) return true
return false
}
const svg = document.svg_nodes.filter(n => n.node_name.toLowerCase() === 'svg')
const specs = [
['RuntimeSendIcon', svg.filter(n => ancestor(n, c => c.split(' ').includes('cr-send-button__icon')))],
['RuntimeMicrophoneIcon', svg.filter(n => ancestor(n, c => c.split(' ').includes('cr-voice-trigger')))],
['RuntimePermissionIcon', svg.filter(n => ancestor(n, c => c.split(' ').includes('cr-permission-setting')) && ancestor(n, c => c.split(' ').includes('cr-input-footer-item__icon')))],
['RuntimeRegenerateIcon', svg.filter(n => ancestor(n, c => c.includes('_actionButton_')) && n.attributes.viewBox === '0 0 14 14')],
]
function subtree(node) {
return { ...node, children: document.svg_nodes.filter(child => child.parent_index === node.node_index).map(subtree) }
}
function jsx(node, root = false) {
const tag = node.node_name.toLowerCase() === 'clippath' ? 'clipPath' : node.node_name
const attrs = Object.entries(node.attributes).filter(([key]) => key !== 'class').map(([key, value]) => `${key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}={${JSON.stringify(value)}}`).join(' ')
return `<${tag} ${attrs}${root ? ' aria-hidden="true" focusable="false" {...props}' : ''}>${node.children.map(child => jsx(child)).join('')}</${tag}>`
}
let output = `// Generated from REA visual Evidence ${reference.evidenceId}; SVG geometry only.\nimport type { SVGProps } from 'react'\n`
const records = []
for (const [name, matches] of specs) {
if (matches.length !== 1) throw new Error(`${name}: expected one captured SVG, found ${matches.length}`)
const tree = subtree(matches[0])
const hash = crypto.createHash('sha256').update(JSON.stringify(tree)).digest('hex')
output += `\n// Captured node ${tree.node_index}; geometry SHA-256 ${hash}.\nexport function ${name}(props: SVGProps<SVGSVGElement>) { return (${jsx(tree, true)}) }\n`
records.push({ name, nodeIndex: tree.node_index, sha256: hash })
}
const destination = 'src/renderer/src/workbuddy/message-actions/RuntimeIcons.tsx'
await fs.writeFile(destination, output)
await fs.writeFile('.tmp/rea-visual-reference/icon-projection.json', JSON.stringify({ evidenceId: reference.evidenceId, destination, records }, null, 2) + '\n')
console.log(JSON.stringify({ evidenceId: reference.evidenceId, destination, icons: records.length }))