import 'package:flutter/material.dart'; import '../models/ink_point.dart'; import '../models/ink_stroke.dart'; import '../models/pen_tool.dart'; import '../widgets/ink_canvas.dart'; /// Transparent overlay widget positioned on top of the PDF viewer. /// /// Reuses the existing [InkCanvas] widget for ink rendering. /// Coordinates are normalized to [0, 1] relative to the overlay size, /// enabling correct mapping to PDF page coordinates during export. class PdfAnnotationLayer extends StatefulWidget { final List strokes; final void Function(InkStroke stroke)? onStrokeComplete; final void Function(String strokeId, List replacements)? onErase; final PenTool tool; final Color color; final double strokeWidth; final bool filled; final InteractionMode interactionMode; final int rotation; const PdfAnnotationLayer({ super.key, required this.strokes, this.onStrokeComplete, this.onErase, this.tool = PenTool.pen, this.color = Colors.black, this.strokeWidth = 2.0, this.filled = false, this.interactionMode = InteractionMode.draw, this.rotation = 0, }); @override State createState() => _PdfAnnotationLayerState(); } class _PdfAnnotationLayerState extends State { Size _canvasSize = Size.zero; /// Applies inverse rotation to normalized coordinates for rendering. /// Converts from stored (possibly rotated) coords back to display coords. Offset _inverseRotate(double nx, double ny, int rotation) { switch (rotation % 360) { case 90: return Offset(1.0 - ny, nx); case 180: return Offset(1.0 - nx, 1.0 - ny); case 270: return Offset(ny, 1.0 - nx); default: return Offset(nx, ny); } } /// Applies forward rotation to normalized coordinates before storage. /// Converts from display coords to the canonical rotated representation. Offset _forwardRotate(double nx, double ny, int rotation) { switch (rotation % 360) { case 90: return Offset(ny, 1.0 - nx); case 180: return Offset(1.0 - nx, 1.0 - ny); case 270: return Offset(1.0 - ny, nx); default: return Offset(nx, ny); } } /// Scales a stroke's points from normalized [0, 1] coordinates to /// the current canvas pixel coordinates for rendering. /// Applies inverse rotation before scaling so strokes render correctly /// on a rotated page. List get _scaledStrokes { if (_canvasSize == Size.zero) return widget.strokes; return widget.strokes.map((stroke) { return InkStroke( id: stroke.id, points: stroke.points.map((pt) { final rotated = _inverseRotate(pt.x, pt.y, widget.rotation); return InkPoint( x: rotated.dx * _canvasSize.width, y: rotated.dy * _canvasSize.height, pressure: pt.pressure, tilt: pt.tilt, timestamp: pt.timestamp, pointerDeviceKind: pt.pointerDeviceKind, ); }).toList(), tool: stroke.tool, color: stroke.color, strokeWidth: stroke.strokeWidth, createdAt: stroke.createdAt, filled: stroke.filled, textContent: stroke.textContent, fontSize: stroke.fontSize, ); }).toList(); } /// Normalizes a stroke's points from canvas pixel coordinates to /// [0, 1] relative to the overlay size. /// Applies forward rotation before storage so the canonical representation /// accounts for the current page rotation. InkStroke _normalizeStroke(InkStroke stroke) { if (_canvasSize == Size.zero) return stroke; return InkStroke( id: stroke.id, points: stroke.points.map((pt) { final nx = pt.x / _canvasSize.width; final ny = pt.y / _canvasSize.height; final rotated = _forwardRotate(nx, ny, widget.rotation); return InkPoint( x: rotated.dx, y: rotated.dy, pressure: pt.pressure, tilt: pt.tilt, timestamp: pt.timestamp, pointerDeviceKind: pt.pointerDeviceKind, ); }).toList(), tool: stroke.tool, color: stroke.color, strokeWidth: stroke.strokeWidth, createdAt: stroke.createdAt, filled: stroke.filled, textContent: stroke.textContent, fontSize: stroke.fontSize, ); } void _onStrokeComplete(InkStroke stroke) { widget.onStrokeComplete?.call(_normalizeStroke(stroke)); } void _onErase(String strokeId, List replacements) { widget.onErase?.call(strokeId, replacements.map(_normalizeStroke).toList()); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { _canvasSize = Size(constraints.maxWidth, constraints.maxHeight); return InkCanvas( strokes: _scaledStrokes, onStrokeComplete: _onStrokeComplete, onErase: _onErase, tool: widget.tool, color: widget.color, strokeWidth: widget.strokeWidth, filled: widget.filled, interactionMode: widget.interactionMode, ); }, ); } }