SketchControl in Novolis.Avalonia.Controls
Policies that keep the org coherent
name: Avalonia Sketch Control overview: Add SketchControl to Novolis.Avalonia.Controls — Excalidraw-inspired freehand strokes that become selectable/resizable shapes, with a resizable grid and on-demand Gridify. Single package; Avalonia + Novolis + nuget.org only. todos:
- id: document-core
content: Add SketchDocument, StrokeShape, grid snap, bounds transform, undo stack, SketchJson under Novolis.Avalonia.Controls status: completed
- id: gridify
content: Implement Gridify (quantize + dedupe) with unit tests in Controls/ status: completed
- id: sketch-control
content: "Implement SketchControl: pen, select/move/resize, pan/zoom, grid render, Gridify API" status: completed
- id: readme-verify
content: Update Controls README + build/test verification status: completed isProject: false
Placement
Home: `Novolis.Avalonia.Controls` — new SketchControl (and supporting types) in namespace Novolis.Avalonia.Controls.
No new packages. Do not create Novolis.Sketch or Novolis.Avalonia.Sketch. Document model, Gridify, JSON helpers, and the Avalonia control all live in Controls.
Dependencies (only):
- Existing:
Avalonia,Avalonia.Controls.DataGrid,Novolis.Avalonia.Layout(csproj) - Optional adds: only free, maintained packages from nuget.org if needed (prefer BCL
System.Text.Json— already available). NoNovolis.IO, no local feeds, no unmaintained/sketchy deps.
MVP scope (v1): freehand pen → stroke shapes, select / move / resize, pan/zoom, visible resizable grid, snap-while-draw/edit toggle, Gridify on selection (or all). Undo/redo via a small command stack. Out of v1: rect/ellipse/arrow/text, rough-js style, collaboration.
Document model (same package)
Pure C# types in Controls (testable without UI when logic is extracted, same pattern as ChoiceDialogLogic):
flowchart LR
pointer[Pointer strokes] --> draft[InProgressStroke]
draft -->|pointer up| shape[StrokeShape in Document]
shape --> select[Select / move / resize]
shape --> gridify[Gridify algorithm]
grid --> snap[Snap while edit]
grid --> gridify
doc[SketchDocument] --> json[SketchJson]- `SketchDocument`: elements list, selection ids,
GridSettings(Size,Visible,SnapEnabled),Version. - `StrokeShape`: id, points (world
doubleX/Y), stroke color/width. Bounds = AABB. - Resize:
ApplyBoundsTransform(oldBounds, newBounds)scales points with the AABB. - JSON:
SketchJson.Serialize/DeserializeviaSystem.Text.Json(in-package helpers — notNovolis.IO).
Gridify
On-demand (not only snap-while-drawing):
- For each selected
StrokeShape(or all if none selected): snap every point to nearest grid intersection using currentGridSize. - Collapse consecutive duplicates.
- Light cleanup only (no heavy simplification engine).
- Undoable via the document command stack.
Unit tests under `tests/Novolis.Avalonia.Unit/Controls/`.
Avalonia control (`SketchControl`)
Follow `StarMapControl`: inherit Control, StyledProperty + InvalidateVisual, override Render(DrawingContext). Code-only (no XAML / ControlThemes).
| Tool / mode | Behavior |
|---|---|
| Pen | Sample points (optional live snap) → on release commit `StrokeShape` |
| Select | Hit-test; selection AABB + 8 resize grips; drag move / grip resize |
| Pan | Middle button or Space+drag; wheel zoom toward cursor |
| Grid | Draw from `GridSettings`; `GridSize` / `GridVisible` / `SnapEnabled` as StyledProperties |
API:
Document,Tool(Pen|Select)GridSize,GridVisible,SnapEnabledGridifySelection(),Undo(),Redo(),Clear()DocumentChanged,SelectionChanged
Render order: grid → strokes → selection/grips → in-progress stroke.
Suggested files under src/Novolis.Avalonia.Controls/:
SketchControl.cs— control + pointer/renderSketchDocument.cs,StrokeShape.cs,GridSettings.cs— modelSketchGridify.cs,SketchBounds.cs— pure logicSketchJson.cs— serializeSketchHistory.cs— undo/redo
Scaffolding / packaging
- Add types + control to existing Controls project (no slnx / packages.json package adds).
- Update Controls `README.md` + package
Descriptionto mention sketch. - Unit tests for Gridify, snap, bounds transform, JSON round-trip.
Explicit non-goals (v1)
- No separate Sketch NuGet package.
- No
Novolis.IOdependency. - Do not reuse DraftStudio CAD /
.cadjson. - Imperative
DrawingContextonly (no AvaloniaShapevisual tree). - No rough/hand-drawn stroke styling yet.
Verification
dotnet testControls unit tests exit 0.dotnet buildNovolis.Avalonia.Controls.- Smoke: draw → select → resize → change grid size → Gridify → undo.