When the pen lifts, the in-progress path was cleared immediately but
the completed stroke didn't appear on the backing bitmap until the
async DB save completed — creating a visible flash where the stroke
vanished for several frames.
Fix: transfer the completed path to a "pending" overlay that stays
visible in the dynamic layer until addCompletedStroke() confirms the
stroke is on the backing bitmap. The stroke is now visible continuously
from pen-down through commit with no gap.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Creating a notebook now auto-navigates to its first page editor
(createNotebook returns ID via callback, triggers onNotebookClick)
- Double-buffered rendering: all layers composited to an off-screen
bitmap before presenting to the display in a single drawBitmap call.
Eliminates visible intermediate states (white flash between layers)
that caused flicker on e-ink displays.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SyncSettingsDialog: server URL, username, password fields
- Gear icon in library top bar opens sync settings
- "Sync to server" in notebook overflow menu
- "Sync all" button next to filter/sort bar
- Both check isSyncConfigured() and prompt for settings if needed
- Sync runs on Dispatchers.IO, shows Toast on success/error
- Sync settings stored in SharedPreferences via EngPadApp
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Restored canonical proto from eng-pad-server (PageData, StrokeData
message names, not Notebook/Page/Stroke)
- Added java_package + java_multiple_files options
- Renamed service to EngPadSyncService (buf STANDARD lint compliance)
- Simplified build.gradle.kts: removed broken custom GenerateProtoTask,
proto stubs generated via Makefile `make proto` and checked into git
- Generated stubs in app/src/main/java/gen/
- Fixed SyncClient/SyncManager to match canonical proto schema
- Updated dependency versions: protobuf 4.34.1, grpc 1.80.0, grpcKotlin 1.5.0
- Added buf.yaml with STANDARD lint rules and FILE breaking detection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- BackupExporter: exports notebook as .engpad.zip containing
notebook.json (metadata) and pages/NNN.json (strokes as JSON
float arrays). Uses Dispatchers.IO for file I/O.
- Added "Export backup" to notebook overflow menu in library
- PdfExporter.sanitize changed from private to internal for reuse
- Shares via FileProvider + ACTION_SEND with application/zip MIME type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- gRPC sync sends username+password in metadata on every RPC,
verified by unary interceptor. No login RPC or token management.
- Password stored in Android EncryptedSharedPreferences (Keystore)
- Web UI retains bearer token flow for browser sessions
- FIDO2/U2F scoped to web UI only, not gRPC sync path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- No grid in web view (writing aid only)
- No per-page share links for now (URL structure supports it later)
- No server-side deletion — server mirrors tablet exactly via sync
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers: gRPC sync API (full notebook push), REST API for web viewing,
SVG/JPG/PDF rendering, password + FIDO2/U2F auth via WebAuthn,
shareable links with optional expiry, Android app integration points.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Three-dot overflow button on each notebook card
- Dropdown menu with Rename and Delete options
- Rename dialog with title field (keyboard defaults to Title Case)
- Removed combinedClickable — tap opens notebook, menu for actions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Page selection fix:
- Pass selected page ID as a route parameter (editor/{notebookId}?pageId=X)
instead of savedStateHandle, which was unreliable
- Pop to notebook list then navigate to editor with explicit page ID
- Eliminates stale page selection bug
Library filter/sort:
- Text filter field filters notebooks by name (case-insensitive)
- Sort dropdown: Name, Last Edited, Last Opened
- Clicking the current sort field toggles asc/desc direction
- Defaults to Last Edited descending
- Shows "No matches" when filter has no results vs "No notebooks yet"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Swipe direction is determined by which edge the finger started on AND
the drag direction matching (right edge + swipe left = next, left edge
+ swipe right = prev). Both no-op at their respective boundaries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Edge swipe:
- Only detect from RIGHT edge (swipe left = next page)
- Left edge no longer triggers page nav (avoids conflict with system
back gesture)
- Previous page navigation via binder dropdown or system back only
Page list navigation:
- Simplified selectedPageId handling — read and clear synchronously
during recomposition instead of async StateFlow observation
- Fixes issue where clicking a page in view-all-pages went to last
visited page instead of the selected one
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Page list: Library button moved to actions (right side) with titleLarge
- Notebook list: title changed to "Engineering Pad :: Library"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- X button in top app bar navigates back to notebook list
- Swipe left gesture (>200px) on the page grid closes to notebook list
- Both clear the notebook and view-all-pages state in SharedPreferences
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CopyStrokesAction: use insertAll return value (List<Long>) to get
inserted IDs directly instead of fragile takeLast(n) re-fetch
- StrokeDao.insertAll now returns List<Long>
- Grid bitmap cached separately from stroke bitmap, redrawn only on
zoom/pan/resize changes. Eliminates per-frame grid line rendering
during active drawing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MoveStrokesAction.undo(): was offsetting by (0,0), now restores
original pre-move point data correctly
- Stroke commit flicker: new strokes render incrementally onto existing
backing bitmap instead of triggering full redraw
- Selection state leak: view-side selection cleared on page navigation
via onPageNavigated callback
- Initial page fallback: blank canvas prevented if initialPageId
not found in DB — falls back to first page
- Arrow head Paint: preallocated reusable object, no per-call allocation
- Updated PROGRESS.md with audit findings and backup design notes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drag reorder:
- Separated long-press drag from tap/menu: long-press-and-drag reorders,
tap opens page, overflow menu (three dots) for delete/export
- Fixed combinedClickable conflict that prevented drag from working
Page navigation:
- View-all-pages state persisted in SharedPreferences, restored on startup
- Cleared when navigating back to editor from page list
Delete renumbering:
- After deleting a page, remaining pages are renumbered sequentially
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The reorderable pages list was a plain mutableListOf which Compose
doesn't observe. Changed to mutableStateListOf so the grid recomposes
when pages are loaded.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drag-to-reorder:
- Added sh.calvin.reorderable library for LazyVerticalGrid drag support
- Long-press and drag page thumbnails to reorder
- Page numbers updated in DB on drop
- Visual feedback: slight scale + transparency while dragging
Page navigation fix:
- Clicking a page in "view all pages" now correctly opens that page
(key(pid) forces EditorScreen recreation with the selected page)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Long-press a page thumbnail to show context menu with Delete and
Export as JPG options
- Delete shows confirmation dialog
- Export renders page at 300 DPI and shares via intent
- FAB always creates new pages (no empty-page restriction in page list)
- Note: drag-to-reorder deferred — requires custom grid drag handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Flicker: removed View background and super.onDraw() call — canvas is
filled with white explicitly in onDraw before compositing the backing
bitmap, eliminating the clear-then-redraw flash.
Empty page: swiping right on the last page only creates a new page if
the current page has strokes. Empty pages don't spawn more empties.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Completed strokes are now rendered to a backing bitmap at the view's
pixel resolution. The bitmap is only redrawn when strokes change or
zoom/pan changes. During active drawing, onDraw just composites the
cached bitmap + the in-progress stroke, eliminating flicker from
re-rendering all paths every frame.
Unlike the earlier 1/4-resolution bitmap that caused blur, this one
uses full screen resolution with the view matrix applied, so strokes
remain pixel-perfect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Eraser: angled rectangle with dividing line (eraser tip)
- Select: dashed rectangle (marquee selection)
- Move: four-way arrow cross with arrowheads
All icons match the existing pen dot, line, and box icon style.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Pen sizes consolidated into single button with dot icon showing
current size; long-press for 0.38/0.5/0.6/0.7mm selection
- Line button shows thick line icon; long-press for style variants
- Box button shows rectangle outline icon
- Undo/redo replaced with curved arrow icons (gray when disabled)
- Added PenSize enum (MM_038, MM_050, MM_060, MM_070)
- Removed PEN_FINE/PEN_MEDIUM tools, replaced with single PEN tool
that uses the selected PenSize from CanvasState
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cut + paste: inserts at same position, selects the pasted strokes.
Copy + paste: offsets pasted strokes by ~10mm diagonally to distinguish
from originals, selects the new copies.
Both operations leave the pasted strokes selected so they can be
immediately moved, copied again, or deleted. After a cut+paste,
subsequent pastes behave as copy+paste (no repeated cuts).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Line styles:
- Store stroke style in DB (style column, migration v2->v3)
- Dashed lines rendered with DashPathEffect on the paint
- Arrow heads drawn dynamically from stored two-point line data
- Line tool stores just two endpoints; style determines rendering
- Removed buildLinePoints — arrow heads no longer baked into point data
Move tool:
- If there's an active selection, Move drags the entire selection
- Otherwise, hit-tests a single stroke and drags it
- Selection stays highlighted after move completes
Line dropdown:
- Removed conflicting onClick from Surface, combinedClickable now
correctly handles both tap (select tool) and long-press (style menu)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Line long-press dropdown: removed conflicting onClick from Surface,
combinedClickable now works correctly for long-press style menu
2. Move tool: tap a stroke and drag to reposition it, with undo support
and visual highlight during drag
3. Snap fix: timer now re-arms continuously — pausing at ANY point
during a stroke (not just near origin) triggers snap. Checks pen
stillness at the moment the timer fires (~3mm threshold)
4. EMR eraser: now checks BUTTON_STYLUS_PRIMARY in buttonState
(side button) in addition to TOOL_TYPE_ERASER
5. Notebook title: keyboard defaults to Title Case capitalization
6. Tool order in enum matches toolbar: pen, line, box, eraser, select, move
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Toolbar:
- Replace FilterChip with custom ToolButton (no ripple, instant response)
- Reorder tools: 0.38 0.5 Line Box Eraser Select
- DropdownMenus anchored to buttons via Box wrapper (drop below)
- Page indicator styled as OutlinedButton (visible affordance)
- Cut/Del/Copy/Paste operations (internal clipboard)
Line snap fix:
- Check pen position when timer fires instead of canceling on movement
- Handles EMR stylus micro-tremor correctly
EMR eraser button:
- TOOL_TYPE_ERASER events route to eraser handler regardless of active tool
- Physical eraser end of pen works as temporary eraser
App state:
- Startup navigates to last-visited notebook/page via SharedPreferences
- Closing notebook (X button) clears last notebook so app starts at list
- Updated app icon
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Navigation:
- Notebooks remember last page (last_page_id column, migration v1->v2)
- Opening a notebook goes directly to last page's editor
- Edge swipe (finger from screen edge) navigates prev/next page,
auto-adds pages at end, no-op on first page
- X button closes notebook back to list
- Binder dropdown: "View all pages" and "Go to page" dialog
- Page list shows stroke previews and page numbers below cards
Line tool:
- LINE tool: drag start-to-end for straight lines
- Long-press Line chip for style variants: plain, arrow, double arrow, dashed
- Arrow heads rendered as path segments, stored in stroke data
- Dashed lines use DashPathEffect
Export:
- JPG export at full 300 DPI resolution
- Export dropdown: PDF or JPG options
- Refactored PdfExporter with shared drawStrokes helper
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Updated DESIGN.md: rendering strategy (no backing bitmap, screen-space
grid, viewport clamping), added line snap and box tool documentation
- Updated PROGRESS.md: DC-1 testing results and fixes applied
- Updated CLAUDE.md: key conventions for rendering
- Fix toolbar overlap: use clipToBounds on canvas AndroidView to prevent
it from drawing over the toolbar area
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dynamic min zoom computed from page and view dimensions — page can
never be smaller than the visible area. Combined with pan clamping
from previous commit, the page always covers the full canvas with
no non-drawable area visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Grid:
- Draw grid in screen space with pixel-snapped positions, fixing
the uneven rectangles caused by sub-pixel positioning in canonical space
- 1px screen-space lines for uniform weight at any zoom level
Line snap:
- Track max distance from origin (not per-move), increase threshold
to 60pt (~5mm) to handle EMR stylus hand tremor
- Snap timer stays active until pen moves significantly from start
Viewport:
- Clamp pan so page always covers the full viewport
- Background changed to white (no dark gray borders)
- Page centers when smaller than viewport (e.g., zoomed out landscape)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rendering fixes for e-ink displays:
- Remove backing bitmap (was 1/4 resolution, caused blurry strokes)
- Draw strokes directly as paths for pixel-perfect rendering
- Disable anti-aliasing on stroke and grid paint (no edge fading)
- Grid: darker color (rgb 180,180,180), 2pt stroke width for crispness
New features:
- Line snap: hold pen still for 1.5s to snap to straight line from origin
- Box tool: draw rectangles by dragging corner to corner
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- High-contrast theme with expanded color palette for e-ink readability
- Updated source trees in DESIGN.md
- Updated PROJECT_PLAN.md and PROGRESS.md — all phases complete
(only on-device performance profiling remains)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PdfExporter: creates PdfDocument, scales 300 DPI canonical coords to
72 DPI PDF points, renders strokes without grid
- Share via Intent.ACTION_SEND + FileProvider
- PDF button in editor toolbar exports current page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- UndoableAction interface with AddStrokeAction and DeleteStrokeAction
- UndoManager: undo/redo stacks (depth 50), canUndo/canRedo StateFlows
- EditorViewModel: stroke operations routed through UndoManager,
visual callbacks sync canvas view without full DB reload
- Toolbar: undo/redo buttons with enabled state
- 9 unit tests for UndoManager
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Eraser mode: stylus touch/drag hit-tests against stroke bounding boxes
expanded by 42pt radius (~3.5mm), processes historical points
- Deletes hit strokes from canvas view and Room DB
- Backing bitmap rebuilt automatically on stroke removal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>