affine: Add GeoM.Invert, IsInvertible (#547)

This commit is contained in:
Hajime Hoshi 2018-03-11 19:13:39 +09:00
parent efea65ee58
commit c2872017ab
3 changed files with 137 additions and 1 deletions

12
geom.go
View File

@ -83,6 +83,18 @@ func (g *GeoM) Translate(tx, ty float64) {
g.impl = g.impl.Translate(tx, ty) g.impl = g.impl.Translate(tx, ty)
} }
// IsInvertible returns a boolean value indicating
// whether the matrix g is invertible or not.
func (g *GeoM) IsInvertible() bool {
return g.impl.IsInvertible()
}
// Invert inverts the matrix.
// If g is not invertible, Invert panics.
func (g *GeoM) Invert() {
g.impl = g.impl.Invert()
}
// Rotate rotates the matrix by theta. // Rotate rotates the matrix by theta.
// The unit is radian. // The unit is radian.
func (g *GeoM) Rotate(theta float64) { func (g *GeoM) Rotate(theta float64) {

View File

@ -15,6 +15,7 @@
package ebiten_test package ebiten_test
import ( import (
"fmt"
"math" "math"
"testing" "testing"
@ -116,6 +117,16 @@ func TestGeoMConcatSelf(t *testing.T) {
} }
} }
func geoMToString(g GeoM) string {
a := g.Element(0, 0)
b := g.Element(0, 1)
c := g.Element(1, 0)
d := g.Element(1, 1)
tx := g.Element(0, 2)
ty := g.Element(1, 2)
return fmt.Sprintf("{a: %f, b: %f, c: %f, d: %f, tx: %f, ty: %f}", a, b, c, d, tx, ty)
}
func TestGeoMApply(t *testing.T) { func TestGeoMApply(t *testing.T) {
trans := GeoM{} trans := GeoM{}
trans.Translate(1, 2) trans.Translate(1, 2)
@ -172,7 +183,91 @@ func TestGeoMApply(t *testing.T) {
for _, c := range cases { for _, c := range cases {
rx, ry := c.GeoM.Apply(c.InX, c.InY) rx, ry := c.GeoM.Apply(c.InX, c.InY)
if math.Abs(rx-c.OutX) > c.Delta || math.Abs(ry-c.OutY) > c.Delta { if math.Abs(rx-c.OutX) > c.Delta || math.Abs(ry-c.OutY) > c.Delta {
t.Errorf("%v.Apply(%v, %v) = (%v, %v), want (%v, %v)", c.GeoM, c.InX, c.InY, rx, ry, c.OutX, c.OutY) t.Errorf("%s.Apply(%f, %f) = (%f, %f), want (%f, %f)", geoMToString(c.GeoM), c.InX, c.InY, rx, ry, c.OutX, c.OutY)
}
}
}
func TestGeoMIsInvert(t *testing.T) {
zero := GeoM{}
zero.Scale(0, 0)
trans := GeoM{}
trans.Translate(1, 2)
scale := GeoM{}
scale.Scale(1.5, 2.5)
cpx := GeoM{}
cpx.Rotate(math.Pi)
cpx.Scale(1.5, 2.5)
cpx.Translate(-2, -3)
cases := []struct {
GeoM GeoM
Invertible bool
}{
{
GeoM: zero,
Invertible: false,
},
{
GeoM: GeoM{},
Invertible: true,
},
{
GeoM: trans,
Invertible: true,
},
{
GeoM: scale,
Invertible: true,
},
{
GeoM: cpx,
Invertible: true,
},
}
pts := []struct {
X float64
Y float64
}{
{
X: 0,
Y: 0,
},
{
X: 1,
Y: 1,
},
{
X: 3.14159,
Y: 2.81828,
},
{
X: -1000,
Y: 1000,
},
}
const delta = 0.00001
for _, c := range cases {
if c.GeoM.IsInvertible() != c.Invertible {
t.Errorf("%s.IsInvertible(): got: %t, want: %t", geoMToString(c.GeoM), c.GeoM.IsInvertible(), c.Invertible)
}
if !c.GeoM.IsInvertible() {
continue
}
invGeoM := c.GeoM
invGeoM.Invert()
for _, p := range pts {
x, y := p.X, p.Y
gotX, gotY := invGeoM.Apply(c.GeoM.Apply(x, y))
if math.Abs(gotX-x) > delta || math.Abs(gotY-y) > delta {
t.Errorf("%s.Apply(%s.Apply(%f, %f)): got: (%f, %f), want: (%f, %f)", geoMToString(invGeoM), geoMToString(c.GeoM), x, y, gotX, gotY, x, y)
}
} }
} }
} }

View File

@ -183,3 +183,32 @@ func (g *GeoM) Rotate(theta float64) *GeoM {
ty: sin*g.tx + cos*g.ty, ty: sin*g.tx + cos*g.ty,
} }
} }
func (g *GeoM) det() float64 {
if g == nil {
return 1
}
return (g.a_1+1)*(g.d_1+1) - g.b - g.c
}
func (g *GeoM) IsInvertible() bool {
return g.det() != 0
}
func (g *GeoM) Invert() *GeoM {
if g == nil {
return nil
}
det := g.det()
if det == 0 {
panic("affine: g is not invertible")
}
return &GeoM{
a_1: ((g.d_1 + 1) / det) - 1,
b: -g.b / det,
c: -g.c / det,
d_1: ((g.a_1 + 1) / det) - 1,
tx: (-(g.d_1+1)*g.tx + g.b*g.ty) / det,
ty: (g.c*g.tx + -(g.a_1+1)*g.ty) / det,
}
}