Rotation Support
This commit is contained in:
parent
cfe838e03b
commit
32d747c6da
@ -10,8 +10,8 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 5;
|
||||
}
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 6;
|
||||
} // namespace
|
||||
|
||||
void Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
||||
@ -27,7 +27,8 @@ void Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
|
||||
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
||||
const int marginRight, const int marginBottom, const int marginLeft,
|
||||
const bool extraParagraphSpacing) const {
|
||||
const bool extraParagraphSpacing, const int screenWidth,
|
||||
const int screenHeight) const {
|
||||
std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str());
|
||||
serialization::writePod(outputFile, SECTION_FILE_VERSION);
|
||||
serialization::writePod(outputFile, fontId);
|
||||
@ -37,13 +38,15 @@ void Section::writeCacheMetadata(const int fontId, const float lineCompression,
|
||||
serialization::writePod(outputFile, marginBottom);
|
||||
serialization::writePod(outputFile, marginLeft);
|
||||
serialization::writePod(outputFile, extraParagraphSpacing);
|
||||
serialization::writePod(outputFile, screenWidth);
|
||||
serialization::writePod(outputFile, screenHeight);
|
||||
serialization::writePod(outputFile, pageCount);
|
||||
outputFile.close();
|
||||
}
|
||||
|
||||
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
||||
const int marginRight, const int marginBottom, const int marginLeft,
|
||||
const bool extraParagraphSpacing) {
|
||||
const bool extraParagraphSpacing, const int screenWidth, const int screenHeight) {
|
||||
if (!SD.exists(cachePath.c_str())) {
|
||||
return false;
|
||||
}
|
||||
@ -69,6 +72,7 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c
|
||||
int fileFontId, fileMarginTop, fileMarginRight, fileMarginBottom, fileMarginLeft;
|
||||
float fileLineCompression;
|
||||
bool fileExtraParagraphSpacing;
|
||||
int fileScreenWidth, fileScreenHeight;
|
||||
serialization::readPod(inputFile, fileFontId);
|
||||
serialization::readPod(inputFile, fileLineCompression);
|
||||
serialization::readPod(inputFile, fileMarginTop);
|
||||
@ -76,10 +80,13 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c
|
||||
serialization::readPod(inputFile, fileMarginBottom);
|
||||
serialization::readPod(inputFile, fileMarginLeft);
|
||||
serialization::readPod(inputFile, fileExtraParagraphSpacing);
|
||||
serialization::readPod(inputFile, fileScreenWidth);
|
||||
serialization::readPod(inputFile, fileScreenHeight);
|
||||
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
|
||||
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing) {
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || screenWidth != fileScreenWidth ||
|
||||
screenHeight != fileScreenHeight) {
|
||||
inputFile.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
||||
clearCache();
|
||||
@ -116,7 +123,7 @@ bool Section::clearCache() const {
|
||||
|
||||
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
||||
const int marginRight, const int marginBottom, const int marginLeft,
|
||||
const bool extraParagraphSpacing) {
|
||||
const bool extraParagraphSpacing, const int screenWidth, const int screenHeight) {
|
||||
const auto localPath = epub->getSpineItem(spineIndex);
|
||||
|
||||
// TODO: Should we get rid of this file all together?
|
||||
@ -147,7 +154,8 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
|
||||
return false;
|
||||
}
|
||||
|
||||
writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, extraParagraphSpacing);
|
||||
writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, extraParagraphSpacing,
|
||||
screenWidth, screenHeight);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ class Section {
|
||||
std::string cachePath;
|
||||
|
||||
void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||
int marginLeft, bool extraParagraphSpacing) const;
|
||||
int marginLeft, bool extraParagraphSpacing, int screenWidth, int screenHeight) const;
|
||||
void onPageComplete(std::unique_ptr<Page> page);
|
||||
|
||||
public:
|
||||
@ -26,10 +26,10 @@ class Section {
|
||||
}
|
||||
~Section() = default;
|
||||
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||
int marginLeft, bool extraParagraphSpacing);
|
||||
int marginLeft, bool extraParagraphSpacing, int screenWidth, int screenHeight);
|
||||
void setupCacheDir() const;
|
||||
bool clearCache() const;
|
||||
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||
int marginLeft, bool extraParagraphSpacing);
|
||||
int marginLeft, bool extraParagraphSpacing, int screenWidth, int screenHeight);
|
||||
std::unique_ptr<Page> loadPageFromSD() const;
|
||||
};
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
// Default to portrait orientation for all callers
|
||||
GfxRenderer::Orientation GfxRenderer::orientation = GfxRenderer::Orientation::Portrait;
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
|
||||
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
@ -13,15 +16,35 @@ 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;
|
||||
|
||||
// Bounds checking (portrait: 480x800)
|
||||
switch (orientation) {
|
||||
case Orientation::Portrait: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees clockwise
|
||||
rotatedX = y;
|
||||
rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||
break;
|
||||
}
|
||||
case Orientation::LandscapeNormal: {
|
||||
// Logical landscape (800x480) aligned with panel orientation
|
||||
rotatedX = x;
|
||||
rotatedY = y;
|
||||
break;
|
||||
}
|
||||
case Orientation::LandscapeFlipped: {
|
||||
// Logical landscape (800x480) rotated 180° (swap top/bottom and left/right)
|
||||
rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x;
|
||||
rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +138,17 @@ 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 {
|
||||
switch (orientation) {
|
||||
case Orientation::Portrait:
|
||||
// Flip X and Y for portrait mode
|
||||
einkDisplay.drawImage(bitmap, y, x, height, width);
|
||||
break;
|
||||
case Orientation::LandscapeNormal:
|
||||
case Orientation::LandscapeFlipped:
|
||||
// Native landscape coordinates
|
||||
einkDisplay.drawImage(bitmap, x, y, width, height);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
|
||||
@ -193,6 +225,8 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons
|
||||
}
|
||||
|
||||
void GfxRenderer::displayWindow(const int x, const int y, const int width, const int height) const {
|
||||
switch (orientation) {
|
||||
case Orientation::Portrait: {
|
||||
// Rotate coordinates from portrait (480x800) to landscape (800x480)
|
||||
// Rotation: 90 degrees clockwise
|
||||
// Portrait coordinates: (x, y) with dimensions (width, height)
|
||||
@ -204,11 +238,42 @@ void GfxRenderer::displayWindow(const int x, const int y, const int width, const
|
||||
const int rotatedHeight = width;
|
||||
|
||||
einkDisplay.displayWindow(rotatedX, rotatedY, rotatedWidth, rotatedHeight);
|
||||
break;
|
||||
}
|
||||
case Orientation::LandscapeNormal:
|
||||
case Orientation::LandscapeFlipped:
|
||||
// Native landscape coordinates
|
||||
einkDisplay.displayWindow(x, y, width, height);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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; }
|
||||
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
||||
int GfxRenderer::getScreenWidth() {
|
||||
switch (orientation) {
|
||||
case Orientation::Portrait:
|
||||
// 480px wide in portrait logical coordinates
|
||||
return EInkDisplay::DISPLAY_HEIGHT;
|
||||
case Orientation::LandscapeNormal:
|
||||
case Orientation::LandscapeFlipped:
|
||||
// 800px wide in landscape logical coordinates
|
||||
return EInkDisplay::DISPLAY_WIDTH;
|
||||
}
|
||||
return EInkDisplay::DISPLAY_HEIGHT;
|
||||
}
|
||||
|
||||
int GfxRenderer::getScreenHeight() {
|
||||
switch (orientation) {
|
||||
case Orientation::Portrait:
|
||||
// 800px tall in portrait logical coordinates
|
||||
return EInkDisplay::DISPLAY_WIDTH;
|
||||
case Orientation::LandscapeNormal:
|
||||
case Orientation::LandscapeFlipped:
|
||||
// 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) {
|
||||
|
||||
@ -12,12 +12,22 @@ class GfxRenderer {
|
||||
public:
|
||||
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
||||
|
||||
// Logical screen orientation from the perspective of callers
|
||||
enum class Orientation {
|
||||
Portrait, // 480x800 logical coordinates (current default)
|
||||
LandscapeNormal, // 800x480 logical coordinates, native panel orientation
|
||||
LandscapeFlipped // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
||||
};
|
||||
|
||||
private:
|
||||
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
||||
static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
||||
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE,
|
||||
"BW buffer chunking does not line up with display buffer size");
|
||||
|
||||
// Global orientation used for all rendering operations
|
||||
static Orientation orientation;
|
||||
|
||||
EInkDisplay& einkDisplay;
|
||||
RenderMode renderMode;
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
@ -33,11 +43,15 @@ class GfxRenderer {
|
||||
// Setup
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
|
||||
// Orientation control (affects logical width/height and coordinate transforms)
|
||||
static void setOrientation(Orientation o) { orientation = o; }
|
||||
static Orientation getOrientation() { return orientation; }
|
||||
|
||||
// Screen ops
|
||||
static int getScreenWidth();
|
||||
static int getScreenHeight();
|
||||
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
|
||||
// EXPERIMENTAL: Windowed update - display only a rectangular region (portrait coordinates)
|
||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||
void displayWindow(int x, int y, int width, int height) const;
|
||||
void invertScreen() const;
|
||||
void clearScreen(uint8_t color = 0xFF) const;
|
||||
|
||||
@ -12,7 +12,8 @@ CrossPointSettings CrossPointSettings::instance;
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||
constexpr uint8_t SETTINGS_COUNT = 3;
|
||||
// Increment this when adding new persisted settings fields
|
||||
constexpr uint8_t SETTINGS_COUNT = 5;
|
||||
constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin";
|
||||
} // namespace
|
||||
|
||||
@ -26,6 +27,8 @@ bool CrossPointSettings::saveToFile() const {
|
||||
serialization::writePod(outputFile, whiteSleepScreen);
|
||||
serialization::writePod(outputFile, extraParagraphSpacing);
|
||||
serialization::writePod(outputFile, shortPwrBtn);
|
||||
serialization::writePod(outputFile, landscapeReading);
|
||||
serialization::writePod(outputFile, landscapeFlipped);
|
||||
outputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||
@ -51,7 +54,7 @@ bool CrossPointSettings::loadFromFile() {
|
||||
uint8_t fileSettingsCount = 0;
|
||||
serialization::readPod(inputFile, fileSettingsCount);
|
||||
|
||||
// load settings that exist
|
||||
// load settings that exist (support older files with fewer fields)
|
||||
uint8_t settingsRead = 0;
|
||||
do {
|
||||
serialization::readPod(inputFile, whiteSleepScreen);
|
||||
@ -60,6 +63,10 @@ bool CrossPointSettings::loadFromFile() {
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, shortPwrBtn);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, landscapeReading);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, landscapeFlipped);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
} while (false);
|
||||
|
||||
inputFile.close();
|
||||
|
||||
@ -21,6 +21,11 @@ class CrossPointSettings {
|
||||
uint8_t extraParagraphSpacing = 1;
|
||||
// Duration of the power button press
|
||||
uint8_t shortPwrBtn = 0;
|
||||
// EPUB reading orientation settings
|
||||
// 0 = portrait (default), 1 = landscape
|
||||
uint8_t landscapeReading = 0;
|
||||
// When in landscape mode: 0 = normal, 1 = flipped (swap top/bottom)
|
||||
uint8_t landscapeFlipped = 0;
|
||||
|
||||
~CrossPointSettings() = default;
|
||||
|
||||
|
||||
@ -29,6 +29,17 @@ void EpubReaderActivity::onEnter() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure screen orientation based on settings
|
||||
if (SETTINGS.landscapeReading) {
|
||||
if (SETTINGS.landscapeFlipped) {
|
||||
GfxRenderer::setOrientation(GfxRenderer::Orientation::LandscapeFlipped);
|
||||
} else {
|
||||
GfxRenderer::setOrientation(GfxRenderer::Orientation::LandscapeNormal);
|
||||
}
|
||||
} else {
|
||||
GfxRenderer::setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
}
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
epub->setupCacheDir();
|
||||
@ -56,6 +67,9 @@ void EpubReaderActivity::onEnter() {
|
||||
}
|
||||
|
||||
void EpubReaderActivity::onExit() {
|
||||
// Reset orientation back to portrait for the rest of the UI
|
||||
GfxRenderer::setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
@ -208,7 +222,8 @@ void EpubReaderActivity::renderScreen() {
|
||||
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
||||
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
|
||||
SETTINGS.extraParagraphSpacing)) {
|
||||
SETTINGS.extraParagraphSpacing, GfxRenderer::getScreenWidth(),
|
||||
GfxRenderer::getScreenHeight())) {
|
||||
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
||||
|
||||
{
|
||||
@ -227,7 +242,8 @@ void EpubReaderActivity::renderScreen() {
|
||||
|
||||
section->setupCacheDir();
|
||||
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
|
||||
marginLeft, SETTINGS.extraParagraphSpacing)) {
|
||||
marginLeft, SETTINGS.extraParagraphSpacing, GfxRenderer::getScreenWidth(),
|
||||
GfxRenderer::getScreenHeight())) {
|
||||
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
||||
section.reset();
|
||||
return;
|
||||
@ -322,7 +338,9 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page) {
|
||||
}
|
||||
|
||||
void EpubReaderActivity::renderStatusBar() const {
|
||||
constexpr auto textY = 776;
|
||||
// Position status bar near the bottom of the logical screen, regardless of orientation
|
||||
const auto screenHeight = GfxRenderer::getScreenHeight();
|
||||
const auto textY = screenHeight - 24;
|
||||
|
||||
// Calculate progress in book
|
||||
float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
|
||||
@ -345,7 +363,7 @@ void EpubReaderActivity::renderStatusBar() const {
|
||||
constexpr int batteryWidth = 15;
|
||||
constexpr int batteryHeight = 10;
|
||||
constexpr int x = marginLeft;
|
||||
constexpr int y = 783;
|
||||
const int y = screenHeight - 17;
|
||||
|
||||
// Top line
|
||||
renderer.drawLine(x, y, x + batteryWidth - 4, y);
|
||||
|
||||
@ -10,7 +10,9 @@
|
||||
const SettingInfo SettingsActivity::settingsList[settingsCount] = {
|
||||
{"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen},
|
||||
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing},
|
||||
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn}};
|
||||
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn},
|
||||
{"Landscape Reading", SettingType::TOGGLE, &CrossPointSettings::landscapeReading},
|
||||
{"Flip Landscape (swap top/bottom)", SettingType::TOGGLE, &CrossPointSettings::landscapeFlipped}};
|
||||
|
||||
void SettingsActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<SettingsActivity*>(param);
|
||||
|
||||
@ -29,7 +29,7 @@ class SettingsActivity final : public Activity {
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
// Static settings list
|
||||
static constexpr int settingsCount = 3; // Number of settings
|
||||
static constexpr int settingsCount = 5; // Number of settings
|
||||
static const SettingInfo settingsList[settingsCount];
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user