Mir

Merge lp:~andreas-pokorny/mir/add-geometry-transformation into lp:mir

Proposed by Andreas Pokorny
Status: Rejected
Rejected by: Andreas Pokorny
Proposed branch: lp:~andreas-pokorny/mir/add-geometry-transformation
Merge into: lp:mir
Diff against target: 764 lines (+688/-3)
7 files modified
include/shared/mir/geometry/point.h (+21/-1)
include/shared/mir/geometry/transformation.h (+149/-0)
src/shared/geometry/CMakeLists.txt (+2/-1)
src/shared/geometry/transformation.cpp (+243/-0)
tests/unit-tests/geometry/CMakeLists.txt (+1/-0)
tests/unit-tests/geometry/test-point.cpp (+25/-1)
tests/unit-tests/geometry/test-transformation.cpp (+247/-0)
To merge this branch: bzr merge lp:~andreas-pokorny/mir/add-geometry-transformation
Reviewer Review Type Date Requested Status
Daniel van Vugt Disapprove
PS Jenkins bot (community) continuous-integration Needs Fixing
Review via email: mp+211733@code.launchpad.net

Commit message

Adds a class describing a co-ordinate transformation

geometry::Transformation describes a combination of scaling, translation and rotation. Rotation is limited to rotation around the centre of the given object. In an upcoming change this class should be used to describe the position of a surface in a scene, and thus also used to do co-ordinate transformations for hit testing and input dispatching.
The transformation is stored in a way that trivial cases can be detected, and that transformation parameters can be read and changed.

Description of the change

This adds a class to geometry that should encapsulate transformations of surfaces in a way that:

* the structure of the transformation is visible - to allow changes of parts of the transformation
* the inverse of the transformation is available and or can be calculated in a cheap way without precision issues - to get relative co-ordinates for touch dispatching
* caches matrices and supports transformation short cuts to avoid more expensive calculations for display or input dispatch

This class is used to remove more of android input stacks InputWindowInfo and to move that into input::Surface instead.

It might need a better name.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:1473
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~andreas-pokorny/mir/add-geometry-transformation/+merge/211733/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/mir-team-mir-development-branch-ci/1111/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/mir-android-trusty-i386-build/1298
    SUCCESS: http://jenkins.qa.ubuntu.com/job/mir-clang-trusty-amd64-build/1296
    SUCCESS: http://jenkins.qa.ubuntu.com/job/mir-mediumtests-trusty-touch/878
    SUCCESS: http://jenkins.qa.ubuntu.com/job/mir-team-mir-development-branch-trusty-amd64-ci/843
        deb: http://jenkins.qa.ubuntu.com/job/mir-team-mir-development-branch-trusty-amd64-ci/843/artifact/work/output/*zip*/output.zip
    SUCCESS: http://jenkins.qa.ubuntu.com/job/mir-team-mir-development-branch-trusty-armhf-ci/848
        deb: http://jenkins.qa.ubuntu.com/job/mir-team-mir-development-branch-trusty-armhf-ci/848/artifact/work/output/*zip*/output.zip
    SUCCESS: http://jenkins.qa.ubuntu.com/job/mir-mediumtests-builder-trusty-armhf/879
        deb: http://jenkins.qa.ubuntu.com/job/mir-mediumtests-builder-trusty-armhf/879/artifact/work/output/*zip*/output.zip
    SUCCESS: http://jenkins.qa.ubuntu.com/job/mir-mediumtests-runner-mako/821
    SUCCESS: http://s-jenkins.ubuntu-ci:8080/job/touch-flash-device/4992

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/mir-team-mir-development-branch-ci/1111/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Daniel van Vugt (vanvugt) wrote :

All points I mentioned yesterday...

(1) "Transformation" is probably too generic a term for something with very specific operations like this. It needs a name that better distinguishes it from a common "mat4 transformation".

(2) Rotation is somewhat limited. We presently support rotations around arbitrary axes whereas this proposal limits that to only 3 possible axes. Although given the render_surfaces demo is probably going to be the only user of this, that's OK, but...

(3) As render_surfaces is the only user of rotations, rotations should not be explicitly built into the class. They should be provided by render_surfaces as simply a matrix.

(4) Only supporting one order of operations is too restrictive:
161 + * The transformation is defined as:
162 + * translate(translation) * translate(size*scale/2) * rotation(y,x,z) * scale * translate(-size*scale/2)
Transformations are arbitrary and should always be arbitrary. Imposing only one order and fixed set of operations would be an unacceptable step backwards.

Overall, I think the Transformation class is a mistake. Too restrictive in that it makes transformations less flexible, and more complicated at the same time. We should only deal in mat4's and never try to disassemble a mat4 into separate transformations.

review: Disapprove
Revision history for this message
Daniel van Vugt (vanvugt) wrote :

I think the right answer is to expose a simple interface to convert screen coordinates (touch/pointer coords) into surface space by adding a function:

  geometry::Point Surface::screen_to_local(geometry:Point const& p) const
  {
      // calculate some answer using transformation(), top_left() and size() info.
      // You may even wish to ignore per-surface transformations and only return valid results
      // when transformation() == identity. In which case it could be as trivial as:
      // return p - top_left();
      // or more complicated like:
      // auto centre = top_left() + (size() / 2);
      // return inverse(transformation()) * (p - centre);
  }

That's a function instead of a class. It doesn't even need to be a method of Surface because it just uses existing public interface functions.

Revision history for this message
Andreas Pokorny (andreas-pokorny) wrote :

> All points I mentioned yesterday...
>
> (1) "Transformation" is probably too generic a term for something with very
> specific operations like this. It needs a name that better distinguishes it
> from a common "mat4 transformation".

I was looking for a new name.

> (2) Rotation is somewhat limited. We presently support rotations around
> arbitrary axes whereas this proposal limits that to only 3 possible axes.
> Although given the render_surfaces demo is probably going to be the only user
> of this, that's OK, but...

(2 and 4) Current status is a super set of what was in use so far.
Translation & scaling in the android input stack, and rotations and translation in our examples/surface code.
I want to enhance the interface in later steps. I think the necessary features in that domain are neither arbitrary nor infinite.

> (3) As render_surfaces is the only user of rotations, rotations should not be
> explicitly built into the class. They should be provided by render_surfaces as
> simply a matrix.

I disagree to such an interface, because
* makes the interface more complicated to use
* requires the caller to track additional state since manipulating a matrix incrementally is not practical.
* trivial positioning and orientation cases are not easy to detect.

> (4) Only supporting one order of operations is too restrictive:
> 161 + * The transformation is defined as:
> 162 + * translate(translation) * translate(size*scale/2) *
> rotation(y,x,z) * scale * translate(-size*scale/2)
> Transformations are arbitrary and should always be arbitrary. Imposing only
> one order and fixed set of operations would be an unacceptable step backwards.

see (2) and

>
> Overall, I think the Transformation class is a mistake. Too restrictive in
> that it makes transformations less flexible, and more complicated at the same
> time. We should only deal in mat4's and never try to disassemble a mat4 into
> separate transformations.

Revision history for this message
Daniel van Vugt (vanvugt) wrote :

transformations that will be used in animations are arbitrary, and must remain so. You can't divide an arbitrary transformation matrix into a set of meaningful simple ones. Consider what happens in a cube-style animation where the windows fly away into the screen and layer onto a cube. The cube rotates around a point in space that is "inside the screen" and around an arbitrary axis determined by the mouse drag direction. That is a non-trivial surface transformation which involves translations and rotations in more combinations than this proposal supports. A 4x4 matrix is enough to represent such a transformation. However you cannot unwind that matrix into the simple set of steps used to create the matrix.

I recall sabdfl has used the cube example multiple times when asking about capabilities. Compiz/Ubuntu can do it right now and of course we need to build something better than Compiz.

And of course, multiple animations run at once. Not just one. So surface->transformation() could represent any number of animations being applied simultaneously.

Regarding "complicated", what I am suggesting is a simple function of only a few lines. That is less complicated than this proposal. Although my previous example contained a mistake. It should read:

  geometry::Point Surface::screen_to_local(geometry:Point const& p) const
  {
      auto centre = top_left() + (size() / 2);
      return (inverse(transformation()) * (p - centre)) + centre;
  }

Admittedly, that does require a full inverse.

Revision history for this message
Andreas Pokorny (andreas-pokorny) wrote :

> transformations that will be used in animations are arbitrary, and must remain
> so. You can't divide an arbitrary transformation matrix into a set of
> meaningful simple ones. Consider what happens in a cube-style animation where
> the windows fly away into the screen and layer onto a cube. The cube rotates
> around a point in space that is "inside the screen" and around an arbitrary
> axis determined by the mouse drag direction. That is a non-trivial surface
> transformation which involves translations and rotations in more combinations
> than this proposal supports. A 4x4 matrix is enough to represent such a
> transformation. However you cannot unwind that matrix into the simple set of
> steps used to create the matrix.

The issue is rather the lack of a z co-ordinate that would require ordering of the surfaces before they hit the screen. And the lack of perspective transformation inside the renderer. And then of course the then lacking but necessary ray casting to find the surface for input hit testing - or usage of points vs polygon to do hit testing.

Regarding rotations this case is already possible but requires calculating the final orientation of the surface as three angles and its position. Also not optimal to do.

Again I dont think that interface of the Thing meant to be an encapsulation of the transformations defining orientation and position and scaling of surface is done or complete - it is roughly what we have in use right now. So yes both implementation and interface will change a lot if we get the time to improve. But I think that is something we might evolve when turning mir::scene into a more complete scene graph?

> I recall sabdfl has used the cube example multiple times when asking about
> capabilities. Compiz/Ubuntu can do it right now and of course we need to build
> something better than Compiz.
>
> And of course, multiple animations run at once. Not just one. So
> surface->transformation() could represent any number of animations being
> applied simultaneously.
>
> Regarding "complicated", what I am suggesting is a simple function of only a
> few lines. That is less complicated than this proposal. Although my previous
> example contained a mistake. It should read:
>
> geometry::Point Surface::screen_to_local(geometry:Point const& p) const
> {
> auto centre = top_left() + (size() / 2);
> return (inverse(transformation()) * (p - centre)) + centre;
> }
>
> Admittedly, that does require a full inverse.

Additionally that does not simplify the example above either. And as you noticed you would inverse on each transformation, which is something I thought we both agreed to be bad.

I believe we can find a better way to describe arbitrary placed surfaces than to do:
  void set_transformation( mat4 worldToLocal, mat4 localToWorld);

Revision history for this message
kevin gunn (kgunn72) wrote :

