op/clip, gpu: split complex curves in package gpu instead

This is a first step towards supporting affine drawing transforms.
The rendering algorithm relies on quadratic curves that do not cross
x = 0 more than once, thus curves must be split after any rotation/shear
transforms. Move this logic and the generation of vertices to package gpu.
Also close all curves and draw zero-width edges as preparation for
transform since the will no longer implicitly be vertical with no
effect.

This commit will severely affect performance since vertexes are now
transformed also for cached items, using cpu resources.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
This commit is contained in:
Viktor
2020-06-20 23:29:48 +02:00
committed by Elias Naur
parent ef70b9252e
commit 5b277757cf
6 changed files with 240 additions and 173 deletions
+98
View File
@@ -0,0 +1,98 @@
package gpu
import (
"gioui.org/f32"
"gioui.org/internal/ops"
)
type quadSplitter struct {
verts []byte
bounds f32.Rectangle
contour uint32
d *drawOps
}
func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) {
// NW.
encodeVertex(data, meta, -1, 1, from, ctrl, to)
// NE.
encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to)
// SW.
encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to)
// SE.
encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to)
}
func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, from, ctrl, to f32.Point) {
var corner float32
if cornerx == 1 {
corner += .5
}
if cornery == 1 {
corner += .25
}
v := vertex{
Corner: corner,
FromX: from.X,
FromY: from.Y,
CtrlX: ctrl.X,
CtrlY: ctrl.Y,
ToX: to.X,
ToY: to.Y,
}
v.encode(data, meta)
}
func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) {
data := qs.d.writeVertCache(vertStride * 4)
encodeQuadTo(data, qs.contour, from, ctrl, to)
}
func (qs *quadSplitter) splitAndEncode(quad ops.Quad) {
cbnd := f32.Rectangle{
Min: quad.From,
Max: quad.To,
}.Canon()
from, ctrl, to := quad.From, quad.Ctrl, quad.To
// If the curve contain areas where a vertical line
// intersects it twice, split the curve in two x monotone
// lower and upper curves. The stencil fragment program
// expects only one intersection per curve.
// Find the t where the derivative in x is 0.
v0 := ctrl.Sub(from)
v1 := to.Sub(ctrl)
d := v0.X - v1.X
// t = v0 / d. Split if t is in ]0;1[.
if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
t := v0.X / d
ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t))
ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
qs.encodeQuadTo(from, ctrl0, mid)
qs.encodeQuadTo(mid, ctrl1, to)
if mid.X > cbnd.Max.X {
cbnd.Max.X = mid.X
}
if mid.X < cbnd.Min.X {
cbnd.Min.X = mid.X
}
} else {
qs.encodeQuadTo(from, ctrl, to)
}
// Find the y extremum, if any.
d = v0.Y - v1.Y
if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
t := v0.Y / d
y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
if y > cbnd.Max.Y {
cbnd.Max.Y = y
}
if y < cbnd.Min.Y {
cbnd.Min.Y = y
}
}
qs.bounds = qs.bounds.Union(cbnd)
}
+49 -18
View File
@@ -22,7 +22,6 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/internal/opconst" "gioui.org/internal/opconst"
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/internal/path"
gunsafe "gioui.org/internal/unsafe" gunsafe "gioui.org/internal/unsafe"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
@@ -55,6 +54,7 @@ type drawOps struct {
profile bool profile bool
reader ops.Reader reader ops.Reader
cache *resourceCache cache *resourceCache
vertCache []byte
viewport image.Point viewport image.Point
clearColor f32color.RGBA clearColor f32color.RGBA
imageOps []imageOp imageOps []imageOp
@@ -64,6 +64,7 @@ type drawOps struct {
zimageOps []imageOp zimageOps []imageOp
pathOps []*pathOp pathOps []*pathOp
pathOpCache []pathOp pathOpCache []pathOp
qs quadSplitter
} }
type drawState struct { type drawState struct {
@@ -659,6 +660,7 @@ func (d *drawOps) reset(cache *resourceCache, viewport image.Point) {
d.zimageOps = d.zimageOps[:0] d.zimageOps = d.zimageOps[:0]
d.pathOps = d.pathOps[:0] d.pathOps = d.pathOps[:0]
d.pathOpCache = d.pathOpCache[:0] d.pathOpCache = d.pathOpCache[:0]
d.vertCache = d.vertCache[:0]
} }
func (d *drawOps) collect(cache *resourceCache, root *op.Ops, viewport image.Point) { func (d *drawOps) collect(cache *resourceCache, root *op.Ops, viewport image.Point) {
@@ -693,29 +695,29 @@ loop:
state.t = state.t.Multiply(op.TransformOp(dop)) state.t = state.t.Multiply(op.TransformOp(dop))
case opconst.TypeAux: case opconst.TypeAux:
aux = encOp.Data[opconst.TypeAuxLen:] aux = encOp.Data[opconst.TypeAuxLen:]
// The first data byte stores whether the MaxY
// fields have been initialized.
maxyFilled := aux[0] == 1
aux[0] = 1
aux = aux[1:]
if !maxyFilled {
fillMaxY(aux)
}
auxKey = encOp.Key auxKey = encOp.Key
case opconst.TypeClip: case opconst.TypeClip:
var op clipOp var op clipOp
op.decode(encOp.Data) op.decode(encOp.Data)
off := state.t.Transform(f32.Point{}) off := state.t.Transform(f32.Point{})
state.clip = state.clip.Intersect(op.bounds.Add(off))
bounds := op.bounds
if len(aux) > 0 {
// there is a clipping path, bounds is not filled before for performance
aux, bounds = d.buildVerts(aux)
}
state.clip = state.clip.Intersect(bounds.Add(off))
if state.clip.Empty() { if state.clip.Empty() {
continue continue
} }
npath := d.newPathOp() npath := d.newPathOp()
*npath = pathOp{ *npath = pathOp{
parent: state.cpath, parent: state.cpath,
off: off, off: off,
} }
state.cpath = npath state.cpath = npath
if len(aux) > 0 { if len(aux) > 0 {
state.rect = false state.rect = false
state.cpath.pathKey = auxKey state.cpath.pathKey = auxKey
@@ -1002,17 +1004,17 @@ func fillMaxY(verts []byte) {
for len(verts) > 0 { for len(verts) > 0 {
maxy := float32(math.Inf(-1)) maxy := float32(math.Inf(-1))
i := 0 i := 0
for ; i+path.VertStride*4 <= len(verts); i += path.VertStride * 4 { for ; i+vertStride*4 <= len(verts); i += vertStride * 4 {
vert := verts[i : i+path.VertStride] vert := verts[i : i+vertStride]
// MaxY contains the integer contour index. // MaxY contains the integer contour index.
pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY)):])) pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):]))
if contour != pathContour { if contour != pathContour {
contour = pathContour contour = pathContour
break break
} }
fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).FromY)):])) fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):]))
ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).CtrlY)):])) ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):]))
toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).ToY)):])) toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):]))
if fromy > maxy { if fromy > maxy {
maxy = fromy maxy = fromy
} }
@@ -1030,8 +1032,37 @@ func fillMaxY(verts []byte) {
func fillContourMaxY(maxy float32, verts []byte) { func fillContourMaxY(maxy float32, verts []byte) {
bo := binary.LittleEndian bo := binary.LittleEndian
for i := 0; i < len(verts); i += path.VertStride { for i := 0; i < len(verts); i += vertStride {
off := int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY)) off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY))
bo.PutUint32(verts[i+off:], math.Float32bits(maxy)) bo.PutUint32(verts[i+off:], math.Float32bits(maxy))
} }
} }
func (d *drawOps) writeVertCache(n int) []byte {
d.vertCache = append(d.vertCache, make([]byte, n)...)
return d.vertCache[len(d.vertCache)-n:]
}
func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) {
// split paths as needed, calculate maxY, bounds and create
// vertices that will be sent to GPU.
inf := float32(math.Inf(+1))
d.qs.bounds = f32.Rectangle{
Min: f32.Point{X: inf, Y: inf},
Max: f32.Point{X: -inf, Y: -inf},
}
d.qs.d = d
bo := binary.LittleEndian
startLength := len(d.vertCache)
for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
d.qs.contour = bo.Uint32(aux)
quad := ops.DecodeQuad(aux[4:])
d.qs.splitAndEncode(quad)
aux = aux[ops.QuadSize+4:]
}
fillMaxY(d.vertCache[startLength:])
return d.vertCache[startLength:], d.qs.bounds
}
+42 -9
View File
@@ -6,13 +6,14 @@ package gpu
// Pathfinder (https://github.com/servo/pathfinder). // Pathfinder (https://github.com/servo/pathfinder).
import ( import (
"encoding/binary"
"image" "image"
"math"
"unsafe" "unsafe"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/gpu/backend" "gioui.org/gpu/backend"
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/internal/path"
gunsafe "gioui.org/internal/unsafe" gunsafe "gioui.org/internal/unsafe"
) )
@@ -104,9 +105,33 @@ type pathData struct {
data backend.Buffer data backend.Buffer
} }
// vertex data suitable for passing to vertex programs.
type vertex struct {
// Corner encodes the corner: +0.5 for south, +.25 for east.
Corner float32
MaxY float32
FromX, FromY float32
CtrlX, CtrlY float32
ToX, ToY float32
}
func (v vertex) encode(d []byte, maxy uint32) {
bo := binary.LittleEndian
bo.PutUint32(d[0:], math.Float32bits(v.Corner))
bo.PutUint32(d[4:], maxy)
bo.PutUint32(d[8:], math.Float32bits(v.FromX))
bo.PutUint32(d[12:], math.Float32bits(v.FromY))
bo.PutUint32(d[16:], math.Float32bits(v.CtrlX))
bo.PutUint32(d[20:], math.Float32bits(v.CtrlY))
bo.PutUint32(d[24:], math.Float32bits(v.ToX))
bo.PutUint32(d[28:], math.Float32bits(v.ToY))
}
const ( const (
// Number of path quads per draw batch. // Number of path quads per draw batch.
pathBatchSize = 10000 pathBatchSize = 10000
// Size of a vertex as sent to gpu
vertStride = 7*4 + 2*2
) )
func newPather(ctx backend.Device) *pather { func newPather(ctx backend.Device) *pather {
@@ -152,11 +177,11 @@ func newStenciler(ctx backend.Device) *stenciler {
panic(err) panic(err)
} }
progLayout, err := ctx.NewInputLayout(shader_stencil_vert, []backend.InputDesc{ progLayout, err := ctx.NewInputLayout(shader_stencil_vert, []backend.InputDesc{
{Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).Corner))}, {Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))},
{Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).MaxY))}, {Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))},
{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).FromX))}, {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))},
{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).CtrlX))}, {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))},
{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).ToX))}, {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))},
}) })
if err != nil { if err != nil {
panic(err) panic(err)
@@ -269,7 +294,7 @@ func buildPath(ctx backend.Device, p []byte) *pathData {
panic(err) panic(err)
} }
return &pathData{ return &pathData{
ncurves: len(p) / path.VertStride, ncurves: len(p) / vertStride,
data: buf, data: buf,
} }
} }
@@ -329,8 +354,8 @@ func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv ima
if max := pathBatchSize; batch > max { if max := pathBatchSize; batch > max {
batch = max batch = max
} }
off := path.VertStride * start * 4 off := vertStride * start * 4
s.ctx.BindVertexBuffer(data.data, path.VertStride, off) s.ctx.BindVertexBuffer(data.data, vertStride, off)
s.ctx.DrawElements(backend.DrawModeTriangles, 0, batch*6) s.ctx.DrawElements(backend.DrawModeTriangles, 0, batch*6)
start += batch start += batch
} }
@@ -358,3 +383,11 @@ func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, o
p.UploadUniforms() p.UploadUniforms()
c.ctx.DrawArrays(backend.DrawModeTriangleStrip, 0, 4) c.ctx.DrawArrays(backend.DrawModeTriangleStrip, 0, 4)
} }
func init() {
// Check that struct vertex has the expected size and
// that it contains no padding.
if unsafe.Sizeof(*(*vertex)(nil)) != vertStride {
panic("unexpected struct size")
}
}
+27
View File
@@ -11,6 +11,33 @@ import (
"gioui.org/op" "gioui.org/op"
) )
const QuadSize = 4 * 2 * 3
type Quad struct {
From, Ctrl, To f32.Point
}
func EncodeQuad(d []byte, q Quad) {
bo := binary.LittleEndian
bo.PutUint32(d[0:], math.Float32bits(q.From.X))
bo.PutUint32(d[4:], math.Float32bits(q.From.Y))
bo.PutUint32(d[8:], math.Float32bits(q.Ctrl.X))
bo.PutUint32(d[12:], math.Float32bits(q.Ctrl.Y))
bo.PutUint32(d[16:], math.Float32bits(q.To.X))
bo.PutUint32(d[20:], math.Float32bits(q.To.Y))
}
func DecodeQuad(d []byte) (q Quad) {
bo := binary.LittleEndian
q.From.X = math.Float32frombits(bo.Uint32(d[0:]))
q.From.Y = math.Float32frombits(bo.Uint32(d[4:]))
q.Ctrl.X = math.Float32frombits(bo.Uint32(d[8:]))
q.Ctrl.Y = math.Float32frombits(bo.Uint32(d[12:]))
q.To.X = math.Float32frombits(bo.Uint32(d[16:]))
q.To.Y = math.Float32frombits(bo.Uint32(d[20:]))
return
}
func DecodeTransformOp(d []byte) op.TransformOp { func DecodeTransformOp(d []byte) op.TransformOp {
bo := binary.LittleEndian bo := binary.LittleEndian
if opconst.OpType(d[0]) != opconst.TypeTransform { if opconst.OpType(d[0]) != opconst.TypeTransform {
-27
View File
@@ -1,27 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package path
import (
"unsafe"
)
// The vertex data suitable for passing to vertex programs.
type Vertex struct {
// Corner encodes the corner: +0.5 for south, +.25 for east.
Corner float32
MaxY float32
FromX, FromY float32
CtrlX, CtrlY float32
ToX, ToY float32
}
const VertStride = 7*4 + 2*2
func init() {
// Check that struct vertex has the expected size and
// that it contains no padding.
if unsafe.Sizeof(*(*Vertex)(nil)) != VertStride {
panic("unexpected struct size")
}
}
+24 -119
View File
@@ -9,7 +9,7 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/opconst" "gioui.org/internal/opconst"
"gioui.org/internal/path" "gioui.org/internal/ops"
"gioui.org/op" "gioui.org/op"
) )
@@ -21,12 +21,11 @@ import (
// Path generates no garbage and can be used for dynamic paths; path // Path generates no garbage and can be used for dynamic paths; path
// 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 *op.Ops ops *op.Ops
contour int contour int
pen f32.Point pen f32.Point
bounds f32.Rectangle macro op.MacroOp
hasBounds bool start f32.Point
macro op.MacroOp
} }
// Op sets the current clip to the intersection of // Op sets the current clip to the intersection of
@@ -54,22 +53,24 @@ func (p Op) Add(o *op.Ops) {
func (p *Path) Begin(ops *op.Ops) { func (p *Path) Begin(ops *op.Ops) {
p.ops = ops p.ops = ops
p.macro = op.Record(ops) p.macro = op.Record(ops)
// Write the TypeAux opcode and a byte for marking whether the // Write the TypeAux opcode
// path has had its MaxY filled out. If not, the gpu will fill it data := ops.Write(opconst.TypeAuxLen)
// before using it.
data := ops.Write(2)
data[0] = byte(opconst.TypeAux) data[0] = byte(opconst.TypeAux)
} }
// MoveTo moves the pen to the given position. // MoveTo moves the pen to the given position.
func (p *Path) Move(to f32.Point) { func (p *Path) Move(to f32.Point) {
p.end()
to = to.Add(p.pen) to = to.Add(p.pen)
p.end()
p.pen = to p.pen = to
p.start = to
} }
// end completes the current contour. // end completes the current contour.
func (p *Path) end() { func (p *Path) end() {
if p.pen != p.start {
p.lineTo(p.start)
}
p.contour++ p.contour++
} }
@@ -93,56 +94,15 @@ func (p *Path) Quad(ctrl, to f32.Point) {
} }
func (p *Path) quadTo(ctrl, to f32.Point) { func (p *Path) quadTo(ctrl, to f32.Point) {
// Zero width curves don't contribute to stenciling. data := p.ops.Write(ops.QuadSize + 4)
if p.pen.X == to.X && p.pen.X == ctrl.X { bo := binary.LittleEndian
p.pen = to bo.PutUint32(data[0:], uint32(p.contour))
return ops.EncodeQuad(data[4:], ops.Quad{
} From: p.pen,
Ctrl: ctrl,
bounds := f32.Rectangle{ To: to,
Min: p.pen, })
Max: to, p.pen = to
}.Canon()
// If the curve contain areas where a vertical line
// intersects it twice, split the curve in two x monotone
// lower and upper curves. The stencil fragment program
// expects only one intersection per curve.
// Find the t where the derivative in x is 0.
v0 := ctrl.Sub(p.pen)
v1 := to.Sub(ctrl)
d := v0.X - v1.X
// t = v0 / d. Split if t is in ]0;1[.
if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
t := v0.X / d
ctrl0 := p.pen.Mul(1 - t).Add(ctrl.Mul(t))
ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
p.simpleQuadTo(ctrl0, mid)
p.simpleQuadTo(ctrl1, to)
if mid.X > bounds.Max.X {
bounds.Max.X = mid.X
}
if mid.X < bounds.Min.X {
bounds.Min.X = mid.X
}
} else {
p.simpleQuadTo(ctrl, to)
}
// Find the y extremum, if any.
d = v0.Y - v1.Y
if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
t := v0.Y / d
y := (1-t)*(1-t)*p.pen.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
if y > bounds.Max.Y {
bounds.Max.Y = y
}
if y < bounds.Min.Y {
bounds.Min.Y = y
}
}
p.expand(bounds)
} }
// Cube records a cubic Bézier from the pen through // Cube records a cubic Bézier from the pen through
@@ -223,68 +183,12 @@ func (p *Path) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Po
return splits return splits
} }
func (p *Path) expand(b f32.Rectangle) {
if !p.hasBounds {
p.hasBounds = true
inf := float32(math.Inf(+1))
p.bounds = f32.Rectangle{
Min: f32.Point{X: inf, Y: inf},
Max: f32.Point{X: -inf, Y: -inf},
}
}
p.bounds = p.bounds.Union(b)
}
func (p *Path) vertex(cornerx, cornery int16, ctrl, to f32.Point) {
var corner float32
// Encode corner.
if cornerx == 1 {
corner += .5
}
if cornery == 1 {
corner += .25
}
v := path.Vertex{
Corner: corner,
FromX: p.pen.X,
FromY: p.pen.Y,
CtrlX: ctrl.X,
CtrlY: ctrl.Y,
ToX: to.X,
ToY: to.Y,
}
data := p.ops.Write(path.VertStride)
bo := binary.LittleEndian
bo.PutUint32(data[0:], math.Float32bits(corner))
// Put the contour index in MaxY.
bo.PutUint32(data[4:], uint32(p.contour))
bo.PutUint32(data[8:], math.Float32bits(v.FromX))
bo.PutUint32(data[12:], math.Float32bits(v.FromY))
bo.PutUint32(data[16:], math.Float32bits(v.CtrlX))
bo.PutUint32(data[20:], math.Float32bits(v.CtrlY))
bo.PutUint32(data[24:], math.Float32bits(v.ToX))
bo.PutUint32(data[28:], math.Float32bits(v.ToY))
}
func (p *Path) simpleQuadTo(ctrl, to f32.Point) {
// NW.
p.vertex(-1, 1, ctrl, to)
// NE.
p.vertex(1, 1, ctrl, to)
// SW.
p.vertex(-1, -1, ctrl, to)
// SE.
p.vertex(1, -1, ctrl, to)
p.pen = to
}
// End the path and return a clip operation that represents it. // End the path and return a clip operation that represents it.
func (p *Path) End() Op { func (p *Path) End() Op {
p.end() p.end()
c := p.macro.Stop() c := p.macro.Stop()
return Op{ return Op{
call: c, call: c,
bounds: p.bounds,
} }
} }
@@ -332,6 +236,7 @@ func roundRect(ops *op.Ops, r f32.Rectangle, se, sw, nw, ne float32) Op {
var p Path var p Path
p.Begin(ops) p.Begin(ops)
p.Move(r.Min) p.Move(r.Min)
p.Move(f32.Point{X: w, Y: h - se}) p.Move(f32.Point{X: w, Y: h - se})
p.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE p.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE
p.Line(f32.Point{X: sw - w + se, Y: 0}) p.Line(f32.Point{X: sw - w + se, Y: 0})