mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
op/clip: remove complex stroke support
In a discussion with Raph Levien, the author of our compute renderer implementation, it became clear to me that it's not at all certain that complex strokes will ever be efficiently supported by a GPU renderer. At the same time, the machinery for converting a complex stroke to a GPU-friendly outline has a significant maintenance cost. Further, it is surprising to users that complex strokes are significantly slower and allocate memory. This change removes support for complex strokes, leaving only round-capped, round-joined strokes supported by the compute renderer. The default renderer still converts all strokes to outline, but it also caches the result. This is an API change. The complex stroke conversion code has been moved to the external gioui.org/x/stroke package, with a similar API. Updats gio#282 (Inkeliz brought up the allocation issue) Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+19
-74
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user