diff --git a/README.md b/README.md index a0e63b5..e534a16 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This is not an ORM in a fake mustache. It is for cases where you want: That keeps the polymorphic boundary tight without making callers write boilerplate by candlelight. -## Hugin Inspector +## Huginn Inspector CultCache files may use the `.cc` extension. The bytes are still the canonical `cultcache.store.v1` MessagePack snapshot; the extension is the human handle, not @@ -56,7 +56,7 @@ Build the desktop package: npm run dist:inspector ``` -Hugin is read-only. Drop a `.cc`, `.msgpack`, or `.mpack` file onto the window to +Huginn is read-only. Drop a `.cc`, `.msgpack`, or `.mpack` file onto the window to inspect the snapshot header, schema catalog, records, decoded MessagePack payload previews, and an EpiphanyGraph view of the file's structured data tree without registering application schemas. diff --git a/electron-builder.json b/electron-builder.json index df9ecb3..28df4bf 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -1,7 +1,7 @@ { "appId": "com.gamecult.cultcache.inspector", - "productName": "Hugin", - "executableName": "Hugin", + "productName": "Huginn", + "executableName": "Huginn", "asar": false, "icon": "inspector/public/hugin.ico", "extraMetadata": { @@ -17,7 +17,7 @@ "win": { "signAndEditExecutable": false, "target": ["portable"], - "artifactName": "Hugin-${version}.${ext}" + "artifactName": "Huginn-${version}.${ext}" }, "mac": { "target": ["dir"] diff --git a/electron/main.cjs b/electron/main.cjs index e321a15..2cf76b6 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1,7 +1,22 @@ const { app, BrowserWindow } = require("electron"); +const fs = require("node:fs"); +const os = require("node:os"); const path = require("node:path"); const devServer = process.env.CULTCACHE_INSPECTOR_DEV_SERVER; +app.setName("Huginn"); +const logPath = path.join(app.getPath("userData"), "huginn-renderer.log"); + +function writeLog(message) { + const line = `[${new Date().toISOString()}] ${message}${os.EOL}`; + try { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.appendFileSync(logPath, line); + } catch { + // Logging must never be the reason the inspector refuses to open. + } + console.log(line.trimEnd()); +} function createWindow() { const window = new BrowserWindow({ @@ -9,7 +24,7 @@ function createWindow() { height: 780, minWidth: 820, minHeight: 560, - title: "Hugin", + title: "Huginn", icon: path.join(__dirname, "..", "dist-inspector", "hugin-64.png"), backgroundColor: "#05090d", autoHideMenuBar: true, @@ -20,6 +35,19 @@ function createWindow() { }, }); + window.webContents.on("console-message", (_event, level, message, line, sourceId) => { + writeLog(`renderer console[${level}] ${sourceId}:${line} ${message}`); + }); + window.webContents.on("did-fail-load", (_event, code, description, validatedUrl) => { + writeLog(`renderer failed load ${code} ${description} ${validatedUrl}`); + }); + window.webContents.on("render-process-gone", (_event, details) => { + writeLog(`renderer process gone ${details.reason} exitCode=${details.exitCode}`); + }); + window.webContents.on("did-finish-load", () => { + writeLog(`renderer loaded ${window.webContents.getURL()}`); + }); + if (devServer) { void window.loadURL(devServer); } else { diff --git a/inspector/index.html b/inspector/index.html index 488dd3c..73efecc 100644 --- a/inspector/index.html +++ b/inspector/index.html @@ -3,7 +3,7 @@ - Hugin + Huginn
diff --git a/inspector/src/HuginnFieldCanvas.tsx b/inspector/src/HuginnFieldCanvas.tsx new file mode 100644 index 0000000..89fbdc8 --- /dev/null +++ b/inspector/src/HuginnFieldCanvas.tsx @@ -0,0 +1,420 @@ +import { useEffect, useRef } from "react"; + +type Brush = { + x: number; + y: number; + size: number; + r: number; + g: number; + b: number; + alpha: number; + energy: number; + curve: number; + gx: number; + gy: number; +}; + +type FieldSample = { + width: number; + height: number; + rgba: Uint8ClampedArray; + luma: Float32Array; + alpha: Float32Array; +}; + +type PyramidLevel = { + width: number; + height: number; + values: Float32Array; + energy: Float32Array; + gx: Float32Array; + gy: Float32Array; + curve: Float32Array; +}; + +const FIELD_SIZE = 64; +const MAX_DEPTH = 6; +const MIN_BRUSH_SIZE = 2; +const ENERGY_SPLIT_THRESHOLD = 0.045; + +export function HuginnFieldCanvas({ imageUrl }: { imageUrl: string }) { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + let cancelled = false; + const image = new Image(); + image.decoding = "async"; + image.src = imageUrl; + + const render = async () => { + try { + await image.decode(); + } catch { + return; + } + if (cancelled) { + return; + } + + const sample = sampleImage(image); + const pyramid = buildCurvaturePyramid(sample); + const brushes = buildBrushes(sample, pyramid); + const observer = new ResizeObserver(() => drawField(canvas, brushes, sample)); + observer.observe(canvas); + drawField(canvas, brushes, sample); + + return () => observer.disconnect(); + }; + + let cleanup: (() => void) | undefined; + void render().then((nextCleanup) => { + cleanup = nextCleanup; + }); + + return () => { + cancelled = true; + cleanup?.(); + }; + }, [imageUrl]); + + return