// rnx screenshot layers — engine-rendered layer isolation export. // // captures one screen as a stack the 3d stage can pull apart: `full.png` is // the normal frame, `base.png` re-renders the frame with the target subtrees // hidden (content and glass blur behind them are real, not inpainted), and // each `layer-.png` draws exactly that subtree alone on transparency // at its on-screen position. `manifest.json` records device metrics and the // logical rect of every layer. import { mkdirSync, writeFileSync } from 'fs' import { resolve } from 'path' import { devices } from 'sootsim-engine/settings' import { buildResolveRectEval, type SampleRect } from '../browser-evals' import { rnxExit } from '../run-rnx' import { callInBridge, createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge' import { inspectWaitReady, waitReadyReason } from './inspect/core' interface ScreenshotLayersOptions { port?: number verbose?: boolean } export async function runScreenshotLayers( args: string[], opts: ScreenshotLayersOptions, ): Promise { if (args.includes('--help') || args.includes('-h')) { console.log(` rnx screenshot layers — export a screen as engine-isolated layers usage: rnx screenshot layers --ids [--out-dir ] options: --ids comma-separated testIDs of the subtrees to isolate --out-dir output directory (default: /tmp/rnx-layers) output: full.png the normal frame base.png the frame re-rendered with every listed subtree hidden layer-.png each subtree rendered alone on a transparent frame manifest.json device metrics + logical rect per layer `) rnxExit(0) } const idsArg = args.find((_, i) => args[i - 1] === '--ids') if (!idsArg) { console.error(' --ids is required (comma-separated testIDs)') rnxExit(1) } const testIds = idsArg .split(',') .map((entry) => entry.trim()) .filter(Boolean) if (testIds.length === 0) { console.error(' --ids resolved to an empty list') rnxExit(1) } const outDirArg = args.find((_, i) => args[i - 1] === '--out-dir') const outDir = resolve(process.cwd(), outDirArg ?? '/tmp/rnx-layers') const parsed = parseBridgeCliArgs(args, { port: opts.port, stripValueFlags: ['--ids', '--out-dir'], }) const bridge = createBridgeFromParsed({ ...parsed, commandTimeoutMs: 45000 }) try { const readiness = await inspectWaitReady(bridge, 90_000) if (!readiness.ready) { throw new Error( `app not ready to capture after ${Math.round(readiness.elapsedMs / 1000)}s ` + `(${waitReadyReason(readiness)})`, ) } const rects: Record = {} for (const testId of testIds) { const node = await bridge.send({ type: 'evaluate', code: buildResolveRectEval({ id: testId }), }) if (!node) throw new Error(`no node with testID "${testId}"`) rects[testId] = node as SampleRect } mkdirSync(outDir, { recursive: true }) const savePng = (name: string, dataUrl: unknown) => { if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) { throw new Error(`capture for ${name} returned no image data`) } const path = resolve(outDir, name) writeFileSync( path, Buffer.from(dataUrl.slice('data:image/png;base64,'.length), 'base64'), ) console.log(` saved: ${path}`) } savePng('full.png', await bridge.send({ type: 'screenshot' })) savePng( 'base.png', await bridge.send({ type: 'screenshot', captureFilter: { hideTestIds: testIds }, }), ) for (const testId of testIds) { // every other listed target hides inside this solo, so nested targets // (a button inside a lifted card) land in exactly one layer. savePng( `layer-${testId}.png`, await bridge.send({ type: 'screenshot', captureFilter: { soloTestId: testId, hideTestIds: testIds.filter((other) => other !== testId), }, }), ) } const settings = (await callInBridge | null>( bridge, 'SootSim.bridges.settings.get', )) ?? { deviceModel: null } const model = typeof settings.deviceModel === 'string' && settings.deviceModel in devices ? settings.deviceModel : null const device = model ? devices[model as keyof typeof devices] : null const manifest = { device: device ? { model, width: device.width, height: device.height, scale: device.scale } : { model }, layers: testIds.map((testId) => ({ testId, rect: rects[testId] })), } const manifestPath = resolve(outDir, 'manifest.json') writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) console.log(` saved: ${manifestPath}`) } finally { bridge.close() } }