Skip to content

Commit 6fa082f

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
fix: Robust fallback for Custom coloring and unique scheme IDs
1 parent 202a1c6 commit 6fa082f

1 file changed

Lines changed: 110 additions & 82 deletions

File tree

src/components/ProteinViewer.tsx

Lines changed: 110 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,14 +1832,30 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18321832
});
18331833
} else if (currentColoring === 'custom') {
18341834
// --- CUSTOM COLORING MODE: Smooth Gradient Rendering ---
1835-
// This mode uses residue-level color processing with gradient blending
1835+
// Improved robustness and fallback logic
1836+
1837+
// 1. If no custom rules, just use element coloring directly (simple & robust)
1838+
if (!hasValidCustomRules) {
1839+
if (repType === 'cartoon') {
1840+
component.addRepresentation('cartoon', {
1841+
color: 'element',
1842+
aspectRatio: 5,
1843+
subdiv: 12,
1844+
radialSegments: 20
1845+
});
1846+
} else {
1847+
component.addRepresentation(repType, { color: 'element' });
1848+
}
1849+
} else {
1850+
// 2. We have rules - use the custom scheme with gradients
1851+
1852+
// Use unique ID to prevent NGL caching stale schemes
1853+
const schemeId = `custom_smooth_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
18361854

1837-
const schemeId = 'custom_smooth_coloring';
1838-
const atomColorMap = new Map<number, number>();
1839-
const residueColorMap = new Map<string, number>();
1855+
const atomColorMap = new Map<number, number>();
1856+
const residueColorMap = new Map<string, number>();
18401857

1841-
// Build residue-level color map from custom rules
1842-
if (hasValidCustomRules) {
1858+
// Build residue-level color map from custom rules
18431859
customColors.forEach(rule => {
18441860
if (rule.color && rule.target) {
18451861
const colorHex = new window.NGL.Color(rule.color).getHex();
@@ -1853,95 +1869,107 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18531869
}
18541870
}
18551871
});
1856-
}
18571872

1858-
// Helper: blend two colors
1859-
const blendColors = (c1: number, c2: number, factor: number) => {
1860-
const r = Math.round(((c1 >> 16) & 0xFF) + (((c2 >> 16) & 0xFF) - ((c1 >> 16) & 0xFF)) * factor);
1861-
const g = Math.round(((c1 >> 8) & 0xFF) + (((c2 >> 8) & 0xFF) - ((c1 >> 8) & 0xFF)) * factor);
1862-
const b = Math.round((c1 & 0xFF) + ((c2 & 0xFF) - (c1 & 0xFF)) * factor);
1863-
return (r << 16) | (g << 8) | b;
1864-
};
1873+
// Helper: blend two colors
1874+
const blendColors = (c1: number, c2: number, factor: number) => {
1875+
const r = Math.round(((c1 >> 16) & 0xFF) + (((c2 >> 16) & 0xFF) - ((c1 >> 16) & 0xFF)) * factor);
1876+
const g = Math.round(((c1 >> 8) & 0xFF) + (((c2 >> 8) & 0xFF) - ((c1 >> 8) & 0xFF)) * factor);
1877+
const b = Math.round((c1 & 0xFF) + ((c2 & 0xFF) - (c1 & 0xFF)) * factor);
1878+
return (r << 16) | (g << 8) | b;
1879+
};
1880+
1881+
// Apply gradient transitions at boundaries
1882+
const gradWidth = 2; // Number of residues to blend on each side
18651883

1866-
// Apply gradient transitions at boundaries
1867-
const gradWidth = 2; // Number of residues to blend on each side
1868-
component.structure.eachResidue((res: any) => {
1869-
const key = `${res.chainname}:${res.resno}`;
1870-
const customCol = residueColorMap.get(key);
1871-
1872-
// Get element color for this residue
1873-
const firstAtom = Array.from(res.iterateAtom())[0];
1874-
const ElementScheme = window.NGL.ColormakerRegistry.getScheme('element');
1875-
const baseCol = ElementScheme ? new ElementScheme({}).atomColor(firstAtom) : 0xCCCCCC;
1876-
1877-
let finalCol = customCol !== undefined ? customCol : baseCol;
1878-
1879-
// If not custom colored, check for nearby custom residues for gradient
1880-
if (customCol === undefined) {
1881-
let nearestCustom = null;
1882-
let nearestDist = gradWidth + 1;
1883-
1884-
for (let d = -gradWidth; d <= gradWidth; d++) {
1885-
if (d === 0) continue;
1886-
const nKey = `${res.chainname}:${res.resno + d}`;
1887-
const nCol = residueColorMap.get(nKey);
1888-
if (nCol !== undefined && Math.abs(d) < nearestDist) {
1889-
nearestDist = Math.abs(d);
1890-
nearestCustom = nCol;
1884+
try {
1885+
component.structure.eachResidue((res: any) => {
1886+
const key = `${res.chainname}:${res.resno}`;
1887+
const customCol = residueColorMap.get(key);
1888+
1889+
// Get element color for this residue
1890+
let baseCol = 0xCCCCCC;
1891+
try {
1892+
const firstAtom = Array.from(res.iterateAtom())[0];
1893+
const ElementScheme = window.NGL.ColormakerRegistry.getScheme('element');
1894+
if (ElementScheme) {
1895+
const es = new ElementScheme({});
1896+
baseCol = es.atomColor(firstAtom);
1897+
}
1898+
} catch (e) { /* ignore */ }
1899+
1900+
let finalCol = customCol !== undefined ? customCol : baseCol;
1901+
1902+
// If not custom colored, check for nearby custom residues for gradient
1903+
if (customCol === undefined) {
1904+
let nearestCustom = null;
1905+
let nearestDist = gradWidth + 1;
1906+
1907+
for (let d = -gradWidth; d <= gradWidth; d++) {
1908+
if (d === 0) continue;
1909+
const nKey = `${res.chainname}:${res.resno + d}`;
1910+
const nCol = residueColorMap.get(nKey);
1911+
if (nCol !== undefined && Math.abs(d) < nearestDist) {
1912+
nearestDist = Math.abs(d);
1913+
nearestCustom = nCol;
1914+
}
1915+
}
1916+
1917+
if (nearestCustom !== null) {
1918+
const blend = 1 - (nearestDist / (gradWidth + 1));
1919+
finalCol = blendColors(baseCol, nearestCustom, blend);
1920+
}
18911921
}
1892-
}
18931922

1894-
if (nearestCustom !== null) {
1895-
const blend = 1 - (nearestDist / (gradWidth + 1));
1896-
finalCol = blendColors(baseCol, nearestCustom, blend);
1897-
}
1923+
// Apply color to all atoms in residue
1924+
res.eachAtom((atom: any) => {
1925+
atomColorMap.set(atom.index, finalCol);
1926+
});
1927+
});
1928+
} catch (e) {
1929+
console.warn("Error in gradient processing", e);
18981930
}
18991931

1900-
// Apply color to all atoms in residue
1901-
res.eachAtom((atom: any) => {
1902-
atomColorMap.set(atom.index, finalCol);
1903-
});
1904-
});
1905-
1906-
// Fallback: if atomColorMap is empty, populate with element colors
1907-
if (atomColorMap.size === 0) {
1908-
const ElementScheme = window.NGL.ColormakerRegistry.getScheme('element');
1909-
const elementColorer = ElementScheme ? new ElementScheme({}) : null;
1910-
component.structure.eachAtom((atom: any) => {
1911-
const col = elementColorer ? elementColorer.atomColor(atom) : 0xCCCCCC;
1912-
atomColorMap.set(atom.index, col);
1913-
});
1914-
}
1932+
// Fallback: if atomColorMap is still empty (e.g. error above), force populate
1933+
if (atomColorMap.size === 0) {
1934+
try {
1935+
const ElementScheme = window.NGL.ColormakerRegistry.getScheme('element');
1936+
const es = ElementScheme ? new ElementScheme({}) : null;
1937+
component.structure.eachAtom((atom: any) => {
1938+
atomColorMap.set(atom.index, es ? es.atomColor(atom) : 0xCCCCCC);
1939+
});
1940+
} catch (e) { console.error("Fallback population failed", e); }
1941+
}
19151942

1943+
// Register the custom color scheme
1944+
window.NGL.ColormakerRegistry.addScheme(function (this: any) {
1945+
this.atomColor = function (atom: any) {
1946+
return atomColorMap.get(atom.index) || 0xCCCCCC;
1947+
};
1948+
}, schemeId);
19161949

1917-
// Register the custom color scheme
1918-
window.NGL.ColormakerRegistry.addScheme(function (this: any) {
1919-
this.atomColor = function (atom: any) {
1920-
return atomColorMap.get(atom.index) || 0xCCCCCC;
1921-
};
1922-
}, schemeId);
1950+
// Create single representation with custom scheme
1951+
if (repType === 'cartoon') {
1952+
try {
1953+
component.structure.eachModel((m: any) => {
1954+
if (m.calculateSecondaryStructure) m.calculateSecondaryStructure();
1955+
});
1956+
} catch (e) { }
19231957

1924-
// Create single representation with custom scheme
1925-
if (repType === 'cartoon') {
1926-
try {
1927-
component.structure.eachModel((m: any) => {
1928-
if (m.calculateSecondaryStructure) m.calculateSecondaryStructure();
1958+
component.addRepresentation('cartoon', {
1959+
color: schemeId,
1960+
aspectRatio: 5,
1961+
subdiv: 12,
1962+
radialSegments: 20,
19291963
});
1930-
} catch (e) { }
1964+
} else {
1965+
component.addRepresentation(repType, {
1966+
color: schemeId
1967+
});
1968+
}
19311969

1932-
component.addRepresentation('cartoon', {
1933-
color: schemeId,
1934-
aspectRatio: 5,
1935-
subdiv: 12,
1936-
radialSegments: 20,
1937-
});
1938-
} else {
1939-
component.addRepresentation(repType, {
1940-
color: schemeId
1941-
});
1970+
console.log(`Custom coloring applied: ${schemeId}`);
19421971
}
19431972

1944-
console.log(`Custom coloring: ${residueColorMap.size} residues colored, ${atomColorMap.size} atoms total`);
19451973
} else {
19461974
// Standard Coloring for other modes (sstruc, element, etc.) -> Robust Native NGL
19471975
// REVERTED to use 'color' property as previously working.

0 commit comments

Comments
 (0)