diff --git a/geom.go b/geom.go index 2f449ae20..f94d7081c 100644 --- a/geom.go +++ b/geom.go @@ -83,6 +83,18 @@ func (g *GeoM) Translate(tx, ty float64) { 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. // The unit is radian. func (g *GeoM) Rotate(theta float64) { diff --git a/geom_test.go b/geom_test.go index 11ac27938..8cecf03b9 100644 --- a/geom_test.go +++ b/geom_test.go @@ -15,6 +15,7 @@ package ebiten_test import ( + "fmt" "math" "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) { trans := GeoM{} trans.Translate(1, 2) @@ -172,7 +183,91 @@ func TestGeoMApply(t *testing.T) { for _, c := range cases { rx, ry := c.GeoM.Apply(c.InX, c.InY) 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) + } } } } diff --git a/internal/affine/geom.go b/internal/affine/geom.go index cfb947a34..6cb73f0d6 100644 --- a/internal/affine/geom.go +++ b/internal/affine/geom.go @@ -183,3 +183,32 @@ func (g *GeoM) Rotate(theta float64) *GeoM { 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, + } +}