Rotation Support (#77)

•  What is the goal of this PR?  
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.

•  What changes are included?
◦  Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦  Settings / Configuration
▪  Extended CrossPointSettings with:
▪  landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪  Updated SettingsActivity to expose two new toggles:
▪  “Landscape Reading”
▪  “Flip Landscape (swap top/bottom)”
◦  EPUB Reader
▪  In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦  EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪  Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.



Additional Context

•  Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
•  Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
•  Testing suggestions / areas to focus on
◦  Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪  Landscape reading in both directions:
▪  Landscape Reading = ON, Flip Landscape = OFF.
▪  Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦  Open the same book:
▪  In portrait first, then switch to landscape and reopen it.
▪  Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
Tannay
2025-12-28 05:33:20 -05:00
committed by GitHub
parent bf031fd999
commit dd280bdc97
22 changed files with 297 additions and 139 deletions

View File

@@ -4,6 +4,37 @@
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const {
switch (orientation) {
case Portrait: {
// Logical portrait (480x800) → panel (800x480)
// Rotation: 90 degrees clockwise
*rotatedX = y;
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
break;
}
case LandscapeClockwise: {
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x;
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y;
break;
}
case PortraitInverted: {
// Logical portrait (480x800) → panel (800x480)
// Rotation: 90 degrees counter-clockwise
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y;
*rotatedY = x;
break;
}
case LandscapeCounterClockwise: {
// Logical landscape (800x480) aligned with panel orientation
*rotatedX = x;
*rotatedY = y;
break;
}
}
}
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
@@ -13,15 +44,14 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
return;
}
// Rotate coordinates: portrait (480x800) -> landscape (800x480)
// Rotation: 90 degrees clockwise
const int rotatedX = y;
const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
int rotatedX = 0;
int rotatedY = 0;
rotateCoordinates(x, y, &rotatedX, &rotatedY);
// Bounds checking (portrait: 480x800)
// Bounds checking against physical panel dimensions
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d)\n", millis(), x, y);
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
return;
}
@@ -115,8 +145,11 @@ void GfxRenderer::fillRect(const int x, const int y, const int width, const int
}
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
// Flip X and Y for portrait mode
einkDisplay.drawImage(bitmap, y, x, height, width);
// TODO: Rotate bits
int rotatedX = 0;
int rotatedY = 0;
rotateCoordinates(x, y, &rotatedX, &rotatedY);
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height);
}
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
@@ -205,23 +238,34 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons
einkDisplay.displayBuffer(refreshMode);
}
void GfxRenderer::displayWindow(const int x, const int y, const int width, const int height) const {
// Rotate coordinates from portrait (480x800) to landscape (800x480)
// Rotation: 90 degrees clockwise
// Portrait coordinates: (x, y) with dimensions (width, height)
// Landscape coordinates: (rotatedX, rotatedY) with dimensions (rotatedWidth, rotatedHeight)
const int rotatedX = y;
const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x - width + 1;
const int rotatedWidth = height;
const int rotatedHeight = width;
einkDisplay.displayWindow(rotatedX, rotatedY, rotatedWidth, rotatedHeight);
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
int GfxRenderer::getScreenWidth() const {
switch (orientation) {
case Portrait:
case PortraitInverted:
// 480px wide in portrait logical coordinates
return EInkDisplay::DISPLAY_HEIGHT;
case LandscapeClockwise:
case LandscapeCounterClockwise:
// 800px wide in landscape logical coordinates
return EInkDisplay::DISPLAY_WIDTH;
}
return EInkDisplay::DISPLAY_HEIGHT;
}
// Note: Internal driver treats screen in command orientation, this library treats in portrait orientation
int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; }
int GfxRenderer::getScreenHeight() { return EInkDisplay::DISPLAY_WIDTH; }
int GfxRenderer::getScreenHeight() const {
switch (orientation) {
case Portrait:
case PortraitInverted:
// 800px tall in portrait logical coordinates
return EInkDisplay::DISPLAY_WIDTH;
case LandscapeClockwise:
case LandscapeCounterClockwise:
// 480px tall in landscape logical coordinates
return EInkDisplay::DISPLAY_HEIGHT;
}
return EInkDisplay::DISPLAY_WIDTH;
}
int GfxRenderer::getSpaceWidth(const int fontId) const {
if (fontMap.count(fontId) == 0) {
@@ -432,3 +476,32 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
*x += glyph->advanceX;
}
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
switch (orientation) {
case Portrait:
*outTop = VIEWABLE_MARGIN_TOP;
*outRight = VIEWABLE_MARGIN_RIGHT;
*outBottom = VIEWABLE_MARGIN_BOTTOM;
*outLeft = VIEWABLE_MARGIN_LEFT;
break;
case LandscapeClockwise:
*outTop = VIEWABLE_MARGIN_LEFT;
*outRight = VIEWABLE_MARGIN_TOP;
*outBottom = VIEWABLE_MARGIN_RIGHT;
*outLeft = VIEWABLE_MARGIN_BOTTOM;
break;
case PortraitInverted:
*outTop = VIEWABLE_MARGIN_BOTTOM;
*outRight = VIEWABLE_MARGIN_LEFT;
*outBottom = VIEWABLE_MARGIN_TOP;
*outLeft = VIEWABLE_MARGIN_RIGHT;
break;
case LandscapeCounterClockwise:
*outTop = VIEWABLE_MARGIN_RIGHT;
*outRight = VIEWABLE_MARGIN_BOTTOM;
*outBottom = VIEWABLE_MARGIN_LEFT;
*outLeft = VIEWABLE_MARGIN_TOP;
break;
}
}