feat(pdf): anchored scratch links replace board
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Replace the rejected standalone sticky-card board with the real feature: place a link anchor anywhere on a PDF page, tap it to open split view whose right pane is THAT anchor's own infinite scratchpad (keyed by anchor id) — like a paper sticky-note tab. - ScratchLink model + scratch_links table (id, doc, page, nx, ny). - PDF editor: "place link" tool drops/loads/shows tappable markers; tap opens SplitViewScreen for that anchor; long-press deletes. - SplitViewScreen rebuilt on pdfrx (was syncfusion), right scratchpad keyed by scratchLinkId, new brush palette (was AnnotationToolbar). - Remove board_screen + its test + the home board entry. analyze clean, tests green.
This commit is contained in:
@@ -1,33 +1,56 @@
|
||||
// lib/screens/split_view_screen.dart
|
||||
//
|
||||
// Anchor-keyed split view: opened by tapping a PDF-anchored scratch link.
|
||||
// LEFT pane = the source PDF (pdfrx PdfViewer, read-only reference + page
|
||||
// nav), opened at the anchor's page.
|
||||
// RIGHT pane = an INFINITE freehand scratchpad that BELONGS TO THE ANCHOR,
|
||||
// keyed by [scratchLinkId] (not the documentId). Each anchor has
|
||||
// its own private scratch space, persisted via the existing
|
||||
// scratchpad storage (InkStroke JSON, format unchanged).
|
||||
//
|
||||
// The right pane reuses the pen-first PenCanvas world-coord engine and the
|
||||
// Material You brush palette shared with pen_note_screen (BrushPickerButton /
|
||||
// ToolButton / color dots), replacing the old AnnotationToolbar.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../editor/canvas/pen_canvas.dart';
|
||||
import '../editor/canvas/pen_palette_widgets.dart';
|
||||
import '../editor/canvas/pen_stroke.dart';
|
||||
import '../editor/engine/brush.dart';
|
||||
import '../editor/input/pen_config.dart' show kDefaultEraserRadius;
|
||||
import '../editor/notebook/ink_stroke_adapter.dart';
|
||||
import '../models/ink_stroke.dart';
|
||||
import '../models/pen_tool.dart';
|
||||
import '../models/pressure_curve.dart';
|
||||
import '../services/database_service.dart';
|
||||
import '../services/undo_manager.dart';
|
||||
import '../utils/stroke_stabilizer.dart';
|
||||
import '../widgets/annotation_toolbar.dart';
|
||||
|
||||
/// Split-view derivation mode: left pane = reference PDF, right pane = infinite
|
||||
/// scratchpad for formula derivation. Scratchpad strokes are persisted per
|
||||
/// document via [DatabaseService.saveScratchpad] / [DatabaseService.loadScratchpad].
|
||||
/// Split-view derivation surface for a single scratch-link anchor: left pane =
|
||||
/// the reference PDF (at [initialPage]), right pane = the anchor's private
|
||||
/// infinite scratchpad. Scratchpad strokes persist per ANCHOR via
|
||||
/// [DatabaseService.saveScratchpad] / [DatabaseService.loadScratchpad], keyed by
|
||||
/// [scratchLinkId].
|
||||
class SplitViewScreen extends StatefulWidget {
|
||||
final String filePath;
|
||||
final String documentId;
|
||||
|
||||
/// The owning anchor id. Doubles as the scratchpad storage key so this is the
|
||||
/// anchor's private scratch space.
|
||||
final String scratchLinkId;
|
||||
|
||||
/// 0-based page the anchor sits on; the left PDF opens here.
|
||||
final int initialPage;
|
||||
|
||||
const SplitViewScreen({
|
||||
super.key,
|
||||
required this.filePath,
|
||||
required this.documentId,
|
||||
required this.scratchLinkId,
|
||||
this.initialPage = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -37,63 +60,55 @@ class SplitViewScreen extends StatefulWidget {
|
||||
class _SplitViewState extends State<SplitViewScreen> {
|
||||
// -- PDF (left pane) --
|
||||
final PdfViewerController _pdfController = PdfViewerController();
|
||||
int _currentPage = 0;
|
||||
int _currentPage = 0; // 0-based
|
||||
int _pageCount = 0;
|
||||
String _fileName = '';
|
||||
|
||||
// -- Split divider --
|
||||
double _leftPaneFraction = 0.5;
|
||||
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.
|
||||
// Infinite WORLD: strokes stored in absolute world pixels ([InkStroke],
|
||||
// unchanged persistence format), rendered through PenCanvas by normalizing
|
||||
// against the CURRENT world size. World auto-expands without moving ink.
|
||||
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();
|
||||
|
||||
/// Set once the initial view has been framed onto existing ink.
|
||||
bool _scratchCentered = false;
|
||||
|
||||
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 (new Material You brush palette) --
|
||||
CanvasTool _tool = CanvasTool.pen;
|
||||
BrushKind _penBrush = BrushKind.fountainPen;
|
||||
Color _color = Colors.black;
|
||||
|
||||
// -- Tool state --
|
||||
PenTool _currentTool = PenTool.pen;
|
||||
Color _currentColor = Colors.black;
|
||||
double _currentStrokeWidth = 2.0;
|
||||
bool _filled = false;
|
||||
PressureCurveType _pressureCurveType = PressureCurveType.linear;
|
||||
StabilizationLevel _stabilizationLevel = StabilizationLevel.none;
|
||||
static const double _penWidthFraction = 0.006;
|
||||
static const double _highlighterWidthFraction = 0.02;
|
||||
|
||||
static const List<Color> _palette = [
|
||||
Colors.black,
|
||||
Colors.red,
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
];
|
||||
|
||||
// -- Auto-save debounce --
|
||||
Timer? _saveTimer;
|
||||
bool _dirty = false;
|
||||
|
||||
// -- Page link markers (optional feature) --
|
||||
final List<_PageLink> _pageLinks = [];
|
||||
|
||||
static const double _edgeThreshold = 200.0;
|
||||
static const double _expandAmount = 1000.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentPage = widget.initialPage;
|
||||
_loadScratchpad();
|
||||
}
|
||||
|
||||
@@ -101,27 +116,26 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
void dispose() {
|
||||
_saveTimer?.cancel();
|
||||
_saveImmediate();
|
||||
_pdfController.dispose();
|
||||
// PdfViewerController (pdfrx) has no dispose(); it detaches with the viewer.
|
||||
_scratchTransform.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// -- Persistence --
|
||||
// -- Persistence (keyed by the ANCHOR id, not the documentId) --
|
||||
|
||||
Future<void> _loadScratchpad() async {
|
||||
final db = await DatabaseService.getInstance();
|
||||
final strokes = await db.loadScratchpad(widget.documentId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
final strokes = await db.loadScratchpad(widget.scratchLinkId);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _scheduleSave() {
|
||||
@@ -135,13 +149,11 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
_dirty = false;
|
||||
final db = await DatabaseService.getInstance();
|
||||
final json = jsonEncode(_strokes.map((s) => s.toJson()).toList());
|
||||
await db.saveScratchpad(widget.documentId, json);
|
||||
await db.saveScratchpad(widget.scratchLinkId, json);
|
||||
}
|
||||
|
||||
// -- Scratchpad stroke callbacks --
|
||||
|
||||
/// 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());
|
||||
@@ -153,8 +165,6 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
_scheduleSave();
|
||||
}
|
||||
|
||||
/// 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(() {
|
||||
@@ -229,62 +239,32 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
setState(() => _isDraggingDivider = false);
|
||||
}
|
||||
|
||||
// -- PDF page navigation --
|
||||
// -- PDF page navigation (left pane) --
|
||||
|
||||
void _prevPage() {
|
||||
if (_currentPage > 0) {
|
||||
_pdfController.previousPage();
|
||||
}
|
||||
if (_currentPage > 0) _pdfController.goToPage(pageNumber: _currentPage);
|
||||
}
|
||||
|
||||
void _nextPage() {
|
||||
if (_currentPage < _pageCount - 1) {
|
||||
_pdfController.nextPage();
|
||||
_pdfController.goToPage(pageNumber: _currentPage + 2);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Page link creation (long-press on left pane) --
|
||||
CanvasTool get _activeTool => _tool;
|
||||
|
||||
void _onPdfLongPress(int pageNumber) {
|
||||
// Place a page link marker at the current scratchpad viewport center.
|
||||
// We approximate the viewport center as (0, 0) since InteractiveViewer
|
||||
// manages its own transform — the user can reposition by panning.
|
||||
setState(() {
|
||||
_pageLinks.add(
|
||||
_PageLink(
|
||||
pageNumber: pageNumber,
|
||||
position: const Offset(100, 100), // default top-left area
|
||||
),
|
||||
);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Page link marker added for page $pageNumber')),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPageLinkTap(_PageLink link) {
|
||||
_pdfController.jumpToPage(link.pageNumber);
|
||||
setState(() {
|
||||
_currentPage = link.pageNumber - 1;
|
||||
});
|
||||
}
|
||||
|
||||
void _deletePageLink(_PageLink link) {
|
||||
setState(() {
|
||||
_pageLinks.remove(link);
|
||||
});
|
||||
}
|
||||
double get _strokeWidth => _tool == CanvasTool.highlighter
|
||||
? _highlighterWidthFraction
|
||||
: _penWidthFraction;
|
||||
|
||||
// -- Build --
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_fileName.isEmpty ? 'Split View' : _fileName,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
title: const Text('Scratch link', style: TextStyle(fontSize: 16)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
@@ -293,7 +273,6 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
// Left pane page navigation
|
||||
IconButton(
|
||||
icon: const Icon(Icons.navigate_before),
|
||||
tooltip: 'Previous page (PDF)',
|
||||
@@ -314,7 +293,6 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
onPressed: _currentPage < _pageCount - 1 ? _nextPage : null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Canvas info
|
||||
Tooltip(
|
||||
message:
|
||||
'Scratchpad size: ${_canvasWidth.round()} x ${_canvasHeight.round()}',
|
||||
@@ -332,7 +310,6 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Label clarifying that the toolbar controls the scratchpad pane.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12, top: 4),
|
||||
child: Align(
|
||||
@@ -340,47 +317,22 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
child: Text(
|
||||
'Scratchpad tools',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Toolbar (applies to scratchpad only)
|
||||
AnnotationToolbar(
|
||||
currentTool: _currentTool,
|
||||
currentColor: _currentColor,
|
||||
currentStrokeWidth: _currentStrokeWidth,
|
||||
filled: _filled,
|
||||
pressureCurveType: _pressureCurveType,
|
||||
stabilizationLevel: _stabilizationLevel,
|
||||
canUndo: _undoManager.canUndo,
|
||||
canRedo: _undoManager.canRedo,
|
||||
onToolChanged: (tool) => setState(() => _currentTool = tool),
|
||||
onColorChanged: (color) => setState(() => _currentColor = color),
|
||||
onStrokeWidthChanged: (w) =>
|
||||
setState(() => _currentStrokeWidth = w),
|
||||
onFilledChanged: (f) => setState(() => _filled = f),
|
||||
onPressureCurveChanged: (v) =>
|
||||
setState(() => _pressureCurveType = v),
|
||||
onStabilizationChanged: (v) =>
|
||||
setState(() => _stabilizationLevel = v),
|
||||
onUndo: _undo,
|
||||
onRedo: _redo,
|
||||
),
|
||||
// Split view body
|
||||
_buildBrushPalette(cs),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final totalWidth = constraints.maxWidth;
|
||||
final leftWidth = totalWidth * _leftPaneFraction;
|
||||
final rightWidth =
|
||||
totalWidth - leftWidth - 12; // 12px divider hit area
|
||||
final rightWidth = totalWidth - leftWidth - 12;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Left pane: PDF reference (read-only)
|
||||
SizedBox(width: leftWidth, child: _buildPdfPane()),
|
||||
// Draggable divider: 12px hit area, 4px visual strip.
|
||||
GestureDetector(
|
||||
onHorizontalDragStart: _onDividerDragStart,
|
||||
onHorizontalDragUpdate: (d) =>
|
||||
@@ -401,7 +353,6 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Right pane: Infinite scratchpad
|
||||
SizedBox(width: rightWidth, child: _buildScratchpadPane()),
|
||||
],
|
||||
);
|
||||
@@ -413,72 +364,118 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPdfPane() {
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onLongPress: () {
|
||||
// Long-press on PDF to create page link marker
|
||||
_onPdfLongPress(_currentPage + 1);
|
||||
},
|
||||
child: SfPdfViewer.file(
|
||||
File(widget.filePath),
|
||||
controller: _pdfController,
|
||||
canShowScrollHead: true,
|
||||
canShowScrollStatus: true,
|
||||
onPageChanged: (PdfPageChangedDetails details) {
|
||||
setState(() {
|
||||
_currentPage = details.newPageNumber - 1;
|
||||
});
|
||||
},
|
||||
onDocumentLoaded: (PdfDocumentLoadedDetails details) {
|
||||
setState(() {
|
||||
_pageCount = details.document.pages.count;
|
||||
_fileName = widget.filePath.split(Platform.pathSeparator).last;
|
||||
});
|
||||
},
|
||||
/// Material You brush palette (shared chrome with the pen-first editors):
|
||||
/// brush picker + highlighter + eraser, undo/redo, color dots.
|
||||
Widget _buildBrushPalette(ColorScheme cs) {
|
||||
return Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
elevation: 3,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BrushPickerButton(
|
||||
selected: _penBrush,
|
||||
active: _tool == CanvasTool.pen,
|
||||
tooltip: 'Brush',
|
||||
labelFor: brushLabelEn,
|
||||
onSelected: (b) => setState(() {
|
||||
_penBrush = b;
|
||||
_tool = CanvasTool.pen;
|
||||
}),
|
||||
),
|
||||
ToolButton(
|
||||
icon: Icons.brush_outlined,
|
||||
selected: _tool == CanvasTool.highlighter,
|
||||
tooltip: 'Highlighter',
|
||||
onPressed: () => setState(() => _tool = CanvasTool.highlighter),
|
||||
),
|
||||
ToolButton(
|
||||
icon: Icons.cleaning_services_outlined,
|
||||
selected: _tool == CanvasTool.eraser,
|
||||
tooltip: 'Eraser',
|
||||
onPressed: () => setState(() => _tool = CanvasTool.eraser),
|
||||
),
|
||||
PaletteDivider(cs: cs),
|
||||
ToolButton(
|
||||
icon: Icons.undo,
|
||||
selected: false,
|
||||
tooltip: 'Undo',
|
||||
onPressed: _undoManager.canUndo ? _undo : null,
|
||||
),
|
||||
ToolButton(
|
||||
icon: Icons.redo,
|
||||
selected: false,
|
||||
tooltip: 'Redo',
|
||||
onPressed: _undoManager.canRedo ? _redo : null,
|
||||
),
|
||||
PaletteDivider(cs: cs),
|
||||
for (final c in _palette) _colorDot(c, cs),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Page link markers overlay (on PDF pane, showing linked pages)
|
||||
if (_pageLinks.isNotEmpty)
|
||||
Positioned(bottom: 8, left: 8, child: _buildPageLinkChips()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageLinkChips() {
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: _pageLinks.map((link) {
|
||||
return GestureDetector(
|
||||
onTap: () => _onPageLinkTap(link),
|
||||
onLongPress: () => _deletePageLink(link),
|
||||
child: Chip(
|
||||
avatar: const Icon(Icons.link, size: 14, color: Colors.white),
|
||||
label: Text(
|
||||
'p${link.pageNumber}',
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
),
|
||||
backgroundColor: Colors.blue.shade600,
|
||||
padding: EdgeInsets.zero,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
Widget _colorDot(Color c, ColorScheme cs) {
|
||||
final selected =
|
||||
_color.toARGB32() == c.toARGB32() && _tool != CanvasTool.eraser;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_color = c;
|
||||
if (_tool == CanvasTool.eraser) _tool = CanvasTool.pen;
|
||||
}),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: c,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: selected ? cs.onSurface : cs.outlineVariant,
|
||||
width: selected ? 3 : 1,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPdfPane() {
|
||||
// Read-only reference PDF on the same engine (pdfrx) as the rest of the
|
||||
// app, opened at the anchor's page.
|
||||
return PdfViewer.file(
|
||||
widget.filePath,
|
||||
controller: _pdfController,
|
||||
params: PdfViewerParams(
|
||||
onViewerReady: (document, controller) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_pageCount = document.pages.length;
|
||||
});
|
||||
final target = widget.initialPage.clamp(0, _pageCount - 1);
|
||||
if (target > 0) {
|
||||
controller.goToPage(pageNumber: target + 1);
|
||||
}
|
||||
},
|
||||
onPageChanged: (pageNumber) {
|
||||
if (pageNumber == null || !mounted) return;
|
||||
final idx = pageNumber - 1;
|
||||
if (idx != _currentPage) setState(() => _currentPage = idx);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// On first layout, frame the view so existing ink is actually visible
|
||||
// (otherwise identity shows only the empty top-left corner of the huge
|
||||
// world). Empty scratchpad falls back to a comfortable 1:1 near origin.
|
||||
if (!_scratchCentered) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
@@ -493,10 +490,11 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
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.
|
||||
tool: _activeTool,
|
||||
brush: _penBrush,
|
||||
color: _color,
|
||||
strokeWidth: _strokeWidth,
|
||||
eraserRadius: kDefaultEraserRadius,
|
||||
minScale: 0.1,
|
||||
maxScale: 8.0,
|
||||
onStrokeComplete: _onStrokeComplete,
|
||||
@@ -508,9 +506,8 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Position the scratchpad so existing ink is on-screen. Fits the strokes'
|
||||
/// world bounding box into [pane] (with padding, scale clamped); for an empty
|
||||
/// scratchpad, shows the top-left working area at 1:1.
|
||||
/// Position the scratchpad so existing ink is on-screen (fit its bbox into
|
||||
/// [pane], scale clamped); an empty scratchpad shows the top-left at 1:1.
|
||||
void _frameScratchpad(Size pane) {
|
||||
if (pane.isEmpty) return;
|
||||
if (_strokes.isEmpty) {
|
||||
@@ -534,9 +531,10 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
const pad = 80.0;
|
||||
final boxW = (maxX - minX) + pad * 2;
|
||||
final boxH = (maxY - minY) + pad * 2;
|
||||
final scale =
|
||||
(pane.width / boxW < pane.height / boxH ? pane.width / boxW : pane.height / boxH)
|
||||
.clamp(0.15, 1.5);
|
||||
final scale = (pane.width / boxW < pane.height / boxH
|
||||
? pane.width / boxW
|
||||
: pane.height / boxH)
|
||||
.clamp(0.15, 1.5);
|
||||
final cx = (minX + maxX) / 2;
|
||||
final cy = (minY + maxY) / 2;
|
||||
final tx = pane.width / 2 - scale * cx;
|
||||
@@ -548,11 +546,3 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
..setTranslationRaw(tx, ty, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// A marker linking a scratchpad position to a specific PDF page.
|
||||
class _PageLink {
|
||||
final int pageNumber;
|
||||
final Offset position;
|
||||
|
||||
const _PageLink({required this.pageNumber, required this.position});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user