all: introduce Outline and Stroke builders

This CL introduces 2 new path builders:
- Outline which takes a PathSpec to be outlined
- Stroke which takes a PathSpec and a stroke style, to stroke a path.

typically, code like this:

  var p clip.Path
  ...
  p.Outline().Add(o)

should be replaced with:

  var p clip.Path
  ...
  clip.Outline{Path: p.End()}.Op().Add(o)

similarly, stroking should be modified from:

  var p clip.Path
  ...
  p.Stroke(width, clip.StrokeStyle{...}).Add(o)

to:

  var p clip.Path
  ...
  clip.Stroke{Path: p.End(), Style: clip.StrokeStyle{Width:...}}.Op().Add(o)

here are tentative 'rf' scripts (see rsc.io/rf for more details):

  ```
  ex {
  	import "gioui.org/op";
  	import "gioui.org/op/clip";

  	var p clip.Path;
  	var o *op.Ops;

  	p.Outline().Add(o) -> clip.Outline{Path:p.End()}.Op().Add(o);
  }

  ex {
  	import "gioui.org/op";
  	import "gioui.org/op/clip";

  	var o *op.Ops;
  	var p clip.Path;
  	var sty clip.StrokeStyle;
  	var width float32;

  	p.Stroke(width, sty).Add(o) ->   \
	    clip.Stroke{                 \
		Path:p.End(),            \
		Style: clip.StrokeStyle{ \
		    Width: width,        \
	    }}.Op().Add(o);
  }
  ```

