~eliasnaur/gio-patches

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
3 3

[PATCH gio v4] internal/stroke: fix normal vector size and direction

Walter Werner SCHNEIDER <contact@schnwalter.eu>
Details
Message ID
<20240514201203.2639-1-contact@schnwalter.eu>
DKIM signature
pass
Download raw message
Patch: +99 -2
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

[gio/patches] build failed

builds.sr.ht <builds@sr.ht>
Details
Message ID
<D19NBNK0F2PT.1IW8C66Z36XOP@fra01>
In-Reply-To
<20240514201203.2639-1-contact@schnwalter.eu> (view parent)
DKIM signature
missing
Download raw message
gio/patches: FAILED in 10m12s

[internal/stroke: fix normal vector size and direction][0] v4 from [Walter Werner SCHNEIDER][1]

[0]: https://lists.sr.ht/~eliasnaur/gio-patches/patches/51854
[1]: contact@schnwalter.eu

✓ #1220747 SUCCESS gio/patches/apple.yml   https://builds.sr.ht/~eliasnaur/job/1220747
✓ #1220749 SUCCESS gio/patches/linux.yml   https://builds.sr.ht/~eliasnaur/job/1220749
✗ #1220750 FAILED  gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/1220750
✓ #1220748 SUCCESS gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/1220748
Details
Message ID
<CAFcc3FSTJWAwQjYKApXKMpiHqb0vn8s6Y_amUAiuwVgnODaekg@mail.gmail.com>
In-Reply-To
<20240514201203.2639-1-contact@schnwalter.eu> (view parent)
DKIM signature
pass
Download raw message
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
>
Details
Message ID
<CAMAFT9VmEaxRvRKW0HJjDFVhdYge-1Sq8-XMbTZzFUYQY5=65g@mail.gmail.com>
In-Reply-To
<CAFcc3FSTJWAwQjYKApXKMpiHqb0vn8s6Y_amUAiuwVgnODaekg@mail.gmail.com> (view parent)
Sender timestamp
1736618424
DKIM signature
pass
Download raw message
I think there's a comment about the abs function that is unanswered.

Also, it would be nice with a test case so that future changes to this
subtle area keeps corner cases working

Elias
Reply to thread Export thread (mbox)