forked from joejulian/gio
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:
+11
-4
@@ -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))
|
||||||
|
|||||||
@@ -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 |
@@ -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{
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user