diff --git a/gpu/compute.go b/gpu/compute.go index ee4eccc3..1645e061 100644 --- a/gpu/compute.go +++ b/gpu/compute.go @@ -28,7 +28,6 @@ import ( "gioui.org/internal/scene" "gioui.org/layout" "gioui.org/op" - "gioui.org/op/clip" "gioui.org/shader" "gioui.org/shader/gio" "gioui.org/shader/piet" @@ -236,10 +235,10 @@ type encoderState struct { // clipKey completely describes a clip operation (along with its path) and is appropriate // for hashing and equality checks. type clipKey struct { - bounds f32.Rectangle - stroke clip.StrokeStyle - relTrans f32.Affine2D - pathHash uint64 + bounds f32.Rectangle + strokeWidth float32 + relTrans f32.Affine2D + pathHash uint64 } // paintKey completely defines a paint operation. It is suitable for hashing and @@ -1683,7 +1682,7 @@ func (c *opsCollector) reset() { c.layers = c.layers[:0] } -func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, path []byte, key ops.Key, hash uint64, stroke clip.StrokeStyle, push bool) { +func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, path []byte, key ops.Key, hash uint64, strokeWidth float32, push bool) { // Rectangle clip regions. if len(path) == 0 && !push { // If the rectangular clip region contains a previous path it can be discarded. @@ -1713,10 +1712,10 @@ func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, intersect: intersect, push: push, clipKey: clipKey{ - bounds: bounds, - relTrans: state.relTrans, - stroke: stroke, - pathHash: hash, + bounds: bounds, + relTrans: state.relTrans, + strokeWidth: strokeWidth, + pathHash: hash, }, }) state.clip = &c.clipStates[len(c.clipStates)-1] @@ -1742,9 +1741,9 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur key ops.Key hash uint64 } - str clip.StrokeStyle + strWidth float32 ) - c.addClip(&state, fview, fview, nil, ops.Key{}, 0, clip.StrokeStyle{}, false) + c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false) for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { switch ops.OpType(encOp.Data[0]) { case ops.TypeProfile: @@ -1763,7 +1762,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur state.t = st.t state.relTrans = st.relTrans case ops.TypeStroke: - str = decodeStrokeOp(encOp.Data) + strWidth = decodeStrokeOp(encOp.Data) case ops.TypePath: hash := bo.Uint64(encOp.Data[1:]) encOp, ok = r.Decode() @@ -1776,9 +1775,9 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur case ops.TypeClip: var op clipOp op.decode(encOp.Data) - c.addClip(&state, fview, op.bounds, pathData.data, pathData.key, pathData.hash, str, op.push) + c.addClip(&state, fview, op.bounds, pathData.data, pathData.key, pathData.hash, strWidth, op.push) pathData.data = nil - str = clip.StrokeStyle{} + strWidth = 0 case ops.TypePopClip: for { push := state.clip.push @@ -1806,7 +1805,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur if paintState.matType == materialTexture { // Clip to the bounds of the image, to hide other images in the atlas. bounds := paintState.image.src.Bounds() - c.addClip(&paintState, fview, layout.FRect(bounds), nil, ops.Key{}, 0, clip.StrokeStyle{}, false) + c.addClip(&paintState, fview, layout.FRect(bounds), nil, ops.Key{}, 0, 0, false) } intersect := paintState.clip.intersect if intersect.Empty() { @@ -2101,9 +2100,9 @@ func encodeOp(viewport image.Point, absOff image.Point, enc *encoder, texOps []t enc.transform(inv) for i := len(op.clipStack) - 1; i >= 0; i-- { cl := op.clipStack[i] - if str := cl.state.stroke; str.Width > 0 { + if w := cl.state.strokeWidth; w > 0 { enc.fillMode(scene.FillModeStroke) - enc.lineWidth(str.Width) + enc.lineWidth(w) fillMode = scene.FillModeStroke } else if fillMode != scene.FillModeNonzero { enc.fillMode(scene.FillModeNonzero) diff --git a/gpu/gpu.go b/gpu/gpu.go index 7cd7df40..0ca6b689 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -27,7 +27,6 @@ import ( "gioui.org/internal/stroke" "gioui.org/layout" "gioui.org/op" - "gioui.org/op/clip" "gioui.org/shader" "gioui.org/shader/gio" @@ -135,15 +134,13 @@ type imageOp struct { place placement } -func decodeStrokeOp(data []byte) clip.StrokeStyle { +func decodeStrokeOp(data []byte) float32 { _ = data[4] if ops.OpType(data[0]) != ops.TypeStroke { panic("invalid op") } bo := binary.LittleEndian - return clip.StrokeStyle{ - Width: math.Float32frombits(bo.Uint32(data[1:])), - } + return math.Float32frombits(bo.Uint32(data[1:])) } type quadsOp struct { @@ -907,9 +904,9 @@ func (k opKey) SetTransform(t f32.Affine2D) opKey { func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) { var ( - quads quadsOp - str clip.StrokeStyle - state drawState + quads quadsOp + strWidth float32 + state drawState ) reset := func() { state = drawState{ @@ -934,7 +931,7 @@ loop: d.transStack = d.transStack[:n-1] case ops.TypeStroke: - str = decodeStrokeOp(encOp.Data) + strWidth = decodeStrokeOp(encOp.Data) case ops.TypePath: encOp, ok = r.Decode() @@ -960,7 +957,7 @@ loop: op.bounds = v.bounds } else { pathData, bounds := d.buildVerts( - quads.aux, trans, op.outline, str, + quads.aux, trans, op.outline, strWidth, ) op.bounds = bounds quads.aux = pathData @@ -974,7 +971,7 @@ loop: } d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, op.push) quads = quadsOp{} - str = clip.StrokeStyle{} + strWidth = 0 case ops.TypePopClip: for { push := state.cpath.push @@ -1343,7 +1340,7 @@ func (d *drawOps) writeVertCache(n int) []byte { } // transform, split paths as needed, calculate maxY, bounds and create GPU vertices. -func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) { +func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, strWidth float32) (verts []byte, bounds f32.Rectangle) { inf := float32(math.Inf(+1)) d.qs.bounds = f32.Rectangle{ Min: f32.Point{X: inf, Y: inf}, @@ -1353,15 +1350,12 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str startLength := len(d.vertCache) switch { - case str.Width > 0: + case strWidth > 0: // Stroke path. ss := stroke.StrokeStyle{ - Width: str.Width, - Miter: str.Miter, - Cap: stroke.StrokeCap(str.Cap), - Join: stroke.StrokeJoin(str.Join), + Width: strWidth, } - quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData) + quads := stroke.StrokePathCommands(ss, pathData) for _, quad := range quads { d.qs.contour = quad.Contour quad.Quad = quad.Quad.Transform(tr) diff --git a/gpu/internal/rendertest/clip_test.go b/gpu/internal/rendertest/clip_test.go index 895adddb..dbee98fc 100644 --- a/gpu/internal/rendertest/clip_test.go +++ b/gpu/internal/rendertest/clip_test.go @@ -125,10 +125,8 @@ func TestTexturedStrokeClipped(t *testing.T) { smallSquares.Add(o) defer op.Offset(f32.Pt(50, 50)).Push(o).Pop() defer clip.Stroke{ - Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), - Style: clip.StrokeStyle{ - Width: 10, - }, + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Width: 10, }.Op().Push(o).Pop() defer clip.RRect{Rect: f32.Rect(-30, -30, 60, 60)}.Push(o).Pop() defer op.Offset(f32.Pt(-10, -10)).Push(o).Pop() @@ -142,10 +140,8 @@ func TestTexturedStroke(t *testing.T) { smallSquares.Add(o) defer op.Offset(f32.Pt(50, 50)).Push(o).Pop() defer clip.Stroke{ - Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), - Style: clip.StrokeStyle{ - Width: 10, - }, + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Width: 10, }.Op().Push(o).Pop() defer op.Offset(f32.Pt(-10, -10)).Push(o).Pop() paint.PaintOp{}.Add(o) @@ -165,175 +161,8 @@ func TestPaintClippedTexture(t *testing.T) { }) } -func TestStrokedPathBevelFlat(t *testing.T) { - run(t, func(o *op.Ops) { - p := newStrokedPath(o) - defer clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2.5, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o).Pop() - - paint.Fill(o, red) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Red) - }) -} - -func TestStrokedPathBevelRound(t *testing.T) { - run(t, func(o *op.Ops) { - p := newStrokedPath(o) - defer clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2.5, - Cap: clip.RoundCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o).Pop() - - paint.Fill(o, red) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Red) - }) -} - -func TestStrokedPathBevelSquare(t *testing.T) { - run(t, func(o *op.Ops) { - p := newStrokedPath(o) - defer clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2.5, - Cap: clip.SquareCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o).Pop() - - paint.Fill(o, red) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Red) - }) -} - -func TestStrokedPathRoundRound(t *testing.T) { - run(t, func(o *op.Ops) { - p := newStrokedPath(o) - defer clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2.5, - Cap: clip.RoundCap, - Join: clip.RoundJoin, - }, - }.Op().Push(o).Pop() - - paint.Fill(o, red) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Red) - }) -} - -func TestStrokedPathFlatMiter(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: 5, - }, - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, colornames.Red) - }) -} - -func TestStrokedPathFlatMiterInf(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, colornames.Red) - }) -} - func TestStrokedPathZeroWidth(t *testing.T) { run(t, func(o *op.Ops) { - { - p := new(clip.Path) - p.Begin(o) - p.Move(f32.Pt(10, 50)) - p.Line(f32.Pt(50, 0)) - cl := clip.Stroke{ - Path: p.End(), - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - - paint.Fill(o, black) - cl.Pop() - } - { p := new(clip.Path) p.Begin(o) @@ -354,213 +183,3 @@ func TestStrokedPathZeroWidth(t *testing.T) { r.expect(65, 50, transparent) }) } - -func TestDashedPathFlatCapEllipse(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newEllipsePath(o) - - var dash clip.Dash - dash.Begin(o) - dash.Dash(5) - dash.Dash(3) - - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - Dashes: dash.End(), - }.Op().Push(o) - - paint.Fill( - o, - red, - ) - cl.Pop() - } - { - p := newEllipsePath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - }, - }.Op().Push(o) - - paint.Fill( - o, - black, - ) - cl.Pop() - } - - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(0, 62, colornames.Red) - r.expect(0, 65, colornames.Black) - }) -} - -func TestDashedPathFlatCapZ(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newZigZagPath(o) - var dash clip.Dash - dash.Begin(o) - dash.Dash(5) - dash.Dash(3) - - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - Dashes: dash.End(), - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, colornames.Red) - r.expect(46, 12, transparent) - }) -} - -func TestDashedPathFlatCapZNoDash(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newZigZagPath(o) - var dash clip.Dash - dash.Begin(o) - dash.Phase(1) - - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - Dashes: dash.End(), - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - { - cl := clip.Stroke{ - Path: newZigZagPath(o), - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, colornames.Red) - r.expect(46, 12, colornames.Red) - }) -} - -func TestDashedPathFlatCapZNoPath(t *testing.T) { - run(t, func(o *op.Ops) { - { - var dash clip.Dash - dash.Begin(o) - dash.Dash(0) - cl := clip.Stroke{ - Path: newZigZagPath(o), - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - Dashes: dash.End(), - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, transparent) - r.expect(46, 12, transparent) - }) -} - -func newStrokedPath(o *op.Ops) clip.PathSpec { - p := new(clip.Path) - p.Begin(o) - p.Move(f32.Pt(10, 50)) - p.Line(f32.Pt(10, 0)) - p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi) - p.Line(f32.Pt(10, 0)) - p.Line(f32.Pt(10, 10)) - p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi) - p.Line(f32.Pt(-20, 0)) - p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30)) - return p.End() -} - -func newZigZagPath(o *op.Ops) clip.PathSpec { - p := new(clip.Path) - p.Begin(o) - p.Move(f32.Pt(40, 10)) - p.Line(f32.Pt(50, 0)) - p.Line(f32.Pt(-50, 50)) - p.Line(f32.Pt(50, 0)) - p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50)) - p.Line(f32.Pt(50, 0)) - return p.End() -} - -func newEllipsePath(o *op.Ops) clip.PathSpec { - p := new(clip.Path) - p.Begin(o) - p.Move(f32.Pt(0, 65)) - p.Line(f32.Pt(20, 0)) - p.Arc(f32.Pt(20, 0), f32.Pt(70, 0), 2*math.Pi) - return p.End() -} diff --git a/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png b/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png deleted file mode 100644 index 79bae384..00000000 Binary files a/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png b/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png deleted file mode 100644 index 12212e99..00000000 Binary files a/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png b/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png deleted file mode 100644 index d315f0f8..00000000 Binary files a/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png b/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png deleted file mode 100644 index 94c160ef..00000000 Binary files a/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png b/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png deleted file mode 100644 index 9d442f5a..00000000 Binary files a/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png b/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png deleted file mode 100644 index a37235cc..00000000 Binary files a/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png b/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png deleted file mode 100644 index 8d2919d3..00000000 Binary files a/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png deleted file mode 100644 index ae6472a7..00000000 Binary files a/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png deleted file mode 100644 index d315f0f8..00000000 Binary files a/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png and /dev/null differ diff --git a/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png b/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png deleted file mode 100644 index 8ef5a942..00000000 Binary files a/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png and /dev/null differ diff --git a/internal/stroke/dash.go b/internal/stroke/dash.go deleted file mode 100644 index 58745379..00000000 --- a/internal/stroke/dash.go +++ /dev/null @@ -1,394 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// The algorithms to compute dashes have been extracted, adapted from -// (and used as a reference implementation): -// - github.com/tdewolff/canvas (Licensed under MIT) - -package stroke - -import ( - "math" - "sort" - - "gioui.org/f32" -) - -type DashOp struct { - Phase float32 - Dashes []float32 -} - -func IsSolidLine(sty DashOp) bool { - return sty.Phase == 0 && len(sty.Dashes) == 0 -} - -func (qs StrokeQuads) dash(sty DashOp) StrokeQuads { - sty = dashCanonical(sty) - - switch { - case len(sty.Dashes) == 0: - return qs - case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0: - return StrokeQuads{} - } - - if len(sty.Dashes)%2 == 1 { - // If the dash pattern is of uneven length, dash and space lengths - // alternate. The following duplicates the pattern so that uneven - // indices are always spaces. - sty.Dashes = append(sty.Dashes, sty.Dashes...) - } - - var ( - i0, pos0 = dashStart(sty) - out StrokeQuads - - contour uint32 = 1 - ) - - for _, ps := range qs.split() { - var ( - i = i0 - pos = pos0 - t []float64 - length = ps.len() - ) - for pos+sty.Dashes[i] < length { - pos += sty.Dashes[i] - if 0.0 < pos { - t = append(t, float64(pos)) - } - i++ - if i == len(sty.Dashes) { - i = 0 - } - } - - j0 := 0 - endsInDash := i%2 == 0 - if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash { - j0 = 1 - } - - var ( - qd StrokeQuads - pd = ps.splitAt(&contour, t...) - ) - for j := j0; j < len(pd)-1; j += 2 { - qd = qd.append(pd[j]) - } - if endsInDash { - if ps.closed() { - qd = pd[len(pd)-1].append(qd) - } else { - qd = qd.append(pd[len(pd)-1]) - } - } - out = out.append(qd) - contour++ - } - return out -} - -func dashCanonical(sty DashOp) DashOp { - var ( - o = sty - ds = o.Dashes - ) - - if len(sty.Dashes) == 0 { - return sty - } - - // Remove zeros except first and last. - for i := 1; i < len(ds)-1; i++ { - if f32Eq(ds[i], 0.0) { - ds[i-1] += ds[i+1] - ds = append(ds[:i], ds[i+2:]...) - i-- - } - } - - // Remove first zero, collapse with second and last. - if f32Eq(ds[0], 0.0) { - if len(ds) < 3 { - return DashOp{ - Phase: 0.0, - Dashes: []float32{0.0}, - } - } - o.Phase -= ds[1] - ds[len(ds)-1] += ds[1] - ds = ds[2:] - } - - // Remove last zero, collapse with fist and second to last. - if f32Eq(ds[len(ds)-1], 0.0) { - if len(ds) < 3 { - return DashOp{} - } - o.Phase += ds[len(ds)-2] - ds[0] += ds[len(ds)-2] - ds = ds[:len(ds)-2] - } - - // If there are zeros or negatives, don't draw dashes. - for i := 0; i < len(ds); i++ { - if ds[i] < 0.0 || f32Eq(ds[i], 0.0) { - return DashOp{ - Phase: 0.0, - Dashes: []float32{0.0}, - } - } - } - - // Remove repeated patterns. -loop: - for len(ds)%2 == 0 { - mid := len(ds) / 2 - for i := 0; i < mid; i++ { - if !f32Eq(ds[i], ds[mid+i]) { - break loop - } - } - ds = ds[:mid] - } - return o -} - -func dashStart(sty DashOp) (int, float32) { - i0 := 0 // i0 is the index into dashes. - for sty.Dashes[i0] <= sty.Phase { - sty.Phase -= sty.Dashes[i0] - i0++ - if i0 == len(sty.Dashes) { - i0 = 0 - } - } - // pos0 may be negative if the offset lands halfway into dash. - pos0 := -sty.Phase - if sty.Phase < 0.0 { - var sum float32 - for _, d := range sty.Dashes { - sum += d - } - pos0 = -(sum + sty.Phase) // handle negative offsets - } - return i0, pos0 -} - -func (qs StrokeQuads) len() float32 { - var sum float32 - for i := range qs { - q := qs[i].Quad - sum += quadBezierLen(q.From, q.Ctrl, q.To) - } - return sum -} - -// splitAt splits the path into separate paths at the specified intervals -// along the path. -// splitAt updates the provided contour counter as it splits the segments. -func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads { - if len(ts) == 0 { - qs.setContour(*contour) - return []StrokeQuads{qs} - } - - sort.Float64s(ts) - if ts[0] == 0 { - ts = ts[1:] - } - - var ( - j int // index into ts - t float64 // current position along curve - ) - - var oo []StrokeQuads - var oi StrokeQuads - push := func() { - oo = append(oo, oi) - oi = nil - } - - for _, ps := range qs.split() { - for _, q := range ps { - if j == len(ts) { - oi = append(oi, q) - continue - } - speed := func(t float64) float64 { - return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl, q.Quad.To, float32(t)))) - } - invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, speed, 0, 1) - - var ( - t0 float64 - r0 = q.Quad.From - r1 = q.Quad.Ctrl - r2 = q.Quad.To - - // from keeps track of the start of the 'running' segment. - from = r0 - ) - for j < len(ts) && t < ts[j] && ts[j] <= t+dt { - tj := invL(ts[j] - t) - tsub := (tj - t0) / (1.0 - t0) - t0 = tj - - var q1 f32.Point - _, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, float32(tsub)) - - oi = append(oi, StrokeQuad{ - Contour: *contour, - Quad: QuadSegment{ - From: from, - Ctrl: q1, - To: r0, - }, - }) - push() - (*contour)++ - - from = r0 - j++ - } - if !f64Eq(t0, 1) { - if len(oi) > 0 { - r0 = oi.pen() - } - oi = append(oi, StrokeQuad{ - Contour: *contour, - Quad: QuadSegment{ - From: r0, - Ctrl: r1, - To: r2, - }, - }) - } - t += dt - } - } - if len(oi) > 0 { - push() - (*contour)++ - } - - return oo -} - -func f32Eq(a, b float32) bool { - const epsilon = 1e-10 - return math.Abs(float64(a-b)) < epsilon -} - -func f64Eq(a, b float64) bool { - const epsilon = 1e-10 - return math.Abs(a-b) < epsilon -} - -func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc, fp func(float64) float64, tmin, tmax float64) (func(float64) float64, float64) { - // The TODOs below are copied verbatim from tdewolff/canvas: - // - // TODO: find better way to determine N. For Arc 10 seems fine, for some - // Quads 10 is too low, for Cube depending on inflection points is - // maybe not the best indicator - // - // TODO: track efficiency, how many times is fp called? - // Does a look-up table make more sense? - fLength := func(t float64) float64 { - return math.Abs(gaussLegendre(fp, tmin, t)) - } - totalLength := fLength(tmax) - t := func(L float64) float64 { - return bisectionMethod(fLength, L, tmin, tmax) - } - return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin, tmax), totalLength -} - -func polynomialChebyshevApprox(N int, f func(float64) float64, xmin, xmax, ymin, ymax float64) func(float64) float64 { - var ( - invN = 1.0 / float64(N) - fs = make([]float64, N) - ) - for k := 0; k < N; k++ { - u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN) - fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1)) - } - - c := make([]float64, N) - for j := 0; j < N; j++ { - var a float64 - for k := 0; k < N; k++ { - a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N)) - } - c[j] = 2 * invN * a - } - - if ymax < ymin { - ymin, ymax = ymax, ymin - } - return func(x float64) float64 { - x = math.Min(xmax, math.Max(xmin, x)) - u := (x-xmin)/(xmax-xmin)*2 - 1 - var a float64 - for j := 0; j < N; j++ { - a += c[j] * math.Cos(float64(j)*math.Acos(u)) - } - y := -0.5*c[0] + a - if !math.IsNaN(ymin) && !math.IsNaN(ymax) { - y = math.Min(ymax, math.Max(ymin, y)) - } - return y - } -} - -// bisectionMethod finds the value x for which f(x) = y in the interval x -// in [xmin, xmax] using the bisection method. -func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 { - const ( - maxIter = 100 - tolerance = 0.001 // 0.1% - ) - - var ( - n = 0 - x float64 - tolX = math.Abs(xmax-xmin) * tolerance - tolY = math.Abs(f(xmax)-f(xmin)) * tolerance - ) - for { - x = 0.5 * (xmin + xmax) - if n >= maxIter { - return x - } - - dy := f(x) - y - switch { - case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX: - return x - case dy > 0: - xmax = x - default: - xmin = x - } - n++ - } -} - -type gaussLegendreFunc func(func(float64) float64, float64, float64) float64 - -// Gauss-Legendre quadrature integration from a to b with n=7 -func gaussLegendre7(f func(float64) float64, a, b float64) float64 { - c := 0.5 * (b - a) - d := 0.5 * (a + b) - Qd1 := f(-0.949108*c + d) - Qd2 := f(-0.741531*c + d) - Qd3 := f(-0.405845*c + d) - Qd4 := f(d) - Qd5 := f(0.405845*c + d) - Qd6 := f(0.741531*c + d) - Qd7 := f(0.949108*c + d) - return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4) -} diff --git a/internal/stroke/stroke.go b/internal/stroke/stroke.go index fda42dbd..95025eb8 100644 --- a/internal/stroke/stroke.go +++ b/internal/stroke/stroke.go @@ -40,26 +40,8 @@ import ( // op/clip, eliminating the duplicate types. type StrokeStyle struct { Width float32 - Miter float32 - Cap StrokeCap - Join StrokeJoin } -type StrokeCap uint8 - -const ( - RoundCap StrokeCap = iota - FlatCap - SquareCap -) - -type StrokeJoin uint8 - -const ( - RoundJoin StrokeJoin = iota - BevelJoin -) - // strokeTolerance is used to reconcile rounding errors arising // when splitting quads into smaller and smaller segments to approximate // them into straight lines, and when joining back segments. @@ -97,12 +79,6 @@ func (qs *StrokeQuads) pen() f32.Point { return (*qs)[len(*qs)-1].Quad.To } -func (qs *StrokeQuads) closed() bool { - beg := (*qs)[0].Quad.From - end := (*qs)[len(*qs)-1].Quad.To - return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y) -} - func (qs *StrokeQuads) lineTo(pt f32.Point) { end := qs.pen() *qs = append(*qs, StrokeQuad{ @@ -155,11 +131,7 @@ func (qs StrokeQuads) split() []StrokeQuads { return o } -func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { - if !IsSolidLine(dashes) { - qs = qs.dash(dashes) - } - +func (qs StrokeQuads) stroke(stroke StrokeStyle) StrokeQuads { var ( o StrokeQuads hw = 0.5 * stroke.Width @@ -441,29 +413,6 @@ func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point { return p.Mul(2) } -// quadBezierLen returns the length of the Bézier curve. -// See: -// https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/ -func quadBezierLen(p0, p1, p2 f32.Point) float32 { - a := p0.Sub(p1.Mul(2)).Add(p2) - b := p1.Mul(2).Sub(p0.Mul(2)) - A := float64(4 * dotPt(a, a)) - B := float64(4 * dotPt(a, b)) - C := float64(dotPt(b, b)) - if f64Eq(A, 0.0) { - // p1 is in the middle between p0 and p2, - // so it is a straight line from p0 to p2. - return lenPt(p2.Sub(p0)) - } - - Sabc := 2 * math.Sqrt(A+B+C) - A2 := math.Sqrt(A) - A32 := 2 * A * A2 - C2 := 2 * math.Sqrt(C) - BA := B / A2 - return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32)) -} - func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads { // Gio strokes are only quadratic Bézier curves, w/o any inflection point. // So we just have to flatten them. @@ -549,27 +498,7 @@ 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 operation. func strokePathJoin(stroke 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 stroke.Join { - case BevelJoin: - strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) - case RoundJoin: - strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) - default: - panic("impossible") - } -} - -func strokePathBevelJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { - - rp := pivot.Add(n1) - lp := pivot.Sub(n1) - - rhs.lineTo(rp) - lhs.lineTo(lp) + strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) } func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { @@ -594,79 +523,9 @@ func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Po } } -func strokePathMiterJoin(stroke 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(stroke.Miter), 1.001) - - cw := dotPt(rot90CW(n0), n1) >= 0.0 - if cw { - // hw is used to calculate |R|. - // When running CW, n0 and n1 point the other way, - // so the sign of r0 and r1 is negated. - hw = -hw - } - hw64 := float64(hw) - - cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1)))) - d := hw64 / cos - if math.Abs(limit*hw64) < math.Abs(d) { - 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))) - - rp := pivot.Add(n1) - lp := pivot.Sub(n1) - switch { - case cw: - // Path bends to the right, ie. CW. - lhs.lineTo(mid) - default: - // Path bends to the left, ie. CCW. - rhs.lineTo(mid) - } - rhs.lineTo(rp) - lhs.lineTo(lp) -} - // strokePathCap caps the provided path qs, according to the provided stroke operation. func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { - switch stroke.Cap { - case FlatCap: - strokePathFlatCap(qs, hw, pivot, n0) - case SquareCap: - strokePathSquareCap(qs, hw, pivot, n0) - case RoundCap: - strokePathRoundCap(qs, hw, pivot, n0) - default: - panic("impossible") - } -} - -// strokePathFlatCap caps the start or end of a path with a flat cap. -func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { - end := pivot.Sub(n0) - qs.lineTo(end) -} - -// strokePathSquareCap caps the start or end of a path with a square cap. -func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { - var ( - e = pivot.Add(rot90CCW(n0)) - corner1 = e.Add(n0) - corner2 = e.Sub(n0) - end = pivot.Sub(n0) - ) - - qs.lineTo(corner1) - qs.lineTo(corner2) - qs.lineTo(end) + strokePathRoundCap(qs, hw, pivot, n0) } // strokePathRoundCap caps the start or end of a path with a round cap. @@ -763,9 +622,9 @@ func dist(p1, p2 f32.Point) float64 { return math.Hypot(dx, dy) } -func StrokePathCommands(style StrokeStyle, dashes DashOp, scene []byte) StrokeQuads { +func StrokePathCommands(style StrokeStyle, scene []byte) StrokeQuads { quads := decodeToStrokeQuads(scene) - return quads.stroke(style, dashes) + return quads.stroke(style) } // decodeToStrokeQuads decodes scene commands to quads ready to stroke. diff --git a/op/clip/clip.go b/op/clip/clip.go index 7a0a2f26..ad79e2db 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -21,8 +21,7 @@ type Op struct { path PathSpec outline bool - stroke StrokeStyle - dashes DashSpec + width float32 } // Stack represents an Op pushed on the clip stack. @@ -54,17 +53,8 @@ func (p Op) Add(o *op.Ops) { } func (p Op) add(o *op.Ops, push bool) { - str := p.stroke path := p.path outline := p.outline - approx := str.Width > 0 && !(p.dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap) - if approx { - // If the stroke is not natively supported by the compute renderer, construct a filled path - // that approximates it. - path = p.approximateStroke(o) - str = StrokeStyle{} - outline = true - } bo := binary.LittleEndian if path.hasSegments { @@ -75,9 +65,9 @@ func (p Op) add(o *op.Ops, push bool) { } bounds := path.bounds - if str.Width > 0 { + if p.width > 0 { // Expand bounds to cover stroke. - half := int(str.Width*.5 + .5) + half := int(p.width*.5 + .5) bounds.Min.X -= half bounds.Min.Y -= half bounds.Max.X += half @@ -85,7 +75,7 @@ func (p Op) add(o *op.Ops, push bool) { data := o.Internal.Write(ops.TypeStrokeLen) data[0] = byte(ops.TypeStroke) bo := binary.LittleEndian - bo.PutUint32(data[1:], math.Float32bits(str.Width)) + bo.PutUint32(data[1:], math.Float32bits(p.width)) } data := o.Internal.Write(ops.TypeClipLen) @@ -108,66 +98,6 @@ func (s Stack) Pop() { data[0] = byte(ops.TypePopClip) } -func (p Op) approximateStroke(o *op.Ops) PathSpec { - if !p.path.hasSegments { - return PathSpec{} - } - - var r ops.Reader - // Add path op for us to decode. Use a macro to omit it from later decodes. - ignore := op.Record(o) - r.ResetAt(&o.Internal, o.Internal.PC()) - p.path.spec.Add(o) - ignore.Stop() - encOp, ok := r.Decode() - if !ok || ops.OpType(encOp.Data[0]) != ops.TypeAux { - panic("corrupt path data") - } - pathData := encOp.Data[ops.TypeAuxLen:] - - // Decode dashes in a similar way. - var dashes stroke.DashOp - if p.dashes.phase != 0 || p.dashes.size > 0 { - ignore := op.Record(o) - r.ResetAt(&o.Internal, o.Internal.PC()) - p.dashes.spec.Add(o) - ignore.Stop() - encOp, ok := r.Decode() - if !ok || ops.OpType(encOp.Data[0]) != ops.TypeAux { - panic("corrupt dash data") - } - dashes.Dashes = make([]float32, p.dashes.size) - dashData := encOp.Data[ops.TypeAuxLen:] - bo := binary.LittleEndian - for i := range dashes.Dashes { - dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:])) - } - dashes.Phase = p.dashes.phase - } - - // Approximate and output path data. - var outline Path - outline.Begin(o) - ss := stroke.StrokeStyle{ - Width: p.stroke.Width, - Miter: p.stroke.Miter, - Cap: stroke.StrokeCap(p.stroke.Cap), - Join: stroke.StrokeJoin(p.stroke.Join), - } - quads := stroke.StrokePathCommands(ss, dashes, pathData) - pen := f32.Pt(0, 0) - for _, quad := range quads { - q := quad.Quad - if q.From != pen { - pen = q.From - outline.MoveTo(pen) - } - outline.contour = int(quad.Contour) - outline.QuadTo(q.Ctrl, q.To) - } - return outline.End() -} - type PathSpec struct { spec op.CallOp // open is true if any path contour is not closed. A closed contour starts @@ -382,6 +312,21 @@ func (p *Path) Close() { p.end() } +// Stroke represents a stroked path. +type Stroke struct { + Path PathSpec + // Width of the stroked path. + Width float32 +} + +// Op returns a clip operation representing the stroke. +func (s Stroke) Op() Op { + return Op{ + path: s.Path, + width: s.Width, + } +} + // Outline represents the area inside of a path, according to the // non-zero winding rule. type Outline struct { diff --git a/op/clip/stroke.go b/op/clip/stroke.go deleted file mode 100644 index 623d57a9..00000000 --- a/op/clip/stroke.go +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package clip - -import ( - "encoding/binary" - "math" - - "gioui.org/internal/ops" - "gioui.org/op" -) - -// Stroke represents a stroked path. -type Stroke struct { - Path PathSpec - Style StrokeStyle - - // Dashes specify the dashes of the stroke. - // The empty value denotes no dashes. - Dashes DashSpec -} - -// Op returns a clip operation representing the stroke. -func (s Stroke) Op() Op { - return Op{ - path: s.Path, - stroke: s.Style, - dashes: s.Dashes, - } -} - -// StrokeStyle describes how a path should be stroked. -type StrokeStyle struct { - Width float32 // Width of the stroked path. - - // Miter is the limit to apply to a miter joint. - // The zero Miter disables the miter joint; setting Miter to +∞ - // unconditionally enables the miter joint. - Miter float32 - Cap StrokeCap // Cap describes the head or tail of a stroked path. - Join StrokeJoin // Join describes how stroked paths are collated. -} - -// StrokeCap describes the head or tail of a stroked path. -type StrokeCap uint8 - -const ( - // RoundCap caps stroked paths with a round cap, joining the right-hand and - // left-hand sides of a stroked path with a half disc of diameter the - // stroked path's width. - RoundCap StrokeCap = iota - - // FlatCap caps stroked paths with a flat cap, joining the right-hand - // and left-hand sides of a stroked path with a straight line. - FlatCap - - // SquareCap caps stroked paths with a square cap, joining the right-hand - // and left-hand sides of a stroked path with a half square of length - // the stroked path's width. - SquareCap -) - -// StrokeJoin describes how stroked paths are collated. -type StrokeJoin uint8 - -const ( - // RoundJoin joins path segments with a round segment. - RoundJoin StrokeJoin = iota - - // BevelJoin joins path segments with sharp bevels. - BevelJoin -) - -// Dash records dashes' lengths and phase for a stroked path. -type Dash struct { - ops *ops.Ops - macro op.MacroOp - phase float32 - size uint8 // size of the pattern -} - -func (d *Dash) Begin(o *op.Ops) { - d.ops = &o.Internal - d.macro = op.Record(o) - // Write the TypeAux opcode - data := d.ops.Write(ops.TypeAuxLen) - data[0] = byte(ops.TypeAux) -} - -func (d *Dash) Phase(v float32) { - d.phase = v -} - -func (d *Dash) Dash(length float32) { - if d.size == math.MaxUint8 { - panic("clip: dash pattern too large") - } - data := d.ops.Write(4) - bo := binary.LittleEndian - bo.PutUint32(data[0:], math.Float32bits(length)) - d.size++ -} - -func (d *Dash) End() DashSpec { - c := d.macro.Stop() - return DashSpec{ - spec: c, - phase: d.phase, - size: d.size, - } -} - -// DashSpec describes a dashed pattern. -type DashSpec struct { - spec op.CallOp - phase float32 - size uint8 // size of the pattern -} diff --git a/widget/border.go b/widget/border.go index 694c9281..8d1bcd10 100644 --- a/widget/border.go +++ b/widget/border.go @@ -35,7 +35,7 @@ func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { b.Color, clip.Stroke{ Path: clip.UniformRRect(r, rr).Path(gtx.Ops), - Style: clip.StrokeStyle{Width: width}, + Width: width, }.Op(), ) diff --git a/widget/material/loader.go b/widget/material/loader.go index 7a57bbe0..904a214d 100644 --- a/widget/material/loader.go +++ b/widget/material/loader.go @@ -74,10 +74,7 @@ func clipLoader(ops *op.Ops, startAngle, endAngle, radius float32) clip.Op { p.Move(pen) p.Arc(center, center, delta) return clip.Stroke{ - Path: p.End(), - Style: clip.StrokeStyle{ - Width: width, - Cap: clip.FlatCap, - }, + Path: p.End(), + Width: width, }.Op() }