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,