Improved harness - with card selection

This commit is contained in:
2026-09-15 06:08:43 -07:00
parent 97ed7f05c8
commit 786f63a042
29 changed files with 1398 additions and 494 deletions

View File

@@ -0,0 +1,76 @@
import { createHash } from 'node:crypto'
import { readFile, rename, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url))
export const surfaceDefaultsPath = resolve(here, '..', 'config', 'surface-defaults.json')
const finishNames = ['Printed ink', 'Foil', 'Holographic', 'Gold leaf', 'Frosted glass']
const materialNames = ['Paper', 'Linen', 'Metal', 'Wood', 'Leather']
function number(value, min, max, label) {
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
throw new Error(`${label} must be between ${min} and ${max}`)
}
}
export function validateSurfaceDefaults(value) {
if (!value || typeof value !== 'object' || value.schemaVersion !== 1 ||
!value.finishes || typeof value.finishes !== 'object' ||
!value.materials || typeof value.materials !== 'object') {
throw new Error('Surface defaults use an unsupported format')
}
for (const name of finishNames) {
const item = value.finishes[name]
if (!item || typeof item !== 'object') throw new Error(`Missing defaults for finish ${name}`)
if (Object.keys(item).some(key => key !== 'finishStrength')) throw new Error(`Unsupported setting for finish ${name}`)
number(item.finishStrength, 0, 1, `${name} finish strength`)
}
for (const name of materialNames) {
const item = value.materials[name]
if (!item || typeof item !== 'object') throw new Error(`Missing defaults for material ${name}`)
if (Object.keys(item).some(key => !['surfaceDetail', 'roughness', 'metalBrushHorizontal'].includes(key))) {
throw new Error(`Unsupported setting for material ${name}`)
}
number(item.surfaceDetail, 0, 1, `${name} surface detail`)
number(item.roughness, 0.05, 0.8, `${name} roughness`)
if (item.metalBrushHorizontal !== undefined && typeof item.metalBrushHorizontal !== 'boolean') {
throw new Error(`${name} metal grain orientation must be a boolean`)
}
}
return value
}
function revision(bytes) {
return createHash('sha256').update(bytes).digest('hex').slice(0, 16)
}
export async function readSurfaceDefaults(writable = false, path = surfaceDefaultsPath) {
const bytes = await readFile(path)
return { defaults: validateSurfaceDefaults(JSON.parse(bytes)), revision: revision(bytes), writable }
}
export async function updateSurfaceDefaults(update, path = surfaceDefaultsPath) {
const current = await readSurfaceDefaults(true, path)
if (!update || update.revision !== current.revision) {
const error = new Error('Surface defaults changed since this page loaded; refresh and try again')
error.statusCode = 409
throw error
}
const next = structuredClone(current.defaults)
if (update.group === 'finish' && finishNames.includes(update.name)) {
next.finishes[update.name] = update.values
} else if (update.group === 'material' && materialNames.includes(update.name)) {
next.materials[update.name] = update.values
} else {
const error = new Error('Unknown surface-default group or selection')
error.statusCode = 400
throw error
}
validateSurfaceDefaults(next)
const bytes = `${JSON.stringify(next, null, 2)}\n`
const temporary = `${path}.tmp`
await writeFile(temporary, bytes)
await rename(temporary, path)
return { defaults: next, revision: revision(bytes), writable: true }
}