Compare commits
6 Commits
d4d6e41a9d
...
d94e9eeb3d
| Author | SHA1 | Date | |
|---|---|---|---|
| d94e9eeb3d | |||
| d1b5cb54f4 | |||
| b36011d9e8 | |||
| 421171f1a3 | |||
| 3b12be497e | |||
| f615ed5654 |
@@ -96,19 +96,33 @@ class _DrawingBoardState extends State<DrawingBoard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Drawing mode: wrap with InteractiveViewer for zoom/pan
|
// Drawing mode: wrap with InteractiveViewer for zoom/pan
|
||||||
return Align(
|
// Use LayoutBuilder to get available size and center content manually
|
||||||
alignment: widget.alignment,
|
return LayoutBuilder(
|
||||||
child: InteractiveViewer(
|
builder: (context, constraints) {
|
||||||
|
// Calculate padding to center the content
|
||||||
|
final horizontalPadding =
|
||||||
|
(constraints.maxWidth - widget.boardSize.width) / 2;
|
||||||
|
final verticalPadding =
|
||||||
|
(constraints.maxHeight - widget.boardSize.height) / 2;
|
||||||
|
|
||||||
|
return InteractiveViewer(
|
||||||
transformationController: _transformationController,
|
transformationController: _transformationController,
|
||||||
minScale: widget.minScale,
|
minScale: widget.minScale,
|
||||||
maxScale: widget.maxScale,
|
maxScale: widget.maxScale,
|
||||||
boundaryMargin: EdgeInsets.zero,
|
boundaryMargin: const EdgeInsets.all(double.infinity),
|
||||||
constrained: true,
|
constrained: false,
|
||||||
panEnabled: !_isDrawing,
|
panEnabled: !_isDrawing,
|
||||||
scaleEnabled: !_isDrawing,
|
scaleEnabled: !_isDrawing,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: horizontalPadding > 0 ? horizontalPadding : 0,
|
||||||
|
top: verticalPadding > 0 ? verticalPadding : 0,
|
||||||
|
),
|
||||||
child: content,
|
child: content,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the drawing layer with gesture handling.
|
/// Builds the drawing layer with gesture handling.
|
||||||
|
|||||||
@@ -55,10 +55,7 @@ class DrawingPainter extends CustomPainter {
|
|||||||
for (int i = 1; i < line.points.length - 1; i++) {
|
for (int i = 1; i < line.points.length - 1; i++) {
|
||||||
final p0 = _toCanvasPoint(line.points[i]);
|
final p0 = _toCanvasPoint(line.points[i]);
|
||||||
final p1 = _toCanvasPoint(line.points[i + 1]);
|
final p1 = _toCanvasPoint(line.points[i + 1]);
|
||||||
final midPoint = Offset(
|
final midPoint = Offset((p0.dx + p1.dx) / 2, (p0.dy + p1.dy) / 2);
|
||||||
(p0.dx + p1.dx) / 2,
|
|
||||||
(p0.dy + p1.dy) / 2,
|
|
||||||
);
|
|
||||||
path.quadraticBezierTo(p0.dx, p0.dy, midPoint.dx, midPoint.dy);
|
path.quadraticBezierTo(p0.dx, p0.dy, midPoint.dx, midPoint.dy);
|
||||||
}
|
}
|
||||||
// Draw to the last point
|
// Draw to the last point
|
||||||
@@ -116,61 +113,3 @@ class DrawingOverlay extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget that handles drawing input and renders lines.
|
|
||||||
///
|
|
||||||
/// Converts touch/pointer events to normalized coordinates and
|
|
||||||
/// passes them to the [DrawingController].
|
|
||||||
class DrawingCanvas extends StatelessWidget {
|
|
||||||
final DrawingController controller;
|
|
||||||
final Size canvasSize;
|
|
||||||
final bool enabled;
|
|
||||||
|
|
||||||
const DrawingCanvas({
|
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
required this.canvasSize,
|
|
||||||
this.enabled = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Listener(
|
|
||||||
onPointerDown: enabled ? _onPointerDown : null,
|
|
||||||
onPointerMove: enabled ? _onPointerMove : null,
|
|
||||||
onPointerUp: enabled ? _onPointerUp : null,
|
|
||||||
onPointerCancel: enabled ? _onPointerCancel : null,
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
child: DrawingOverlay(
|
|
||||||
controller: controller,
|
|
||||||
canvasSize: canvasSize,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onPointerDown(PointerDownEvent event) {
|
|
||||||
final normalized = _toNormalized(event.localPosition);
|
|
||||||
controller.startLine(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onPointerMove(PointerMoveEvent event) {
|
|
||||||
final normalized = _toNormalized(event.localPosition);
|
|
||||||
controller.addPoint(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onPointerUp(PointerUpEvent event) {
|
|
||||||
controller.endLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onPointerCancel(PointerCancelEvent event) {
|
|
||||||
controller.endLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts canvas coordinates to normalized coordinates (0-1).
|
|
||||||
Offset _toNormalized(Offset canvasPoint) {
|
|
||||||
return Offset(
|
|
||||||
(canvasPoint.dx / canvasSize.width).clamp(0.0, 1.0),
|
|
||||||
(canvasPoint.dy / canvasSize.height).clamp(0.0, 1.0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ import 'package:flutter/material.dart';
|
|||||||
import 'drawing_line.dart';
|
import 'drawing_line.dart';
|
||||||
import 'paint_preset.dart';
|
import 'paint_preset.dart';
|
||||||
|
|
||||||
|
/// Represents an undoable action.
|
||||||
|
sealed class DrawingAction {
|
||||||
|
const DrawingAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Action for adding a line.
|
||||||
|
class AddLineAction extends DrawingAction {
|
||||||
|
final DrawingLine line;
|
||||||
|
const AddLineAction(this.line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Action for erasing lines.
|
||||||
|
class EraseAction extends DrawingAction {
|
||||||
|
final List<DrawingLine> erasedLines;
|
||||||
|
const EraseAction(this.erasedLines);
|
||||||
|
}
|
||||||
|
|
||||||
/// Controller for managing drawing state with undo/redo support.
|
/// Controller for managing drawing state with undo/redo support.
|
||||||
///
|
///
|
||||||
/// Manages a stack of [DrawingLine] objects and provides methods for
|
/// Manages a stack of [DrawingLine] objects and provides methods for
|
||||||
@@ -13,8 +30,11 @@ class DrawingController extends ChangeNotifier {
|
|||||||
/// All completed lines in the drawing
|
/// All completed lines in the drawing
|
||||||
final List<DrawingLine> _lines = [];
|
final List<DrawingLine> _lines = [];
|
||||||
|
|
||||||
/// Lines that have been undone (for redo functionality)
|
/// Stack of actions for undo functionality
|
||||||
final List<DrawingLine> _undoneLines = [];
|
final List<DrawingAction> _undoStack = [];
|
||||||
|
|
||||||
|
/// Stack of actions for redo functionality
|
||||||
|
final List<DrawingAction> _redoStack = [];
|
||||||
|
|
||||||
/// The line currently being drawn (null when not drawing)
|
/// The line currently being drawn (null when not drawing)
|
||||||
DrawingLine? _currentLine;
|
DrawingLine? _currentLine;
|
||||||
@@ -22,6 +42,12 @@ class DrawingController extends ChangeNotifier {
|
|||||||
/// Current paint preset being used
|
/// Current paint preset being used
|
||||||
PaintPreset _currentPreset = PaintPreset.blackPen;
|
PaintPreset _currentPreset = PaintPreset.blackPen;
|
||||||
|
|
||||||
|
/// Whether eraser mode is active
|
||||||
|
bool _isEraserMode = false;
|
||||||
|
|
||||||
|
/// Lines erased in the current eraser stroke (for undo as single action)
|
||||||
|
final List<DrawingLine> _currentErasedLines = [];
|
||||||
|
|
||||||
/// Maximum number of history steps to keep
|
/// Maximum number of history steps to keep
|
||||||
final int maxHistorySteps;
|
final int maxHistorySteps;
|
||||||
|
|
||||||
@@ -40,11 +66,14 @@ class DrawingController extends ChangeNotifier {
|
|||||||
/// Current paint preset
|
/// Current paint preset
|
||||||
PaintPreset get currentPreset => _currentPreset;
|
PaintPreset get currentPreset => _currentPreset;
|
||||||
|
|
||||||
|
/// Whether eraser mode is active
|
||||||
|
bool get isEraserMode => _isEraserMode;
|
||||||
|
|
||||||
/// Whether undo is available
|
/// Whether undo is available
|
||||||
bool get canUndo => _lines.isNotEmpty;
|
bool get canUndo => _undoStack.isNotEmpty;
|
||||||
|
|
||||||
/// Whether redo is available
|
/// Whether redo is available
|
||||||
bool get canRedo => _undoneLines.isNotEmpty;
|
bool get canRedo => _redoStack.isNotEmpty;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Drawing Operations
|
// Drawing Operations
|
||||||
@@ -52,6 +81,12 @@ class DrawingController extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Starts a new line at the given normalized position.
|
/// Starts a new line at the given normalized position.
|
||||||
void startLine(Offset normalizedPoint) {
|
void startLine(Offset normalizedPoint) {
|
||||||
|
if (_isEraserMode) {
|
||||||
|
_currentErasedLines.clear();
|
||||||
|
_eraseAtPoint(normalizedPoint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_currentLine = DrawingLine(
|
_currentLine = DrawingLine(
|
||||||
points: [normalizedPoint],
|
points: [normalizedPoint],
|
||||||
color: _currentPreset.color,
|
color: _currentPreset.color,
|
||||||
@@ -62,21 +97,41 @@ class DrawingController extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Adds a point to the current line.
|
/// Adds a point to the current line.
|
||||||
void addPoint(Offset normalizedPoint) {
|
void addPoint(Offset normalizedPoint) {
|
||||||
|
if (_isEraserMode) {
|
||||||
|
_eraseAtPoint(normalizedPoint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_currentLine == null) return;
|
if (_currentLine == null) return;
|
||||||
|
|
||||||
|
// Filter points that are too close to reduce memory usage
|
||||||
|
if (_currentLine!.isPointTooClose(normalizedPoint)) return;
|
||||||
|
|
||||||
_currentLine = _currentLine!.addPoint(normalizedPoint);
|
_currentLine = _currentLine!.addPoint(normalizedPoint);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Completes the current line and adds it to the history.
|
/// Completes the current line and adds it to the history.
|
||||||
void endLine() {
|
void endLine() {
|
||||||
|
if (_isEraserMode) {
|
||||||
|
// If we erased lines in this stroke, record as single action
|
||||||
|
if (_currentErasedLines.isNotEmpty) {
|
||||||
|
_undoStack.add(EraseAction(List.from(_currentErasedLines)));
|
||||||
|
_redoStack.clear();
|
||||||
|
_trimHistory();
|
||||||
|
_currentErasedLines.clear();
|
||||||
|
notifyListeners(); // Update UI to enable undo button
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_currentLine == null) return;
|
if (_currentLine == null) return;
|
||||||
|
|
||||||
// Only add lines with at least 2 points
|
// Only add lines with at least 2 points
|
||||||
if (_currentLine!.points.length >= 2) {
|
if (_currentLine!.points.length >= 2) {
|
||||||
_lines.add(_currentLine!);
|
_lines.add(_currentLine!);
|
||||||
// Clear redo stack when new action is performed
|
_undoStack.add(AddLineAction(_currentLine!));
|
||||||
_undoneLines.clear();
|
_redoStack.clear();
|
||||||
_trimHistory();
|
_trimHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +141,98 @@ class DrawingController extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Trims history to maxHistorySteps to prevent memory growth.
|
/// Trims history to maxHistorySteps to prevent memory growth.
|
||||||
void _trimHistory() {
|
void _trimHistory() {
|
||||||
while (_lines.length > maxHistorySteps) {
|
while (_undoStack.length > maxHistorySteps) {
|
||||||
_lines.removeAt(0);
|
_undoStack.removeAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Eraser Operations
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Eraser hit radius in normalized coordinates
|
||||||
|
static const double _eraserRadius = 0.015;
|
||||||
|
|
||||||
|
/// Checks if a point is near a line and erases it if so.
|
||||||
|
void _eraseAtPoint(Offset point) {
|
||||||
|
// Find all lines that intersect with the eraser point
|
||||||
|
final linesToRemove = <DrawingLine>[];
|
||||||
|
|
||||||
|
for (final line in _lines) {
|
||||||
|
if (_lineIntersectsPoint(
|
||||||
|
line, point, _eraserRadius + line.strokeWidth / 2)) {
|
||||||
|
linesToRemove.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove intersecting lines and track them for undo
|
||||||
|
for (final line in linesToRemove) {
|
||||||
|
_lines.remove(line);
|
||||||
|
_currentErasedLines.add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linesToRemove.isNotEmpty) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a line intersects with a circular point.
|
||||||
|
bool _lineIntersectsPoint(DrawingLine line, Offset point, double radius) {
|
||||||
|
// Check if any point is within radius
|
||||||
|
for (final linePoint in line.points) {
|
||||||
|
if ((linePoint - point).distance <= radius) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check line segments
|
||||||
|
for (int i = 0; i < line.points.length - 1; i++) {
|
||||||
|
if (_pointToSegmentDistance(point, line.points[i], line.points[i + 1]) <=
|
||||||
|
radius) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the distance from a point to a line segment.
|
||||||
|
double _pointToSegmentDistance(Offset point, Offset segStart, Offset segEnd) {
|
||||||
|
final dx = segEnd.dx - segStart.dx;
|
||||||
|
final dy = segEnd.dy - segStart.dy;
|
||||||
|
final lengthSquared = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
if (lengthSquared == 0) {
|
||||||
|
// Segment is a point
|
||||||
|
return (point - segStart).distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project point onto the line, clamping to segment
|
||||||
|
var t = ((point.dx - segStart.dx) * dx + (point.dy - segStart.dy) * dy) /
|
||||||
|
lengthSquared;
|
||||||
|
t = t.clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
final projection = Offset(
|
||||||
|
segStart.dx + t * dx,
|
||||||
|
segStart.dy + t * dy,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (point - projection).distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Eraser Mode
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Toggles eraser mode.
|
||||||
|
void toggleEraserMode() {
|
||||||
|
_isEraserMode = !_isEraserMode;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets eraser mode.
|
||||||
|
void setEraserMode(bool enabled) {
|
||||||
|
if (_isEraserMode != enabled) {
|
||||||
|
_isEraserMode = enabled;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,29 +240,55 @@ class DrawingController extends ChangeNotifier {
|
|||||||
// Undo/Redo
|
// Undo/Redo
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Undoes the last line drawn.
|
/// Undoes the last action.
|
||||||
void undo() {
|
void undo() {
|
||||||
if (!canUndo) return;
|
if (!canUndo) return;
|
||||||
|
|
||||||
final line = _lines.removeLast();
|
final action = _undoStack.removeLast();
|
||||||
_undoneLines.add(line);
|
|
||||||
|
switch (action) {
|
||||||
|
case AddLineAction(:final line):
|
||||||
|
// Remove the line that was added
|
||||||
|
_lines.remove(line);
|
||||||
|
_redoStack.add(action);
|
||||||
|
case EraseAction(:final erasedLines):
|
||||||
|
// Restore the lines that were erased
|
||||||
|
_lines.addAll(erasedLines);
|
||||||
|
_redoStack.add(action);
|
||||||
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Redoes the last undone line.
|
/// Redoes the last undone action.
|
||||||
void redo() {
|
void redo() {
|
||||||
if (!canRedo) return;
|
if (!canRedo) return;
|
||||||
|
|
||||||
final line = _undoneLines.removeLast();
|
final action = _redoStack.removeLast();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case AddLineAction(:final line):
|
||||||
|
// Re-add the line
|
||||||
_lines.add(line);
|
_lines.add(line);
|
||||||
|
_undoStack.add(action);
|
||||||
|
case EraseAction(:final erasedLines):
|
||||||
|
// Re-erase the lines
|
||||||
|
for (final line in erasedLines) {
|
||||||
|
_lines.remove(line);
|
||||||
|
}
|
||||||
|
_undoStack.add(action);
|
||||||
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears all lines from the canvas.
|
/// Clears all lines from the canvas.
|
||||||
void clear() {
|
void clear() {
|
||||||
_lines.clear();
|
_lines.clear();
|
||||||
_undoneLines.clear();
|
_undoStack.clear();
|
||||||
|
_redoStack.clear();
|
||||||
_currentLine = null;
|
_currentLine = null;
|
||||||
|
_currentErasedLines.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,9 +296,10 @@ class DrawingController extends ChangeNotifier {
|
|||||||
// Paint Preset
|
// Paint Preset
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Sets the current paint preset.
|
/// Sets the current paint preset and disables eraser mode.
|
||||||
void setPreset(PaintPreset preset) {
|
void setPreset(PaintPreset preset) {
|
||||||
_currentPreset = preset;
|
_currentPreset = preset;
|
||||||
|
_isEraserMode = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,8 +320,10 @@ class DrawingController extends ChangeNotifier {
|
|||||||
/// Imports lines from a JSON-serializable list.
|
/// Imports lines from a JSON-serializable list.
|
||||||
void fromJsonList(List<Map<String, dynamic>> jsonList) {
|
void fromJsonList(List<Map<String, dynamic>> jsonList) {
|
||||||
_lines.clear();
|
_lines.clear();
|
||||||
_undoneLines.clear();
|
_undoStack.clear();
|
||||||
|
_redoStack.clear();
|
||||||
_currentLine = null;
|
_currentLine = null;
|
||||||
|
_currentErasedLines.clear();
|
||||||
|
|
||||||
for (final json in jsonList) {
|
for (final json in jsonList) {
|
||||||
_lines.add(DrawingLine.fromJson(json));
|
_lines.add(DrawingLine.fromJson(json));
|
||||||
@@ -183,7 +357,8 @@ class DrawingController extends ChangeNotifier {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_lines.clear();
|
_lines.clear();
|
||||||
_undoneLines.clear();
|
_undoStack.clear();
|
||||||
|
_redoStack.clear();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import 'dart:ui';
|
|||||||
///
|
///
|
||||||
/// This allows drawings to scale correctly when the canvas size changes.
|
/// This allows drawings to scale correctly when the canvas size changes.
|
||||||
class DrawingLine {
|
class DrawingLine {
|
||||||
|
/// The minimal squared distance between to points which are normalized so that this point is allowed to be added to the line
|
||||||
|
static const minNormalizedPointDistanceSquared = 0.001 * 0.001;
|
||||||
|
|
||||||
/// Points in normalized coordinates (0.0 to 1.0)
|
/// Points in normalized coordinates (0.0 to 1.0)
|
||||||
final List<Offset> points;
|
final List<Offset> points;
|
||||||
|
|
||||||
@@ -27,10 +30,9 @@ class DrawingLine {
|
|||||||
/// Creates a DrawingLine from JSON data.
|
/// Creates a DrawingLine from JSON data.
|
||||||
factory DrawingLine.fromJson(Map<String, dynamic> json) {
|
factory DrawingLine.fromJson(Map<String, dynamic> json) {
|
||||||
final pointsList = (json['points'] as List)
|
final pointsList = (json['points'] as List)
|
||||||
.map((p) => Offset(
|
.map(
|
||||||
(p['x'] as num).toDouble(),
|
(p) => Offset((p['x'] as num).toDouble(), (p['y'] as num).toDouble()),
|
||||||
(p['y'] as num).toDouble(),
|
)
|
||||||
))
|
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return DrawingLine(
|
return DrawingLine(
|
||||||
@@ -58,6 +60,14 @@ class DrawingLine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isPointTooClose(Offset nextNormalizedPoint) {
|
||||||
|
if (points.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (points.last - nextNormalizedPoint).distanceSquared <
|
||||||
|
minNormalizedPointDistanceSquared;
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a copy with updated points.
|
/// Creates a copy with updated points.
|
||||||
DrawingLine copyWith({
|
DrawingLine copyWith({
|
||||||
List<Offset>? points,
|
List<Offset>? points,
|
||||||
@@ -85,9 +95,5 @@ class DrawingLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(Object.hashAll(points), color, strokeWidth);
|
||||||
Object.hashAll(points),
|
|
||||||
color,
|
|
||||||
strokeWidth,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,24 @@ class DrawingToolbar extends StatelessWidget {
|
|||||||
...PaintPreset.quickAccess.map((preset) => _buildPresetButton(
|
...PaintPreset.quickAccess.map((preset) => _buildPresetButton(
|
||||||
context,
|
context,
|
||||||
preset,
|
preset,
|
||||||
isSelected: controller.currentPreset == preset,
|
isSelected: !controller.isEraserMode &&
|
||||||
|
controller.currentPreset == preset,
|
||||||
)),
|
)),
|
||||||
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildDivider(context),
|
_buildDivider(context),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
|
// Eraser button
|
||||||
|
_buildEraserButton(
|
||||||
|
context,
|
||||||
|
isSelected: controller.isEraserMode,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildDivider(context),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
// Undo button
|
// Undo button
|
||||||
_buildActionButton(
|
_buildActionButton(
|
||||||
context,
|
context,
|
||||||
@@ -127,6 +138,37 @@ class DrawingToolbar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEraserButton(
|
||||||
|
BuildContext context, {
|
||||||
|
required bool isSelected,
|
||||||
|
}) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Tooltip(
|
||||||
|
message: 'Eraser',
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => controller.setEraserMode(true),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: isSelected
|
||||||
|
? Border.all(color: colorScheme.primary, width: 2)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.auto_fix_high,
|
||||||
|
size: 20,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildActionButton(
|
Widget _buildActionButton(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class PaintPreset {
|
|||||||
static const yellowMarker = PaintPreset(
|
static const yellowMarker = PaintPreset(
|
||||||
name: 'Yellow Marker',
|
name: 'Yellow Marker',
|
||||||
color: Color(0x80FFEB3B), // Yellow with 50% opacity
|
color: Color(0x80FFEB3B), // Yellow with 50% opacity
|
||||||
strokeWidth: 0.015, // Thicker for highlighting
|
strokeWidth: 0.02, // Thicker for highlighting
|
||||||
icon: Icons.highlight,
|
icon: Icons.highlight,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class PaintPreset {
|
|||||||
static const greenMarker = PaintPreset(
|
static const greenMarker = PaintPreset(
|
||||||
name: 'Green Marker',
|
name: 'Green Marker',
|
||||||
color: Color(0x804CAF50), // Green with 50% opacity
|
color: Color(0x804CAF50), // Green with 50% opacity
|
||||||
strokeWidth: 0.015,
|
strokeWidth: 0.018,
|
||||||
icon: Icons.highlight,
|
icon: Icons.highlight,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,7 +91,10 @@ class PaintPreset {
|
|||||||
static const List<PaintPreset> quickAccess = [
|
static const List<PaintPreset> quickAccess = [
|
||||||
blackPen,
|
blackPen,
|
||||||
redPen,
|
redPen,
|
||||||
|
bluePen,
|
||||||
yellowMarker,
|
yellowMarker,
|
||||||
|
greenMarker,
|
||||||
|
pinkMarker,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -271,8 +271,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
widget.config.fullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
|
widget.config.fullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
|
||||||
),
|
),
|
||||||
tooltip:
|
tooltip: widget.config.fullscreen
|
||||||
widget.config.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
|
? 'Exit Fullscreen'
|
||||||
|
: 'Enter Fullscreen',
|
||||||
onPressed: _toggleFullscreen,
|
onPressed: _toggleFullscreen,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -284,8 +285,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
widget.config.twoPageMode ? Icons.filter_1 : Icons.filter_2,
|
widget.config.twoPageMode ? Icons.filter_1 : Icons.filter_2,
|
||||||
),
|
),
|
||||||
tooltip:
|
tooltip: widget.config.twoPageMode
|
||||||
widget.config.twoPageMode ? 'Single Page Mode' : 'Two Page Mode',
|
? 'Single Page Mode'
|
||||||
|
: 'Two Page Mode',
|
||||||
onPressed: _toggleTwoPageMode,
|
onPressed: _toggleTwoPageMode,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -317,8 +319,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
|
|||||||
currentPageNumber: _currentPage,
|
currentPageNumber: _currentPage,
|
||||||
config: widget.config,
|
config: widget.config,
|
||||||
leftDrawingController: _leftDrawingController,
|
leftDrawingController: _leftDrawingController,
|
||||||
rightDrawingController:
|
rightDrawingController: widget.config.twoPageMode
|
||||||
widget.config.twoPageMode ? _rightDrawingController : null,
|
? _rightDrawingController
|
||||||
|
: null,
|
||||||
drawingEnabled: _isPaintMode,
|
drawingEnabled: _isPaintMode,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -334,7 +337,6 @@ class _SheetViewerPageState extends State<SheetViewerPage>
|
|||||||
onToggleFullscreen: _toggleFullscreen,
|
onToggleFullscreen: _toggleFullscreen,
|
||||||
onExit: () => Navigator.pop(context),
|
onExit: () => Navigator.pop(context),
|
||||||
onPageTurn: _turnPage,
|
onPageTurn: _turnPage,
|
||||||
child: pageDisplay,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,10 +358,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
message,
|
message,
|
||||||
style: Theme.of(context)
|
style: Theme.of(
|
||||||
.textTheme
|
context,
|
||||||
.titleMedium
|
).textTheme.titleMedium?.copyWith(color: Colors.red),
|
||||||
?.copyWith(color: Colors.red),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ typedef PageTurnCallback = dynamic Function(int delta);
|
|||||||
/// - Right side: Turn page forward (+1 or +2 in two-page mode)
|
/// - Right side: Turn page forward (+1 or +2 in two-page mode)
|
||||||
class TouchNavigationLayer extends StatelessWidget {
|
class TouchNavigationLayer extends StatelessWidget {
|
||||||
final PdfPageDisplay pageDisplay;
|
final PdfPageDisplay pageDisplay;
|
||||||
final Widget child;
|
|
||||||
final Config config;
|
final Config config;
|
||||||
final VoidCallback onToggleFullscreen;
|
final VoidCallback onToggleFullscreen;
|
||||||
final VoidCallback onExit;
|
final VoidCallback onExit;
|
||||||
@@ -23,7 +22,6 @@ class TouchNavigationLayer extends StatelessWidget {
|
|||||||
const TouchNavigationLayer({
|
const TouchNavigationLayer({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pageDisplay,
|
required this.pageDisplay,
|
||||||
required this.child,
|
|
||||||
required this.config,
|
required this.config,
|
||||||
required this.onToggleFullscreen,
|
required this.onToggleFullscreen,
|
||||||
required this.onExit,
|
required this.onExit,
|
||||||
@@ -35,7 +33,7 @@ class TouchNavigationLayer extends StatelessWidget {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTapUp: (details) => _handleTap(context, details),
|
onTapUp: (details) => _handleTap(context, details),
|
||||||
child: child,
|
child: pageDisplay,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user