@tranformation being just completely the wrong approach
personally I can see that a generic and yet simple (meaning non-arbitrary) convenience interface might still be useful, as in the definition of convenient. I guess the test question to ask is...is that something people would find useful to use in general, e.g. is it common enough??
i see the argument for rotation it'd only used by render_surfaces demo today, but would there be other customers potentially?

@ providing a simple function
looks like more arguing ahead :) might just be worth it to deliver a simple function to progress. Please consider.

@inverse - why is it bad ?

@ finding a better way for arbitrary surface positioning and mat4
my thinking would be, some conveniences are worthhwile, but at the same time, surely there's a breakover point where you would just use set_transformation( mat4 worldToLocal, mat4 localToWorld);
It goes back to the test question...if we build a better interface, who will use it?

Revision history for this message
Daniel van Vugt (vanvugt) wrote :

"lack of a z coordinate" is both untrue, and not an issue. It's important to remember that stacking depth does not and will not affect the Z coordinate used to render a surface. The stacking depth only affects the surface's position in the render list. Surfaces are all given zero Z coordinates by default, but that can and will change as the result of transformations.

We do support Z coordinates, although they're all set to zero right now. And we can easily support perspective projection in the renderer. Just no one has implemented it yet.

That does raise the issue of you technically having to include the display_transformation to get accurate input coordinates, which we're not doing. I've repeated this point every time we've discussed the topic. You can either choose to ignore full-screen transformations for input handling, or somehow work to extract that info from the renderer. Either is feasible but I'd prefer the former.

Always remember: a 4x4 matrix represents ANY number of transformations in ANY order. That's a fact that all graphics programmers depend on. And we should not in any way limit the number or order of transformations that a single matrix can represent.

Revision history for this message
Daniel van Vugt (vanvugt) wrote :
Revision history for this message
Andreas Pokorny (andreas-pokorny) wrote :

Strange world. This all started when I noticed that the transformation in BasicSurface was not used properly in the class and in input stack - ignored in some cases. When I suggested to fix that there were performance concerns, and concerns that we never need transformations at all. So I thought a way to "support" both by making it easy to detect possible short cut, not because it has a performance impact, but to avoid the concerns.

If we can settle on making mir calculate co-ordinates correctly in every case I am happy to not have that MP go into devel.

Unmerged revisions

1473. By Andreas Pokorny

Adds a class describing a co-ordinate transformation

geometry::Transformation describes a combination of scaling, translation and rotation. Rotation is limited to rotation around the center of the given object. In an upcoming change this class should be used to describe the position of a surface in a scene, and thus also used to do co-ordinate transformations for hit testing and input dispatching.
The transformation is storesd in a way that trivial cases can be detected, and that tranformation parameters can be read and changed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'include/shared/mir/geometry/point.h'
2--- include/shared/mir/geometry/point.h 2014-03-06 06:05:17 +0000
3+++ include/shared/mir/geometry/point.h 2014-03-19 14:02:20 +0000
4@@ -1,5 +1,5 @@
5 /*
6- * Copyright © 2012 Canonical Ltd.
7+ * Copyright © 2012,2014 Canonical Ltd.
8 *
9 * This program is free software: you can redistribute it and/or modify it
10 * under the terms of the GNU Lesser General Public License version 3,
11@@ -21,6 +21,7 @@
12
13 #include "dimensions.h"
14 #include <iosfwd>
15+#include <cmath>
16
17 namespace mir
18 {
19@@ -50,6 +51,25 @@
20 return lhs.x != rhs.x || lhs.y != rhs.y;
21 }
22
23+inline Point operator *(float lhs, Point const& rhs)
24+{
25+ return Point{std::round(rhs.x.as_float()*lhs),
26+ std::round(rhs.y.as_float()*lhs)};
27+}
28+
29+inline Point operator *(Point const& lhs, float rhs)
30+{
31+ return rhs*lhs;
32+}
33+
34+inline Point operator /(Point const& lhs, float rhs)
35+{
36+ return Point{std::round(lhs.x.as_float()/rhs),
37+ std::round(lhs.y.as_float()/rhs)};
38+}
39+
40+
41+
42 std::ostream& operator<<(std::ostream& out, Point const& value);
43 }
44 }
45
46=== added file 'include/shared/mir/geometry/transformation.h'
47--- include/shared/mir/geometry/transformation.h 1970-01-01 00:00:00 +0000
48+++ include/shared/mir/geometry/transformation.h 2014-03-19 14:02:20 +0000
49@@ -0,0 +1,149 @@
50+/*
51+ * Copyright © 2014 Canonical Ltd.
52+ *
53+ * This program is free software: you can redistribute it and/or modify it
54+ * under the terms of the GNU Lesser General Public License version 3,
55+ * as published by the Free Software Foundation.
56+ *
57+ * This program is distributed in the hope that it will be useful,
58+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
59+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
60+ * GNU Lesser General Public License for more details.
61+ *
62+ * You should have received a copy of the GNU Lesser General Public License
63+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
64+ *
65+ * Authored by: Andreas Pokorny <andreas.pokorny@canonical.com>
66+ */
67+
68+#ifndef MIR_GEOMETRY_TRANSFORMATION_H_
69+#define MIR_GEOMETRY_TRANSFORMATION_H_
70+
71+#include "mir/geometry/displacement.h"
72+#include "mir/geometry/size.h"
73+#include "mir/geometry/rectangle.h"
74+
75+#include "glm/glm.hpp"
76+
77+namespace mir
78+{
79+
80+namespace geometry
81+{
82+
83+/*!
84+ * \brief Transformation describes transformations applied to a rectangle.
85+ */
86+class Transformation
87+{
88+public:
89+ Transformation();
90+ explicit Transformation(Size rectangle_size);
91+ explicit Transformation(Rectangle rectangle);
92+
93+ void update_size(Size rectangle_size);
94+ /*!
95+ * \brief Returns true when the transformation does not affect the co-ordinates of an object.
96+ */
97+ bool is_identity() const;
98+ /*!
99+ * \brief Returns true when the transformation only moves the object.
100+ */
101+ bool is_translation() const;
102+ /*!
103+ * \brief Returns true when the transformation only scales the object.
104+ */
105+ bool is_scaling() const;
106+ /*!
107+ * \brief Returns true when the transformation only moves the object and scales it with a single factor.
108+ */
109+ bool is_scaling_translation() const;
110+ /*!
111+ * \brief Returns true when the transformation contains at least a rotation,
112+ * and might also contain movements and scaling operations.
113+ */
114+ bool requires_matrix_transformation() const;
115+
116+ /*!
117+ * \brief Changes the scaling factor by the given \a scale.
118+ */
119+ void set_scale(float scale);
120+ void set_translation(Displacement const& movement);
121+ /*!
122+ * \brief Rotation of the surface described as yxz angles
123+ * \param y angle around the rotated y axis (yaw)
124+ * \param x angle around the rotated x axis (pitch)
125+ * \param z angle around the original z axis (roll)
126+ */
127+ void set_rotation(float y, float x, float z);
128+ /*!
129+ * \brief Rotation of the surface described as rotation around the z axis
130+ */
131+ void set_rotation(float z);
132+
133+ float get_z_rotation() const
134+ {
135+ return z_angle;
136+ }
137+
138+ float get_x_rotation() const
139+ {
140+ return x_angle;
141+ }
142+
143+ float get_y_rotation() const
144+ {
145+ return y_angle;
146+ }
147+
148+ Displacement get_translation() const
149+ {
150+ return translation;
151+ }
152+
153+ float get_scale() const
154+ {
155+ return scale;
156+ }
157+
158+ /*!
159+ * \brief Retuns the complete transformation matrix
160+ *
161+ * The transformation is defined as:
162+ * translate(translation) * translate(size*scale/2) * rotation(y,x,z) * scale * translate(-size*scale/2)
163+ */
164+ glm::mat4 get_matrix() const;
165+ /*!
166+ * \brief Retuns the inverted transformation matrix
167+ */
168+ glm::mat4 get_inverse_matrix() const;
169+ /*!
170+ * \brief Returns a transformation matrix lacking the translation.
171+ */
172+ glm::mat4 get_center_matrix() const;
173+
174+ Point transform_to_local(Point const& point) const;
175+ Point transform_to_screen(Point const& point) const;
176+private:
177+ float scale;
178+ Displacement translation;
179+ Size size;
180+
181+ float x_angle, y_angle, z_angle;
182+ mutable glm::mat4 matrix;
183+ // center_matrix is just Rotate*Scale
184+ mutable glm::mat4 center_matrix;
185+ mutable glm::mat4 inverse_matrix;
186+ mutable bool dirty;
187+
188+ void update_matrices() const;
189+ bool has_scale() const;
190+ bool has_translation() const;
191+ bool has_rotation() const;
192+};
193+}
194+
195+}
196+
197+#endif
198+
199
200=== modified file 'src/shared/geometry/CMakeLists.txt'
201--- src/shared/geometry/CMakeLists.txt 2014-03-06 06:05:17 +0000
202+++ src/shared/geometry/CMakeLists.txt 2014-03-19 14:02:20 +0000
203@@ -17,8 +17,9 @@
204 add_library(
205 mirsharedgeometry STATIC
206
207+ ostream.cpp
208 rectangle.cpp
209 rectangles.cpp
210- ostream.cpp
211+ transformation.cpp
212 )
213
214
215=== added file 'src/shared/geometry/transformation.cpp'
216--- src/shared/geometry/transformation.cpp 1970-01-01 00:00:00 +0000
217+++ src/shared/geometry/transformation.cpp 2014-03-19 14:02:20 +0000
218@@ -0,0 +1,243 @@
219+/*
220+ * Copyright © 2014 Canonical Ltd.
221+ *
222+ * This program is free software: you can redistribute it and/or modify it
223+ * under the terms of the GNU Lesser General Public License version 3,
224+ * as published by the Free Software Foundation.
225+ *
226+ * This program is distributed in the hope that it will be useful,
227+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
228+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
229+ * GNU Lesser General Public License for more details.
230+ *
231+ * You should have received a copy of the GNU Lesser General Public License
232+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
233+ *
234+ * Authored by: Andreas Pokorny <andreas.pokorny@canonical.com>
235+ */
236+
237+#include "mir/geometry/transformation.h"
238+
239+#define GLM_FORCE_RADIANS
240+#include "glm/gtx/euler_angles.hpp"
241+#include "glm/gtx/transform.hpp"
242+
243+#include <cmath>
244+
245+namespace geom = mir::geometry;
246+
247+//TODO
248+// * Consider supporting separate scale values for x and y co-ordinates.
249+// * Consider different forms of orientation / rotation descriptions.
250+// * clamp scales close to 1.0f and angles close to 0.0f when they cannot be noticed
251+// on screen: i.e. sin(angle)*height/2 <1.0f or cos(angle)*width/2 < 1.0f
252+
253+geom::Transformation::Transformation(Rectangle rectangle)
254+ : scale(1.0f),
255+ translation(rectangle.top_left - geom::Point()),
256+ size(rectangle.size),
257+ x_angle(0.0f),
258+ y_angle(0.0f),
259+ z_angle(0.0f),
260+ dirty(has_translation())
261+{
262+}
263+geom::Transformation::Transformation(Size rectangle_size)
264+ : Transformation(Rectangle{Point{0,0}, rectangle_size})
265+{
266+}
267+
268+geom::Transformation::Transformation()
269+ : Transformation(Size(0,0) )
270+{}
271+
272+bool geom::Transformation::has_scale() const
273+{
274+ return scale != 1.0f;
275+}
276+
277+bool geom::Transformation::has_translation() const
278+{
279+ return translation != Displacement{};
280+}
281+
282+bool geom::Transformation::has_rotation() const
283+{
284+ return !(x_angle == 0.0f && y_angle == 0.0f && z_angle == 0.0f);
285+}
286+
287+bool geom::Transformation::is_identity() const
288+{
289+ return !has_translation() && !has_scale() && !has_rotation();
290+}
291+
292+bool geom::Transformation::is_scaling() const
293+{
294+ return !has_translation() && has_scale() && !has_rotation();
295+}
296+
297+bool geom::Transformation::is_translation() const
298+{
299+ return has_translation() && !has_scale() && !has_rotation();
300+}
301+
302+bool geom::Transformation::is_scaling_translation() const
303+{
304+ return has_translation() && has_scale() && !has_rotation();
305+}
306+
307+bool geom::Transformation::requires_matrix_transformation() const
308+{
309+ return has_rotation();
310+}
311+
312+void geom::Transformation::update_size(Size size)
313+{
314+ if (this->size != size)
315+ {
316+ this->size = size;
317+ dirty = true;
318+ }
319+}
320+
321+void geom::Transformation::set_scale(float scale)
322+{
323+ if (this->scale != scale)
324+ {
325+ dirty = true;
326+ this->scale = scale;
327+ }
328+}
329+
330+void geom::Transformation::set_translation(Displacement const& movement)
331+{
332+ if (translation != movement)
333+ {
334+ dirty = true;
335+ translation = movement;
336+ }
337+}
338+
339+glm::mat4 geom::Transformation::get_matrix() const
340+{
341+ update_matrices();
342+ return matrix;
343+}
344+
345+glm::mat4 geom::Transformation::get_center_matrix() const
346+{
347+ update_matrices();
348+ return center_matrix;
349+}
350+
351+void geom::Transformation::set_rotation(float y, float x, float z)
352+{
353+ if (y != y_angle ||
354+ x != x_angle ||
355+ z != z_angle)
356+ {
357+ dirty = true;
358+ y_angle = y;
359+ x_angle = x;
360+ z_angle = z;
361+ }
362+}
363+
364+void geom::Transformation::set_rotation(float z)
365+{
366+ if (0.0f != y_angle ||
367+ 0.0f != x_angle ||
368+ z != z_angle)
369+ {
370+ dirty = true;
371+ y_angle = 0.0f;
372+ x_angle = 0.0f;
373+ z_angle = z;
374+ }
375+}
376+
377+void geom::Transformation::update_matrices() const
378+{
379+ if (!dirty)
380+ return;
381+
382+ dirty = false;
383+
384+ if (has_rotation())
385+ {
386+ glm::vec3 center{size.width.as_float()/2.0f, size.height.as_float()/2.0f, 0.0};
387+
388+ if (x_angle == 0.0f && y_angle == 0.0f)
389+ {
390+ center_matrix = glm::eulerAngleZ(glm::radians(z_angle));
391+ }
392+ else
393+ {
394+ center_matrix = glm::eulerAngleYXZ(
395+ glm::radians(y_angle),
396+ glm::radians(x_angle),
397+ glm::radians(z_angle));
398+ }
399+
400+ matrix = glm::translate(center) * center_matrix * glm::translate(-center);
401+ inverse_matrix = glm::translate(center) * glm::transpose(center_matrix) * glm::translate(-center);
402+ }
403+ else
404+ {
405+ center_matrix = glm::mat4{};
406+ matrix = glm::mat4{};
407+ inverse_matrix = glm::mat4{};
408+ }
409+ if (has_scale())
410+ {
411+ matrix = glm::scale(glm::vec3(scale)) * matrix;
412+ center_matrix = glm::scale(glm::vec3(scale)) * center_matrix;
413+ inverse_matrix *= glm::scale(glm::vec3(1.0f/scale));
414+ }
415+
416+ if (has_translation())
417+ {
418+ inverse_matrix *= glm::translate(glm::vec3{-translation.dx.as_float(), -translation.dy.as_float(), 0.0});
419+
420+ matrix = glm::translate(glm::vec3{translation.dx.as_float(), translation.dy.as_float(), 0.0}) * matrix;
421+ }
422+}
423+
424+glm::mat4 geom::Transformation::get_inverse_matrix() const
425+{
426+ update_matrices();
427+
428+ return inverse_matrix;
429+}
430+
431+geom::Point geom::Transformation::transform_to_local(geom::Point const& point) const
432+{
433+ if (is_identity())
434+ return point;
435+ if (is_translation())
436+ return point - translation;
437+ if (is_scaling())
438+ return point / scale;
439+ if (is_scaling_translation())
440+ return (point - translation)/ scale;
441+
442+ glm::mat4 inv( get_inverse_matrix() );
443+ glm::vec4 pos = inv * glm::vec4{point.x.as_float(), point.y.as_float(), 0.0f, 1.0f};
444+ return geom::Point{ std::round(pos.x), std::round(pos.y) };
445+}
446+
447+geom::Point geom::Transformation::transform_to_screen(geom::Point const& point) const
448+{
449+ if (is_identity())
450+ return point;
451+ if (is_translation())
452+ return point + translation;
453+ if (is_scaling())
454+ return point * scale;
455+ if (is_scaling_translation())
456+ return point * scale + translation;
457+
458+ glm::mat4 trans( get_matrix() );
459+ glm::vec4 pos = trans * glm::vec4{point.x.as_float(), point.y.as_float(), 0.0f, 1.0f};
460+ return geom::Point{ std::round(pos.x), std::round(pos.y) };
461+}
462
463=== modified file 'tests/unit-tests/geometry/CMakeLists.txt'
464--- tests/unit-tests/geometry/CMakeLists.txt 2013-08-28 03:41:48 +0000
465+++ tests/unit-tests/geometry/CMakeLists.txt 2014-03-19 14:02:20 +0000
466@@ -5,6 +5,7 @@
467 ${CMAKE_CURRENT_SOURCE_DIR}/test-displacement.cpp
468 ${CMAKE_CURRENT_SOURCE_DIR}/test-rectangle.cpp
469 ${CMAKE_CURRENT_SOURCE_DIR}/test-rectangles.cpp
470+ ${CMAKE_CURRENT_SOURCE_DIR}/test-transformation.cpp
471 )
472
473 set(UNIT_TEST_SOURCES ${UNIT_TEST_SOURCES} PARENT_SCOPE)
474
475=== modified file 'tests/unit-tests/geometry/test-point.cpp'
476--- tests/unit-tests/geometry/test-point.cpp 2013-08-28 03:41:48 +0000
477+++ tests/unit-tests/geometry/test-point.cpp 2014-03-19 14:02:20 +0000
478@@ -1,5 +1,5 @@
479 /*
480- * Copyright © 2012 Canonical Ltd.
481+ * Copyright © 2012,2014 Canonical Ltd.
482 *
483 * This program is free software: you can redistribute it and/or modify
484 * it under the terms of the GNU General Public License version 3 as
485@@ -54,3 +54,27 @@
486 EXPECT_EQ(X(x), p.x);
487 EXPECT_EQ(Y(y), p.y);
488 }
489+
490+TEST(geometry, point_can_be_scaled)
491+{
492+ using namespace geom;
493+ int x = 2;
494+ int y = 4;
495+ auto p1 = 2.0f * Point{x, y};
496+ auto p2 = Point{x, y} * 3.5f;
497+ EXPECT_EQ(X(x*2), p1.x);
498+ EXPECT_EQ(Y(y*2), p1.y);
499+ EXPECT_EQ(X(7), p2.x);
500+ EXPECT_EQ(Y(14), p2.y);
501+}
502+
503+TEST(geometry, point_can_be_scaled_with_div)
504+{
505+ using namespace geom;
506+ int x = 12;
507+ int y = 6;
508+ auto p1 = Point{x, y}/3.0f;
509+ EXPECT_EQ(X(x/3), p1.x);
510+ EXPECT_EQ(Y(y/3), p1.y);
511+}
512+
513
514=== added file 'tests/unit-tests/geometry/test-transformation.cpp'
515--- tests/unit-tests/geometry/test-transformation.cpp 1970-01-01 00:00:00 +0000
516+++ tests/unit-tests/geometry/test-transformation.cpp 2014-03-19 14:02:20 +0000
517@@ -0,0 +1,247 @@
518+/*
519+ * Copyright © 2014 Canonical Ltd.
520+ *
521+ * This program is free software: you can redistribute it and/or modify
522+ * it under the terms of the GNU General Public License version 3 as
523+ * published by the Free Software Foundation.
524+ *
525+ * This program is distributed in the hope that it will be useful,
526+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
527+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
528+ * GNU General Public License for more details.
529+ *
530+ * You should have received a copy of the GNU General Public License
531+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
532+ *
533+ * Authored by: Andreas Pokorny <andreas.pokorny@canonical.com>
534+ */
535+
536+#include "mir/geometry/transformation.h"
537+
538+#include <gtest/gtest.h>
539+
540+#define GLM_FORCE_RADIANS
541+#include "glm/gtx/euler_angles.hpp"
542+#include "glm/gtx/transform.hpp"
543+
544+namespace geom = mir::geometry;
545+
546+::testing::AssertionResult MatricesMatch(glm::mat4 const& expected, glm::mat4 const& tested)
547+{
548+ for (int i = 0; i != 4; ++i)
549+ {
550+ for (int j = 0; j != 4; ++j)
551+ {
552+ float diff = std::abs(expected[i][j] - tested[i][j]);
553+ if (diff > 1.0E-7f)
554+ {
555+ return ::testing::AssertionFailure() << "item (" << i << ", "<< j << ") differs:"
556+ << tested[i][j] << " instead of " << expected[i][j] << " off by " << diff;
557+ }
558+ }
559+ }
560+
561+ return ::testing::AssertionSuccess();
562+}
563+
564+namespace
565+{
566+
567+glm::vec4 to_vec4(geom::Point const& p)
568+{
569+ return glm::vec4{p.x.as_float(), p.y.as_float(), 0.0f, 1.0f};
570+}
571+
572+geom::Point to_point(glm::vec4 const& p)
573+{
574+ return {std::round(p.x), std::round(p.y)};
575+}
576+
577+}
578+
579+TEST(geometry_transformation, identity_on_creation)
580+{
581+ using namespace geom;
582+ Transformation identity;
583+ EXPECT_TRUE(identity.is_identity());
584+ EXPECT_FALSE(identity.is_translation());
585+ EXPECT_FALSE(identity.is_scaling_translation());
586+ EXPECT_EQ(1.0f, identity.get_scale());
587+ EXPECT_EQ(0.0f, identity.get_z_rotation());
588+ EXPECT_EQ(0.0f, identity.get_x_rotation());
589+ EXPECT_EQ(0.0f, identity.get_y_rotation());
590+ EXPECT_EQ(geom::Displacement(), identity.get_translation());
591+}
592+
593+TEST(geometry_transformation, scale_may_destroy_identity)
594+{
595+ using namespace geom;
596+ Transformation identity;
597+ identity.set_scale(1230.f);
598+ EXPECT_FALSE(identity.is_identity());
599+ EXPECT_FALSE(identity.is_scaling_translation());
600+ EXPECT_TRUE(identity.is_scaling());
601+ identity.set_scale(1.0f);
602+ EXPECT_TRUE(identity.is_identity());
603+}
604+
605+TEST(geometry_transformation, translation)
606+{
607+ using namespace geom;
608+ Transformation movement;
609+ movement.set_translation(geom::Displacement{13,14});
610+ EXPECT_TRUE(movement.is_translation());
611+ EXPECT_FALSE(movement.is_scaling_translation());
612+ EXPECT_FALSE(movement.requires_matrix_transformation());
613+ EXPECT_TRUE(movement.is_translation());
614+
615+ geom::Displacement mov{13,14};
616+ EXPECT_EQ(mov, movement.get_translation());
617+}
618+
619+TEST(geometry_transformation, identity_matrices)
620+{
621+ using namespace geom;
622+ Transformation identity;
623+ glm::mat4 expected;
624+ EXPECT_TRUE(MatricesMatch(expected, identity.get_matrix()));
625+ EXPECT_TRUE(MatricesMatch(expected, identity.get_inverse_matrix()));
626+ EXPECT_TRUE(MatricesMatch(expected, identity.get_center_matrix()));
627+
628+ Point test_p(4124,1231);
629+ EXPECT_EQ(test_p, identity.transform_to_local(test_p));
630+ EXPECT_EQ(test_p, identity.transform_to_screen(test_p));
631+ EXPECT_EQ(test_p, to_point(identity.get_matrix() * to_vec4(test_p)));
632+ EXPECT_EQ(test_p, to_point(identity.get_inverse_matrix() * to_vec4(test_p)));
633+}
634+
635+TEST(geometry_transformation, translation_matrices)
636+{
637+ using namespace geom;
638+ Transformation translation;
639+ Displacement amount{13,42};
640+ translation.set_translation(amount);
641+ glm::mat4 expected_center, expected;
642+
643+ expected[3] = glm::vec4{amount.dx.as_float(), amount.dy.as_float(), 0.0f, 1.0f};
644+
645+ EXPECT_TRUE(MatricesMatch(expected, translation.get_matrix()));
646+ EXPECT_TRUE(MatricesMatch(glm::inverse(expected), translation.get_inverse_matrix()));
647+ EXPECT_TRUE(MatricesMatch(expected_center, translation.get_center_matrix()));
648+
649+ Point test_p(4124, 1147);
650+ Point screen_p(4137, 1189);
651+ EXPECT_EQ(screen_p, translation.transform_to_screen(test_p));
652+ EXPECT_EQ(test_p, translation.transform_to_local(screen_p));
653+ EXPECT_EQ(screen_p, to_point(translation.get_matrix() * to_vec4(test_p)));
654+ EXPECT_EQ(test_p, to_point(translation.get_inverse_matrix() * to_vec4(screen_p)));
655+}
656+
657+TEST(geometry_transformation, scaling_matrices)
658+{
659+ using namespace geom;
660+ Transformation scaling{Size{120,60}};
661+ scaling.set_scale(3.5f);
662+ glm::mat4 inv_expected, expected;
663+ expected[0].x = expected[1].y = expected[2].z = 3.5f;
664+ inv_expected[0].x = inv_expected[1].y = inv_expected[2].z = 1.0f/3.5f;
665+
666+ EXPECT_TRUE(MatricesMatch(expected, scaling.get_matrix()));
667+ EXPECT_TRUE(MatricesMatch(expected, scaling.get_center_matrix()));
668+ EXPECT_TRUE(MatricesMatch(inv_expected, scaling.get_inverse_matrix()));
669+
670+ Point test_p(10, 20);
671+ Point screen_p(35, 70);
672+ EXPECT_EQ(screen_p, scaling.transform_to_screen(test_p));
673+ EXPECT_EQ(test_p, scaling.transform_to_local(screen_p));
674+ EXPECT_EQ(screen_p, to_point(scaling.get_matrix() * to_vec4(test_p)));
675+ EXPECT_EQ(test_p, to_point(scaling.get_inverse_matrix() * to_vec4(screen_p)));
676+}
677+
678+TEST(geometry_transformation, scale_and_translate_matrices)
679+{
680+ using namespace geom;
681+ Transformation scale_and_translate{Size{30,10}};
682+ float scale = 1.33f;
683+ float x_pos = 60.0f;
684+ float y_pos = 50.0f;
685+ scale_and_translate.set_scale(scale);
686+ scale_and_translate.set_translation(Displacement{x_pos, y_pos});
687+ glm::mat4 inv_expected, expected;
688+ expected[0].x = expected[1].y = expected[2].z = scale;
689+ expected[3].x = x_pos;
690+ expected[3].y = y_pos;
691+ inv_expected = glm::scale(glm::vec3(1.0f/scale))* glm::translate(-glm::vec3{x_pos,y_pos,0});
692+
693+ EXPECT_TRUE(MatricesMatch(expected, scale_and_translate.get_matrix()));
694+ EXPECT_TRUE(MatricesMatch(inv_expected, scale_and_translate.get_inverse_matrix()));
695+
696+ Point test_p(10, 20);
697+ Point screen_p(73, 77);
698+ EXPECT_EQ(screen_p, scale_and_translate.transform_to_screen(test_p));
699+ EXPECT_EQ(test_p, scale_and_translate.transform_to_local(screen_p));
700+ EXPECT_EQ(screen_p, to_point(scale_and_translate.get_matrix() * to_vec4(test_p)));
701+ EXPECT_EQ(test_p, to_point(scale_and_translate.get_inverse_matrix() * to_vec4(screen_p)));
702+}
703+
704+TEST(geometry_transformation, rotate_by_90_matrices)
705+{
706+ using namespace geom;
707+ Transformation rotation(Size(10,10));
708+ rotation.set_rotation(90.0f);
709+ glm::mat4 expected_center;
710+ expected_center[0] = glm::vec4{0.0f, 1.0f, 0.0f, 0.0f};
711+ expected_center[1] = glm::vec4{-1.0f, 0.0f, 0.0f, 0.0f};
712+ glm::mat4 expected_matrix = expected_center;
713+ expected_matrix[3] = glm::vec4{10.0f, 0.0f, 0.0f, 1.0f};
714+ glm::mat4 expected_inverse = glm::inverse(expected_matrix);
715+
716+ EXPECT_TRUE(MatricesMatch(expected_matrix, rotation.get_matrix()));
717+ EXPECT_TRUE(MatricesMatch(expected_center, rotation.get_center_matrix()));
718+ EXPECT_TRUE(MatricesMatch(expected_inverse, rotation.get_inverse_matrix()));
719+
720+ Point test_p(0, 0);
721+ Point screen_p(10, 0);
722+ EXPECT_EQ(screen_p, rotation.transform_to_screen(test_p));
723+ EXPECT_EQ(test_p, rotation.transform_to_local(screen_p));
724+ EXPECT_EQ(screen_p, to_point(rotation.get_matrix() * to_vec4(test_p)));
725+ EXPECT_EQ(test_p, to_point(rotation.get_inverse_matrix() * to_vec4(screen_p)));
726+}
727+
728+TEST(geometry_transformation, all_combined_matrices)
729+{
730+ using namespace geom;
731+ glm::vec3 center(10,5,0);
732+ float angle = 20.0f;
733+ float scale = 3.65f;
734+ glm::vec3 translation(100,100,0);
735+
736+ Transformation all_combined(Size(center.x*2,center.y*2));
737+ all_combined.set_rotation(angle);
738+ all_combined.set_scale(scale);
739+ all_combined.set_translation(Displacement(translation.x,translation.y));
740+
741+ glm::mat4 expected =
742+ glm::translate(translation) *
743+ glm::scale(glm::vec3(scale)) *
744+ glm::translate(center) *
745+ glm::eulerAngleZ(glm::radians(angle)) *
746+ glm::translate(-center);
747+
748+ glm::mat4 inverse =
749+ glm::translate(center) *
750+ glm::eulerAngleZ(glm::radians(-angle)) *
751+ glm::translate(-center) *
752+ glm::scale(glm::vec3(1.0f / scale)) *
753+ glm::translate(-translation);
754+
755+ EXPECT_TRUE(MatricesMatch(expected, all_combined.get_matrix()));
756+ EXPECT_TRUE(MatricesMatch(inverse, all_combined.get_inverse_matrix()));
757+
758+ Point test_p(20, 10);
759+ Point screen_p(165, 148);
760+ EXPECT_EQ(screen_p, all_combined.transform_to_screen(test_p));
761+ EXPECT_EQ(test_p, all_combined.transform_to_local(screen_p));
762+ EXPECT_EQ(screen_p, to_point(all_combined.get_matrix() * to_vec4(test_p)));
763+ EXPECT_EQ(test_p, to_point(all_combined.get_inverse_matrix() * to_vec4(screen_p)));
764+}

Subscribers

People subscribed via source and target branches