Skip to content

Commit 28c81d2

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: Implement smooth custom color transitions using NGL custom schemes
1 parent b54033f commit 28c81d2

1 file changed

Lines changed: 68 additions & 25 deletions

File tree

src/components/ProteinViewer.tsx

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
136136
const highlightComponentRef = useRef<any>(null);
137137
const isMounted = useRef(true);
138138
const onHoverRef = useRef(onHover);
139+
const viewerInstanceId = useRef(crypto.randomUUID()); // Unique ID for custom schemes
139140

140141
// Update ref when prop changes
141142
useEffect(() => {
@@ -1761,6 +1762,16 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
17611762

17621763
console.log("Coloring Debug:", { currentColoring, repType, hasValidCustomRules, rules: customColors });
17631764

1765+
// Capture the base coloring intent (e.g. 'chainid') to use as fallback in our custom scheme
1766+
const baseColoringForScheme = currentColoring;
1767+
1768+
// If we have custom rules, we want to bypass the manual 'chainid' and 'charge' blocks below
1769+
// and fall through to the generic 'else' block where our new Custom Scheme logic lives.
1770+
// We force currentColoring to a value that won't match the specific if-blocks.
1771+
if (hasValidCustomRules) {
1772+
currentColoring = 'custom_override' as any;
1773+
}
1774+
17641775
// --- STRATEGY: MULTI-REPRESENTATION OVERLAY (RESTORED & IMPROVED) ---
17651776
// NGL Custom Schemes proved fragile for this user.
17661777
// We implementation "High Contrast Chain Coloring" by EXPLICITLY adding a representation for each chain.
@@ -1857,7 +1868,58 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18571868
return PALETTES[p] || undefined;
18581869
};
18591870

1860-
// Unified cartoon with optimized parameters for arrows and helices
1871+
// 2. Custom Color Scheme with Smooth Blending
1872+
// Instead of overlays (which cause jagged geometry), we create a custom NGL scheme
1873+
// that delegates to the base scheme for non-overridden atoms.
1874+
1875+
let finalColoring = currentColoring;
1876+
1877+
if (hasValidCustomRules) {
1878+
const schemeId = `custom_scheme_${viewerInstanceId.current}`;
1879+
const atomColorMap = new Map<number, number>();
1880+
1881+
// Pre-calculate atom colors
1882+
customColors.forEach(rule => {
1883+
if (rule.color && rule.target) {
1884+
const colorHex = new window.NGL.Color(rule.color).getHex();
1885+
try {
1886+
// Efficiently populate map using NGL selection
1887+
component.structure.eachAtom((atom: any) => {
1888+
atomColorMap.set(atom.index, colorHex);
1889+
}, new window.NGL.Selection(rule.target));
1890+
} catch (e) {
1891+
console.warn("Invalid selection for custom rule:", rule.target);
1892+
}
1893+
}
1894+
});
1895+
1896+
// Register (or overwrite) the custom scheme
1897+
// We capture 'atomColorMap' and 'currentColoring' in the closure
1898+
window.NGL.ColormakerRegistry.addScheme(function (this: any, params: any) {
1899+
this.parameters = params;
1900+
1901+
// Instantiate the base scheme (fallback)
1902+
// We must handle 'standard' or other aliases if necessary, but usually NGL names match
1903+
// If currentColoring is not a registered scheme, fallback to 'uniform' (white)
1904+
const BaseSchemeClass = window.NGL.ColormakerRegistry.getScheme(baseColoringForScheme);
1905+
this.baseScheme = BaseSchemeClass ? new BaseSchemeClass(params) : null;
1906+
1907+
this.atomColor = function (atom: any) {
1908+
const custom = atomColorMap.get(atom.index);
1909+
if (custom !== undefined) return custom;
1910+
1911+
if (this.baseScheme) {
1912+
return this.baseScheme.atomColor(atom);
1913+
}
1914+
return 0xFFFFFF; // Fallback white
1915+
};
1916+
}, schemeId);
1917+
1918+
finalColoring = schemeId;
1919+
console.log(`Applied smooth custom coloring: ${schemeId} with ${atomColorMap.size} atom overrides`);
1920+
}
1921+
1922+
// Unified cartoon with optimized parameters
18611923
if (repType === 'cartoon') {
18621924
// Force recalculation of secondary structure
18631925
try {
@@ -1867,10 +1929,10 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18671929
} catch (e) { }
18681930

18691931
const params: any = {
1870-
color: currentColoring,
1871-
aspectRatio: 5, // Flat arrows for sheets
1872-
subdiv: 12, // Smooth curves
1873-
radialSegments: 20, // Smooth helix cylinders
1932+
color: finalColoring,
1933+
aspectRatio: 5,
1934+
subdiv: 12,
1935+
radialSegments: 20,
18741936
};
18751937

18761938
const scale = getColorScale(colorPalette);
@@ -1881,7 +1943,7 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18811943
} else {
18821944
// Non-cartoon representations
18831945
const params: any = {
1884-
color: currentColoring
1946+
color: finalColoring
18851947
};
18861948
const scale = getColorScale(colorPalette);
18871949
if (scale && scale.length > 0) {
@@ -1891,25 +1953,6 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18911953
}
18921954
}
18931955

1894-
// 2. Add Custom Representations (Overlay)
1895-
if (hasValidCustomRules) {
1896-
console.log("Applying Custom Rules as Overlays:", customColors.length);
1897-
customColors.forEach((rule, idx) => {
1898-
if (rule.color && rule.target) {
1899-
try {
1900-
// Add a separate representation for this rule
1901-
component.addRepresentation(repType, {
1902-
color: new NGL.Color(rule.color).getHex(),
1903-
sele: rule.target,
1904-
name: `custom_rule_${idx}`
1905-
});
1906-
} catch (e) {
1907-
console.warn("Failed to apply custom rule:", rule, e);
1908-
}
1909-
}
1910-
});
1911-
}
1912-
19131956
// --- OVERLAYS ---
19141957
const tryApply = (r: string, c: string, sele: string, params: any = {}) => {
19151958
try { component.addRepresentation(r, { color: c, sele: sele, ...params }); } catch (e) { }

0 commit comments

Comments
 (0)