Signed-off-by: Sebastien Binet <s@sbinet.org>
This commit is contained in:
Sebastien Binet
2020-12-08 14:55:26 +00:00
committed by Elias Naur
parent 7c5bcd3db8
commit be89f8b945
12 changed files with 355 additions and 259 deletions
+79 -40
View File
@@ -109,6 +109,35 @@ type imageOp struct {
place placement
}
func decodeStrokeOp(data []byte) clip.StrokeStyle {
_ = data[10]
if opconst.OpType(data[0]) != opconst.TypeStroke {
panic("invalid op")
}
bo := binary.LittleEndian
return clip.StrokeStyle{
Width: math.Float32frombits(bo.Uint32(data[1:])),
Miter: math.Float32frombits(bo.Uint32(data[5:])),
Cap: clip.StrokeCap(data[9]),
Join: clip.StrokeJoin(data[10]),
}
}
type quadsOp struct {
quads uint32
key ops.Key
aux []byte
}
func decodeQuadsOp(data []byte) uint32 {
_ = data[:1+4]
if opconst.OpType(data[0]) != opconst.TypePath {
panic("invalid op")
}
bo := binary.LittleEndian
return bo.Uint32(data[1:])
}
type material struct {
material materialType
opaque bool
@@ -125,9 +154,8 @@ type material struct {
// clipOp is the shadow of clip.Op.
type clipOp struct {
// TODO: Use image.Rectangle?
bounds f32.Rectangle
width float32
style clip.StrokeStyle
bounds f32.Rectangle
outline bool
}
// imageOpData is the shadow of paint.ImageOp.
@@ -159,13 +187,8 @@ func (op *clipOp) decode(data []byte) {
},
}
*op = clipOp{
bounds: layout.FRect(r),
width: math.Float32frombits(bo.Uint32(data[17:])),
style: clip.StrokeStyle{
Cap: clip.StrokeCap(data[21]),
Join: clip.StrokeJoin(data[22]),
Miter: math.Float32frombits(bo.Uint32(data[23:])),
},
bounds: layout.FRect(r),
outline: data[17] == 1,
}
}
@@ -786,8 +809,10 @@ func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) {
}
func (d *drawOps) collectOps(r *ops.Reader, state drawState) int {
var aux []byte
var auxKey ops.Key
var (
quads quadsOp
stroke clip.StrokeStyle
)
loop:
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch opconst.OpType(encOp.Data[0]) {
@@ -796,38 +821,51 @@ loop:
case opconst.TypeTransform:
dop := ops.DecodeTransform(encOp.Data)
state.t = state.t.Mul(dop)
case opconst.TypeAux:
aux = encOp.Data[opconst.TypeAuxLen:]
auxKey = encOp.Key
case opconst.TypeStroke:
stroke = decodeStrokeOp(encOp.Data)
case opconst.TypePath:
quads.quads = decodeQuadsOp(encOp.Data)
if quads.quads > 0 {
encOp, ok = r.Decode()
if !ok {
break loop
}
quads.aux = encOp.Data[opconst.TypeAuxLen:]
quads.key = encOp.Key
}
case opconst.TypeClip:
var op clipOp
op.decode(encOp.Data)
bounds := op.bounds
trans, off := splitTransform(state.t)
if len(aux) > 0 {
if len(quads.aux) > 0 {
// There is a clipping path, build the gpu data and update the
// cache key such that it will be equal only if the transform is the
// same also. Use cached data if we have it.
auxKey = auxKey.SetTransform(trans)
if v, ok := d.pathCache.get(auxKey); ok {
quads.key = quads.key.SetTransform(trans)
if v, ok := d.pathCache.get(quads.key); ok {
// Since the GPU data exists in the cache aux will not be used.
// Why is this not used for the offset shapes?
op.bounds = v.bounds
} else {
aux, op.bounds = d.buildVerts(aux, trans, op.width, op.style)
quads.aux, op.bounds = d.buildVerts(quads.aux, trans, op.outline, stroke)
// add it to the cache, without GPU data, so the transform can be
// reused.
d.pathCache.put(auxKey, opCacheValue{bounds: op.bounds})
d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds})
}
} else {
aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans)
auxKey = encOp.Key
auxKey.SetTransform(trans)
quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans)
quads.key = encOp.Key
quads.key.SetTransform(trans)
}
state.clip = state.clip.Intersect(op.bounds.Add(off))
d.addClipPath(&state, aux, auxKey, op.bounds, off)
aux = nil
auxKey = ops.Key{}
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off)
quads = quadsOp{}
stroke = clip.StrokeStyle{}
case opconst.TypeColor:
state.matType = materialColor
state.color = decodeColorOp(encOp.Data)
@@ -1213,7 +1251,7 @@ func (d *drawOps) writeVertCache(n int) []byte {
}
// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) {
func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) {
inf := float32(math.Inf(+1))
d.qs.bounds = f32.Rectangle{
Min: f32.Point{X: inf, Y: inf},
@@ -1224,18 +1262,7 @@ func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty cli
startLength := len(d.vertCache)
switch {
default:
// Outline path.
for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
d.qs.contour = bo.Uint32(aux)
quad := ops.DecodeQuad(aux[4:])
quad = quad.Transform(tr)
d.qs.splitAndEncode(quad)
aux = aux[ops.QuadSize+4:]
}
case width > 0:
case stroke.Width > 0:
// Stroke path.
quads := make(strokeQuads, 0, 2*len(aux)/(ops.QuadSize+4))
for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
@@ -1246,13 +1273,25 @@ func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty cli
quads = append(quads, quad)
aux = aux[ops.QuadSize+4:]
}
quads = quads.stroke(width, sty)
quads = quads.stroke(stroke)
for _, quad := range quads {
d.qs.contour = quad.contour
quad.quad = quad.quad.Transform(tr)
d.qs.splitAndEncode(quad.quad)
}
case outline:
// Outline path.
for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
d.qs.contour = bo.Uint32(aux)
quad := ops.DecodeQuad(aux[4:])
quad = quad.Transform(tr)
d.qs.splitAndEncode(quad)
aux = aux[ops.QuadSize+4:]
}
}
fillMaxY(d.vertCache[startLength:])
+21 -20
View File
@@ -106,14 +106,14 @@ func (qs strokeQuads) split() []strokeQuads {
return o
}
func (qs strokeQuads) stroke(width float32, sty clip.StrokeStyle) strokeQuads {
func (qs strokeQuads) stroke(stroke clip.StrokeStyle) strokeQuads {
var (
o strokeQuads
hw = 0.5 * width
hw = 0.5 * stroke.Width
)
for _, ps := range qs.split() {
rhs, lhs := ps.offset(hw, sty)
rhs, lhs := ps.offset(hw, stroke)
switch lhs {
case nil:
o = o.append(rhs)
@@ -132,13 +132,14 @@ func (qs strokeQuads) stroke(width float32, sty clip.StrokeStyle) strokeQuads {
}
}
}
return o
}
// offset returns the right-hand and left-hand sides of the path, offset by
// the half-width hw.
// The stroke style sty handles how segments are joined and ends are capped.
func (qs strokeQuads) offset(hw float32, sty clip.StrokeStyle) (rhs, lhs strokeQuads) {
// The stroke handles how segments are joined and ends are capped.
func (qs strokeQuads) offset(hw float32, stroke clip.StrokeStyle) (rhs, lhs strokeQuads) {
var (
states []strokeState
beg = qs[0].quad.From
@@ -180,7 +181,7 @@ func (qs strokeQuads) offset(hw float32, sty clip.StrokeStyle) (rhs, lhs strokeQ
next = states[0]
}
if state.n1 != next.n0 {
strokePathJoin(sty, &rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0)
strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0)
}
}
}
@@ -196,10 +197,10 @@ func (qs strokeQuads) offset(hw float32, sty clip.StrokeStyle) (rhs, lhs strokeQ
// Default to counter-clockwise direction.
lhs = lhs.reverse()
strokePathCap(sty, &rhs, hw, qend.p1, qend.n1)
strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1)
rhs = rhs.append(lhs)
strokePathCap(sty, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1))
strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1))
rhs.close()
@@ -421,13 +422,13 @@ func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32
}
// strokePathJoin joins the two paths rhs and lhs, according to the provided
// stroke style sty.
func strokePathJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
if sty.Miter > 0 {
strokePathMiterJoin(sty, rhs, lhs, hw, pivot, n0, n1, r0, r1)
// stroke operation.
func strokePathJoin(stroke clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
if stroke.Miter > 0 {
strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
return
}
switch sty.Join {
switch stroke.Join {
case clip.BevelJoin:
strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
case clip.RoundJoin:
@@ -468,14 +469,14 @@ func strokePathRoundJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Po
}
}
func strokePathMiterJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
func strokePathMiterJoin(stroke clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
if n0 == n1.Mul(-1) {
strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
return
}
// This is to handle nearly linear joints that would be clipped otherwise.
limit := math.Max(float64(sty.Miter), 1.001)
limit := math.Max(float64(stroke.Miter), 1.001)
cw := dotPt(rot90CW(n0), n1) >= 0.0
if cw {
@@ -489,8 +490,8 @@ func strokePathMiterJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32
cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1))))
d := hw64 / cos
if math.Abs(limit*hw64) < math.Abs(d) {
sty.Miter = 0 // Set miter to zero to disable the miter joint.
strokePathJoin(sty, rhs, lhs, hw, pivot, n0, n1, r0, r1)
stroke.Miter = 0 // Set miter to zero to disable the miter joint.
strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
return
}
mid := pivot.Add(normPt(n0.Add(n1), float32(d)))
@@ -509,9 +510,9 @@ func strokePathMiterJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32
lhs.lineTo(lp)
}
// strokePathCap caps the provided path qs, according to the provided stroke style sty.
func strokePathCap(sty clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
switch sty.Cap {
// strokePathCap caps the provided path qs, according to the provided stroke operation.
func strokePathCap(stroke clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
switch stroke.Cap {
case clip.FlatCap:
strokePathFlatCap(qs, hw, pivot, n0)
case clip.SquareCap: