diff --git a/src/App.jsx b/src/App.jsx index 26f9324..0feebd6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,6 +13,66 @@ import { const COLOR_PALETTE = ['#38bdf8', '#a855f7', '#f97316', '#22c55e', '#e11d48', '#6366f1', '#14b8a6', '#f59e0b']; +const INITIAL_SELECTED_LENS_IDS = defaultLenses.slice(0, 3).map((lens) => lens.id); + +const createInitialColorAssignments = (lensIds) => { + const assignments = {}; + lensIds.forEach((lensId, index) => { + assignments[lensId] = index % COLOR_PALETTE.length; + }); + return assignments; +}; + +const reconcileColorAssignments = (selectedLensIds, previousAssignments) => { + if (selectedLensIds.length === 0) { + return selectedLensIds.length === Object.keys(previousAssignments).length ? previousAssignments : {}; + } + + const nextAssignments = {}; + const usedIndices = new Set(); + + selectedLensIds.forEach((lensId) => { + const priorIndex = previousAssignments[lensId]; + if (typeof priorIndex === 'number' && !usedIndices.has(priorIndex)) { + nextAssignments[lensId] = priorIndex; + usedIndices.add(priorIndex); + } + }); + + const availableIndices = []; + for (let i = 0; i < COLOR_PALETTE.length; i += 1) { + if (!usedIndices.has(i)) { + availableIndices.push(i); + } + } + + let overflowIndex = 0; + selectedLensIds.forEach((lensId) => { + if (typeof nextAssignments[lensId] === 'number') { + return; + } + + let assignedIndex; + if (availableIndices.length > 0) { + assignedIndex = availableIndices.shift(); + } else { + assignedIndex = overflowIndex % COLOR_PALETTE.length; + overflowIndex += 1; + } + + nextAssignments[lensId] = assignedIndex; + usedIndices.add(assignedIndex); + }); + + const previousKeys = Object.keys(previousAssignments); + const nextKeys = Object.keys(nextAssignments); + const assignmentsChanged = + previousKeys.length !== nextKeys.length || + selectedLensIds.some((lensId) => nextAssignments[lensId] !== previousAssignments[lensId]); + + return assignmentsChanged ? nextAssignments : previousAssignments; +}; + const plotTypeOptions = [ { value: PlotTypes.DISTANCE, label: 'Absolute scene distance' }, { value: PlotTypes.NORMALIZED, label: 'Blur vs normalized distance' }, @@ -80,6 +140,8 @@ const PlotSection = ({ lensCurves, plotType, setPlotType }) => { const filteredDataset = filterDatasetForPlotType(curve.dataset, plotType); const xValues = filteredDataset.map(axisConfig.accessor); const yValues = filteredDataset.map((point) => point.blurPercentOfDiagonal); + const paletteIndex = + typeof curve.colorIndex === 'number' ? curve.colorIndex % COLOR_PALETTE.length : index % COLOR_PALETTE.length; return { x: xValues, @@ -96,7 +158,7 @@ const PlotSection = ({ lensCurves, plotType, setPlotType }) => { name: `${curve.lens.name} (${formatAperture(curve.apertureUsed)})`, line: { width: 2, - color: COLOR_PALETTE[index % COLOR_PALETTE.length], + color: COLOR_PALETTE[paletteIndex], }, hovertemplate: `%{text}
${axisConfig.label}: %{x:.2f}
Blur: %{customdata[0]:.2f}% diag (%{customdata[1]:.2f} mm)`, text: Array(filteredDataset.length).fill(curve.lens.name), @@ -453,7 +515,7 @@ const OpticsPanel = ({ const App = () => { - const [selectedLensIds, setSelectedLensIds] = useState(defaultLenses.slice(0, 3).map((lens) => lens.id)); + const [selectedLensIds, setSelectedLensIds] = useState(INITIAL_SELECTED_LENS_IDS); const [framingMode, setFramingMode] = useState(FramingModes.MATCH_HEIGHT); const [subjectHeight, setSubjectHeight] = useState(1.7); const [frameFill, setFrameFill] = useState(0.75); @@ -467,6 +529,7 @@ const App = () => { const [maxSeparationLimit, setMaxSeparationLimit] = useState(80); const [checkpointsInput, setCheckpointsInput] = useState('3,6,10'); const [activeLensId, setActiveLensId] = useState(null); + const [lensColors, setLensColors] = useState(() => createInitialColorAssignments(INITIAL_SELECTED_LENS_IDS)); const checkpoints = useMemo(() => { const userValues = parseCheckpointInput(checkpointsInput); @@ -492,6 +555,10 @@ const App = () => { }); }, []); + useEffect(() => { + setLensColors((currentAssignments) => reconcileColorAssignments(selectedLensIds, currentAssignments)); + }, [selectedLensIds]); + const lensCurves = useMemo(() => { return selectedLenses .map((lens) => { @@ -536,10 +603,11 @@ const App = () => { dataset, checkpointStats, entrancePupil: checkpointStats[0]?.entrancePupil ?? 0, + colorIndex: lensColors[lens.id] ?? 0, }; }) .filter(Boolean); - }, [selectedLenses, framingMode, fixedSubjectDistance, subjectHeight, frameFill, customSubjectDistance, customDimension, separationSamples, checkpoints]); + }, [selectedLenses, framingMode, fixedSubjectDistance, subjectHeight, frameFill, customSubjectDistance, customDimension, separationSamples, checkpoints, lensColors]); const statsPayload = lensCurves.map((curve) => ({ lens: curve.lens,