kerf-engine/app/src/views/ImportConvert.tsx
jlightner a97629c390 test: Created OutputInfoBar with color-coded stats, wired Use This butt…
- "app/src/components/OutputInfoBar.tsx"
- "app/src/components/__tests__/OutputInfoBar.test.tsx"
- "app/src/views/ImportConvert.tsx"
- "app/src/App.css"

GSD-Task: S01/T04
2026-03-26 05:17:48 +00:00

117 lines
3.5 KiB
TypeScript

import { useCallback, useState } from 'react';
import type { PresetConfig, TraceMetadata } from '../types/engine';
import FileUpload from '../components/FileUpload';
import PresetSelector from '../components/PresetSelector';
import ParameterSliders from '../components/ParameterSliders';
import SvgPreview from '../components/SvgPreview';
import OutputInfoBar from '../components/OutputInfoBar';
import { useDebouncedTrace } from '../hooks/useDebouncedTrace';
import styles from './ImportConvert.module.css';
interface ImportConvertProps {
onUseThis: (svgOutput: string, metadata: TraceMetadata) => void;
}
/**
* Extract default slider parameters from a preset config.
*/
function defaultParamsFromPreset(config: PresetConfig): Record<string, unknown> {
const mode = config.vectorization.mode;
const params: Record<string, unknown> = {
epsilon: config.postprocessing.epsilon ?? 2.5,
};
if (mode === 'potrace') {
params.turdsize = config.vectorization.potrace?.turdsize ?? 10;
params.alphamax = config.vectorization.potrace?.alphamax ?? 1.0;
} else {
params.filter_speckle = config.vectorization.vtracer?.filter_speckle ?? 20;
params.corner_threshold = config.vectorization.vtracer?.corner_threshold ?? 60;
}
return params;
}
export default function ImportConvert({ onUseThis }: ImportConvertProps) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isSvgMode, setIsSvgMode] = useState(false);
const [selectedPreset, setSelectedPreset] = useState('sign');
const [presetConfig, setPresetConfig] = useState<PresetConfig | null>(null);
const [currentParams, setCurrentParams] = useState<Record<string, unknown>>({
epsilon: 2.5,
turdsize: 10,
alphamax: 1.0,
});
// Hook handles params stabilization via JSON.stringify internally
const { svgOutput, metadata, isLoading, error } = useDebouncedTrace(
selectedFile,
selectedPreset,
currentParams,
300,
);
const handleFileSelect = useCallback(
(file: File, isSvg: boolean) => {
setSelectedFile(file);
setIsSvgMode(isSvg);
},
[],
);
const handlePresetSelect = useCallback(
(name: string, config: PresetConfig) => {
setSelectedPreset(name);
setPresetConfig(config);
setCurrentParams(defaultParamsFromPreset(config));
},
[],
);
const handleParamsChange = useCallback(
(params: Record<string, unknown>) => {
setCurrentParams(params);
},
[],
);
const handleUseThis = useCallback(() => {
if (svgOutput && metadata) {
onUseThis(svgOutput, metadata);
}
}, [svgOutput, metadata, onUseThis]);
return (
<div className={styles.container}>
<div className={styles.leftPanel}>
<FileUpload onFileSelect={handleFileSelect} selectedFile={selectedFile} />
<PresetSelector
selectedPreset={selectedPreset}
onPresetSelect={handlePresetSelect}
/>
<ParameterSliders
presetConfig={presetConfig}
params={currentParams}
onChange={handleParamsChange}
/>
<button
type="button"
className="use-this-btn"
disabled={!svgOutput || isLoading}
onClick={handleUseThis}
>
Use This
</button>
</div>
<div className={styles.rightPanel}>
<SvgPreview
svgOutput={svgOutput}
isLoading={isLoading}
error={error}
metadata={metadata}
/>
<OutputInfoBar metadata={metadata} />
</div>
</div>
);
}