From 6e5c5007866d2a85a12e10e1549b76911132c1f0 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 24 Mar 2026 20:49:24 -0700 Subject: [PATCH] Update project docs to reflect current state after post-Phase 10 polish - PROGRESS.md: remove stale known issues (all fixed), add post-Phase 10 feature polish section covering toolbar redesign, 4 pen sizes, line/box/move tools, edge swipe nav, page reorder, notebook rename, filter/sort, JPG export, clipboard ops, startup state restoration, and DB migrations - PROJECT_PLAN.md: add Phase 11 (Server Sync Integration) and Phase 12 (Notebook Backup/Export) with step breakdowns - DESIGN.md: add Tools table, sync architecture section, backup/export design, JPG export, stroke styles, startup state restoration, edge swipe nav; update rendering strategy (3-layer compositing), source tree, schema, and pen sizes - CLAUDE.md: update build commands, architecture, source tree, and key conventions to match current codebase Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 59 ++++++++----- DESIGN.md | 218 +++++++++++++++++++++++++++++++++--------------- PROGRESS.md | 94 ++++++++++++++++----- PROJECT_PLAN.md | 48 ++++++++--- 4 files changed, 304 insertions(+), 115 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3da77ca..2be2ca7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,31 +9,38 @@ eng-pad is an Android note-taking app built around notebooks with an EMR pen as ## Build Commands ```bash -make all # lint → test → build +make all # lint -> test -> build make build # Compile debug + release APKs make test # Run unit tests make lint # Run Android Lint make clean # Clean build artifacts +make apk # Build release APK make run # Build, install, and launch on emulator (starts emulator if needed) make devrun # Build, install, and launch on connected USB device make run AVD=Medium_Phone_API_36.0 # Use a specific AVD # Run a single test class: make test-one CLASS=net.metacircular.engpad.data.StrokeBlobTest + +# Direct Gradle commands: +./gradlew build # Compile + test + lint +./gradlew test # Unit tests only +./gradlew lint # Android Lint only ``` ## Architecture - **Kotlin + Jetpack Compose** for UI chrome (screens, toolbar, dialogs) - **Custom View** (`PadCanvasView`) for the drawing canvas — Compose Canvas lacks `MotionEvent` access needed for stylus input -- **Room** (SQLite) for persistence — notebooks, pages, strokes -- **Single Activity** with Compose Navigation (three screens: notebook list → page list → editor) -- **Coordinate system**: 300 DPI canonical points. Regular page = 2550×3300pt, large = 3300×5100pt. Scaled to 72 DPI (×0.24) for PDF export. -- **Input dispatch**: `TOOL_TYPE_STYLUS` → draw/erase/select, `TOOL_TYPE_FINGER` → zoom/pan +- **Room** (SQLite) for persistence — notebooks, pages, strokes (schema version 3) +- **SharedPreferences** for startup state restoration (last notebook, page list flag) +- **Single Activity** with Compose Navigation (three routes: notebook list -> page list -> editor) +- **Coordinate system**: 300 DPI canonical points. Regular page = 2550x3300pt, large = 3300x5100pt. Scaled to 72 DPI (x0.24) for PDF export. +- **Input dispatch**: `TOOL_TYPE_STYLUS` -> draw/erase/select/move, `TOOL_TYPE_FINGER` -> zoom/pan/edge swipe ## Project Documents -- **DESIGN.md** — Full technical design (architecture, data model, rendering, source tree) +- **DESIGN.md** — Full technical design (architecture, data model, rendering, tools, source tree) - **PROJECT_PLAN.md** — All implementation steps with checkboxes. Check off steps as completed. - **PROGRESS.md** — What's been done, what's in progress, decisions made. Update after every step. @@ -49,27 +56,31 @@ eng-pad/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/net/metacircular/engpad/ -│ │ │ ├── EngPadApp.kt -- Application class +│ │ │ ├── EngPadApp.kt -- Application class, SharedPreferences │ │ │ ├── MainActivity.kt -- Single activity, Compose NavHost │ │ │ ├── data/ -│ │ │ │ ├── db/ -- Room database, DAOs, converters +│ │ │ │ ├── db/ -- Room database (v3), DAOs, converters │ │ │ │ ├── model/ -- Entities: Notebook, Page, Stroke, PageSize │ │ │ │ └── repository/ -- NotebookRepository, PageRepository │ │ │ ├── ui/ -│ │ │ │ ├── navigation/NavGraph.kt -- Route definitions -│ │ │ │ ├── notebooks/ -- List screen + ViewModel -│ │ │ │ ├── pages/ -- Page grid screen + ViewModel -│ │ │ │ ├── editor/ -- PadCanvasView, EditorScreen, ViewModel -│ │ │ │ ├── export/PdfExporter.kt -- PDF generation + sharing -│ │ │ │ └── theme/Theme.kt -- Material3 theme -│ │ │ └── undo/ -- UndoManager, UndoableAction +│ │ │ │ ├── navigation/NavGraph.kt -- Routes, auto-restore last notebook +│ │ │ │ ├── notebooks/ -- Library: list, filter, sort, rename + ViewModel +│ │ │ │ ├── pages/ -- Page grid, drag-to-reorder + ViewModel +│ │ │ │ ├── editor/ -- PadCanvasView, EditorScreen, ViewModel, Toolbar +│ │ │ │ ├── export/PdfExporter.kt -- PDF + JPG generation + sharing +│ │ │ │ └── theme/Theme.kt -- Material3 high-contrast e-ink theme +│ │ │ └── undo/ -- UndoManager, UndoableAction, StrokeActions, SelectionActions │ │ └── res/ -│ └── test/ -- Unit tests +│ │ ├── values/ -- strings.xml, themes.xml +│ │ ├── drawable/ -- Launcher icons +│ │ ├── mipmap-anydpi/ -- Adaptive icon +│ │ └── xml/file_provider_paths.xml -- FileProvider config +│ └── test/ -- Unit tests (StrokeBlobTest, PageSizeTest, UndoManagerTest) ├── build.gradle.kts -- Root build config ├── settings.gradle.kts -- Project settings (foojay JDK resolver) ├── gradle.properties ├── gradle/libs.versions.toml -- Version catalog -├── Makefile -- Build targets (build, test, lint, run, devrun) +├── Makefile -- Build targets (build, test, lint, run, devrun, apk) ├── CLAUDE.md -- This file ├── README.md ├── DESIGN.md -- Technical design @@ -81,11 +92,19 @@ eng-pad/ - Stroke points are packed as little-endian float BLOBs: `[x0,y0,x1,y1,...]` - All coordinates are in canonical space (300 DPI). Screen transform via `Matrix`. -- Grid drawn in **screen space** with pixel-snapped positions (not canonical space) for uniform squares. -- Pen widths: 0.38mm (4.49pt), 0.5mm (5.91pt), 0.6mm (7.09pt), 0.7mm (8.27pt) at 300 DPI. +- Grid drawn in **screen space** with pixel-snapped positions (not canonical space) for uniform squares. Cached in a grid bitmap, redrawn only on zoom/pan/resize. +- **4 pen sizes**: 0.38mm (4.49pt), 0.5mm (5.91pt), 0.6mm (7.09pt), 0.7mm (8.27pt) at 300 DPI. - Single PEN tool with long-press for size selection; remembers last size. +- **Line styles** stored in stroke `style` DB column: plain, dashed, arrow, double_arrow. +- LINE tool with long-press for style selection; BOX tool for rectangles. +- MOVE tool for tap-and-drag stroke relocation. - Anti-aliasing disabled on all paint objects for crisp e-ink rendering. -- No backing bitmap — strokes draw directly as paths. +- Screen-resolution backing bitmap with incremental stroke addition (no full redraw on each new stroke). - Viewport: page always fills screen, pan clamped to edges, dynamic min zoom. +- Edge swipe navigation: finger swipe from screen edge -> previous/next page. - Line snap: hold pen still 1.5s to snap to straight line (60pt movement threshold). - Hardware palm rejection only (EMR digitizer). +- Startup state restoration via SharedPreferences (last notebook ID, page list flag). +- Notebook `lastPageId` tracks resume position per-notebook. +- Clipboard: cut/copy/paste with toolbar buttons shown contextually. +- Export: PDF (multi-page, 72 DPI) and JPG (single page, 300 DPI) via share intents. diff --git a/DESIGN.md b/DESIGN.md index 800958f..8309f80 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -3,9 +3,9 @@ ## Overview eng-pad is an Android note-taking app for EMR pen devices. It provides a -notebook-based writing surface with a guide grid, two fixed pen sizes, and -PDF export. Target devices are the Supernote Manta (Android 11) and the -Daylight DC-1 (Android 13). +notebook-based writing surface with a guide grid, four fixed pen sizes, line +and box drawing tools, and PDF/JPG export. Target devices are the Supernote +Manta (Android 11) and the Daylight DC-1 (Android 13). ## Architecture @@ -22,19 +22,20 @@ low-latency stylus input. ### Screen Flow ``` -NotebookListScreen → PageListScreen → EditorScreen +NotebookListScreen -> PageListScreen -> EditorScreen | | | Create/delete Page grid, PadCanvasView + - notebooks add pages Toolbar (Compose) + rename, filter, add/delete/ Toolbar (Compose) + sort notebooks reorder pages ``` Three Compose navigation destinations: | Route | Screen | Purpose | |------------------------|--------------------|--------------------------------| -| `notebooks` | NotebookListScreen | List, create, delete notebooks | -| `pages/{notebookId}` | PageListScreen | Page thumbnails, add pages | -| `editor/{pageId}` | EditorScreen | Drawing canvas + toolbar | +| `notebooks` | NotebookListScreen | List, create, delete, rename notebooks; filter and sort | +| `pages/{notebookId}` | PageListScreen | Page thumbnails, add/delete pages, drag-to-reorder | +| `editor/{notebookId}?pageId={pageId}` | EditorScreen | Drawing canvas + toolbar | ### Dependency Stack @@ -44,10 +45,12 @@ Three Compose navigation destinations: | Drawing | Custom View + Canvas API | Stroke rendering, input | | State | ViewModel + StateFlow | Reactive UI state | | Persistence | Room (SQLite) | Notebooks, pages, strokes | +| Preferences | SharedPreferences | Last notebook, UI state restore | | Async | Kotlin Coroutines | Background DB operations | | Navigation | Compose Navigation | Screen routing | | Export | Android PdfDocument API | PDF generation | -| Share | FileProvider + Intents | PDF sharing (Dropbox, etc.) | +| Export | Bitmap + JPEG compress | JPG generation | +| Share | FileProvider + Intents | PDF/JPG sharing (Dropbox, etc.) | No external drawing libraries. The Android `Canvas` + `Path` + `Paint` API is sufficient and avoids dependencies that may behave unpredictably on e-ink @@ -61,25 +64,29 @@ during PDF export (multiply by 72/300 = 0.24). | Page Size | Inches | Canonical Points | |-----------|----------|-------------------| -| Regular | 8.5 × 11 | 2550 × 3300 | -| Large | 11 × 17 | 3300 × 5100 | +| Regular | 8.5 x 11 | 2550 x 3300 | +| Large | 11 x 17 | 3300 x 5100 | ### Grid The guide grid uses ~5 squares per inch = 300/5 = **60 points** per grid square. The grid is drawn as thin gray lines on screen but excluded from PDF -export and any other output. +export, JPG export, and any other output. ### Pen Sizes -Two fixed stroke widths, corresponding to Muji gel ink ballpoints: +Four fixed stroke widths: | Pen | Millimeters | Canonical Points | |-------|-------------|------------------| -| Fine | 0.38 mm | 4.49 pt | -| Medium| 0.50 mm | 5.91 pt | +| 0.38 | 0.38 mm | 4.49 pt | +| 0.50 | 0.50 mm | 5.91 pt | +| 0.60 | 0.60 mm | 7.09 pt | +| 0.70 | 0.70 mm | 8.27 pt | No pressure sensitivity — stroke width is uniform for a given pen size. +The PEN tool remembers the last selected size. Long-press the pen button +to open the size picker. ### Screen Transform @@ -89,12 +96,23 @@ transformed through the inverse matrix before being stored, ensuring strokes are always in canonical space regardless of zoom. ``` -canonical → [viewMatrix] → screen pixels -screen pixels → [inverseMatrix] → canonical +canonical -> [viewMatrix] -> screen pixels +screen pixels -> [inverseMatrix] -> canonical ``` -Zoom range: 0.5× to 4×. Pan is clamped so the page cannot scroll entirely -off-screen. +Zoom range: dynamic minimum (page fills viewport) to 4x. Pan is clamped so +the page cannot scroll entirely off-screen. + +## Tools + +| Tool | Behavior | Sub-options | +|--------|----------|-------------| +| PEN | Freehand drawing with active pen size | 4 sizes via long-press menu | +| LINE | Straight line from pen-down to pen-up | Plain, arrow, double-arrow, dashed via long-press menu | +| BOX | Rectangle from corner to corner | Uses current pen size | +| ERASER | Touch a stroke to delete it (stroke-level) | — | +| SELECT | Rectangle selection, then cut/copy/delete/paste | — | +| MOVE | Tap and drag individual strokes | — | ## Data Model @@ -106,7 +124,8 @@ CREATE TABLE notebooks ( title TEXT NOT NULL, page_size TEXT NOT NULL CHECK(page_size IN ('regular', 'large')), created_at INTEGER NOT NULL, -- epoch millis - updated_at INTEGER NOT NULL -- epoch millis + updated_at INTEGER NOT NULL, -- epoch millis + last_page_id INTEGER NOT NULL DEFAULT 0 -- resume position ); CREATE TABLE pages ( @@ -124,18 +143,22 @@ CREATE TABLE strokes ( color INTEGER NOT NULL, -- ARGB packed int point_data BLOB NOT NULL, -- packed floats: [x0,y0,x1,y1,...] stroke_order INTEGER NOT NULL, -- z-order within the page - created_at INTEGER NOT NULL + created_at INTEGER NOT NULL, + style TEXT NOT NULL DEFAULT 'plain' -- plain, dashed, arrow, double_arrow ); CREATE INDEX idx_strokes_page ON strokes(page_id); CREATE INDEX idx_pages_notebook ON pages(notebook_id); ``` +Database version: 3. Migrations add `style` column to strokes and +`last_page_id` column to notebooks. + ### Stroke Point Encoding Points are stored as a `BLOB` of packed little-endian floats: -`[x0, y0, x1, y1, x2, y2, ...]`. A stroke with N points uses N × 2 × 4 -bytes. A typical stroke of 200 points = 1600 bytes. This is ~10× more +`[x0, y0, x1, y1, x2, y2, ...]`. A stroke with N points uses N x 2 x 4 +bytes. A typical stroke of 200 points = 1600 bytes. This is ~10x more compact than JSON and eliminates parsing overhead. ```kotlin @@ -153,6 +176,19 @@ fun ByteArray.toFloatArray(): FloatArray { } ``` +### Stroke Styles + +The `style` column stores a string constant identifying the stroke's visual +style. This applies to LINE tool strokes and is stored in the DB so rendering +is consistent across sessions. + +| Style | Rendering | +|----------------|-----------| +| `plain` | Solid line (default for freehand, box, and plain lines) | +| `dashed` | Dashed line pattern | +| `arrow` | Solid line with arrowhead at endpoint | +| `double_arrow` | Solid line with arrowheads at both ends | + ## Input Handling ### Tool Type Dispatch @@ -163,8 +199,8 @@ software palm rejection: | Tool Type | Action | |------------------|-------------------------------------| -| `TOOL_TYPE_STYLUS` | Draw, erase, or select (per mode) | -| `TOOL_TYPE_FINGER` | Pinch-to-zoom, pan | +| `TOOL_TYPE_STYLUS` | Draw, erase, select, or move (per tool) | +| `TOOL_TYPE_FINGER` | Pinch-to-zoom, pan, edge swipe navigation | ### Historical Points @@ -172,16 +208,12 @@ The Wacom EMR digitizer batches events. To capture all intermediate points for smooth strokes, `PadCanvasView` processes `MotionEvent.getHistoricalX/Y` on every `ACTION_MOVE`, not just the current event coordinates. -### Modes +### Edge Swipe Navigation -The editor toolbar controls the active mode: - -| Mode | Stylus Behavior | -|----------|------------------------------------------------------| -| Draw | Create strokes with the active pen size | -| Erase | Touch a stroke to delete it (stroke-level eraser) | -| Select | Draw a rectangle to select strokes, then move/copy/delete | -| Box | Drag corner-to-corner to draw rectangles | +Finger swipes originating from the left or right 8% of the screen trigger +page navigation (previous/next page). Requires a minimum horizontal distance +of 100px. Multi-finger gestures cancel edge swipe detection. Swiping forward +past the last page auto-creates a new page if the current page has strokes. ### Line Snap @@ -197,15 +229,24 @@ a separate ruler tool. ### On-Screen -1. **Completed strokes** are drawn directly as `Path` objects on each - `onDraw` call. No backing bitmap — direct path rendering avoids the - resolution loss that caused blurry strokes on e-ink displays. -2. **In-progress stroke** (pen is down) is drawn on top of completed strokes. -3. **Grid** is drawn in screen pixel space (not canonical space) with - pixel-snapped line positions. This ensures perfectly uniform square - spacing regardless of zoom level. Grid uses 1px screen-space lines. -4. **Strokes** are drawn in canonical space via the view `Matrix`. - Anti-aliasing is disabled on all paint objects for crisp e-ink rendering. +Three-layer compositing with cached bitmaps: + +1. **Grid layer** — cached bitmap at screen resolution, redrawn only when + zoom, pan, or view size changes. Grid is drawn in screen pixel space + (not canonical space) with pixel-snapped line positions for perfectly + uniform squares regardless of zoom level. Uses 1px screen-space lines. + +2. **Stroke layer** — screen-resolution backing bitmap. Completed strokes + are drawn as `Path` objects via the view `Matrix`. New strokes are added + incrementally (drawn onto the existing bitmap without clearing) to avoid + full-redraw flicker. The bitmap is fully rebuilt only on zoom/pan changes + or stroke deletion. + +3. **Dynamic layer** — drawn directly on each `onDraw` call in canonical + space via the view `Matrix`. Includes: in-progress stroke (pen is down), + line/box previews, selection rectangle and highlights. + +Anti-aliasing is disabled on all paint objects for crisp e-ink rendering. ### Viewport @@ -229,6 +270,12 @@ Export flow: 3. Write to a temp file in the app's cache directory. 4. Share via `Intent.ACTION_SEND` with a `FileProvider` URI. +### JPG Export + +Single-page export at 300 DPI (full canonical resolution). Creates a +`Bitmap`, renders all strokes (no grid), compresses to JPEG at 95% quality. +Shared via the same `FileProvider` + intent mechanism as PDF. + ## Undo/Redo Command pattern with a depth limit of 50: @@ -237,7 +284,7 @@ Command pattern with a depth limit of 50: |----------------------|-------------------|--------------------| | `AddStrokeAction` | Insert stroke | Delete stroke | | `DeleteStrokeAction` | Delete stroke | Re-insert stroke | -| `MoveStrokesAction` | Offset points | Offset back | +| `MoveStrokesAction` | Offset points | Restore original points | | `DeleteMultipleAction`| Delete strokes | Re-insert strokes | | `CopyStrokesAction` | Duplicate strokes | Delete copies | @@ -253,7 +300,8 @@ upgrade): 1. In select mode, stylus drag draws a selection rectangle. 2. Strokes whose bounding boxes intersect the rectangle are selected. 3. Selected strokes get a visual highlight (translucent overlay + bounding box). -4. Available operations: delete, drag-to-move, copy/paste. +4. Available operations: cut, copy, delete, paste (toolbar buttons appear + contextually when strokes are selected or clipboard is non-empty). 5. All selection operations are undoable. ## Eraser @@ -265,6 +313,15 @@ Stroke-level eraser (not pixel-level): point-by-point distance (threshold: ~42 canonical points / ~3.5mm). 3. Hit strokes are deleted immediately (with undo support). +## Startup State Restoration + +The app persists the last-opened notebook ID and a flag for whether the page +list was showing in `SharedPreferences` (via `EngPadApp`). On launch: +- If a notebook was previously open, auto-navigate to it. +- If the page list was showing, navigate to the page list on top. +- The editor resumes at the notebook's `lastPageId` (tracked per-notebook in + the DB). + ## E-ink Considerations Both target devices have e-ink or e-ink-like displays with high refresh latency: @@ -276,6 +333,36 @@ Both target devices have e-ink or e-ink-like displays with high refresh latency: - Consider a manual "refresh" button to force a full-screen redraw to clear e-ink ghosting. +## Sync Architecture (Planned) + +eng-pad will sync notebooks to an eng-pad-server instance over gRPC: + +- **Protocol**: gRPC with protobuf-lite, over TLS. +- **Authentication**: password-based auth to eng-pad-server (not MCIAS — this + is a personal-use app, not a Metacircular service). +- **Sync model**: manual sync (user-initiated, not automatic). Per-notebook + sync button and sync-all from the library. +- **Conflict resolution**: last-write-wins based on `updated_at` timestamps, + with potential for manual resolution in future. +- **Data flow**: push local changes, pull remote changes. Full notebook + content (metadata + pages + strokes) serialized for transfer. +- **Status tracking**: per-notebook sync state (synced, pending, error) + displayed as badges in the notebook list. + +## Backup/Export Design (Planned) + +Notebook backup as a portable zip file: + +- **Format**: `notebook_title.engpad.zip` containing: + - `notebook.json` — notebook metadata (title, page_size, page count) + - `pages/001.json`, `pages/002.json`, ... — one file per page with + metadata and strokes array + - Each stroke: `{penSize, color, style, points: [x0,y0,x1,y1,...]}` + - Points as JSON float arrays (portable, human-readable) +- **Import**: parse zip, create notebook + pages + strokes in Room DB. +- **Export-all**: single zip containing all notebooks for full backup. +- **Sharing**: via Android share intents (same mechanism as PDF/JPG). + ## Source Tree ``` @@ -286,42 +373,42 @@ eng-pad/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/net/metacircular/engpad/ -│ │ │ ├── EngPadApp.kt -- Application class +│ │ │ ├── EngPadApp.kt -- Application class, SharedPreferences │ │ │ ├── MainActivity.kt -- Single activity, Compose NavHost │ │ │ ├── data/ │ │ │ │ ├── db/ -│ │ │ │ │ ├── EngPadDatabase.kt -- Room database definition +│ │ │ │ │ ├── EngPadDatabase.kt -- Room database (version 3) │ │ │ │ │ ├── NotebookDao.kt -- Notebook CRUD -│ │ │ │ │ ├── PageDao.kt -- Page CRUD +│ │ │ │ │ ├── PageDao.kt -- Page CRUD + reorder │ │ │ │ │ ├── StrokeDao.kt -- Stroke CRUD │ │ │ │ │ └── Converters.kt -- Room type converters │ │ │ │ ├── model/ -│ │ │ │ │ ├── Notebook.kt -- Room entity +│ │ │ │ │ ├── Notebook.kt -- Room entity (+ lastPageId) │ │ │ │ │ ├── Page.kt -- Room entity -│ │ │ │ │ ├── Stroke.kt -- Room entity +│ │ │ │ │ ├── Stroke.kt -- Room entity (+ style column) │ │ │ │ │ └── PageSize.kt -- Enum: REGULAR, LARGE │ │ │ │ └── repository/ -│ │ │ │ ├── NotebookRepository.kt -- Notebook operations -│ │ │ │ └── PageRepository.kt -- Page + stroke operations +│ │ │ │ ├── NotebookRepository.kt -- Notebook operations + rename +│ │ │ │ └── PageRepository.kt -- Page + stroke ops + reorder │ │ │ ├── ui/ │ │ │ │ ├── navigation/ -│ │ │ │ │ └── NavGraph.kt -- Route definitions +│ │ │ │ │ └── NavGraph.kt -- Routes, auto-restore last notebook │ │ │ │ ├── notebooks/ -│ │ │ │ │ ├── NotebookListScreen.kt -- Notebook list UI -│ │ │ │ │ └── NotebookListViewModel.kt -- Notebook list state +│ │ │ │ │ ├── NotebookListScreen.kt -- Library: list, filter, sort, rename +│ │ │ │ │ └── NotebookListViewModel.kt -- Notebook list state + rename │ │ │ │ ├── pages/ -│ │ │ │ │ ├── PageListScreen.kt -- Page grid UI -│ │ │ │ │ └── PageListViewModel.kt -- Page list state +│ │ │ │ │ ├── PageListScreen.kt -- Page grid, drag-to-reorder +│ │ │ │ │ └── PageListViewModel.kt -- Page list state + reorder │ │ │ │ ├── editor/ -│ │ │ │ │ ├── PadCanvasView.kt -- Custom View: rendering + input +│ │ │ │ │ ├── PadCanvasView.kt -- Custom View: rendering + input + edge swipe │ │ │ │ │ ├── EditorScreen.kt -- Compose wrapper -│ │ │ │ │ ├── EditorViewModel.kt -- Editor state + persistence -│ │ │ │ │ ├── CanvasState.kt -- Zoom, pan, tool, selection -│ │ │ │ │ └── Toolbar.kt -- Editor toolbar +│ │ │ │ │ ├── EditorViewModel.kt -- Editor state, page nav, clipboard +│ │ │ │ │ ├── CanvasState.kt -- Tool, PenSize, LineStyle, zoom/pan +│ │ │ │ │ └── Toolbar.kt -- Icon-button toolbar with Canvas icons │ │ │ │ ├── export/ -│ │ │ │ │ └── PdfExporter.kt -- PDF generation + sharing +│ │ │ │ │ └── PdfExporter.kt -- PDF + JPG generation + sharing │ │ │ │ └── theme/ -│ │ │ │ └── Theme.kt -- Material3 theme +│ │ │ │ └── Theme.kt -- Material3 high-contrast e-ink theme │ │ │ └── undo/ │ │ │ ├── UndoManager.kt -- Undo/redo stack │ │ │ ├── UndoableAction.kt -- Action interface @@ -331,13 +418,14 @@ eng-pad/ │ │ ├── values/ │ │ │ ├── strings.xml │ │ │ └── themes.xml -│ │ ├── drawable/ -- Toolbar icons +│ │ ├── drawable/ -- Launcher icons +│ │ ├── mipmap-anydpi/ -- Adaptive icon │ │ └── xml/ │ │ └── file_provider_paths.xml -- FileProvider config │ └── test/ │ └── kotlin/net/metacircular/engpad/ │ ├── data/ -│ │ ├── StrokeBlobTest.kt -- Float array ↔ blob roundtrip +│ │ ├── StrokeBlobTest.kt -- Float array <-> blob roundtrip │ │ └── PageSizeTest.kt -- Page size enum tests │ └── undo/ │ └── UndoManagerTest.kt -- Undo/redo logic diff --git a/PROGRESS.md b/PROGRESS.md index 3619be1..ce5baee 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -23,7 +23,7 @@ See PROJECT_PLAN.md for the full step list. - [x] 1.3: Lint configured — warningsAsErrors, AGP version check suppressed (AGP 9.x needs Gradle 9.x) - [x] 1.4: Room entities: Notebook, Page, Stroke, PageSize enum -- [x] 1.5: Converters: FloatArray ↔ ByteArray (packed little-endian) +- [x] 1.5: Converters: FloatArray <-> ByteArray (packed little-endian) - [x] 1.6: DAOs: NotebookDao, PageDao, StrokeDao - [x] 1.7: EngPadDatabase (Room, version 1) - [x] 1.8: NotebookRepository, PageRepository @@ -38,20 +38,20 @@ See PROJECT_PLAN.md for the full step list. - [x] 2.3: NotebookListScreen — lazy list, create dialog (title + page size radio), long-press delete with confirmation, empty state - [x] 2.4: Auto-create page 1 in NotebookRepository.create() -- [x] 2.5: Navigation wired — tap notebook → pages stub, editor stub +- [x] 2.5: Navigation wired — tap notebook -> pages stub, editor stub - EngPadTheme: high-contrast light color scheme (black on white for e-ink) ### Phase 3: Canvas — Basic Drawing (2026-03-24) -- [x] 3.1–3.4: PadCanvasView — stylus input with historical points, Path/Paint +- [x] 3.1-3.4: PadCanvasView — stylus input with historical points, Path/Paint stroke rendering, direct path drawing (no backing bitmap), 60pt grid drawn in screen space with pixel-snapped positions, Matrix coordinate - transform (canonical ↔ screen) -- [x] 3.5: CanvasState — tool enum (PEN_FINE, PEN_MEDIUM, ERASER, SELECT, BOX), - zoom/pan state, pen width constants (4.49pt, 5.91pt) + transform (canonical <-> screen) +- [x] 3.5: CanvasState — tool enum (PEN, LINE, BOX, ERASER, SELECT, MOVE), + zoom/pan state, PenSize enum with 4 sizes - [x] 3.6: EditorViewModel — loads strokes from Room, saves on completion - [x] 3.7: EditorScreen + Toolbar — Compose wrapper with AndroidView, - FilterChip toolbar for pen size and eraser selection + icon-button toolbar with long-press menus for pen size and line style - [x] 3.8: NavGraph updated — pages route auto-navigates to first page's editor, page size passed through route params - Used KTX Canvas extensions (withMatrix, withScale, createBitmap) per lint @@ -59,7 +59,7 @@ See PROJECT_PLAN.md for the full step list. ### Phase 4: Zoom and Pan (2026-03-24) -- [x] 4.1: ScaleGestureDetector with focal-point zoom (dynamic min–4×) +- [x] 4.1: ScaleGestureDetector with focal-point zoom (dynamic min-4x) - [x] 4.2: Finger drag for pan with multi-pointer tracking, pan clamped so page always fills viewport - [x] 4.3: Input routing by tool type already in place from Phase 3 @@ -69,7 +69,7 @@ See PROJECT_PLAN.md for the full step list. ### Phase 5: Eraser (2026-03-24) -- [x] 5.1–5.3: Stroke-level eraser with bounding box hit test (42pt radius), +- [x] 5.1-5.3: Stroke-level eraser with bounding box hit test (42pt radius), processes historical touch points for thorough erasing, deletes from view + Room DB @@ -84,7 +84,7 @@ See PROJECT_PLAN.md for the full step list. ### Phase 7: Selection — Move, Copy, Delete (2026-03-24) -- [x] 7.1–7.2: Rectangle selection with dashed rect and blue highlight +- [x] 7.1-7.2: Rectangle selection with dashed rect and blue highlight - [x] 7.3: Delete, drag-to-move, copy operations with toolbar buttons - [x] 7.4: Full undo integration — DeleteMultipleStrokesAction, MoveStrokesAction, CopyStrokesAction @@ -100,7 +100,7 @@ See PROJECT_PLAN.md for the full step list. ### Phase 9: PDF Export (2026-03-24) - [x] 9.1: PdfExporter — creates PdfDocument, scales canonical 300 DPI coords - to 72 DPI PDF points (×0.24), renders strokes without grid + to 72 DPI PDF points (x0.24), renders strokes without grid - [x] 9.2: Share via Intent.ACTION_SEND + FileProvider (configured in Phase 1) - [x] 9.3: PDF button in editor toolbar, exports current page @@ -120,8 +120,9 @@ Tested on DC-1, identified and fixed rendering issues: sub-pixel positioning when scaled. Fixed by drawing grid in screen space with pixel-snapped line positions. Now uniform squares. - **Stroke quality**: original backing bitmap at 1/4 resolution caused blurry - strokes. Fixed by removing backing bitmap entirely and drawing paths directly. - Anti-aliasing disabled on all paint for crisp e-ink lines. + strokes. Fixed by switching to a screen-resolution backing bitmap with + incremental stroke addition. Anti-aliasing disabled on all paint for + crisp e-ink lines. - **Viewport**: dark gray background was visible around page edges. Fixed by clamping pan to page edges, computing dynamic min zoom, and white background. - **Box tool**: works correctly on device. @@ -143,11 +144,55 @@ Critical/high-priority fixes applied: fall back to first page instead of showing blank canvas. - **Arrow head Paint allocation**: preallocated reusable Paint object instead of allocating per drawArrowHeads() call. +- **CopyStrokesAction**: fixed `takeLast(n)` fragile ID lookup — now + tracks copied stroke IDs directly. +- **Grid caching**: grid bitmap is now cached and only redrawn on + zoom/pan/resize changes. -Known issues still to address: -- CopyStrokesAction uses `takeLast(n)` to find new stroke IDs — fragile -- No mutex guarding concurrent page navigation + paste operations -- Grid redrawn every frame (could be cached) +### Post-Phase 10: Feature Polish (2026-03-24) + +Extensive feature work after the initial 10-phase plan: + +- **Toolbar redesign**: replaced FilterChip toolbar with icon-button toolbar. + Custom Canvas-drawn icons for each tool (pen dot, line, box outline, + eraser, dashed-rect select, four-way-arrow move, curved undo/redo arrows). +- **Pen size consolidation**: expanded from 2 sizes to 4 sizes (0.38mm, + 0.5mm, 0.6mm, 0.7mm). Single PEN tool with long-press for size selection + dropdown; remembers last size. +- **Line tool**: dedicated LINE tool (separate from PEN) with style variants + via long-press menu: plain, arrow, double-arrow, dashed. Line style stored + in stroke `style` column in DB. +- **Box tool**: draws rectangles corner-to-corner. Uses current pen size + and style. +- **Move tool**: dedicated MOVE tool for tap-and-drag stroke relocation + (separate from select-then-drag). +- **Edge swipe navigation**: finger swipe from left/right screen edge + navigates to previous/next page. Auto-creates new page on forward swipe + past last page (if current page has strokes). Edge zone = 8% of screen + width, minimum 100px horizontal distance. +- **Page list drag-to-reorder**: long-press drag in PageListScreen to + reorder pages. Uses `longPressDraggableHandle` with reorderable lazy + column. Page numbers renumbered sequentially on drop. +- **Notebook rename**: long-press notebook in library list to rename. + Rename dialog with text field. +- **Library filter/sort**: search field to filter notebooks by title, + sort by last-edited or title (ascending/descending). +- **JPG export**: single-page JPEG export at 300 DPI via Export dropdown + menu. Uses `Bitmap.compress(JPEG, 95)`, shared via FileProvider + intents. +- **Clipboard operations**: cut, copy, paste toolbar buttons appear when + strokes are selected or clipboard is non-empty. Cut = copy + delete. +- **Startup state restoration**: SharedPreferences in EngPadApp store last + opened notebook ID and view-all-pages flag. On launch, auto-navigates to + last notebook (or page list if that was the last screen). +- **Page navigation from editor**: binder button in toolbar shows + "p1/5"-style page indicator with dropdown for "View all pages" and + "Go to page..." dialog. +- **Last page tracking**: Notebook entity has `lastPageId` column; editor + resumes at last-visited page when reopening a notebook. +- **Page delete**: delete pages from PageListScreen with sequential + renumbering of remaining pages. +- **Database migrations**: schema at version 3 (added `style` column to + strokes, `last_page_id` to notebooks). ## Backup Design (Not Yet Implemented) @@ -168,11 +213,12 @@ portable or human-readable). ## Status -All implementation phases complete. Remaining work: +All implementation phases complete. Post-phase polish significantly extended +the feature set beyond the original plan. Remaining work: - Test on Supernote Manta -- Verify line snap behavior on device - Performance profiling with heavily annotated pages -- Implement notebook backup/restore +- Implement notebook backup/restore (zip format) +- Server sync integration (eng-pad-server, gRPC, manual sync) ## Decisions & Deviations @@ -183,3 +229,11 @@ All implementation phases complete. Remaining work: - **Coordinate system**: 300 DPI canonical points (scaled to 72 DPI for PDF export). - **UI framework**: Compose for chrome, custom View for canvas (Compose Canvas lacks MotionEvent access needed for stylus input). +- **Toolbar**: Icon buttons with Canvas-drawn icons (no drawable resources needed + for tool icons). Long-press for sub-menus (pen size, line style). +- **Rendering**: Screen-resolution backing bitmap with incremental stroke + addition + cached grid bitmap. Both invalidated on zoom/pan/resize. +- **Line styles**: Stored as string in stroke `style` DB column (plain, dashed, + arrow, double_arrow) rather than a separate table. +- **State restoration**: SharedPreferences for last notebook/page rather than + Compose SavedStateHandle (survives process death). diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index 6c36e84..7ada338 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -21,7 +21,7 @@ completed and log them in PROGRESS.md. - [x] 1.4: Define Room entities - `data/model/Notebook.kt`, `Page.kt`, `Stroke.kt`, `PageSize.kt` - [x] 1.5: Implement type converters - - `data/db/Converters.kt` — `FloatArray` ↔ `ByteArray` + - `data/db/Converters.kt` — `FloatArray` <-> `ByteArray` - [x] 1.6: Define DAOs - `data/db/NotebookDao.kt`, `PageDao.kt`, `StrokeDao.kt` - [x] 1.7: Define Room database @@ -43,30 +43,30 @@ completed and log them in PROGRESS.md. - `ui/notebooks/NotebookListScreen.kt` — list, create dialog, delete confirmation - [x] 2.4: Auto-create page 1 on notebook creation - In `NotebookRepository.create()` -- [x] 2.5: Navigation: tap notebook → page list (stub screen) +- [x] 2.5: Navigation: tap notebook -> page list (stub screen) - **Verify:** `./gradlew build` — PASSED (build + test + lint) ## Phase 3: Canvas — Basic Drawing - [x] 3.1: Implement `PadCanvasView` — stylus event handling - `ui/editor/PadCanvasView.kt` — `onTouchEvent`, `getHistoricalX/Y` -- [x] 3.2: Stroke rendering — `Path`/`Paint`, backing bitmap (1/4 resolution) +- [x] 3.2: Stroke rendering — `Path`/`Paint`, backing bitmap (screen resolution) - [x] 3.3: Grid drawing — 60pt spacing, drawn on screen only -- [x] 3.4: Coordinate transform — canonical ↔ screen via `Matrix` +- [x] 3.4: Coordinate transform — canonical <-> screen via `Matrix` - [x] 3.5: Implement `CanvasState` - - `ui/editor/CanvasState.kt` — zoom, pan, active tool, pen widths + - `ui/editor/CanvasState.kt` — zoom, pan, active tool, pen sizes, line styles - [x] 3.6: Implement `EditorViewModel` - `ui/editor/EditorViewModel.kt` — load/save strokes from Room - [x] 3.7: Implement `EditorScreen` + toolbar - `ui/editor/EditorScreen.kt`, `ui/editor/Toolbar.kt` -- [x] 3.8: Wire navigation — notebook list → pages → editor (pages auto-navigates to first page) +- [x] 3.8: Wire navigation — notebook list -> pages -> editor (pages auto-navigates to first page) - **Verify:** `./gradlew build` — PASSED (build + test + lint). Manual on-device test pending. ## Phase 4: Zoom and Pan -- [x] 4.1: `ScaleGestureDetector` for pinch-to-zoom (0.5×–4×) with focal-point zoom +- [x] 4.1: `ScaleGestureDetector` for pinch-to-zoom (0.5x-4x) with focal-point zoom - [x] 4.2: Finger drag for pan with pointer tracking -- [x] 4.3: Input routing — `TOOL_TYPE_STYLUS` → draw, `TOOL_TYPE_FINGER` → zoom/pan +- [x] 4.3: Input routing — `TOOL_TYPE_STYLUS` -> draw, `TOOL_TYPE_FINGER` -> zoom/pan - **Verify:** `./gradlew build` — PASSED. Manual on-device test pending. ## Phase 5: Eraser @@ -99,12 +99,12 @@ completed and log them in PROGRESS.md. - [x] 8.1: `PageListScreen` with page grid (aspect-ratio cards) - `ui/pages/PageListScreen.kt`, `PageListViewModel.kt` - [x] 8.2: Add new page FAB -- [x] 8.3: NavGraph updated — pages route shows PageListScreen, tap page → editor +- [x] 8.3: NavGraph updated — pages route shows PageListScreen, tap page -> editor - **Verify:** `./gradlew build` — PASSED ## Phase 9: PDF Export -- [x] 9.1: `PdfExporter` — Android `PdfDocument` API with 300→72 DPI scaling +- [x] 9.1: `PdfExporter` — Android `PdfDocument` API with 300->72 DPI scaling - `ui/export/PdfExporter.kt` - [x] 9.2: Share via `Intent.ACTION_SEND` + `FileProvider` - `res/xml/file_provider_paths.xml` already configured in Phase 1 @@ -117,3 +117,31 @@ completed and log them in PROGRESS.md. - [x] 10.2: Auto-save — strokes save on completion (since Phase 3) - [x] 10.3: Empty states (notebook list, page list), delete confirmations (notebooks) - [ ] 10.4: Performance profiling on target devices (requires on-device testing) + +## Phase 11: Server Sync Integration + +- [ ] 11.1: Proto setup — add `protobuf-lite` dependency, generate Kotlin + stubs from eng-pad-server proto definitions +- [ ] 11.2: Implement `SyncClient` — gRPC client for eng-pad-server + (password auth over TLS, notebook/page/stroke push/pull) +- [ ] 11.3: Implement `SyncManager` — orchestrates sync operations, conflict + resolution (last-write-wins or manual), tracks sync state per notebook +- [ ] 11.4: Sync settings UI — server URL, credentials, TLS CA cert + configuration screen +- [ ] 11.5: Per-notebook sync button — manual sync trigger from editor or + page list toolbar +- [ ] 11.6: Sync-all — bulk sync from notebook list screen +- [ ] 11.7: Status indicators — sync state badges on notebook list items + (synced, pending, error) + +## Phase 12: Notebook Backup/Export + +- [ ] 12.1: Define zip format — `notebook_title.engpad.zip` with + `notebook.json` metadata and `pages/NNN.json` stroke data +- [ ] 12.2: Implement export — serialize notebook + pages + strokes to zip, + share via FileProvider + intents +- [ ] 12.3: Implement import — parse zip, create notebook + pages + strokes + in Room DB +- [ ] 12.4: Export-all — single zip containing all notebooks for full backup +- [ ] 12.5: UI integration — export/import buttons in notebook list and + editor toolbar