The normal vector size and direction depend on the input point and the sign of the unit value provided, this patch fixes all edge cases.
Fixes: https://todo.sr.ht/~eliasnaur/gio/576
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
---
internal/stroke/stroke.go | 13 ++++-
internal/stroke/stroke_test.go | 88 ++++++++++++++++++++++++++++++++++
2 files changed, 99 insertions(+), 2 deletions(-)
diff --git a/internal/stroke/stroke.go b/internal/stroke/stroke.go
index 1a647de5..8ac348e1 100644
--- a/internal/stroke/stroke.go
+++ b/internal/stroke/stroke.go
@@ -327,8 +327,12 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
func normPt(p f32.Point, l float32) f32.Point {
- if (p.X == 0 && p.Y == l) || (p.Y == 0 && p.X == l) {
- return f32.Point{X: p.X, Y: p.Y}
+ if p.X == 0 && abs(p.Y) == abs(l) || p.Y == 0 && abs(p.X) == abs(l) {
+ if math.Signbit(float64(l)) {
+ return f32.Point{X: -p.X, Y: -p.Y}
+ } else {
+ return f32.Point{X: p.X, Y: p.Y}
+ }
}
d := math.Hypot(float64(p.X), float64(p.Y))
l64 := float64(l)
@@ -749,3 +753,8 @@ func approxCubeTo(quads *[]QuadSegment, splits int, maxDistSq float32, from, ctr
splits = approxCubeTo(quads, splits, maxDistSq, c0112, c12, c2, to)
return splits
}
+
+// abs is a 32bit version of math.Abs
+func abs(x float32) float32 {
+ return math.Float32frombits(math.Float32bits(x) &^ (1 << 31))
+}
diff --git a/internal/stroke/stroke_test.go b/internal/stroke/stroke_test.go
index b6ffc547..a3706319 100644
--- a/internal/stroke/stroke_test.go
+++ b/internal/stroke/stroke_test.go
@@ -9,6 +9,94 @@ import (
"gioui.org/internal/f32"
)
+func TestNormPt(t *testing.T) {
+ type scenario struct {
+ l float32
+ ptIn, ptOut f32.Point
+ }
+
+ scenarios := []scenario{
+ // l>0 & X
+ {l: +20, ptIn: f32.Point{X: +30, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
+ {l: +20, ptIn: f32.Point{X: +20, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
+ {l: +20, ptIn: f32.Point{X: +10, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
+ {l: +20, ptIn: f32.Point{X: -10, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
+ {l: +20, ptIn: f32.Point{X: -20, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
+ {l: +20, ptIn: f32.Point{X: -30, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
+
+ // l<0 & X
+ {l: -20, ptIn: f32.Point{X: +30, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
+ {l: -20, ptIn: f32.Point{X: +20, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
+ {l: -20, ptIn: f32.Point{X: +10, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
+ {l: -20, ptIn: f32.Point{X: -10, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
+ {l: -20, ptIn: f32.Point{X: -20, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
+ {l: -20, ptIn: f32.Point{X: -30, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
+
+ // l>0 & Y
+ {l: +20, ptIn: f32.Point{X: 0, Y: +30}, ptOut: f32.Point{X: 0, Y: +20}},
+ {l: +20, ptIn: f32.Point{X: 0, Y: +20}, ptOut: f32.Point{X: 0, Y: +20}},
+ {l: +20, ptIn: f32.Point{X: 0, Y: +10}, ptOut: f32.Point{X: 0, Y: +20}},
+ {l: +20, ptIn: f32.Point{X: 0, Y: -10}, ptOut: f32.Point{X: 0, Y: -20}},
+ {l: +20, ptIn: f32.Point{X: 0, Y: -20}, ptOut: f32.Point{X: 0, Y: -20}},
+ {l: +20, ptIn: f32.Point{X: 0, Y: -30}, ptOut: f32.Point{X: 0, Y: -20}},
+
+ // l<0 & Y
+ {l: -20, ptIn: f32.Point{X: 0, Y: +30}, ptOut: f32.Point{X: 0, Y: -20}},
+ {l: -20, ptIn: f32.Point{X: 0, Y: +20}, ptOut: f32.Point{X: 0, Y: -20}},
+ {l: -20, ptIn: f32.Point{X: 0, Y: +10}, ptOut: f32.Point{X: 0, Y: -20}},
+ {l: -20, ptIn: f32.Point{X: 0, Y: -10}, ptOut: f32.Point{X: 0, Y: +20}},
+ {l: -20, ptIn: f32.Point{X: 0, Y: -20}, ptOut: f32.Point{X: 0, Y: +20}},
+ {l: -20, ptIn: f32.Point{X: 0, Y: -30}, ptOut: f32.Point{X: 0, Y: +20}},
+
+ // l>0 && X=Y
+ {l: +20, ptIn: f32.Point{X: +90, Y: +90}, ptOut: f32.Point{X: +14.142137, Y: +14.142137}},
+ {l: +20, ptIn: f32.Point{X: +30, Y: +30}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
+ {l: +20, ptIn: f32.Point{X: +20, Y: +20}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
+ {l: +20, ptIn: f32.Point{X: +10, Y: +10}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
+ {l: +20, ptIn: f32.Point{X: -10, Y: -10}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
+ {l: +20, ptIn: f32.Point{X: -20, Y: -20}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
+ {l: +20, ptIn: f32.Point{X: -30, Y: -30}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
+ {l: +20, ptIn: f32.Point{X: -90, Y: -90}, ptOut: f32.Point{X: -14.142137, Y: -14.142137}},
+
+ // l>0 && X=-Y
+ {l: +20, ptIn: f32.Point{X: +90, Y: -90}, ptOut: f32.Point{X: +14.142137, Y: -14.142137}},
+ {l: +20, ptIn: f32.Point{X: +30, Y: -30}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
+ {l: +20, ptIn: f32.Point{X: +20, Y: -20}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
+ {l: +20, ptIn: f32.Point{X: +10, Y: -10}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
+ {l: +20, ptIn: f32.Point{X: -10, Y: +10}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
+ {l: +20, ptIn: f32.Point{X: -20, Y: +20}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
+ {l: +20, ptIn: f32.Point{X: -30, Y: +30}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
+ {l: +20, ptIn: f32.Point{X: -90, Y: +90}, ptOut: f32.Point{X: -14.142137, Y: +14.142137}},
+
+ // l<0 && X=Y
+ {l: -20, ptIn: f32.Point{X: +90, Y: +90}, ptOut: f32.Point{X: -14.142137, Y: -14.142137}},
+ {l: -20, ptIn: f32.Point{X: +30, Y: +30}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
+ {l: -20, ptIn: f32.Point{X: +20, Y: +20}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
+ {l: -20, ptIn: f32.Point{X: +10, Y: +10}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
+ {l: -20, ptIn: f32.Point{X: -10, Y: -10}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
+ {l: -20, ptIn: f32.Point{X: -20, Y: -20}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
+ {l: -20, ptIn: f32.Point{X: -30, Y: -30}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
+ {l: -20, ptIn: f32.Point{X: -90, Y: -90}, ptOut: f32.Point{X: +14.142137, Y: +14.142137}},
+
+ // l<0 && X=-Y
+ {l: -20, ptIn: f32.Point{X: +90, Y: -90}, ptOut: f32.Point{X: -14.142137, Y: +14.142137}},
+ {l: -20, ptIn: f32.Point{X: +30, Y: -30}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
+ {l: -20, ptIn: f32.Point{X: +20, Y: -20}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
+ {l: -20, ptIn: f32.Point{X: +10, Y: -10}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
+ {l: -20, ptIn: f32.Point{X: -10, Y: +10}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
+ {l: -20, ptIn: f32.Point{X: -20, Y: +20}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
+ {l: -20, ptIn: f32.Point{X: -30, Y: +30}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
+ {l: -20, ptIn: f32.Point{X: -90, Y: +90}, ptOut: f32.Point{X: +14.142137, Y: -14.142137}},
+ }
+
+ for i, s := range scenarios {
+ actual := normPt(s.ptIn, s.l)
+ if actual != s.ptOut {
+ t.Errorf("%v: in: %v*%v, expected: %v, actual: %v", i, s.l, s.ptIn, s.ptOut, actual)
+ }
+ }
+}
+
func BenchmarkSplitCubic(b *testing.B) {
type scenario struct {
segments int
--
2.39.2
Elias, I think this one fell through the cracks. I'd prefer you to
review it, as I'm less familiar with this section of code, but I'll do
my best if you don't get to it in a week or so.
Cheers,
Chris
On Tue, May 14, 2024 at 4:12 PM Walter Werner SCHNEIDER
<contact@schnwalter.eu> wrote:
>
> The normal vector size and direction depend on the input point and the sign of the unit value provided, this patch fixes all edge cases.
>
> Fixes: https://todo.sr.ht/~eliasnaur/gio/576
> Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
> ---
> internal/stroke/stroke.go | 13 ++++-
> internal/stroke/stroke_test.go | 88 ++++++++++++++++++++++++++++++++++
> 2 files changed, 99 insertions(+), 2 deletions(-)
>
> diff --git a/internal/stroke/stroke.go b/internal/stroke/stroke.go
> index 1a647de5..8ac348e1 100644
> --- a/internal/stroke/stroke.go
> +++ b/internal/stroke/stroke.go
> @@ -327,8 +327,12 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
> func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
>
> func normPt(p f32.Point, l float32) f32.Point {
> - if (p.X == 0 && p.Y == l) || (p.Y == 0 && p.X == l) {
> - return f32.Point{X: p.X, Y: p.Y}
> + if p.X == 0 && abs(p.Y) == abs(l) || p.Y == 0 && abs(p.X) == abs(l) {
> + if math.Signbit(float64(l)) {
> + return f32.Point{X: -p.X, Y: -p.Y}
> + } else {
> + return f32.Point{X: p.X, Y: p.Y}
> + }
> }
> d := math.Hypot(float64(p.X), float64(p.Y))
> l64 := float64(l)
> @@ -749,3 +753,8 @@ func approxCubeTo(quads *[]QuadSegment, splits int, maxDistSq float32, from, ctr
> splits = approxCubeTo(quads, splits, maxDistSq, c0112, c12, c2, to)
> return splits
> }
> +
> +// abs is a 32bit version of math.Abs
> +func abs(x float32) float32 {
> + return math.Float32frombits(math.Float32bits(x) &^ (1 << 31))
> +}
> diff --git a/internal/stroke/stroke_test.go b/internal/stroke/stroke_test.go
> index b6ffc547..a3706319 100644
> --- a/internal/stroke/stroke_test.go
> +++ b/internal/stroke/stroke_test.go
> @@ -9,6 +9,94 @@ import (
> "gioui.org/internal/f32"
> )
>
> +func TestNormPt(t *testing.T) {
> + type scenario struct {
> + l float32
> + ptIn, ptOut f32.Point
> + }
> +
> + scenarios := []scenario{
> + // l>0 & X
> + {l: +20, ptIn: f32.Point{X: +30, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
> + {l: +20, ptIn: f32.Point{X: +20, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
> + {l: +20, ptIn: f32.Point{X: +10, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
> + {l: +20, ptIn: f32.Point{X: -10, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
> + {l: +20, ptIn: f32.Point{X: -20, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
> + {l: +20, ptIn: f32.Point{X: -30, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
> +
> + // l<0 & X
> + {l: -20, ptIn: f32.Point{X: +30, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
> + {l: -20, ptIn: f32.Point{X: +20, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
> + {l: -20, ptIn: f32.Point{X: +10, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}},
> + {l: -20, ptIn: f32.Point{X: -10, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
> + {l: -20, ptIn: f32.Point{X: -20, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
> + {l: -20, ptIn: f32.Point{X: -30, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}},
> +
> + // l>0 & Y
> + {l: +20, ptIn: f32.Point{X: 0, Y: +30}, ptOut: f32.Point{X: 0, Y: +20}},
> + {l: +20, ptIn: f32.Point{X: 0, Y: +20}, ptOut: f32.Point{X: 0, Y: +20}},
> + {l: +20, ptIn: f32.Point{X: 0, Y: +10}, ptOut: f32.Point{X: 0, Y: +20}},
> + {l: +20, ptIn: f32.Point{X: 0, Y: -10}, ptOut: f32.Point{X: 0, Y: -20}},
> + {l: +20, ptIn: f32.Point{X: 0, Y: -20}, ptOut: f32.Point{X: 0, Y: -20}},
> + {l: +20, ptIn: f32.Point{X: 0, Y: -30}, ptOut: f32.Point{X: 0, Y: -20}},
> +
> + // l<0 & Y
> + {l: -20, ptIn: f32.Point{X: 0, Y: +30}, ptOut: f32.Point{X: 0, Y: -20}},
> + {l: -20, ptIn: f32.Point{X: 0, Y: +20}, ptOut: f32.Point{X: 0, Y: -20}},
> + {l: -20, ptIn: f32.Point{X: 0, Y: +10}, ptOut: f32.Point{X: 0, Y: -20}},
> + {l: -20, ptIn: f32.Point{X: 0, Y: -10}, ptOut: f32.Point{X: 0, Y: +20}},
> + {l: -20, ptIn: f32.Point{X: 0, Y: -20}, ptOut: f32.Point{X: 0, Y: +20}},
> + {l: -20, ptIn: f32.Point{X: 0, Y: -30}, ptOut: f32.Point{X: 0, Y: +20}},
> +
> + // l>0 && X=Y
> + {l: +20, ptIn: f32.Point{X: +90, Y: +90}, ptOut: f32.Point{X: +14.142137, Y: +14.142137}},
> + {l: +20, ptIn: f32.Point{X: +30, Y: +30}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
> + {l: +20, ptIn: f32.Point{X: +20, Y: +20}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
> + {l: +20, ptIn: f32.Point{X: +10, Y: +10}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
> + {l: +20, ptIn: f32.Point{X: -10, Y: -10}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
> + {l: +20, ptIn: f32.Point{X: -20, Y: -20}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
> + {l: +20, ptIn: f32.Point{X: -30, Y: -30}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
> + {l: +20, ptIn: f32.Point{X: -90, Y: -90}, ptOut: f32.Point{X: -14.142137, Y: -14.142137}},
> +
> + // l>0 && X=-Y
> + {l: +20, ptIn: f32.Point{X: +90, Y: -90}, ptOut: f32.Point{X: +14.142137, Y: -14.142137}},
> + {l: +20, ptIn: f32.Point{X: +30, Y: -30}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
> + {l: +20, ptIn: f32.Point{X: +20, Y: -20}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
> + {l: +20, ptIn: f32.Point{X: +10, Y: -10}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
> + {l: +20, ptIn: f32.Point{X: -10, Y: +10}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
> + {l: +20, ptIn: f32.Point{X: -20, Y: +20}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
> + {l: +20, ptIn: f32.Point{X: -30, Y: +30}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
> + {l: +20, ptIn: f32.Point{X: -90, Y: +90}, ptOut: f32.Point{X: -14.142137, Y: +14.142137}},
> +
> + // l<0 && X=Y
> + {l: -20, ptIn: f32.Point{X: +90, Y: +90}, ptOut: f32.Point{X: -14.142137, Y: -14.142137}},
> + {l: -20, ptIn: f32.Point{X: +30, Y: +30}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
> + {l: -20, ptIn: f32.Point{X: +20, Y: +20}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
> + {l: -20, ptIn: f32.Point{X: +10, Y: +10}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}},
> + {l: -20, ptIn: f32.Point{X: -10, Y: -10}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
> + {l: -20, ptIn: f32.Point{X: -20, Y: -20}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
> + {l: -20, ptIn: f32.Point{X: -30, Y: -30}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}},
> + {l: -20, ptIn: f32.Point{X: -90, Y: -90}, ptOut: f32.Point{X: +14.142137, Y: +14.142137}},
> +
> + // l<0 && X=-Y
> + {l: -20, ptIn: f32.Point{X: +90, Y: -90}, ptOut: f32.Point{X: -14.142137, Y: +14.142137}},
> + {l: -20, ptIn: f32.Point{X: +30, Y: -30}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
> + {l: -20, ptIn: f32.Point{X: +20, Y: -20}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
> + {l: -20, ptIn: f32.Point{X: +10, Y: -10}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}},
> + {l: -20, ptIn: f32.Point{X: -10, Y: +10}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
> + {l: -20, ptIn: f32.Point{X: -20, Y: +20}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
> + {l: -20, ptIn: f32.Point{X: -30, Y: +30}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}},
> + {l: -20, ptIn: f32.Point{X: -90, Y: +90}, ptOut: f32.Point{X: +14.142137, Y: -14.142137}},
> + }
> +
> + for i, s := range scenarios {
> + actual := normPt(s.ptIn, s.l)
> + if actual != s.ptOut {
> + t.Errorf("%v: in: %v*%v, expected: %v, actual: %v", i, s.l, s.ptIn, s.ptOut, actual)
> + }
> + }
> +}
> +
> func BenchmarkSplitCubic(b *testing.B) {
> type scenario struct {
> segments int
> --
> 2.39.2
>