feat(postit): signature capture page (dev entry, file persistence)
Builds onf9cfd560(SignaturePadControl + SignaturePadData) with a full Avalonia page that captures signatures, renders them as Polylines, and persists the wire-format payload to ~/.local/share/PostIt/signatures as JSON v1. Scope - New SignaturePage (axaml + code-behind) hosts the render-agnostic control: a fixed-size Border is the hit-test surface, an overlaid Canvas is rebuilt on every RedrawRequested from the Strokes buffer. - SignaturePageViewModel wraps the control: exposes StrokeCount / PointCount / StatusMessage, Clear and CaptureAsync commands, and Attach/Detach for view-lifetime ownership. - CaptureAsync writes a JSON envelope { format, coordinateMax, capturedAtUtc, strokes, strokeCount } to LocalApplicationData/PostIt/signatures/signature-yyyyMMdd-HHmmssfff.json. This is a stop-gap; the production transport will be POST /api/signature/{devisId} on Yavsc.Api (commit 3+). - Entry point is a [DEV] button on MainPage that pushes the page onto the NavigationPage. The production trigger is a SignalR push from Yavsc.Org ("devis received, sign here") landing on a hub handler — the button and its Click handler are explicitly marked dev-only and tracked for removal in the same commit that wires the SignalR handler. Plumbing - App.axaml.cs: SignaturePage and SignaturePageViewModel registered as Transient in the DI container. - ViewLocator: routes SignaturePageViewModel to SignaturePage. - SignaturePadData: adds PointCount (sum of pairs across strokes), used by the VM status bar and the test surface. Tests (57/57 green, 9 new in this commit) - SignaturePageViewModelTests: constructors and dimension validation, Attach/Detach idempotence, StrokeCompleted and Clear propagate to the VM, CaptureAsync on empty buffer is a no-op, CaptureAsync on a non-empty buffer writes a v1 envelope with the expected structure (parsed back via JsonDocument, not text matching), and creates the destination directory if missing. - All previously-green tests (48) remain green. Out of scope - POST /api/signature endpoint on Yavsc.Api (commit 3). - SignalR handler that opens the page on a "devis received" push. - Rasterization: this commit only proves capture and persistence; the visible ink is a Polyline reconstruction, not a PNG, by design (per the wire-format decision in commit 1). Note on SignaturePadData - The PointCount property was added afterf9cfd560landed. It is folded into this commit rather than amendingf9cfd560to keep the existing history readable; the change is mechanical and tested by the new SignaturePageViewModelTests.
This commit is contained in:
parent
f9cfd56089
commit
7397f54ea2
9 changed files with 581 additions and 0 deletions
91
src/PostIt/PostIt/Views/SignaturePage.axaml.cs
Normal file
91
src/PostIt/PostIt/Views/SignaturePage.axaml.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Media;
|
||||
using PostIt.Controls;
|
||||
using PostIt.ViewModels;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class SignaturePage : ContentPage
|
||||
{
|
||||
private static readonly IBrush StrokeBrush = new SolidColorBrush(Color.FromRgb(0x10, 0x10, 0x10));
|
||||
private const double StrokeThickness = 2.0;
|
||||
private const double CoordinateMax = 10_000.0;
|
||||
|
||||
private SignaturePageViewModel? _vm;
|
||||
private SignaturePadControl? _control;
|
||||
|
||||
public SignaturePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Wire the capture area: the Pad itself is the control, the
|
||||
// surrounding Border (PadFrame) is the hit-test region. We
|
||||
// set CaptureArea once the control's template has been
|
||||
// applied — for an inline control with no template, that
|
||||
// happens on first measure, which is guaranteed before
|
||||
// the user can interact, so attaching here is safe.
|
||||
_control = Pad;
|
||||
_control.CaptureArea = PadFrame;
|
||||
|
||||
DataContextChanged += (_, _) => RebindViewModel(DataContext as SignaturePageViewModel);
|
||||
}
|
||||
|
||||
private void RebindViewModel(SignaturePageViewModel? vm)
|
||||
{
|
||||
if (_vm is not null)
|
||||
{
|
||||
_vm.Detach();
|
||||
_control!.RedrawRequested -= OnRedrawRequested;
|
||||
}
|
||||
|
||||
_vm = vm;
|
||||
|
||||
if (_vm is null || _control is null) return;
|
||||
|
||||
_vm.Attach(_control);
|
||||
_control.RedrawRequested += OnRedrawRequested;
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void OnRedrawRequested(object? sender, EventArgs e) => Repaint();
|
||||
|
||||
private void Repaint()
|
||||
{
|
||||
if (_control is null || InkLayer is null) return;
|
||||
|
||||
InkLayer.Children.Clear();
|
||||
var w = PadFrame.Bounds.Width;
|
||||
var h = PadFrame.Bounds.Height;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
var strokes = _control.Strokes;
|
||||
int i = 0;
|
||||
while (i < strokes.Count)
|
||||
{
|
||||
int k = strokes[i];
|
||||
if (k <= 0) break;
|
||||
i++; // skip the length prefix
|
||||
|
||||
var poly = new Polyline
|
||||
{
|
||||
Stroke = StrokeBrush,
|
||||
StrokeThickness = StrokeThickness,
|
||||
StrokeLineCap = PenLineCap.Round,
|
||||
StrokeJoin = PenLineJoin.Round,
|
||||
};
|
||||
var pts = new List<Point>(k);
|
||||
for (int p = 0; p < k; p++)
|
||||
{
|
||||
int nx = strokes[i++];
|
||||
int ny = strokes[i++];
|
||||
pts.Add(new Point(nx / CoordinateMax * w, ny / CoordinateMax * h));
|
||||
}
|
||||
poly.Points = pts;
|
||||
InkLayer.Children.Add(poly);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue