vector: add (*Path).AddPath and AddPathOptions

Closes #3266
This commit is contained in:
Hajime Hoshi
2025-06-29 04:18:54 +09:00
parent ff26882df5
commit f96d89f808
4 changed files with 108 additions and 24 deletions
+7 -3
View File
@@ -60,9 +60,13 @@ func (g *Game) Draw(screen *ebiten.Image) {
op.Width = 2*(float32(math.Sin(float64(g.tick)*2*math.Pi/180))+1) + 1
op.LineJoin = vector.LineJoinRound
op.LineCap = vector.LineCapRound
var geoM ebiten.GeoM
geoM.Translate(50, 0)
vector.StrokePath(screen, g.path.ApplyGeoM(geoM), color.White, true, op)
op2 := &vector.AddPathOptions{}
op2.GeoM.Translate(50, 0)
var newPath vector.Path
newPath.AddPath(&g.path, op2)
vector.StrokePath(screen, &newPath, color.White, true, op)
}
func (*Game) Layout(width, height int) (int, int) {
+6 -4
View File
@@ -149,15 +149,17 @@ func (g *Game) drawEbitenLogo(screen *ebiten.Image, x, y int, aa bool, line bool
path.LineTo(unit, 4*unit)
path.Close()
var geoM ebiten.GeoM
geoM.Translate(float64(x), float64(y))
var newPath vector.Path
op := &vector.AddPathOptions{}
op.GeoM.Translate(float64(x), float64(y))
newPath.AddPath(&path, op)
if line {
op := &vector.StrokeOptions{}
op.Width = 5
op.LineJoin = vector.LineJoinRound
vector.StrokePath(screen, path.ApplyGeoM(geoM), color.RGBA{0xdb, 0x56, 0x20, 0xff}, aa, op)
vector.StrokePath(screen, &newPath, color.RGBA{0xdb, 0x56, 0x20, 0xff}, aa, op)
} else {
vector.DrawFilledPath(screen, path.ApplyGeoM(geoM), color.RGBA{0xdb, 0x56, 0x20, 0xff}, aa, vector.FillRuleNonZero)
vector.DrawFilledPath(screen, &newPath, color.RGBA{0xdb, 0x56, 0x20, 0xff}, aa, vector.FillRuleNonZero)
}
}
+52 -17
View File
@@ -622,29 +622,64 @@ func appendVerticesAndIndicesForFilling[T ~uint16 | ~uint32](path *Path, vertice
}
// ApplyGeoM applies the given GeoM to the path and returns a new path.
//
// Deprecated: as of v2.9. Use [Path.AddPath] instead.
func (p *Path) ApplyGeoM(geoM ebiten.GeoM) *Path {
// Flat paths are not copied.
np := &Path{
subPaths: make([]subPath, len(p.subPaths)),
}
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))
var np Path
op := &AddPathOptions{}
op.GeoM = geoM
np.AddPath(p, op)
return &np
}
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)},
}
// AddPathOptions is options for [Path.AddPath].
type AddPathOptions struct {
// GeoM is a transformation matrix to apply to the path.
//
// The default (zero) value is an identity matrix.
GeoM ebiten.GeoM
}
// AddPath adds the given path src to this path p as a sub-path.
func (p *Path) AddPath(src *Path, options *AddPathOptions) {
p.resetFlatPaths()
if options == nil {
options = &AddPathOptions{}
}
origN := len(src.subPaths)
n := len(p.subPaths)
p.subPaths = append(p.subPaths, make([]subPath, len(src.subPaths))...)
// Note that p and src might be the same.
for i, origSubPath := range src.subPaths[:origN] {
sx, sy := options.GeoM.Apply(float64(origSubPath.start.x), float64(origSubPath.start.y))
p.subPaths[n+i] = subPath{
ops: make([]op, len(origSubPath.ops)),
start: point{x: float32(sx), y: float32(sy)},
closed: origSubPath.closed,
}
for j, o := range origSubPath.ops {
switch o.typ {
case opTypeLineTo:
x1, y1 := options.GeoM.Apply(float64(o.p1.x), float64(o.p1.y))
p.subPaths[n+i].ops[j] = op{
typ: o.typ,
p1: point{x: float32(x1), y: float32(y1)},
}
case opTypeQuadTo:
x1, y1 := options.GeoM.Apply(float64(o.p1.x), float64(o.p1.y))
x2, y2 := options.GeoM.Apply(float64(o.p2.x), float64(o.p2.y))
p.subPaths[n+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
}
// LineCap represents the way in which how the ends of the stroke are rendered.
+43
View File
@@ -154,3 +154,46 @@ func TestMoveToAndClose(t *testing.T) {
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)
}
}