Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions libs/hardware/InputManager/include/InputManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ class InputManager {
// contact centroids, normalized to 0..1. Pinches and sub-threshold turns are
// rejected, and an accepted rotation cannot also become a translation.
bool wasMultiTouchRotation(float& degrees, float& nxCenter, float& nyCenter, unsigned long& durationMs) const;
// One-shot two-contact pinch/spread on release. `scale` is end separation
// divided by start separation (<1.0 = pinch in / zoom out, >1.0 = spread /
// zoom in). The center is the average of the start/end contact centroids,
// normalized to 0..1. Rotations, tiny scale changes, and ambiguous contacts
// are rejected, and an accepted pinch cannot also become a translation.
bool wasMultiTouchPinch(float& scale, float& nxCenter, float& nyCenter, unsigned long& durationMs) const;
// One-shot long-press: fires WHILE the finger is still down, once a
// stationary contact (within tap slop) has been held TOUCH_LONG_PRESS_MS.
// Position is the touch-down point, normalized like wasTouchTap. Fires at
Expand Down Expand Up @@ -241,6 +247,10 @@ class InputManager {
// wasMultiTouchRotation().
bool popMultiTouchRotation(float& degrees, float& nxCenter, float& nyCenter, unsigned long& durationMs);

// Pop a queued completed two-contact pinch/spread. Values use the same scale,
// normalized center, and duration contract as wasMultiTouchPinch().
bool popMultiTouchPinch(float& scale, float& nxCenter, float& nyCenter, unsigned long& durationMs);

// --- Diagnostics -----------------------------------------------------------
// A live sample of one button-group ADC pin: the raw reading plus the BTN_*
// it currently classifies as (-1 = no band matched). On the Xteink ADC ladder
Expand Down Expand Up @@ -282,6 +292,13 @@ class InputManager {
uint16_t durationMs;
};
QueueHandle_t _asyncMultiTouchRotationQueue = nullptr;
struct QueuedMultiTouchPinch {
float scale;
uint16_t centerX;
uint16_t centerY;
uint16_t durationMs;
};
QueueHandle_t _asyncMultiTouchPinchQueue = nullptr;
TaskHandle_t _asyncTask = nullptr;
uint32_t _asyncPollMs = 15;
static void asyncTaskTrampoline(void* self);
Expand Down Expand Up @@ -341,6 +358,7 @@ class InputManager {
bool hasEligibleRotationScale() const;
bool isMultiTouchTranslation(unsigned long now) const;
bool classifyMultiTouchRotation(unsigned long now);
bool classifyMultiTouchPinch(unsigned long now);
void normalizeTouchPoint(uint16_t x, uint16_t y, float& nx, float& ny) const;

uint8_t currentState;
Expand Down Expand Up @@ -397,6 +415,11 @@ class InputManager {
uint16_t multiTouchRotationCenterX = 0;
uint16_t multiTouchRotationCenterY = 0;
uint16_t multiTouchRotationDurationMs = 0;
bool multiTouchPinchEvent = false;
float multiTouchPinchScale = 1.0f;
uint16_t multiTouchPinchCenterX = 0;
uint16_t multiTouchPinchCenterY = 0;
uint16_t multiTouchPinchDurationMs = 0;
TouchPoint touchDownPoint = {false, 0, 0, 0}; // first sample of the current contact (tap routing)
TouchPoint touchUpPoint = {false, 0, 0, 0}; // last sample before release (swipe routing)
unsigned long lastTouchHeldDurationMs = 0; // contact duration, latched at release
Expand Down
62 changes: 62 additions & 0 deletions libs/hardware/InputManager/src/InputManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ void InputManager::beginAsync(const uint8_t taskPriority, const uint32_t pollMs,
_asyncSwipeQueue = xQueueCreate(queueLen, sizeof(float) * 4);
_asyncMultiTouchSwipeQueue = xQueueCreate(queueLen, sizeof(QueuedMultiTouchSwipe));
_asyncMultiTouchRotationQueue = xQueueCreate(queueLen, sizeof(QueuedMultiTouchRotation));
_asyncMultiTouchPinchQueue = xQueueCreate(queueLen, sizeof(QueuedMultiTouchPinch));
xTaskCreate(asyncTaskTrampoline, "fi_input", 4096, this, taskPriority, &_asyncTask);
}

Expand Down Expand Up @@ -260,6 +261,11 @@ void InputManager::asyncPoll() {
multiTouchRotationCenterY, multiTouchRotationDurationMs};
xQueueSend(_asyncMultiTouchRotationQueue, &rotation, 0);
}
if (_asyncMultiTouchPinchQueue && multiTouchPinchEvent && !touchSuppressed) {
const QueuedMultiTouchPinch pinch = {multiTouchPinchScale, multiTouchPinchCenterX, multiTouchPinchCenterY,
multiTouchPinchDurationMs};
xQueueSend(_asyncMultiTouchPinchQueue, &pinch, 0);
}
vTaskDelay(pdMS_TO_TICKS(_asyncPollMs));
}
}
Expand Down Expand Up @@ -311,6 +317,16 @@ bool InputManager::popMultiTouchRotation(float& degrees, float& nxCenter, float&
return true;
}

bool InputManager::popMultiTouchPinch(float& scale, float& nxCenter, float& nyCenter, unsigned long& durationMs) {
if (!_asyncMultiTouchPinchQueue) return false;
QueuedMultiTouchPinch pinch{};
if (xQueueReceive(_asyncMultiTouchPinchQueue, &pinch, 0) != pdTRUE) return false;
scale = pinch.scale;
normalizeTouchPoint(pinch.centerX, pinch.centerY, nxCenter, nyCenter);
durationMs = pinch.durationMs;
return true;
}

bool InputManager::isDigitalPressed(const int8_t pin) const { return pin >= 0 && digitalRead(pin) == LOW; }

uint8_t InputManager::getDigitalState() const {
Expand Down Expand Up @@ -507,6 +523,7 @@ void InputManager::update() {
touchLongPressEvent = false;
multiTouchSwipeEvent = false;
multiTouchRotationEvent = false;
multiTouchPinchEvent = false;
touchHomeKeyEvent = false;
touchHomeKeyTapEvent = false;
touchHomeKeyLongEvent = false;
Expand Down Expand Up @@ -782,6 +799,22 @@ bool InputManager::wasMultiTouchRotation(float& degrees, float& nxCenter, float&
#endif
}

bool InputManager::wasMultiTouchPinch(float& scale, float& nxCenter, float& nyCenter, unsigned long& durationMs) const {
#if FREEINK_CAP_TOUCH
if (!multiTouchPinchEvent || touchSuppressed) return false;
scale = multiTouchPinchScale;
normalizeTouchPoint(multiTouchPinchCenterX, multiTouchPinchCenterY, nxCenter, nyCenter);
durationMs = multiTouchPinchDurationMs;
return true;
#else
(void)scale;
(void)nxCenter;
(void)nyCenter;
(void)durationMs;
return false;
#endif
}

bool InputManager::wasTouchLongPress(float& nx, float& ny) const {
#if FREEINK_CAP_TOUCH
if (!touchLongPressEvent || touchMultiContactSequence) return false;
Expand All @@ -803,6 +836,7 @@ void InputManager::suppressTouchContact() {
cancelMultiTouchGesture();
if (_asyncMultiTouchSwipeQueue) xQueueReset(_asyncMultiTouchSwipeQueue);
if (_asyncMultiTouchRotationQueue) xQueueReset(_asyncMultiTouchRotationQueue);
if (_asyncMultiTouchPinchQueue) xQueueReset(_asyncMultiTouchPinchQueue);
#endif
}

Expand Down Expand Up @@ -860,6 +894,7 @@ void InputManager::resetMultiTouchGesture() {
void InputManager::cancelMultiTouchGesture() {
multiTouchSwipeEvent = false;
multiTouchRotationEvent = false;
multiTouchPinchEvent = false;
multiTouchGestureState =
(touchPressed || touchReleasedEvent) ? MultiTouchGestureState::Blocked : MultiTouchGestureState::Idle;
}
Expand Down Expand Up @@ -1064,11 +1099,38 @@ bool InputManager::classifyMultiTouchRotation(const unsigned long now) {
return true;
}

bool InputManager::classifyMultiTouchPinch(const unsigned long now) {
if (trackedTouchContactCount != 2 || now - multiTouchContacts[0].start.timestamp > TOUCH_MULTI_SWIPE_MAX_MS) {
return false;
}

const auto toGesturePoint = [](const TouchPoint& point) {
return freeink::input_detail::GesturePoint{point.x, point.y};
};
freeink::input_detail::PinchResult result;
if (!freeink::input_detail::classifyPinch(
toGesturePoint(multiTouchContacts[0].start), toGesturePoint(multiTouchContacts[1].start),
toGesturePoint(multiTouchContacts[0].last), toGesturePoint(multiTouchContacts[1].last), result)) {
return false;
}

multiTouchPinchScale = result.scale;
multiTouchPinchCenterX = result.centerX;
multiTouchPinchCenterY = result.centerY;
multiTouchPinchDurationMs = static_cast<uint16_t>(now - multiTouchContacts[0].start.timestamp);
multiTouchPinchEvent = true;
return true;
}

void InputManager::finishMultiTouchGesture(const unsigned long now) {
if (classifyMultiTouchRotation(now)) {
multiTouchGestureState = MultiTouchGestureState::Blocked;
return;
}
if (classifyMultiTouchPinch(now)) {
multiTouchGestureState = MultiTouchGestureState::Blocked;
return;
}
if (isMultiTouchTranslation(now)) {
uint32_t startX = 0;
uint32_t startY = 0;
Expand Down
43 changes: 43 additions & 0 deletions libs/hardware/InputManager/src/MultiTouchGestureMath.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ struct RotationResult {
uint16_t centerY = 0;
};

struct PinchResult {
float scale = 1.0f;
uint16_t centerX = 0;
uint16_t centerY = 0;
};

inline int64_t separationSquared(const GesturePoint& first, const GesturePoint& second) {
const int64_t dx = static_cast<int64_t>(second.x) - first.x;
const int64_t dy = static_cast<int64_t>(second.y) - first.y;
Expand Down Expand Up @@ -65,4 +71,41 @@ inline bool classifyRotation(const GesturePoint& startFirst, const GesturePoint&
return true;
}

inline bool classifyPinch(const GesturePoint& startFirst, const GesturePoint& startSecond, const GesturePoint& endFirst,
const GesturePoint& endSecond, PinchResult& result) {
constexpr int64_t minSeparationPxSq = 60LL * 60LL;
constexpr int64_t scalePercentSq = 100LL * 100LL;
constexpr int64_t minPinchPercentSq = 80LL * 80LL;
constexpr int64_t maxPinchPercentSq = 120LL * 120LL;
constexpr float maxRotationDegrees = 15.0f;
constexpr double radiansToDegrees = 57.295779513082320876;

const int64_t startDx = static_cast<int64_t>(startSecond.x) - startFirst.x;
const int64_t startDy = static_cast<int64_t>(startSecond.y) - startFirst.y;
const int64_t endDx = static_cast<int64_t>(endSecond.x) - endFirst.x;
const int64_t endDy = static_cast<int64_t>(endSecond.y) - endFirst.y;
const int64_t startSeparationSq = separationSquared(startFirst, startSecond);
const int64_t endSeparationSq = separationSquared(endFirst, endSecond);

if (startSeparationSq < minSeparationPxSq || endSeparationSq < minSeparationPxSq) return false;

const bool pinchedIn = endSeparationSq * scalePercentSq <= startSeparationSq * minPinchPercentSq;
const bool pinchedOut = endSeparationSq * scalePercentSq >= startSeparationSq * maxPinchPercentSq;
if (!pinchedIn && !pinchedOut) return false;

const int64_t cross = startDx * endDy - startDy * endDx;
const int64_t dot = startDx * endDx + startDy * endDy;
const float degrees =
static_cast<float>(std::atan2(static_cast<double>(cross), static_cast<double>(dot)) * radiansToDegrees);
if (std::fabs(degrees) > maxRotationDegrees) return false;

result.scale =
static_cast<float>(std::sqrt(static_cast<double>(endSeparationSq) / static_cast<double>(startSeparationSq)));
result.centerX =
static_cast<uint16_t>((static_cast<int64_t>(startFirst.x) + startSecond.x + endFirst.x + endSecond.x) / 4);
result.centerY =
static_cast<uint16_t>((static_cast<int64_t>(startFirst.y) + startSecond.y + endFirst.y + endSecond.y) / 4);
return true;
}

} // namespace freeink::input_detail
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

namespace {

using freeink::input_detail::classifyPinch;
using freeink::input_detail::classifyRotation;
using freeink::input_detail::GesturePoint;
using freeink::input_detail::hasRotationScale;
using freeink::input_detail::PinchResult;
using freeink::input_detail::RotationResult;

int checksRun = 0;
Expand Down Expand Up @@ -79,6 +81,54 @@ void testRotationWinsWithTranslation() {
CHECK(result.centerY == 50);
}

void testPinchInAndOut() {
PinchResult result;
CHECK(classifyPinch({0, 0}, {100, 0}, {10, 0}, {70, 0}, result));
CHECK(near(result.scale, 0.6f, 0.01f));
CHECK(result.centerX == 45);
CHECK(result.centerY == 0);

CHECK(classifyPinch({0, 0}, {100, 0}, {-20, 0}, {140, 0}, result));
CHECK(near(result.scale, 1.6f, 0.01f));
CHECK(result.centerX == 55);
}

void testPinchRejectionThresholds() {
PinchResult result;
CHECK(!classifyPinch({0, 0}, {100, 0}, {5, 0}, {95, 0}, result)); // Small scale change.
CHECK(!classifyPinch({0, 0}, {50, 0}, {0, 0}, {90, 0}, result)); // Start span below 60 px.
CHECK(!classifyPinch({0, 0}, {100, 0}, {50, -50}, {50, 90}, result)); // Rotation too large.
CHECK(!classifyPinch({0, 0}, {100, 0}, {60, 80}, {160, 80}, result)); // Translation only.
}

// The two classifiers are gated on the same separation band from opposite
// sides, so no gesture can be accepted by both. Each of these is accepted by
// one and must be rejected by the other.
void testPinchAndRotationAreMutuallyExclusive() {
RotationResult rotation;
PinchResult pinch;

// A pure 90-degree turn holds the separation, so the pinch gate rejects it.
CHECK(classifyRotation({0, 0}, {100, 0}, {50, -50}, {50, 50}, rotation));
CHECK(!classifyPinch({0, 0}, {100, 0}, {50, -50}, {50, 50}, pinch));

// A pure 40% close leaves the band, so the rotation gate rejects it.
CHECK(classifyPinch({0, 0}, {100, 0}, {10, 0}, {70, 0}, pinch));
CHECK(!classifyRotation({0, 0}, {100, 0}, {10, 0}, {70, 0}, rotation));
}

// Contacts that converge by exactly the 20% threshold while both also travel
// 80 px. The translation path tolerates 45 px of separation change, so it would
// accept this as a two-finger swipe; finishMultiTouchGesture() tries pinch
// first, which is what makes it a pinch.
void testPinchWinsWithTranslation() {
PinchResult result;
CHECK(classifyPinch({0, 0}, {100, 0}, {80, 0}, {160, 0}, result));
CHECK(near(result.scale, 0.8f, 0.01f));
CHECK(result.centerX == 85);
CHECK(result.centerY == 0);
}

} // namespace

int main() {
Expand All @@ -88,6 +138,10 @@ int main() {
testRejectionThresholds();
testScaleEligibilityCanLatchIntermediatePinch();
testRotationWinsWithTranslation();
testPinchInAndOut();
testPinchRejectionThresholds();
testPinchAndRotationAreMutuallyExclusive();
testPinchWinsWithTranslation();

std::printf("%d checks, %d failures\n", checksRun, checksFailed);
return checksFailed == 0 ? 0 : 1;
Expand Down