vector: refactoring: introduce subPath and remove opTypeMove and opTypeClose

This commit is contained in:
Hajime Hoshi
2025-06-26 19:18:15 +09:00
parent 4133967934
commit 2bad42b02d
3 changed files with 151 additions and 126 deletions
+13 -23
View File
@@ -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)
}
+102 -85
View File
@@ -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
}
+36 -18
View File
@@ -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)
}
}