fix(split): frame scratchpad on existing ink
Some checks failed
CI / Windows build (push) Has been cancelled

The pen-first scratchpad opened at identity transform, showing only the
empty top-left corner of the 4000x4000 world — so existing ink (drawn
elsewhere) was off-screen and the pane looked blank ("草稿纸根本没看到").

On first layout, fit the strokes' world bounding box into the pane (padded,
scale clamped 0.15-1.5) so saved ink is immediately visible; an empty
scratchpad falls back to a 1:1 view near the origin.

flutter analyze: 0 issues.
This commit is contained in:
2026-06-23 16:52:35 +08:00
parent 8908f42f76
commit df389177e6

View File

@@ -60,6 +60,9 @@ class _SplitViewState extends State<SplitViewScreen> {
/// Pan/zoom transform for the scratchpad world (PenCanvas drives this). /// Pan/zoom transform for the scratchpad world (PenCanvas drives this).
final TransformationController _scratchTransform = TransformationController(); final TransformationController _scratchTransform = TransformationController();
/// Set once the initial view has been framed onto existing ink.
bool _scratchCentered = false;
Size get _worldSize => Size(_canvasWidth, _canvasHeight); Size get _worldSize => Size(_canvasWidth, _canvasHeight);
/// Maps the scratchpad toolbar's [PenTool] to the pen-canvas tool. Shapes and /// Maps the scratchpad toolbar's [PenTool] to the pen-canvas tool. Shapes and
@@ -471,24 +474,79 @@ class _SplitViewState extends State<SplitViewScreen> {
// Render the world through the performant PenCanvas: strokes normalized // Render the world through the performant PenCanvas: strokes normalized
// against the current world size; toolbar width is in world pixels, so the // against the current world size; toolbar width is in world pixels, so the
// pen-canvas fraction is width / worldWidth. // pen-canvas fraction is width / worldWidth.
return Container( return LayoutBuilder(
color: Theme.of(context).scaffoldBackgroundColor, builder: (context, constraints) {
child: PenCanvas( // On first layout, frame the view so existing ink is actually visible
pageSize: _worldSize, // (otherwise identity shows only the empty top-left corner of the huge
strokes: penStrokesFromInk(_strokes, _worldSize), // world). Empty scratchpad falls back to a comfortable 1:1 near origin.
transformationController: _scratchTransform, if (!_scratchCentered) {
tool: _canvasTool, WidgetsBinding.instance.addPostFrameCallback((_) {
color: _currentColor, if (!mounted) return;
strokeWidth: _currentStrokeWidth / _canvasWidth, _frameScratchpad(
// The world is huge, so allow zooming further out to survey it. Size(constraints.maxWidth, constraints.maxHeight));
minScale: 0.1, setState(() => _scratchCentered = true);
maxScale: 8.0, });
onStrokeComplete: _onStrokeComplete, }
onEraseStroke: _onErase, return Container(
pageWidget: const ColoredBox(color: Colors.white), color: Theme.of(context).scaffoldBackgroundColor,
), 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,
onStrokeComplete: _onStrokeComplete,
onEraseStroke: _onErase,
pageWidget: const ColoredBox(color: Colors.white),
),
);
},
); );
} }
/// 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.
void _frameScratchpad(Size pane) {
if (pane.isEmpty) return;
if (_strokes.isEmpty) {
_scratchTransform.value = Matrix4.identity();
return;
}
double minX = double.infinity, minY = double.infinity;
double maxX = -double.infinity, maxY = -double.infinity;
for (final s in _strokes) {
for (final p in s.points) {
if (p.x < minX) minX = p.x;
if (p.y < minY) minY = p.y;
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}
}
if (minX > maxX) {
_scratchTransform.value = Matrix4.identity();
return;
}
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 cx = (minX + maxX) / 2;
final cy = (minY + maxY) / 2;
final tx = pane.width / 2 - scale * cx;
final ty = pane.height / 2 - scale * cy;
_scratchTransform.value = Matrix4.identity()
..setEntry(0, 0, scale)
..setEntry(1, 1, scale)
..setEntry(2, 2, scale)
..setTranslationRaw(tx, ty, 0);
}
} }
/// A marker linking a scratchpad position to a specific PDF page. /// A marker linking a scratchpad position to a specific PDF page.