event_filter: transformation of motion coordinates

The <transform> filter configurably transforms touch and absolute-motion event
coordinates by a sequence of translation (move), scaling, rotation, and flipping
primitives in sub-nodes.

Issue #5105
This commit is contained in:
Christian Helmuth 2024-01-29 17:01:24 +01:00
parent 7304a019e7
commit 5f2691a65b
5 changed files with 376 additions and 0 deletions

View File

@ -96,6 +96,34 @@ one of the following filters:
rectangular area - using the attributes 'xpos', 'ypos', 'width', and rectangular area - using the attributes 'xpos', 'ypos', 'width', and
'height' - and the name of the tapped key as 'key' attribute. 'height' - and the name of the tapped key as 'key' attribute.
:<transform>:
Transforms touch and absolute-motion event coordinates by a sequence of
the following primitives expressed as sub-nodes.
:<translate>:
Moves coordinates by 'x' and 'y' attributes.
:<scale>:
Scales horizontal and vertical axis by 'x' and 'y' attributes.
:<rotate>:
Rotates clockwise (around origin (0,0) by 90, 180, or 270 degrees in
attribute 'angle'.
:<reorient>:
Rotates clockwise (like 'rotate') but also translates results for
desired target 'width' and 'height' attributes.
:<hflip>/<vflip>:
Flip coordinates horizontally/vertically and translate into
'width'/'height'.
Character generator rules Character generator rules
------------------------- -------------------------

View File

@ -0,0 +1,202 @@
/*
* \brief Affine transform of 2-D coordinates
* \author Christian Helmuth
* \date 2024-01-28
*
* Affine transformation of coordinate (vector) x to y is the combination of
* two operations.
*
* - linear map - matrix multiplication (A)
* - translation - vector addition (b)
*
* y = A*x + b
*
* Both operations can be combined by using an augmented matrix that includes
* the translation vector. A sequence of transformations can be combined into a
* single transformation matrix by multiplication of respective matrices.
*/
/*
* Copyright (C) 2024 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _EVENT_FILTER__AFFINE_TRANSFORM_H_
#define _EVENT_FILTER__AFFINE_TRANSFORM_H_
namespace Transform {
struct Matrix;
struct Point;
enum Angle { ANGLE_0, ANGLE_90, ANGLE_180, ANGLE_270 };
static Angle angle_from_degrees(unsigned degrees)
{
switch (degrees) {
case 90: return ANGLE_90;
case 180: return ANGLE_180;
case 270: return ANGLE_270;
default: return ANGLE_0;
}
}
}
/*
* Augmented transformation matrix
*
* | v11 v12 v13 |
* | v21 v22 v23 |
* | 0 0 1 |
*/
struct Transform::Matrix
{
float v11, v12, v13,
v21, v22, v23;
/*
* Identity transform (initial matrix)
*/
static Matrix identity()
{
return { .v11 = 1, .v12 = 0, .v13 = 0,
.v21 = 0, .v22 = 1, .v23 = 0 };
}
Matrix mul(Matrix const &t) const
{
return {
.v11 = t.v11 * v11 + t.v12 * v21 + t.v13 * 0,
.v12 = t.v11 * v12 + t.v12 * v22 + t.v13 * 0,
.v13 = t.v11 * v13 + t.v12 * v23 + t.v13 * 1,
.v21 = t.v21 * v11 + t.v22 * v21 + t.v23 * 0,
.v22 = t.v21 * v12 + t.v22 * v22 + t.v23 * 0,
.v23 = t.v21 * v13 + t.v22 * v23 + t.v23 * 1
};
}
/*
* Translation by (x,y)
*/
Matrix translate(float x, float y) const
{
return mul(Matrix { 1, 0, x,
0, 1, y});
}
/*
* Scaling by (x,y)
*/
Matrix scale(float x, float y) const
{
return mul(Matrix { x, 0, 0,
0, y, 0});
}
/*
* Rotation clock-wise by 90, 180, 270 degrees
*/
Matrix rotate(Angle angle) const
{
/*
* 90° 180° 270°
* cos 0 -1 0
* sin 1 0 -1
*/
switch (angle) {
case ANGLE_90:
return mul(Matrix { 0, -1, 0,
1, 0, 0 });
case ANGLE_180:
return mul(Matrix { -1, 0, 0,
0, -1, 0 });
case ANGLE_270:
return mul(Matrix { 0, 1, 0,
-1, 0, 0 });
default:
return *this;
}
}
/*
* Reflection on (vertical) y-axis
*/
Matrix reflect_vertical_axis() const
{
return mul(Matrix { -1, 0, 0,
0, 1, 0});
}
/*
* Reflection on (horizontal) x-axis
*/
Matrix reflect_horizontal_axis() const
{
return mul(Matrix { 1, 0, 0,
0, -1, 0});
}
/*
* Rotate and adjust origin
*/
Matrix reorient(Angle angle, float width, float height) const
{
switch (angle) {
case ANGLE_90: return rotate(angle).translate(width - 1, 0);
case ANGLE_180: return rotate(angle).translate(width - 1, height - 1);
case ANGLE_270: return rotate(angle).translate(0, height - 1);
default: return *this;
}
}
/*
* Flip (in vertical direction) and adjust origin
*/
Matrix vflip(float height) const
{
return reflect_horizontal_axis().translate(0, height - 1);
}
/*
* Flip (in horizontal direction) and adjust origin
*/
Matrix hflip(float width) const
{
return reflect_vertical_axis().translate(width - 1, 0);
}
};
/*
* Point as augmented vector
*
* | x |
* | y |
* | 1 |
*/
struct Transform::Point
{
float x, y;
Point transform(Matrix const &t) const
{
return {
.x = t.v11 * x + t.v12 * y + t.v13,
.y = t.v21 * x + t.v22 * y + t.v23,
};
}
int int_x() const
{
return x >= 0 ? int(x + 0.5) : -int(-x + 0.5);
}
int int_y() const
{
return y >= 0 ? int(y + 0.5) : -int(-y + 0.5);
}
};
#endif /* _EVENT_FILTER__AFFINE_TRANSFORM_H_ */

View File

@ -27,6 +27,7 @@
#include <log_source.h> #include <log_source.h>
#include <touch_click_source.h> #include <touch_click_source.h>
#include <touch_key_source.h> #include <touch_key_source.h>
#include <transform_source.h>
#include <event_session.h> #include <event_session.h>
namespace Event_filter { struct Main; } namespace Event_filter { struct Main; }
@ -252,6 +253,9 @@ struct Event_filter::Main : Source::Factory, Source::Trigger
if (node.type() == Log_source::name()) if (node.type() == Log_source::name())
return *new (_heap) Log_source(owner, node, *this); return *new (_heap) Log_source(owner, node, *this);
if (node.type() == Transform_source::name())
return *new (_heap) Transform_source(owner, node, *this);
if (node.type() == Touch_click_source::name()) if (node.type() == Touch_click_source::name())
return *new (_heap) Touch_click_source(owner, node, *this); return *new (_heap) Touch_click_source(owner, node, *this);

View File

@ -48,6 +48,7 @@ class Event_filter::Source
|| node.type() == "button-scroll" || node.type() == "button-scroll"
|| node.type() == "accelerate" || node.type() == "accelerate"
|| node.type() == "log" || node.type() == "log"
|| node.type() == "transform"
|| node.type() == "touch-click" || node.type() == "touch-click"
|| node.type() == "touch-key"; || node.type() == "touch-key";

View File

@ -0,0 +1,141 @@
/*
* \brief Input-event source that transforms motion events
* \author Christian Helmuth
* \date 2024-01-28
*
* This filter configurably transforms touch and absolute-motion event
* coordinates by a sequence of translation, scaling, rotation, and flipping
* primitives.
*/
/*
* Copyright (C) 2024 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _EVENT_FILTER__TRANSFORM_SOURCE_H_
#define _EVENT_FILTER__TRANSFORM_SOURCE_H_
/* local includes */
#include <source.h>
#include <affine_transform.h>
namespace Event_filter { class Transform_source; }
class Event_filter::Transform_source : public Source, Source::Filter
{
private:
Owner _owner;
Source &_source;
Transform::Matrix _transform = Transform::Matrix::identity();
void _apply_config(Xml_node const config)
{
config.for_each_sub_node([&] (Xml_node node) {
if (node.has_type("translate")) {
_transform = _transform.translate(
float(node.attribute_value("x", 0.0)),
float(node.attribute_value("y", 0.0)));
return;
}
if (node.has_type("scale")) {
_transform = _transform.scale(
float(node.attribute_value("x", 1.0)),
float(node.attribute_value("y", 1.0)));
return;
}
if (node.has_type("rotate")) {
unsigned degrees = node.attribute_value("angle", 0);
Transform::Angle angle =
Transform::angle_from_degrees(degrees);
if (angle == Transform::ANGLE_0)
warning("invalid transform rotate(", degrees, ")");
else
_transform = _transform.rotate(angle);
return;
}
if (node.has_type("hflip")) {
float width = float(node.attribute_value("width", -1.0));
if (width <= 0)
warning("invalid transform hflip");
else
_transform = _transform.hflip(width);
return;
}
if (node.has_type("vflip")) {
float height = float(node.attribute_value("height", -1.0));
if (height <= 0)
warning("invalid transform vflip");
else
_transform = _transform.vflip(height);
return;
}
if (node.has_type("reorient")) {
float width = float(node.attribute_value("width", -1.0));
float height = float(node.attribute_value("height", -1.0));
unsigned degrees = node.attribute_value("angle", 0);
Transform::Angle angle =
Transform::angle_from_degrees(degrees);
if (width <= 0 || height <= 0 || angle == Transform::ANGLE_0)
warning("invalid transform reorient");
else
_transform = _transform.reorient(angle, width, height);
return;
}
});
}
/**
* Filter interface
*/
void filter_event(Sink &destination, Input::Event const &event) override
{
if (!event.absolute_motion() && !event.touch()) {
destination.submit(event);
return;
}
event.handle_touch([&] (Input::Touch_id id, float x, float y) {
Transform::Point p { x, y };
p = p.transform(_transform);
destination.submit(Input::Touch { id, p.x, p.y });
});
event.handle_absolute_motion([&] (int x, int y) {
Transform::Point p { float(x), float(y) };
p = p.transform(_transform);
destination.submit(Input::Absolute_motion { p.int_x(), p.int_y() });
});
}
public:
static char const *name() { return "transform"; }
Transform_source(Owner &owner, Xml_node config, Source::Factory &factory)
:
Source(owner),
_owner(factory),
_source(factory.create_source(_owner, input_sub_node(config)))
{
_apply_config(config);
}
void generate(Sink &destination) override
{
Source::Filter::apply(destination, *this, _source);
}
};
#endif /* _EVENT_FILTER__TRANSFORM_SOURCE_H_ */