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 (
{text}
{progress}
); } 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)); }