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) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 20:49:24 -07:00
parent b8fb85c5f0
commit 6e5c500786
4 changed files with 304 additions and 115 deletions

218
DESIGN.md
View File

@@ -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