Files
Tileset-Generator/src/App.tsx
T
ben 31d0464a60 feat: V1 prototype — Vite/React/TS tileset generator
- Scaffold: package.json, tsconfig.json, vite.config.ts, index.html
- src/lib/imageProcessor.ts: full pipeline (normalize, offset, seam repair, export, validation)
- src/components/UploadPanel.tsx: drag-and-drop, file picker, clipboard paste
- src/components/SettingsPanel.tsx: all controls per spec
- src/components/PreviewPanel.tsx: Original / Tileable / Repeated tabs
- src/components/ErrorBanner.tsx: dismissible error/warning banners
- src/App.tsx: root component wiring everything together
- src/index.css: dark premium glassmorphism theme w/ Inter font
2026-05-15 01:18:26 -07:00

140 lines
4.5 KiB
TypeScript

import { useState, useCallback, useRef } from 'react';
import UploadPanel from './components/UploadPanel';
import SettingsPanel from './components/SettingsPanel';
import PreviewPanel from './components/PreviewPanel';
import ErrorBanner, { Banner } from './components/ErrorBanner';
import {
defaultSettings,
TilesetSettings,
generateTileableTexture,
exportAsWebP,
} from './lib/imageProcessor';
let bannerCounter = 0;
export default function App() {
const [sourceFile, setSourceFile] = useState<File | null>(null);
const [settings, setSettings] = useState<TilesetSettings>(defaultSettings);
const [resultCanvas, setResultCanvas] = useState<HTMLCanvasElement | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [processingStep, setProcessingStep] = useState('');
const [banners, setBanners] = useState<Banner[]>([]);
const processingRef = useRef(false);
const addBanner = (type: Banner['type'], message: string) => {
const id = String(++bannerCounter);
setBanners((prev) => [...prev, { id, type, message }]);
};
const dismissBanner = (id: string) =>
setBanners((prev) => prev.filter((b) => b.id !== id));
const handleFileSelect = useCallback((file: File) => {
setSourceFile(file);
setResultCanvas(null); // Clear old result when new image loaded
}, []);
const handleGenerate = async () => {
if (!sourceFile || processingRef.current) return;
processingRef.current = true;
setIsProcessing(true);
setResultCanvas(null);
try {
const canvas = await generateTileableTexture(
sourceFile,
settings,
(step) => setProcessingStep(step),
);
setResultCanvas(canvas);
} catch (err) {
const msg = err instanceof Error ? err.message : 'An unknown error occurred.';
addBanner('error', `Processing failed: ${msg}`);
} finally {
setIsProcessing(false);
processingRef.current = false;
setProcessingStep('');
}
};
const handleExport = async () => {
if (!resultCanvas) return;
try {
await exportAsWebP(
resultCanvas,
settings.webpQuality,
settings.outputWidth,
settings.outputHeight,
);
} catch (err) {
addBanner('error', 'Export failed. Your browser may not support WebP export.');
}
};
return (
<div className="app">
{/* Header */}
<header className="app-header">
<div className="app-header-logo">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
<rect width="28" height="28" rx="7" fill="url(#grad)" />
<rect x="2" y="2" width="11" height="11" rx="2" fill="rgba(255,255,255,0.85)" />
<rect x="15" y="2" width="11" height="11" rx="2" fill="rgba(255,255,255,0.45)" />
<rect x="2" y="15" width="11" height="11" rx="2" fill="rgba(255,255,255,0.45)" />
<rect x="15" y="15" width="11" height="11" rx="2" fill="rgba(255,255,255,0.85)" />
<defs>
<linearGradient id="grad" x1="0" y1="0" x2="28" y2="28">
<stop stopColor="#3b82f6" />
<stop offset="1" stopColor="#8b5cf6" />
</linearGradient>
</defs>
</svg>
<div>
<h1>Tileset Generator</h1>
<div className="app-header-subtitle">Upload image Generate tileable WebP texture</div>
</div>
</div>
<div className="app-badge">V1 · Client-Side</div>
</header>
{/* Body */}
<div className="app-body">
{/* Sidebar */}
<aside className="sidebar">
{banners.length > 0 && (
<ErrorBanner banners={banners} onDismiss={dismissBanner} />
)}
<UploadPanel
onFileSelect={handleFileSelect}
onError={(msg) => addBanner('error', msg)}
onWarning={(msg) => addBanner('warning', msg)}
currentFile={sourceFile}
/>
<SettingsPanel
settings={settings}
onChange={setSettings}
onGenerate={handleGenerate}
onExport={handleExport}
canGenerate={!!sourceFile}
canExport={!!resultCanvas && !isProcessing}
isProcessing={isProcessing}
/>
</aside>
{/* Preview */}
<main className="preview-area">
<PreviewPanel
sourceFile={sourceFile}
resultCanvas={resultCanvas}
settings={settings}
isProcessing={isProcessing}
processingStep={processingStep}
/>
</main>
</div>
</div>
);
}