feat(split): scratchpad inks on the pen-first canvas
All checks were successful
CI / Windows build (push) Successful in 20m37s
All checks were successful
CI / Windows build (push) Successful in 20m37s
The split-view scratchpad was the last surface on the old ink_canvas. Move its inking engine to the performant PenCanvas while keeping the infinite auto-expanding world. Key idea: store strokes in absolute WORLD pixels (InkStroke — unchanged saveScratchpad format) and render through PenCanvas by normalizing against the CURRENT world size. When the world auto-expands, stored world coords do not move — only the normalization divisor grows — so ink stays put with zero drift (proven by the world-expand-stability test). - Replace InteractiveViewer+SizedBox+InkCanvas with PenCanvas (own pan/zoom, minScale 0.1 to survey the big world); keep the AnnotationToolbar. - Stroke callbacks go through ink_stroke_adapter; load filters to freehand so the canvas list stays 1:1 with the undo manager. - Pen/highlighter/eraser map from the toolbar's PenTool; width is world px. The left PDF-reference pane (SfPdfViewer, read-only) is unchanged. Tests: world-expand stability added. flutter analyze: 0. Suite: 270/270.
This commit is contained in:
@@ -4,7 +4,11 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../editor/canvas/pen_canvas.dart';
|
||||
import '../editor/canvas/pen_stroke.dart';
|
||||
import '../editor/notebook/ink_stroke_adapter.dart';
|
||||
import '../models/ink_stroke.dart';
|
||||
import '../models/pen_tool.dart';
|
||||
import '../models/pressure_curve.dart';
|
||||
@@ -12,7 +16,6 @@ import '../services/database_service.dart';
|
||||
import '../services/undo_manager.dart';
|
||||
import '../utils/stroke_stabilizer.dart';
|
||||
import '../widgets/annotation_toolbar.dart';
|
||||
import '../widgets/ink_canvas.dart';
|
||||
|
||||
/// Split-view derivation mode: left pane = reference PDF, right pane = infinite
|
||||
/// scratchpad for formula derivation. Scratchpad strokes are persisted per
|
||||
@@ -43,10 +46,29 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
bool _isDraggingDivider = false;
|
||||
|
||||
// -- Scratchpad (right pane) --
|
||||
// The scratchpad is an infinite WORLD: strokes are stored in absolute world
|
||||
// pixels ([InkStroke], unchanged persistence format), and rendered through the
|
||||
// performant PenCanvas by normalizing against the CURRENT world size. When the
|
||||
// world auto-expands, the stored world coords don't move — only the
|
||||
// normalization divisor grows — so ink stays put with zero drift.
|
||||
final UndoManager _undoManager = UndoManager();
|
||||
List<InkStroke> _strokes = [];
|
||||
double _canvasWidth = 4000;
|
||||
double _canvasHeight = 4000;
|
||||
static const _uuid = Uuid();
|
||||
|
||||
/// Pan/zoom transform for the scratchpad world (PenCanvas drives this).
|
||||
final TransformationController _scratchTransform = TransformationController();
|
||||
|
||||
Size get _worldSize => Size(_canvasWidth, _canvasHeight);
|
||||
|
||||
/// Maps the scratchpad toolbar's [PenTool] to the pen-canvas tool. Shapes and
|
||||
/// text fall back to pen (the pen-first scratchpad is freehand).
|
||||
CanvasTool get _canvasTool => switch (_currentTool) {
|
||||
PenTool.eraser => CanvasTool.eraser,
|
||||
PenTool.highlighter => CanvasTool.highlighter,
|
||||
_ => CanvasTool.pen,
|
||||
};
|
||||
|
||||
// -- Tool state --
|
||||
PenTool _currentTool = PenTool.pen;
|
||||
@@ -77,6 +99,7 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
_saveTimer?.cancel();
|
||||
_saveImmediate();
|
||||
_pdfController.dispose();
|
||||
_scratchTransform.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -87,8 +110,11 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
final strokes = await db.loadScratchpad(widget.documentId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_strokes = strokes;
|
||||
for (final s in strokes) {
|
||||
// Keep only freehand strokes so the canvas list stays 1:1 with the
|
||||
// undo manager (shapes/text have no pen-canvas representation).
|
||||
final freehand = strokes.where((s) => isFreehandTool(s.tool)).toList();
|
||||
_strokes = freehand;
|
||||
for (final s in freehand) {
|
||||
_undoManager.addStroke(s);
|
||||
}
|
||||
});
|
||||
@@ -111,7 +137,11 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
|
||||
// -- Scratchpad stroke callbacks --
|
||||
|
||||
void _onStrokeComplete(InkStroke stroke) {
|
||||
/// PenCanvas committed a stroke (normalized to the current world). Convert it
|
||||
/// to absolute world coords for storage.
|
||||
void _onStrokeComplete(PenStroke pen) {
|
||||
final stroke = inkStrokeFromPen(pen, _worldSize,
|
||||
id: _uuid.v4(), createdAt: DateTime.now());
|
||||
setState(() {
|
||||
_strokes.add(stroke);
|
||||
_undoManager.addStroke(stroke);
|
||||
@@ -120,13 +150,19 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
_scheduleSave();
|
||||
}
|
||||
|
||||
void _onErase(String strokeId, List<InkStroke> replacements) {
|
||||
/// PenCanvas erased through stroke [index] (into [_strokes]); [replacements]
|
||||
/// are the surviving sub-strokes (normalized) — convert back to world coords.
|
||||
void _onErase(int index, List<PenStroke> replacements) {
|
||||
if (index < 0 || index >= _strokes.length) return;
|
||||
setState(() {
|
||||
final original = _strokes.where((s) => s.id == strokeId).firstOrNull;
|
||||
if (original != null) {
|
||||
_undoManager.removeStroke(original, replacements: replacements);
|
||||
_strokes = List.from(_undoManager.currentStrokes);
|
||||
}
|
||||
final original = _strokes[index];
|
||||
final inkReplacements = [
|
||||
for (final r in replacements)
|
||||
inkStrokeFromPen(r, _worldSize,
|
||||
id: _uuid.v4(), createdAt: DateTime.now()),
|
||||
];
|
||||
_undoManager.removeStroke(original, replacements: inkReplacements);
|
||||
_strokes = List.from(_undoManager.currentStrokes);
|
||||
});
|
||||
_scheduleSave();
|
||||
}
|
||||
@@ -432,29 +468,24 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
}
|
||||
|
||||
Widget _buildScratchpadPane() {
|
||||
// Render the world through the performant PenCanvas: strokes normalized
|
||||
// against the current world size; toolbar width is in world pixels, so the
|
||||
// pen-canvas fraction is width / worldWidth.
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: InteractiveViewer(
|
||||
constrained: false,
|
||||
minScale: 0.25,
|
||||
child: PenCanvas(
|
||||
pageSize: _worldSize,
|
||||
strokes: penStrokesFromInk(_strokes, _worldSize),
|
||||
transformationController: _scratchTransform,
|
||||
tool: _canvasTool,
|
||||
color: _currentColor,
|
||||
strokeWidth: _currentStrokeWidth / _canvasWidth,
|
||||
// The world is huge, so allow zooming further out to survey it.
|
||||
minScale: 0.1,
|
||||
maxScale: 8.0,
|
||||
boundaryMargin: const EdgeInsets.all(double.infinity),
|
||||
child: SizedBox(
|
||||
width: _canvasWidth,
|
||||
height: _canvasHeight,
|
||||
child: InkCanvas(
|
||||
strokes: _strokes,
|
||||
onStrokeComplete: _onStrokeComplete,
|
||||
onErase: _onErase,
|
||||
tool: _currentTool,
|
||||
color: _currentColor,
|
||||
strokeWidth: _currentStrokeWidth,
|
||||
pressureCurve: PressureCurve(type: _pressureCurveType),
|
||||
stabilizationLevel: _stabilizationLevel,
|
||||
filled: _filled,
|
||||
interactionMode: InteractionMode.draw,
|
||||
),
|
||||
),
|
||||
onStrokeComplete: _onStrokeComplete,
|
||||
onEraseStroke: _onErase,
|
||||
pageWidget: const ColoredBox(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user