import { useState, useCallback, useMemo, useEffect } from 'react';
import { extractColorsFromSVG, extractStructuralGroups, rgbToLab } from './colorUtils';
import { buildFilteredSvgHtml } from './svgFilter';
import { exportPSD } from './psdExport';
import { buildStructuralColorGroups } from './grouping';
function LoadingOverlay({ text, progress, pct }) {
return (
);
}
function SwatchGroup({ group, colors, index, active, onClick }) {
return (
Group {index + 1} ({group.length}色)
{group.map((ci) => (
))}
);
}
export default function App() {
const [svgDoc, setSvgDoc] = useState(null);
const [uniqueColors, setUniqueColors] = useState([]);
const [loading, setLoading] = useState(null);
const [exportStatus, setExportStatus] = useState('');
const [dragActive, setDragActive] = useState(false);
const [previewHtml, setPreviewHtml] = useState('');
const [visibleLayers, setVisibleLayers] = useState(null);
const [structuralGroups, setStructuralGroups] = useState(null);
const [loadError, setLoadError] = useState('');
const groups = useMemo(() => {
if (!structuralGroups) return [];
const avgL = (group) => {
let sum = 0;
for (const ci of group) {
const c = uniqueColors[ci];
sum += rgbToLab(c.r, c.g, c.b)[0];
}
return sum / group.length;
};
const g = [...structuralGroups];
g.sort((a, b) => avgL(b) - avgL(a));
return g;
}, [structuralGroups, uniqueColors]);
// Reset visibility when groups change
useEffect(() => { setVisibleLayers(null); }, [groups]);
const compositePreview = useMemo(() => {
if (!svgDoc || groups.length === 0) return previewHtml;
if (visibleLayers === null) return previewHtml;
if (visibleLayers.size === 0) return '';
const mergedColors = new Set();
for (const gi of visibleLayers) {
for (const ci of groups[gi]) {
mergedColors.add(uniqueColors[ci].hex);
}
}
return buildFilteredSvgHtml(svgDoc, mergedColors);
}, [svgDoc, groups, uniqueColors, visibleLayers, previewHtml]);
const loadSVG = useCallback(async (text) => {
setVisibleLayers(null);
setStructuralGroups(null);
setLoadError('');
setLoading({ text: 'SVGを解析中...', progress: '', pct: 0 });
await tick();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'image/svg+xml');
const svgEl = doc.querySelector('svg');
if (!svgEl) { setLoading(null); return; }
setSvgDoc(doc);
setPreviewHtml(new XMLSerializer().serializeToString(svgEl));
setLoading({ text: '色を抽出中...', progress: '', pct: 30 });
await tick();
const colors = extractColorsFromSVG(svgEl);
setUniqueColors(colors);
const structure = extractStructuralGroups(svgEl);
if (structure) {
setLoading({ text: '構造ベースのグループ化...', progress: `${structure.anchors.length}アンカー + ${structure.strokes.length}ストローク`, pct: 60 });
await tick();
const sGroups = buildStructuralColorGroups(structure, colors);
setStructuralGroups(sGroups);
} else {
setStructuralGroups(null);
setLoadError('色グループ構造()が見つかりません。Vectorizer.aiで Group By を Color に設定してエクスポートしてください。');
}
setVisibleLayers(null);
setLoading(null);
}, []);
const handleFile = useCallback((file) => {
if (!file || !file.name.toLowerCase().endsWith('.svg')) return;
const reader = new FileReader();
reader.onload = (e) => loadSVG(e.target.result);
reader.readAsText(file);
}, [loadSVG]);
const handleExport = useCallback(async () => {
if (!svgDoc || groups.length === 0) return;
setExportStatus('');
setLoading({ text: 'PSDエクスポート準備中...', progress: '', pct: 0 });
try {
await exportPSD(svgDoc, groups, uniqueColors, (text, progress, pct) => {
setLoading({ text, progress, pct });
});
setLoading(null);
setExportStatus('エクスポート完了!');
} catch (err) {
setLoading(null);
setExportStatus(`エラー: ${err.message}`);
console.error(err);
}
}, [svgDoc, groups, uniqueColors]);
const onDragOver = useCallback((e) => { e.preventDefault(); setDragActive(true); }, []);
const onDragLeave = useCallback(() => setDragActive(false), []);
const onDrop = useCallback((e) => {
e.preventDefault();
setDragActive(false);
if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
}, [handleFile]);
const toggleLayer = (gi) => {
setVisibleLayers((prev) => {
if (prev === null) {
const next = new Set(Array.from({ length: groups.length }, (_, i) => i));
next.delete(gi);
return next;
}
const next = new Set(prev);
if (next.has(gi)) next.delete(gi);
else next.add(gi);
if (next.size === groups.length) return null;
return next;
});
};
const isLayerVisible = (gi) => visibleLayers === null || visibleLayers.has(gi);
const loaded = uniqueColors.length > 0;
return (
<>
SVG Color Grouper → PSD
入力
handleFile(e.target.files[0])} />
{loaded && !loadError &&
{uniqueColors.length}色を検出 ({groups.length}グループ)
}
{loadError &&
{loadError}
}
{loaded && groups.length > 0 && (
出力
{exportStatus &&
{exportStatus}
}
)}
{loaded && groups.length > 0 && (
色グループ ({groups.length}グループ / クリックで表示切替)
{groups.map((group, gi) => (
toggleLayer(gi)}
/>
))}
)}
{compositePreview ? (
) : (
{loaded ? 'レイヤーを選択してください' : 'SVGファイルをドラッグ&ドロップ、または左のボタンから選択'}
)}
{loading && (
)}
>
);
}
function tick() {
return new Promise((r) => setTimeout(r, 0));
}