feat(catalog): generate derived scale 32 with preview and backup restore
This commit is contained in:
1 parent
08b0b5021e
commit
cd75361b9f
9 files changed
+749
-11
No files matched your search
@@ -23,7 +23,9 @@ function ScalePanel({
|
||||
? "Not included"
|
||||
: report.state === "issues"
|
||||
? "Needs review"
|
||||
: "Present"}
|
||||
: report.derived
|
||||
? "Derived from 64"
|
||||
: "Present"}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{report.assets.length} sprites · {report.animations.length} animation
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
export function NitroScale32Generator({
|
||||
classname,
|
||||
disabled,
|
||||
onChanged,
|
||||
}: {
|
||||
classname: string;
|
||||
disabled: boolean;
|
||||
onChanged: (metadata: Record<string, unknown>) => void;
|
||||
}) {
|
||||
const [preview, setPreview] = useState<{
|
||||
hash: string;
|
||||
before: string;
|
||||
after: string;
|
||||
count: number;
|
||||
} | null>(null),
|
||||
[undo, setUndo] = useState<{ hash: string; backupHash: string } | null>(
|
||||
null,
|
||||
),
|
||||
[busy, setBusy] = useState(false),
|
||||
[error, setError] = useState("");
|
||||
async function run(action: "preview" | "apply" | "restore") {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await adminFetch("/api/admin/studio/nitro-scale32", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
classname,
|
||||
action,
|
||||
expectedHash: action === "restore" ? undo?.hash : preview?.hash,
|
||||
backupHash: undo?.backupHash,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw Error(data.error || "Generation failed");
|
||||
if (action === "preview") setPreview(data);
|
||||
else {
|
||||
setPreview(null);
|
||||
setUndo(
|
||||
action === "apply"
|
||||
? { hash: data.hash, backupHash: data.backupHash }
|
||||
: null,
|
||||
);
|
||||
onChanged(data.metadata);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Generation failed");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<section className="space-y-3 rounded-lg border p-3">
|
||||
<h3 className="font-semibold">Generate scale 32 from 64</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Adds derived artwork using nearest-neighbor reduction. Existing scale 32
|
||||
is preserved. Preview uses the saved bundle and shows one sprite at its
|
||||
original pixel size.
|
||||
</p>
|
||||
{disabled && (
|
||||
<p className="text-sm">Save or reset your pending edits first.</p>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={busy || disabled}
|
||||
onClick={() => run("preview")}
|
||||
>
|
||||
{busy ? "Working…" : "Preview generation"}
|
||||
</Button>
|
||||
{preview && (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
{preview.count} sprite entries will be added. Check positioning and
|
||||
animations in the client after saving.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
["Original 64", preview.before],
|
||||
["Derived 32", preview.after],
|
||||
].map(([label, src]) => (
|
||||
<figure key={label} className="min-w-0">
|
||||
<figcaption>{label}</figcaption>
|
||||
<div className="overflow-auto p-3">
|
||||
<img
|
||||
src={src}
|
||||
alt={label}
|
||||
style={{ imageRendering: "pixelated", maxWidth: "none" }}
|
||||
/>
|
||||
</div>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
<Button disabled={busy || disabled} onClick={() => run("apply")}>
|
||||
Save with backup
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={busy}
|
||||
onClick={() => setPreview(null)}
|
||||
>
|
||||
Discard preview
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{undo && (
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
Saved. A copy of the original bundle is preserved.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={busy || disabled}
|
||||
onClick={() => run("restore")}
|
||||
>
|
||||
Restore original bundle
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<p role="alert" className="text-sm">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user