Files
ebiten/vector/path_test.go
T
Hajime Hoshi efab369512 all: go fix
2026-03-15 17:46:48 +09:00

440 lines
11 KiB
Go

// Copyright 2024 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package vector_test
import (
"image"
"image/color"
"runtime"
"sync"
"testing"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
func TestIsPointCloseToSegment(t *testing.T) {
testCases := []struct {
p vector.Point
p0 vector.Point
p1 vector.Point
allow float32
want bool
}{
{
p: vector.Point{0.5, 0.5},
p0: vector.Point{0, 0},
p1: vector.Point{1, 0},
allow: 1,
want: true,
},
{
p: vector.Point{0.5, 1.5},
p0: vector.Point{0, 0},
p1: vector.Point{1, 0},
allow: 1,
want: false,
},
{
p: vector.Point{0.5, 0.5},
p0: vector.Point{0, 0},
p1: vector.Point{1, 1},
allow: 0,
want: true,
},
{
p: vector.Point{0, 1},
p0: vector.Point{0, 0},
p1: vector.Point{1, 1},
allow: 0.7,
want: false,
},
{
p: vector.Point{0, 1},
p0: vector.Point{0, 0},
p1: vector.Point{1, 1},
allow: 0.8,
want: true,
},
{
// p0 and p1 are the same.
p: vector.Point{0, 1},
p0: vector.Point{0.5, 0.5},
p1: vector.Point{0.5, 0.5},
allow: 0.7,
want: false,
},
{
// p0 and p1 are the same.
p: vector.Point{0, 1},
p0: vector.Point{0.5, 0.5},
p1: vector.Point{0.5, 0.5},
allow: 0.8,
want: true,
},
}
for _, tc := range testCases {
if got := vector.IsPointCloseToSegment(tc.p, tc.p0, tc.p1, tc.allow); got != tc.want {
t.Errorf("got: %v, want: %v", got, tc.want)
}
}
}
func TestMoveToAndClose(t *testing.T) {
var path vector.Path
if _, ok := vector.CurrentPosition(&path); ok != false {
t.Errorf("expected no last position, got one")
}
if got, want := vector.SubPathCount(&path), 0; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
path.MoveTo(10, 20)
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{10, 20}) || !ok {
t.Errorf("expected last position to be (10, 20), got %v", p)
}
if got, want := vector.SubPathCount(&path), 1; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
path.MoveTo(30, 40)
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{30, 40}) || !ok {
t.Errorf("expected last position to be (30, 40), got %v", p)
}
if got, want := vector.SubPathCount(&path), 1; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
path.LineTo(50, 60)
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{50, 60}) || !ok {
t.Errorf("expected last position to be (50, 60), got %v", p)
}
if got, want := vector.SubPathCount(&path), 1; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
path.Close()
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{30, 40}) || !ok {
t.Errorf("expected last position to be (30, 40) after close, got %v", p)
}
if got, want := vector.SubPathCount(&path), 1; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
path.MoveTo(70, 80)
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{70, 80}) || !ok {
t.Errorf("expected last position to be (70, 80), got %v", p)
}
if got, want := vector.SubPathCount(&path), 2; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
path.LineTo(90, 100)
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{90, 100}) || !ok {
t.Errorf("expected last position to be (50, 60), got %v", p)
}
if got, want := vector.SubPathCount(&path), 2; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
// MoveTo without closing forces to create a new sub-path.
// The previous sub-path is left unclosed.
path.MoveTo(110, 120)
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{110, 120}) || !ok {
t.Errorf("expected last position to be (70, 80), got %v", p)
}
if got, want := vector.SubPathCount(&path), 3; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
}
func TestAddPath(t *testing.T) {
var path vector.Path
path.MoveTo(10, 20)
path.LineTo(30, 40)
path.Close()
op := &vector.AddPathOptions{}
op.GeoM.Translate(100, 100)
var path2 vector.Path
path2.AddPath(&path, op)
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{10, 20}) || !ok {
t.Errorf("expected last position to be (10, 20), got %v", p)
}
if got, want := vector.SubPathCount(&path), 1; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
if p, ok := vector.CurrentPosition(&path2); p != (vector.Point{110, 120}) || !ok {
t.Errorf("expected last position to be (110, 120), got %v", p)
}
if got, want := vector.SubPathCount(&path2), 1; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
}
func TestAddPathSelf(t *testing.T) {
var path vector.Path
path.MoveTo(10, 20)
path.LineTo(30, 40)
path.Close()
op := &vector.AddPathOptions{}
op.GeoM.Translate(100, 100)
path.AddPath(&path, op)
if p, ok := vector.CurrentPosition(&path); p != (vector.Point{110, 120}) || !ok {
t.Errorf("expected last position to be (110, 120), got %v", p)
}
if got, want := vector.SubPathCount(&path), 2; got != want {
t.Errorf("expected close count to be %d, got %d", want, got)
}
}
func TestArcAndGeoM(t *testing.T) {
if runtime.GOOS == "js" {
t.Skip("the result might be flaky in this environment")
}
testCases := []struct {
name string
geoM ebiten.GeoM
origPath vector.Path
refPath vector.Path
}{
{
name: "identity",
geoM: ebiten.GeoM{},
origPath: func() (p vector.Path) {
p.MoveTo(0, 0)
p.ArcTo(16, 0, 16, 16, 16)
return p
}(),
refPath: func() (p vector.Path) {
p.MoveTo(0, 0)
p.ArcTo(16, 0, 16, 16, 16)
return p
}(),
},
{
name: "scale 2x",
geoM: func() (geoM ebiten.GeoM) {
geoM.Scale(2, 2)
return geoM
}(),
origPath: func() (p vector.Path) {
p.MoveTo(0, 0)
p.ArcTo(8, 0, 8, 8, 8)
return p
}(),
refPath: func() (p vector.Path) {
p.MoveTo(0, 0)
p.ArcTo(16, 0, 16, 16, 16)
return p
}(),
},
{
name: "scale 256x",
geoM: func() (geoM ebiten.GeoM) {
geoM.Scale(256, 256)
return geoM
}(),
origPath: func() (p vector.Path) {
p.MoveTo(0, 0)
p.ArcTo(1.0/16.0, 0, 1.0/16.0, 1.0/16.0, 1.0/16.0)
return p
}(),
refPath: func() (p vector.Path) {
p.MoveTo(0, 0)
p.ArcTo(16, 0, 16, 16, 16)
return p
}(),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
origDst := ebiten.NewImage(16, 16)
defer origDst.Deallocate()
refDst := ebiten.NewImage(16, 16)
defer refDst.Deallocate()
var path vector.Path
op := &vector.AddPathOptions{}
op.GeoM = tc.geoM
path.AddPath(&tc.origPath, op)
strokeOp := &vector.StrokeOptions{
Width: 1,
}
// Do not use alpha blending, which can cause non-deterministic results.
vector.StrokePath(origDst, &path, strokeOp, nil)
vector.StrokePath(refDst, &tc.refPath, strokeOp, nil)
for j := range 16 {
for i := range 16 {
got := origDst.At(i, j)
want := refDst.At(i, j)
if got != want {
t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
})
}
}
// Issue #3330
func TestFillPathSubImage(t *testing.T) {
dst := ebiten.NewImage(16, 16)
dst2 := dst.SubImage(image.Rect(0, 0, 8, 8)).(*ebiten.Image)
var p vector.Path
p.MoveTo(0, 0)
p.LineTo(8, 0)
p.LineTo(8, 8)
p.LineTo(0, 8)
p.Close()
op := &vector.DrawPathOptions{}
op.ColorScale.ScaleWithColor(color.White)
op.AntiAlias = true
vector.FillPath(dst2, &p, nil, op)
if got, want := dst.At(5, 5), (color.RGBA{0xff, 0xff, 0xff, 0xff}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
if got, want := dst2.At(5, 5), (color.RGBA{0xff, 0xff, 0xff, 0xff}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
dst3 := dst2.SubImage(image.Rect(4, 4, 8, 8)).(*ebiten.Image)
var p2 vector.Path
p2.MoveTo(4, 4)
p2.LineTo(8, 4)
p2.LineTo(8, 8)
p2.LineTo(4, 8)
p2.Close()
op.ColorScale.Reset()
op.ColorScale.ScaleWithColor(color.Black)
vector.FillPath(dst3, &p2, nil, op)
if got, want := dst.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
if got, want := dst2.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
if got, want := dst3.At(5, 5), (color.RGBA{0x00, 0x00, 0x00, 0xff}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
}
func TestRaceConditionWithSubImage(t *testing.T) {
const w, h = 16, 16
src := ebiten.NewImage(w, h)
var wg sync.WaitGroup
for i := range h {
for j := range w {
wg.Go(func() {
subImg := src.SubImage(image.Rect(i, j, i+1, j+1)).(*ebiten.Image)
var p vector.Path
p.MoveTo(0, 0)
p.LineTo(w, 0)
p.LineTo(w, h)
p.LineTo(0, h)
p.Close()
op := &vector.DrawPathOptions{}
op.ColorScale.ScaleWithColor(color.White)
op.AntiAlias = true
vector.FillPath(subImg, &p, nil, op)
dst := ebiten.NewImage(w, h)
dst.DrawImage(subImg, nil)
})
}
}
wg.Wait()
}
// Issue #3355
func TestFillPathSubImageAndImage(t *testing.T) {
dst := ebiten.NewImage(200, 200)
defer dst.Deallocate()
for i := range 100 {
var path vector.Path
path.LineTo(0, 0)
path.LineTo(0, 100)
path.LineTo(100, 100)
path.LineTo(100, 0)
path.LineTo(0, 0)
path.Close()
drawOp := &vector.DrawPathOptions{}
drawOp.ColorScale.ScaleWithColor(color.RGBA{255, 0, 0, 255})
subDst := dst.SubImage(image.Rect(0, 0, 100, 100)).(*ebiten.Image)
vector.FillPath(subDst, &path, nil, drawOp)
drawOp.ColorScale.Reset()
drawOp.ColorScale.ScaleWithColor(color.RGBA{0, 255, 0, 255})
vector.FillPath(dst, &path, nil, drawOp)
if got, want := dst.At(50, 50), (color.RGBA{0, 255, 0, 255}); got != want {
t.Errorf("%d: got: %v, want: %v", i, got, want)
}
}
}
// Issue #3366
func TestFillPathFillRule(t *testing.T) {
testCases := []struct {
name string
fillRule vector.FillRule
expected color.RGBA
}{
{
name: "evenOdd",
fillRule: vector.FillRuleEvenOdd,
expected: color.RGBA{0, 0, 0, 0},
},
{
name: "nonZero",
fillRule: vector.FillRuleNonZero,
expected: color.RGBA{0xff, 0xff, 0xff, 0xff},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dst := ebiten.NewImage(16, 16)
defer dst.Deallocate()
var p vector.Path
p.MoveTo(0, 0)
p.LineTo(16, 0)
p.LineTo(16, 16)
p.LineTo(0, 16)
p.Close()
p.MoveTo(4, 4)
p.LineTo(12, 4)
p.LineTo(12, 12)
p.LineTo(4, 12)
p.Close()
fillOp := &vector.FillOptions{}
fillOp.FillRule = tc.fillRule
vector.FillPath(dst, &p, fillOp, nil)
if got, want := dst.At(8, 8), tc.expected; got != want {
t.Errorf("got: %v, want: %v", got, want)
}
})
}
}