187 lines
8.0 KiB
Markdown
187 lines
8.0 KiB
Markdown
## Detailed Chat Summary
|
|
|
|
### Phase 1: Audit & Analysis
|
|
Started with a 1800-line Zustand timeline store doing EVERYTHING (core logic + UI state + history). Performed comprehensive audit identifying:
|
|
- ~40 methods mixing core logic with UI orchestration
|
|
- Inconsistent parameter patterns (some destructured, some not)
|
|
- Redundant methods (`updateElementStartTime` vs `updateElementStartTimeWithRipple`)
|
|
- Three separate split operations that should be unified
|
|
- Delete operations conflating UI state with core logic
|
|
- Methods referencing `selectedElements` (UI state) that shouldn't exist in core
|
|
|
|
### Phase 2: Architecture Decision
|
|
Decided on Command Pattern + separation of concerns:
|
|
- **Core Logic** → TimelineManager + Commands
|
|
- **UI State** → Zustand store
|
|
- **Ephemeral Interaction** → Component state (dragState)
|
|
- **History Tracking** → HistoryManager
|
|
|
|
### Phase 3: HistoryManager Creation
|
|
Created generic `HistoryManager<T>`:
|
|
- `execute(command)` returns the command (enables getting return values)
|
|
- Maintains `history` and `redoStack` arrays of Command objects
|
|
- `undo()` pops from history, calls `command.undo()`
|
|
- `redo()` pops from redoStack, calls `command.redo()`
|
|
- Methods: `canUndo()`, `canRedo()`, `clear()`
|
|
|
|
### Phase 4: Command Architecture
|
|
Created base `Command` abstract class in `src/lib/commands/base-command.ts`:
|
|
- Abstract `execute()` method (must implement)
|
|
- `undo()` throws by default (implement if reversible)
|
|
- `redo()` defaults to calling `execute()` again
|
|
|
|
Built 15 command classes organized in folders:
|
|
- **Track operations** (3): `AddTrackCommand`, `RemoveTrackCommand`, `ToggleTrackMuteCommand`
|
|
- **Element CRUD** (5): `AddElementToTrackCommand`, `DeleteElementsCommand`, `DuplicateElementsCommand`, `UpdateElementTrimCommand`, `UpdateElementDurationCommand`
|
|
- **Element properties** (4): `UpdateTextElementCommand`, `ToggleElementsHiddenCommand`, `ToggleElementsMutedCommand`, `SplitElementsCommand`
|
|
- **Unified position** (1): `UpdateElementStartTimeCommand` (replaced 3 old methods)
|
|
- **Clipboard** (1): `PasteCommand`
|
|
|
|
Each command:
|
|
- Takes minimal parameters in constructor
|
|
- Saves state before execution for undo
|
|
- Implements `execute()` and `undo()`
|
|
- Has getters for return values (e.g., `getTrackId()`)
|
|
- Uses `EditorCore.getInstance()` to access timeline manager
|
|
|
|
### Phase 5: AddTrackCommand Details
|
|
Updated `AddTrackCommand` to support optional `index` parameter (unified `addTrack` and `insertTrackAt`):
|
|
- Constructor: `new AddTrackCommand(type, index?)`
|
|
- Splices into array at index if provided, appends if not
|
|
- Returns trackId via `getTrackId()` getter
|
|
- Saves full state for undo
|
|
|
|
### Phase 6: TimelineManager Refactor - Part 1
|
|
Removed ALL UI state fields:
|
|
- ❌ `clipboard` → UI state
|
|
- ❌ `snappingEnabled` → UI preference
|
|
- ❌ `rippleEditingEnabled` → UI preference
|
|
- ❌ `selectedElements` → UI selection
|
|
- ❌ `dragState` → ephemeral interaction
|
|
- ❌ `DragData` import (only used for UI state)
|
|
|
|
### Phase 7: TimelineManager Refactor - Part 2
|
|
Updated method implementations to use commands:
|
|
- `addTrack()` → `new AddTrackCommand(type, index).execute()`
|
|
- `removeTrack()` → `new RemoveTrackCommand(trackId).execute()`
|
|
- `updateElementTrim()` → creates command, conditionally pushes history
|
|
- `updateElementDuration()` → creates command, conditionally pushes history
|
|
- `updateElementStartTime()` → simplified (always through history)
|
|
- `deleteElements()` → simplified (always through history)
|
|
- `splitElements()` → `new SplitElementsCommand(elements, splitTime, retainSide).execute()`
|
|
- `duplicateElements()`, `toggleElementsHidden()`, `toggleElementsMuted()` → via commands
|
|
|
|
### Phase 8: TimelineManager Cleanup
|
|
Removed methods that referenced UI state:
|
|
- ❌ `getSortedTracks()` (stub, redundant)
|
|
- ❌ `toggleSnapping()` (UI preference)
|
|
- ❌ `toggleRippleEditing()` (UI preference)
|
|
- ❌ `selectElement()`, `deselectElement()`, etc. (UI selection)
|
|
- ❌ `setDragState()`, `startDrag()`, `updateDragTime()`, `endDrag()` (UI interaction)
|
|
- ❌ `copySelected()` (orchestrates UI state + core logic)
|
|
- ❌ `deleteSelectedElements()` (uses UI state)
|
|
- ❌ `splitSelected()` (uses UI state)
|
|
- ❌ `toggleSelectedHidden()`, `toggleSelectedMuted()` (use UI state)
|
|
- ❌ `revealElementInMedia()` (UI navigation)
|
|
- ❌ `getContextMenuState()` (UI query)
|
|
- ❌ `addElementAtTime()`, `addElementToNewTrack()` (convenience wrappers)
|
|
|
|
**Result:** TimelineManager reduced from 1800 → 315 lines, now pure core logic only
|
|
|
|
### Phase 9: Timeline Store Refactor
|
|
Completely rewrote store (1800 → 104 lines) to contain ONLY UI state:
|
|
```typescript
|
|
interface TimelineStore {
|
|
// Selection
|
|
selectedElements: { trackId: string; elementId: string }[]
|
|
selectElement()
|
|
deselectElement()
|
|
clearSelectedElements()
|
|
setSelectedElements()
|
|
|
|
// Preferences
|
|
snappingEnabled: boolean
|
|
toggleSnapping()
|
|
|
|
rippleEditingEnabled: boolean
|
|
toggleRippleEditing()
|
|
|
|
// Clipboard
|
|
clipboard: { items: Array<...> } | null
|
|
setClipboard()
|
|
}
|
|
```
|
|
|
|
Removed all methods that:
|
|
- Operated on `_tracks`
|
|
- Managed history
|
|
- Used commands
|
|
- Implemented core timeline logic
|
|
|
|
### Phase 10: Identified dragState Problem
|
|
Analyzed `dragState` usage across three files:
|
|
- **timeline-track.tsx** (producer): Sets/updates drag state on mouse events
|
|
- **timeline-element.tsx** (consumer): Reads `dragState.elementId` and `dragState.currentTime` for visual feedback during drag
|
|
- **index.tsx** (consumer): Reads `dragState.isDragging` to show snap indicator
|
|
|
|
**Problem:** In global Zustand store (wrong scope)
|
|
- Only used during active drag operations
|
|
- Ephemeral state (cleared after drag ends)
|
|
- Causes unnecessary re-renders across entire app
|
|
- Only needs to exist in Timeline component tree
|
|
|
|
**Solution:** Move to Timeline component state:
|
|
- Keep `dragState` as local state in `Timeline` component
|
|
- Pass down via props to `TimelineTrack` and `TimelineElement`
|
|
- Timeline component manages `handleMouseDown`, `handleMouseMove`, `handleMouseUp` in TimelineTrack
|
|
- This keeps scope correct and re-renders localized
|
|
|
|
### Phase 11: Architectural Alignment
|
|
Confirmed final architecture:
|
|
- **Editor Core** (singleton):
|
|
- `history: HistoryManager` (manages undo/redo)
|
|
- `timeline: TimelineManager` (pure data + commands)
|
|
- Other managers (project, media, scene, playback)
|
|
|
|
- **Timeline Component** (React):
|
|
- `dragState` (local state via useState)
|
|
- Props down to children
|
|
|
|
- **Timeline Store** (Zustand):
|
|
- Selection state + methods
|
|
- Preferences (snapping, ripple editing)
|
|
- Clipboard state
|
|
|
|
### Phase 12: Command Pattern Benefits Identified
|
|
1. **Self-contained operations** - Each command knows how to undo itself
|
|
2. **Discoverable API** - All timeline actions are in `/lib/commands/timeline/`
|
|
3. **Type-safe** - Commands are strongly typed
|
|
4. **Testable** - Each command can be unit tested independently
|
|
5. **Scalable** - Adding new operations = creating new command class
|
|
6. **Clean manager** - TimelineManager becomes orchestrator, not logic holder
|
|
7. **Reusable** - Commands can be executed from anywhere (UI, scripts, etc.)
|
|
|
|
### Summary of Changes Made
|
|
|
|
**Files Created:**
|
|
- `src/lib/commands/base-command.ts` (12 lines)
|
|
- `src/lib/commands/index.ts` (3 lines)
|
|
- `src/lib/commands/timeline/index.ts` (4 lines)
|
|
- `src/lib/commands/timeline/track/` (3 commands)
|
|
- `src/lib/commands/timeline/element/` (9 commands)
|
|
- `src/lib/commands/timeline/clipboard/` (1 command)
|
|
|
|
**Files Modified:**
|
|
- `src/core/managers/history-manager.ts` - Refactored to work with Commands, now returns command
|
|
- `src/core/managers/timeline-manager.ts` - Removed UI state, implemented all methods as command-driven
|
|
- `src/stores/timeline-store.ts` - From 1800 → 104 lines, now UI state only
|
|
|
|
**Files Deleted:**
|
|
- Old history logic removed (consolidated into HistoryManager)
|
|
- Removed unused imports from components (cascading changes needed)
|
|
|
|
### Remaining Work
|
|
1. Move `dragState` from store to Timeline component
|
|
2. Update all component imports and method calls
|
|
3. Implement remaining stub methods (`checkElementOverlap`, `findOrCreateTrack`, etc.)
|
|
4. Wire up UI to call new manager methods instead of store methods |