import 'dart:convert'; import 'package:flutter/material.dart'; import 'drawing_line.dart'; import 'paint_preset.dart'; /// Controller for managing drawing state with undo/redo support. /// /// Manages a stack of [DrawingLine] objects and provides methods for /// drawing, undoing, redoing, and serializing the drawing state. class DrawingController extends ChangeNotifier { /// All completed lines in the drawing final List _lines = []; /// Lines that have been undone (for redo functionality) final List _undoneLines = []; /// The line currently being drawn (null when not drawing) DrawingLine? _currentLine; /// Current paint preset being used PaintPreset _currentPreset = PaintPreset.blackPen; /// Maximum number of history steps to keep final int maxHistorySteps; DrawingController({this.maxHistorySteps = 50}); // --------------------------------------------------------------------------- // Getters // --------------------------------------------------------------------------- /// All completed lines (read-only) List get lines => List.unmodifiable(_lines); /// The line currently being drawn DrawingLine? get currentLine => _currentLine; /// Current paint preset PaintPreset get currentPreset => _currentPreset; /// Whether undo is available bool get canUndo => _lines.isNotEmpty; /// Whether redo is available bool get canRedo => _undoneLines.isNotEmpty; // --------------------------------------------------------------------------- // Drawing Operations // --------------------------------------------------------------------------- /// Starts a new line at the given normalized position. void startLine(Offset normalizedPoint) { _currentLine = DrawingLine( points: [normalizedPoint], color: _currentPreset.color, strokeWidth: _currentPreset.strokeWidth, ); notifyListeners(); } /// Adds a point to the current line. void addPoint(Offset normalizedPoint) { if (_currentLine == null) return; _currentLine = _currentLine!.addPoint(normalizedPoint); notifyListeners(); } /// Completes the current line and adds it to the history. void endLine() { if (_currentLine == null) return; // Only add lines with at least 2 points if (_currentLine!.points.length >= 2) { _lines.add(_currentLine!); // Clear redo stack when new action is performed _undoneLines.clear(); _trimHistory(); } _currentLine = null; notifyListeners(); } /// Trims history to maxHistorySteps to prevent memory growth. void _trimHistory() { while (_lines.length > maxHistorySteps) { _lines.removeAt(0); } } // --------------------------------------------------------------------------- // Undo/Redo // --------------------------------------------------------------------------- /// Undoes the last line drawn. void undo() { if (!canUndo) return; final line = _lines.removeLast(); _undoneLines.add(line); notifyListeners(); } /// Redoes the last undone line. void redo() { if (!canRedo) return; final line = _undoneLines.removeLast(); _lines.add(line); notifyListeners(); } /// Clears all lines from the canvas. void clear() { _lines.clear(); _undoneLines.clear(); _currentLine = null; notifyListeners(); } // --------------------------------------------------------------------------- // Paint Preset // --------------------------------------------------------------------------- /// Sets the current paint preset. void setPreset(PaintPreset preset) { _currentPreset = preset; notifyListeners(); } // --------------------------------------------------------------------------- // Serialization // --------------------------------------------------------------------------- /// Exports all lines to a JSON-serializable list. List> toJsonList() { return _lines.map((line) => line.toJson()).toList(); } /// Exports all lines to a JSON string. String toJsonString() { return jsonEncode(toJsonList()); } /// Imports lines from a JSON-serializable list. void fromJsonList(List> jsonList) { _lines.clear(); _undoneLines.clear(); _currentLine = null; for (final json in jsonList) { _lines.add(DrawingLine.fromJson(json)); } notifyListeners(); } /// Imports lines from a JSON string. void fromJsonString(String jsonString) { if (jsonString.isEmpty || jsonString == '[]') { clear(); return; } final decoded = jsonDecode(jsonString) as List; if (decoded.isEmpty) { clear(); return; } fromJsonList(decoded.cast>()); } /// Adds existing lines without clearing (for merging annotations). void addLines(List newLines) { _lines.addAll(newLines); _trimHistory(); notifyListeners(); } @override void dispose() { _lines.clear(); _undoneLines.clear(); super.dispose(); } }