Skip to content

Commit 46cf456

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat(undo): Implement comprehensive undo support for visual states
1 parent 0eaface commit 46cf456

3 files changed

Lines changed: 78 additions & 45 deletions

File tree

src/App.tsx

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -188,35 +188,10 @@ function App() {
188188
const [showIons, setShowIons] = useState(false);
189189

190190
const [showSurface, setShowSurface] = useState(initialUrlState.showSurface || false);
191+
const [measurements, setMeasurements] = useState<Measurement[]>([]);
192+
const [proteinTitle, setProteinTitle] = useState<string | null>(null);
191193

192-
// Undo/Redo Stack
193-
const visualState: VisualState = useMemo(() => ({
194-
representation,
195-
coloring,
196-
colorPalette,
197-
showLigands,
198-
showIons,
199-
showSurface,
200-
customBackgroundColor: customBackgroundColor || '',
201-
customColors
202-
}), [representation, coloring, colorPalette, showLigands, showIons, showSurface, customBackgroundColor, customColors]);
203194

204-
const handleVisualStateChange = useCallback((newState: VisualState) => {
205-
setRepresentation(newState.representation);
206-
setColoring(newState.coloring);
207-
setColorPalette(newState.colorPalette);
208-
setShowLigands(newState.showLigands);
209-
setShowIons(newState.showIons);
210-
setShowSurface(newState.showSurface);
211-
setCustomBackgroundColor(newState.customBackgroundColor || null);
212-
setCustomColors(newState.customColors);
213-
}, []);
214-
215-
const { undo, redo, canUndo, canRedo } = useVisualStack({
216-
state: visualState,
217-
onChange: handleVisualStateChange,
218-
resetTrigger: pdbId // Reset stack when PDB ID changes to avoid cross-structure confusing undos
219-
});
220195

221196
// ... (lines 53-343) ...
222197

@@ -319,29 +294,75 @@ function App() {
319294
// Store previous theme to restore after exiting Publication Mode
320295
const previousThemeRef = useRef(isLightMode);
321296

322-
// Effect to apply Publication Mode settings
323-
useEffect(() => {
324-
if (isPublicationMode) {
325-
// Save current theme before overriding
326-
previousThemeRef.current = isLightMode;
297+
const togglePublicationMode = useCallback((shouldBeEnabled?: boolean) => {
298+
const nextState = shouldBeEnabled !== undefined ? shouldBeEnabled : !isPublicationMode;
299+
300+
if (nextState === isPublicationMode) return;
327301

328-
// Auto-set High Quality Defaults
302+
setIsPublicationMode(nextState);
303+
304+
if (nextState) {
305+
// Enable
306+
previousThemeRef.current = isLightMode;
329307
setRepresentation('cartoon');
330308
setIsCleanMode(true);
331309
setColoring('chainid');
332310
setCustomBackgroundColor('#ffffff'); // White background
333311
setIsLightMode(true); // Ensure light mode for paper look
334312
} else {
335-
// Restore previous configuration
313+
// Disable
336314
setIsCleanMode(false);
337315
setCustomBackgroundColor(null); // Revert to theme
338316
setIsLightMode(previousThemeRef.current); // Restore original theme
339317
}
340-
// eslint-disable-next-line react-hooks/exhaustive-deps
341-
}, [isPublicationMode]);
318+
}, [isPublicationMode, isLightMode]);
319+
320+
321+
342322

343323
// ... (fetchTitle logic) ...
344324

325+
// Undo/Redo Stack (Moved here to access all state variables)
326+
const visualState: VisualState = useMemo(() => ({
327+
representation,
328+
coloring,
329+
colorPalette,
330+
showLigands,
331+
showIons,
332+
showSurface,
333+
customBackgroundColor: customBackgroundColor || '',
334+
customColors,
335+
isSpinning,
336+
isCleanMode,
337+
showContactMap,
338+
isPublicationMode,
339+
highlightedResidue,
340+
measurements
341+
}), [representation, coloring, colorPalette, showLigands, showIons, showSurface, customBackgroundColor, customColors, isSpinning, isCleanMode, showContactMap, isPublicationMode, highlightedResidue, measurements]);
342+
343+
const handleVisualStateChange = useCallback((newState: VisualState) => {
344+
setRepresentation(newState.representation);
345+
setColoring(newState.coloring);
346+
setColorPalette(newState.colorPalette);
347+
setShowLigands(newState.showLigands);
348+
setShowIons(newState.showIons);
349+
setShowSurface(newState.showSurface);
350+
setCustomBackgroundColor(newState.customBackgroundColor || null);
351+
setCustomColors(newState.customColors);
352+
setIsSpinning(newState.isSpinning);
353+
setIsCleanMode(newState.isCleanMode);
354+
setShowContactMap(newState.showContactMap);
355+
setIsPublicationMode(newState.isPublicationMode);
356+
setHighlightedResidue(newState.highlightedResidue);
357+
setMeasurements(newState.measurements);
358+
}, []);
359+
360+
const { undo, redo, canUndo, canRedo } = useVisualStack({
361+
state: visualState,
362+
onChange: handleVisualStateChange,
363+
resetTrigger: pdbId
364+
});
365+
345366
// --- DERIVED STATE (Dr. AI V4) ---
346367
const structureStats = useMemo(() => {
347368
const chainCount = chains.length;
@@ -430,7 +451,7 @@ function App() {
430451
}
431452
}, [showLigands, initialUrlState.showLigands, pdbId, dataSource, addToHistory]);
432453

433-
const [proteinTitle, setProteinTitle] = useState<string | null>(null);
454+
434455

435456
// Consolidate Title & Metadata Fetching
436457
// 1. Remove separate 'fetchTitle' effect that was specific to RCSB PDB
@@ -815,7 +836,7 @@ function App() {
815836
const [hoveredResidue, setHoveredResidue] = useState<ResidueInfo | null>(null);
816837

817838
// --- MEASUREMENT STATE ---
818-
const [measurements, setMeasurements] = useState<Measurement[]>([]);
839+
819840
const [isMeasurementPanelOpen, setIsMeasurementPanelOpen] = useState(false);
820841
const [measurementTextColorMode, setMeasurementTextColorMode] = useState<MeasurementTextColor>('auto');
821842

@@ -968,7 +989,7 @@ function App() {
968989
label: isPublicationMode ? 'Exit Publication Mode' : 'Enter Publication Mode',
969990
icon: Camera,
970991
category: 'View',
971-
perform: () => setIsPublicationMode(prev => !prev)
992+
perform: () => togglePublicationMode()
972993
},
973994
{
974995
id: 'take-snapshot',
@@ -1344,7 +1365,7 @@ function App() {
13441365
isMeasurementMode={isMeasurementMode}
13451366
setIsMeasurementMode={setIsMeasurementMode}
13461367
isPublicationMode={isPublicationMode}
1347-
setIsPublicationMode={setIsPublicationMode}
1368+
onTogglePublicationMode={togglePublicationMode}
13481369
onClearMeasurements={() => {
13491370
setMeasurements([]);
13501371
viewerRef.current?.clearMeasurements();

src/components/Controls.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ interface ControlsProps {
277277
isMeasurementMode: boolean;
278278
setIsMeasurementMode: (mode: boolean) => void;
279279
isPublicationMode: boolean;
280-
setIsPublicationMode: (val: boolean) => void;
280+
onTogglePublicationMode: () => void;
281281
onShare: () => void;
282282
onToggleMeasurement?: () => void;
283283
onClearMeasurements: () => void;
@@ -368,7 +368,7 @@ export const Controls: React.FC<ControlsProps> = ({
368368
onToggleMeasurement,
369369
onClearMeasurements,
370370
isPublicationMode,
371-
setIsPublicationMode,
371+
onTogglePublicationMode,
372372
pdbMetadata,
373373
isLightMode,
374374
setIsLightMode,
@@ -1118,7 +1118,7 @@ export const Controls: React.FC<ControlsProps> = ({
11181118
{/* Tools: Publication & Measure */}
11191119
<div className="pt-2 border-t border-white/5 space-y-2">
11201120
<button
1121-
onClick={() => setIsPublicationMode(!isPublicationMode)}
1121+
onClick={onTogglePublicationMode}
11221122
className={`w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-xs font-semibold transition-all ${isPublicationMode
11231123
? 'bg-purple-600 text-white shadow-lg shadow-purple-500/20'
11241124
: (isLightMode ? 'bg-neutral-100 hover:bg-purple-50 text-neutral-600 hover:text-purple-600 border border-neutral-200' : 'bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-white/10')

src/hooks/useVisualStack.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useCallback, useRef } from 'react';
2-
import type { RepresentationType, ColoringType, ColorPalette, CustomColorRule } from '../types';
2+
import type { RepresentationType, ColoringType, ColorPalette, CustomColorRule, ResidueInfo, Measurement } from '../types';
33

44
export interface VisualState {
55
representation: RepresentationType;
@@ -10,6 +10,12 @@ export interface VisualState {
1010
showSurface: boolean;
1111
customBackgroundColor: string;
1212
customColors: CustomColorRule[];
13+
isSpinning: boolean;
14+
isCleanMode: boolean;
15+
showContactMap: boolean;
16+
isPublicationMode: boolean;
17+
highlightedResidue: ResidueInfo | null;
18+
measurements: Measurement[];
1319
}
1420

1521
interface UseVisualStackProps {
@@ -55,7 +61,13 @@ export function useVisualStack({ state, onChange, resetTrigger }: UseVisualStack
5561
prev.showIons !== state.showIons ||
5662
prev.showSurface !== state.showSurface ||
5763
prev.customBackgroundColor !== state.customBackgroundColor ||
58-
prev.customColors !== state.customColors;
64+
prev.customColors !== state.customColors ||
65+
prev.isSpinning !== state.isSpinning ||
66+
prev.isCleanMode !== state.isCleanMode ||
67+
prev.showContactMap !== state.showContactMap ||
68+
prev.isPublicationMode !== state.isPublicationMode ||
69+
prev.highlightedResidue !== state.highlightedResidue ||
70+
prev.measurements !== state.measurements;
5971

6072
if (hasChanged) {
6173
setPast(prevPast => {

0 commit comments

Comments
 (0)