diff --git a/vector/export_test.go b/vector/export_test.go index e21d9e55c..7c0c578c4 100644 --- a/vector/export_test.go +++ b/vector/export_test.go @@ -14,10 +14,16 @@ package vector +import "fmt" + type Point struct { X, Y float32 } +func (p Point) String() string { + return fmt.Sprintf("(%f, %f)", p.X, p.Y) +} + func IsPointCloseToSegment(p, p0, p1 Point, allow float32) bool { return isPointCloseToSegment(point{ x: p.X, @@ -31,30 +37,14 @@ func IsPointCloseToSegment(p, p0, p1 Point, allow float32) bool { }, allow) } -func LastPosition(path *Path) (x, y float32) { - if len(path.ops) == 0 { - return 0, 0 +func CurrentPosition(path *Path) (Point, bool) { + p, ok := path.currentPosition() + if !ok { + return Point{}, false } - for i := len(path.ops) - 1; i >= 0; i-- { - op := path.ops[i] - switch op.typ { - case opTypeMoveTo: - return op.p1.x, op.p1.y - case opTypeLineTo: - return op.p1.x, op.p1.y - case opTypeQuadTo: - return op.p2.x, op.p2.y - } - } - return 0, 0 + return Point{X: p.x, Y: p.y}, true } -func CloseCount(path *Path) int { - count := 0 - for _, op := range path.ops { - if op.typ == opTypeClose { - count++ - } - } - return count +func SubPathCount(path *Path) int { + return len(path.subPaths) } diff --git a/vector/path.go b/vector/path.go index 317592877..abd2f0b19 100644 --- a/vector/path.go +++ b/vector/path.go @@ -34,10 +34,8 @@ const ( type opType int const ( - opTypeMoveTo opType = iota - opTypeLineTo + opTypeLineTo opType = iota opTypeQuadTo - opTypeClose ) type op struct { @@ -58,6 +56,18 @@ type point struct { y float32 } +type subPath struct { + ops []op + start point + closed bool +} + +func (s *subPath) reset() { + s.ops = s.ops[:0] + s.start = point{} + s.closed = false +} + // flatPath is a flattened sub-path of a path. // A flatPath consists of points for line segments. type flatPath struct { @@ -67,57 +77,59 @@ type flatPath struct { // reset resets the flatPath. // reset doesn't release the allocated memory so that the memory can be reused. -func (s *flatPath) reset() { - s.points = s.points[:0] - s.closed = false +func (f *flatPath) reset() { + f.points = f.points[:0] + f.closed = false } -func (s flatPath) pointCount() int { - return len(s.points) +func (f flatPath) pointCount() int { + return len(f.points) } -func (s flatPath) lastPoint() point { - return s.points[len(s.points)-1] +func (f flatPath) lastPoint() point { + return f.points[len(f.points)-1] } -func (s *flatPath) appendPoint(pt point) { - if s.closed { +func (f *flatPath) appendPoint(pt point) { + if f.closed { panic("vector: a closed flatPath cannot append a new point") } - if len(s.points) > 0 { + if len(f.points) > 0 { // Do not add a too close point to the last point. // This can cause unexpected rendering results. - if lp := s.lastPoint(); abs(lp.x-pt.x) < 1e-2 && abs(lp.y-pt.y) < 1e-2 { + if lp := f.lastPoint(); abs(lp.x-pt.x) < 1e-2 && abs(lp.y-pt.y) < 1e-2 { return } } - s.points = append(s.points, pt) + f.points = append(f.points, pt) } -func (s *flatPath) close() { - s.closed = true +func (f *flatPath) close() { + f.closed = true } // Path represents a collection of vector graphics operations. type Path struct { - ops []op + subPaths []subPath // flatPaths is a cached actual rendering positions. flatPaths []flatPath - - start point - hasStart bool } // Reset resets the path. // Reset doesn't release the allocated memory so that the memory can be reused. func (p *Path) Reset() { - p.ops = p.ops[:0] + p.resetSubPaths() p.resetFlatPaths() - p.start = point{} - p.hasStart = false +} + +func (p *Path) resetSubPaths() { + for i := range p.subPaths { + p.subPaths[i].reset() + } + p.subPaths = p.subPaths[:0] } func (p *Path) resetFlatPaths() { @@ -141,25 +153,25 @@ func (p *Path) appendNewFlatPath(pt point) { } func (p *Path) ensureFlatPaths() []flatPath { - if len(p.flatPaths) > 0 || len(p.ops) == 0 { + if len(p.flatPaths) > 0 || len(p.subPaths) == 0 { return p.flatPaths } - var cur point - for _, op := range p.ops { - switch op.typ { - case opTypeMoveTo: - p.appendNewFlatPath(op.p1) - cur = op.p1 - case opTypeLineTo: - p.appendFlatPathPointsForLine(op.p1) - cur = op.p1 - case opTypeQuadTo: - p.appendFlatPathPointsForQuad(cur, op.p1, op.p2, 0) - cur = op.p2 - case opTypeClose: + for _, subPath := range p.subPaths { + p.appendNewFlatPath(subPath.start) + cur := subPath.start + for _, op := range subPath.ops { + switch op.typ { + case opTypeLineTo: + p.appendFlatPathPointsForLine(op.p1) + cur = op.p1 + case opTypeQuadTo: + p.appendFlatPathPointsForQuad(cur, op.p1, op.p2, 0) + cur = op.p2 + } + } + if subPath.closed { p.closeFlatPath() - cur = point{} } } @@ -171,17 +183,11 @@ func (p *Path) MoveTo(x, y float32) { p.resetFlatPaths() // Always update the start position. - p.start = point{x: x, y: y} - p.hasStart = true - // Overwrite the last move. - if len(p.ops) > 0 && p.ops[len(p.ops)-1].typ == opTypeMoveTo { - p.ops[len(p.ops)-1].p1 = point{x: x, y: y} - return + if len(p.subPaths) == 0 || len(p.subPaths[len(p.subPaths)-1].ops) > 0 { + p.subPaths = append(p.subPaths, subPath{}) } - p.ops = append(p.ops, op{ - typ: opTypeMoveTo, - p1: point{x: x, y: y}, - }) + p.subPaths[len(p.subPaths)-1].start = point{x: x, y: y} + p.subPaths[len(p.subPaths)-1].closed = false } // LineTo adds a line segment to the path, which starts from the last position of the current sub-path @@ -190,11 +196,16 @@ func (p *Path) MoveTo(x, y float32) { func (p *Path) LineTo(x, y float32) { p.resetFlatPaths() - if !p.hasStart { - p.start = point{x: x, y: y} - p.hasStart = true + if len(p.subPaths) == 0 { + p.subPaths = append(p.subPaths, subPath{ + start: point{x: x, y: y}, + }) + } else if p.subPaths[len(p.subPaths)-1].closed { + p.subPaths = append(p.subPaths, subPath{ + start: p.subPaths[len(p.subPaths)-1].start, + }) } - p.ops = append(p.ops, op{ + p.subPaths[len(p.subPaths)-1].ops = append(p.subPaths[len(p.subPaths)-1].ops, op{ typ: opTypeLineTo, p1: point{x: x, y: y}, }) @@ -205,11 +216,16 @@ func (p *Path) LineTo(x, y float32) { func (p *Path) QuadTo(x1, y1, x2, y2 float32) { p.resetFlatPaths() - if !p.hasStart { - p.start = point{x: x1, y: y1} - p.hasStart = true + if len(p.subPaths) == 0 { + p.subPaths = append(p.subPaths, subPath{ + start: point{x: x1, y: y1}, + }) + } else if p.subPaths[len(p.subPaths)-1].closed { + p.subPaths = append(p.subPaths, subPath{ + start: p.subPaths[len(p.subPaths)-1].start, + }) } - p.ops = append(p.ops, op{ + p.subPaths[len(p.subPaths)-1].ops = append(p.subPaths[len(p.subPaths)-1].ops, op{ typ: opTypeQuadTo, p1: point{x: x1, y: y1}, p2: point{x: x2, y: y2}, @@ -220,11 +236,6 @@ func (p *Path) QuadTo(x1, y1, x2, y2 float32) { // (x1, y1) and (x2, y2) are the control points, and (x3, y3) is the destination. func (p *Path) CubicTo(x1, y1, x2, y2, x3, y3 float32) { p.resetFlatPaths() - - if !p.hasStart { - p.start = point{x: x1, y: y1} - p.hasStart = true - } p.cubicTo(x1, y1, x2, y2, x3, y3, 0) } @@ -302,16 +313,14 @@ func isQuadraticCloseEnoughToCubic(start, end, qc1, cc1, cc2 point) bool { func (p *Path) Close() { p.resetFlatPaths() - if p.hasStart { - p.LineTo(p.start.x, p.start.y) - } - p.hasStart = false - if len(p.ops) == 0 || p.ops[len(p.ops)-1].typ == opTypeClose { + if len(p.subPaths) == 0 { return } - p.ops = append(p.ops, op{ - typ: opTypeClose, - }) + if len(p.subPaths[len(p.subPaths)-1].ops) > 0 { + start := p.subPaths[len(p.subPaths)-1].start + p.LineTo(start.x, start.y) + } + p.subPaths[len(p.subPaths)-1].closed = true } func (p *Path) appendFlatPathPointsForLine(pt point) { @@ -392,19 +401,19 @@ func cross(p0, p1 point) float32 { } func (p *Path) currentPosition() (point, bool) { - if len(p.ops) == 0 { + if len(p.subPaths) == 0 { return point{}, false } - op := p.ops[len(p.ops)-1] + ops := p.subPaths[len(p.subPaths)-1].ops + if len(ops) == 0 { + return p.subPaths[len(p.subPaths)-1].start, true + } + op := ops[len(ops)-1] switch op.typ { - case opTypeMoveTo: - return op.p1, true case opTypeLineTo: return op.p1, true case opTypeQuadTo: return op.p2, true - case opTypeClose: - return point{}, false } return point{}, false } @@ -614,18 +623,26 @@ func appendVerticesAndIndicesForFilling[T ~uint16 | ~uint32](path *Path, vertice // ApplyGeoM applies the given GeoM to the path and returns a new path. func (p *Path) ApplyGeoM(geoM ebiten.GeoM) *Path { - // Flat paths (sub-paths) are not copied. + // Flat paths are not copied. np := &Path{ - ops: make([]op, len(p.ops)), + subPaths: make([]subPath, len(p.subPaths)), } - for i, o := range p.ops { - x1, y1 := geoM.Apply(float64(o.p1.x), float64(o.p1.y)) - x2, y2 := geoM.Apply(float64(o.p2.x), float64(o.p2.y)) - np.ops[i] = op{ - typ: o.typ, - p1: point{x: float32(x1), y: float32(y1)}, - p2: point{x: float32(x2), y: float32(y2)}, + for i, subPath := range p.subPaths { + sx, sy := geoM.Apply(float64(subPath.start.x), float64(subPath.start.y)) + np.subPaths[i].start = point{x: float32(sx), y: float32(sy)} + np.subPaths[i].closed = subPath.closed + np.subPaths[i].ops = make([]op, len(subPath.ops)) + + for j, o := range subPath.ops { + x1, y1 := geoM.Apply(float64(o.p1.x), float64(o.p1.y)) + x2, y2 := geoM.Apply(float64(o.p2.x), float64(o.p2.y)) + np.subPaths[i].ops[j] = op{ + typ: o.typ, + p1: point{x: float32(x1), y: float32(y1)}, + p2: point{x: float32(x2), y: float32(y2)}, + } } + } return np } diff --git a/vector/path_test.go b/vector/path_test.go index 5815a56cb..3ee70d7e8 100644 --- a/vector/path_test.go +++ b/vector/path_test.go @@ -89,50 +89,68 @@ func TestIsPointCloseToSegment(t *testing.T) { func TestMoveToAndClose(t *testing.T) { var path vector.Path - if x, y := vector.LastPosition(&path); x != 0 || y != 0 { - t.Errorf("expected last position to be (0, 0), got (%f, %f)", x, y) + if _, ok := vector.CurrentPosition(&path); ok != false { + t.Errorf("expected no last position, got one") } - if got, want := vector.CloseCount(&path), 0; got != want { + 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 x, y := vector.LastPosition(&path); x != 10 || y != 20 { - t.Errorf("expected last position to be (10, 20), got (%f, %f)", x, y) + 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.CloseCount(&path), 0; got != want { + 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 x, y := vector.LastPosition(&path); x != 30 || y != 40 { - t.Errorf("expected last position to be (30, 40), got (%f, %f)", x, y) + 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.CloseCount(&path), 0; got != want { + 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 x, y := vector.LastPosition(&path); x != 50 || y != 60 { - t.Errorf("expected last position to be (50, 60), got (%f, %f)", x, y) + 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.CloseCount(&path), 0; got != want { + if got, want := vector.SubPathCount(&path), 1; got != want { t.Errorf("expected close count to be %d, got %d", want, got) } path.Close() - if x, y := vector.LastPosition(&path); x != 30 || y != 40 { - t.Errorf("expected last position to be (30, 40) after close, got (%f, %f)", x, y) + 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.CloseCount(&path), 1; got != want { + 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 x, y := vector.LastPosition(&path); x != 70 || y != 80 { - t.Errorf("expected last position to be (70, 80), got (%f, %f)", x, y) + 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.CloseCount(&path), 1; got != want { + 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) } }