op/clip: automatically close Path in Outlines

Unclosed path segments in Path will be automatically
closed by a line.

Fixes: https://todo.sr.ht/~eliasnaur/gio/320
Signed-off-by: Pierre Curto <pierre.curto@gmail.com>
This commit is contained in:
Pierre Curto
2021-12-19 17:09:54 +01:00
committed by Elias Naur
parent 0117de71d3
commit 11bb86166a
10 changed files with 123 additions and 28 deletions
+11 -4
View File
@@ -1186,12 +1186,19 @@ func min(p1, p2 f32.Point) f32.Point {
return p return p
} }
func (enc *encoder) encodePath(verts []byte) { func (enc *encoder) encodePath(verts []byte, fillMode int) {
for len(verts) >= scene.CommandSize+4 { for ; len(verts) >= scene.CommandSize+4; verts = verts[scene.CommandSize+4:] {
cmd := ops.DecodeCommand(verts[4:]) cmd := ops.DecodeCommand(verts[4:])
if cmd.Op() == scene.OpGap {
if fillMode != scene.FillModeNonzero {
// Skip gaps in strokes.
continue
}
// Replace them by a straight line in outlines.
cmd = scene.Line(scene.DecodeGap(cmd))
}
enc.scene = append(enc.scene, cmd) enc.scene = append(enc.scene, cmd)
enc.npathseg++ enc.npathseg++
verts = verts[scene.CommandSize+4:]
} }
} }
@@ -2109,7 +2116,7 @@ func encodeOp(viewport image.Point, absOff image.Point, enc *encoder, texOps []t
if len(cl.path) == 0 { if len(cl.path) == 0 {
enc.rect(cl.state.bounds) enc.rect(cl.state.bounds)
} else { } else {
enc.encodePath(cl.path) enc.encodePath(cl.path, fillMode)
} }
if i != 0 { if i != 0 {
enc.beginClip(cl.union.Add(absOfff)) enc.beginClip(cl.union.Add(absOfff))
+6
View File
@@ -1344,6 +1344,12 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
q.Ctrl = q.From.Add(q.To).Mul(.5) q.Ctrl = q.From.Add(q.To).Mul(.5)
q = q.Transform(tr) q = q.Transform(tr)
qs.splitAndEncode(q) qs.splitAndEncode(q)
case scene.OpGap:
var q stroke.QuadSegment
q.From, q.To = scene.DecodeGap(cmd)
q.Ctrl = q.From.Add(q.To).Mul(.5)
q = q.Transform(tr)
qs.splitAndEncode(q)
case scene.OpQuad: case scene.OpQuad:
var q stroke.QuadSegment var q stroke.QuadSegment
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

+54
View File
@@ -357,6 +357,60 @@ func TestImageRGBA(t *testing.T) {
}) })
} }
func TestGapsInPath(t *testing.T) {
ops := new(op.Ops)
var p clip.Path
p.Begin(ops)
// Unclosed square 1
p.MoveTo(f32.Point{X: 10})
p.LineTo(f32.Point{X: 40})
p.LineTo(f32.Point{X: 40, Y: 30})
p.LineTo(f32.Point{X: 10, Y: 30})
// Unclosed square 2
p.MoveTo(f32.Point{X: 50})
p.LineTo(f32.Point{X: 80})
p.LineTo(f32.Point{X: 80, Y: 30})
p.LineTo(f32.Point{X: 50, Y: 30})
spec := p.End()
t.Run("Stroke", func(t *testing.T) {
run(t,
func(ops *op.Ops) {
stack := clip.Stroke{
Path: spec,
Width: 2,
}.Op().Push(ops)
paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops)
paint.PaintOp{}.Add(ops)
stack.Pop()
},
func(r result) {
r.expect(10, 20, color.RGBA{})
r.expect(50, 20, color.RGBA{})
},
)
})
t.Run("Outline", func(t *testing.T) {
run(t,
func(ops *op.Ops) {
stack := clip.Outline{Path: spec}.Op().Push(ops)
paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops)
paint.PaintOp{}.Add(ops)
stack.Pop()
},
func(r result) {
r.expect(10, 20, colornames.Red)
r.expect(20, 20, colornames.Red)
r.expect(50, 20, colornames.Red)
r.expect(60, 20, colornames.Red)
},
)
})
}
// lerp calculates linear interpolation with color b and p. // lerp calculates linear interpolation with color b and p.
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
return f32color.RGBA{ return f32color.RGBA{
+13 -3
View File
@@ -11,6 +11,7 @@ import (
"image/draw" "image/draw"
"image/png" "image/png"
"io/ioutil" "io/ioutil"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"testing" "testing"
@@ -143,11 +144,20 @@ func multiRun(t *testing.T, frames ...frameT) {
func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) { func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) {
// ensure identical to ref data // ensure identical to ref data
path := filepath.Join("refs", t.Name()+".png") var path string
if frame != 0 { if frame == 0 {
path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png") path = t.Name()
} else {
path = t.Name() + "_" + strconv.Itoa(frame)
} }
path = filepath.Join("refs", path+".png")
if *dumpImages { if *dumpImages {
if err := os.MkdirAll(filepath.Dir(path), 0766); err != nil {
if !os.IsExist(err) {
t.Error(err)
return
}
}
saveImage(t, path, img) saveImage(t, path, img)
return true return true
} }
+24 -1
View File
@@ -18,7 +18,7 @@ type Op uint32
type Command [sceneElemSize / 4]uint32 type Command [sceneElemSize / 4]uint32
// GPU commands from scene.h // GPU commands from piet/scene.h in package gioui.org/shaders.
const ( const (
OpNop Op = iota OpNop Op = iota
OpLine OpLine
@@ -31,6 +31,7 @@ const (
OpEndClip OpEndClip
OpFillImage OpFillImage
OpSetFillMode OpSetFillMode
OpGap
) )
// FillModes, from setup.h. // FillModes, from setup.h.
@@ -56,6 +57,9 @@ func (c Command) String() string {
case OpLine: case OpLine:
from, to := DecodeLine(c) from, to := DecodeLine(c)
return fmt.Sprintf("line(%v, %v)", from, to) return fmt.Sprintf("line(%v, %v)", from, to)
case OpGap:
from, to := DecodeLine(c)
return fmt.Sprintf("gap(%v, %v)", from, to)
case OpQuad: case OpQuad:
from, ctrl, to := DecodeQuad(c) from, ctrl, to := DecodeQuad(c)
return fmt.Sprintf("quad(%v, %v, %v)", from, ctrl, to) return fmt.Sprintf("quad(%v, %v, %v)", from, ctrl, to)
@@ -107,6 +111,16 @@ func Line(start, end f32.Point) Command {
} }
} }
func Gap(start, end f32.Point) Command {
return Command{
0: uint32(OpGap),
1: math.Float32bits(start.X),
2: math.Float32bits(start.Y),
3: math.Float32bits(end.X),
4: math.Float32bits(end.Y),
}
}
func Cubic(start, ctrl0, ctrl1, end f32.Point) Command { func Cubic(start, ctrl0, ctrl1, end f32.Point) Command {
return Command{ return Command{
0: uint32(OpCubic), 0: uint32(OpCubic),
@@ -206,6 +220,15 @@ func DecodeLine(cmd Command) (from, to f32.Point) {
return return
} }
func DecodeGap(cmd Command) (from, to f32.Point) {
if cmd[0] != uint32(OpGap) {
panic("invalid command")
}
from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2]))
to = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4]))
return
}
func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) { func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) {
if cmd[0] != uint32(OpQuad) { if cmd[0] != uint32(OpQuad) {
panic("invalid command") panic("invalid command")
+2
View File
@@ -637,6 +637,8 @@ func decodeToStrokeQuads(pathData []byte) StrokeQuads {
Quad: q, Quad: q,
} }
quads = append(quads, quad) quads = append(quads, quad)
case scene.OpGap:
// Ignore gaps for strokes.
case scene.OpQuad: case scene.OpQuad:
var q QuadSegment var q QuadSegment
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
+13 -9
View File
@@ -90,9 +90,6 @@ func (s Stack) Pop() {
type PathSpec struct { type PathSpec struct {
spec op.CallOp spec op.CallOp
// open is true if any path contour is not closed. A closed contour starts
// and ends in the same point.
open bool
// hasSegments tracks whether there are any segments in the path. // hasSegments tracks whether there are any segments in the path.
hasSegments bool hasSegments bool
bounds image.Rectangle bounds image.Rectangle
@@ -109,7 +106,6 @@ type PathSpec struct {
// data is stored directly in the Ops list supplied to Begin. // data is stored directly in the Ops list supplied to Begin.
type Path struct { type Path struct {
ops *ops.Ops ops *ops.Ops
open bool
contour int contour int
pen f32.Point pen f32.Point
macro op.MacroOp macro op.MacroOp
@@ -136,10 +132,10 @@ func (p *Path) Begin(o *op.Ops) {
// End returns a PathSpec ready to use in clipping operations. // End returns a PathSpec ready to use in clipping operations.
func (p *Path) End() PathSpec { func (p *Path) End() PathSpec {
p.gap()
c := p.macro.Stop() c := p.macro.Stop()
return PathSpec{ return PathSpec{
spec: c, spec: c,
open: p.open || p.pen != p.start,
hasSegments: p.hasSegments, hasSegments: p.hasSegments,
bounds: boundRectF(p.bounds), bounds: boundRectF(p.bounds),
hash: p.hash.Sum64(), hash: p.hash.Sum64(),
@@ -157,12 +153,23 @@ func (p *Path) MoveTo(to f32.Point) {
if p.pen == to { if p.pen == to {
return return
} }
p.open = p.open || p.pen != p.start p.gap()
p.end() p.end()
p.pen = to p.pen = to
p.start = to p.start = to
} }
func (p *Path) gap() {
if p.pen != p.start {
// A closed contour starts and ends in the same point.
// This move creates a gap in the contour, register it.
data := ops.Write(p.ops, scene.CommandSize+4)
bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour))
p.cmd(data[4:], scene.Gap(p.pen, p.start))
}
}
// end completes the current contour. // end completes the current contour.
func (p *Path) end() { func (p *Path) end() {
p.contour++ p.contour++
@@ -331,9 +338,6 @@ type Outline struct {
// Op returns a clip operation representing the outline. // Op returns a clip operation representing the outline.
func (o Outline) Op() Op { func (o Outline) Op() Op {
if o.Path.open {
panic("not all path contours are closed")
}
return Op{ return Op{
path: o.Path, path: o.Path,
outline: true, outline: true,
-11
View File
@@ -15,17 +15,6 @@ import (
) )
func TestPathOutline(t *testing.T) { func TestPathOutline(t *testing.T) {
t.Run("unclosed path", func(t *testing.T) {
defer func() {
if err := recover(); err == nil {
t.Error("Outline of an open path didn't panic")
}
}()
var p clip.Path
p.Begin(new(op.Ops))
p.Line(f32.Pt(10, 10))
clip.Outline{Path: p.End()}.Op()
})
t.Run("closed path", func(t *testing.T) { t.Run("closed path", func(t *testing.T) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {