implemented self intersecting polygon support

This commit is contained in:
Thomas Friedel 2019-04-16 14:18:51 +02:00
parent 7c4521b227
commit 70ece785e9
4 changed files with 323 additions and 26 deletions

View file

@ -120,17 +120,22 @@ These features *should* work just like their HTML5 counterparts, but there are l
- shadowOffset(X/Y) - shadowOffset(X/Y)
- shadowBlur - shadowBlur
- isPointInPath - isPointInPath
- self intersecting polygons
# Missing features # Missing features
- globalCompositeOperation - globalCompositeOperation
- isPointInStroke - isPointInStroke
- textBaseline hanging and ideographic (currently work just like top and bottom) - textBaseline hanging and ideographic (currently work just like top and bottom)
- full self intersecting polygon support
- image patterns with repeat and transform - image patterns with repeat and transform
# Version history # Version history
v0.8.0
- Self intersecting polygon support
- isPointInPath implemented
v0.7.2 v0.7.2
- Fixed build tags for macOS and iOS - Fixed build tags for macOS and iOS

View file

@ -287,21 +287,21 @@ func (p *Path2D) Rect(x, y, w, h float64) {
// func (p *Path2D) Ellipse(...) { // func (p *Path2D) Ellipse(...) {
// } // }
func runSubPaths(path *Path2D, fn func(subPath []pathPoint) bool) { func runSubPaths(path []pathPoint, fn func(subPath []pathPoint) bool) {
start := 0 start := 0
for i, p := range path.p { for i, p := range path {
if p.flags&pathMove == 0 { if p.flags&pathMove == 0 {
continue continue
} }
if i >= start+3 { if i >= start+3 {
if fn(path.p[start:i]) { if fn(path[start:i]) {
return return
} }
} }
start = i start = i
} }
if len(path.p) >= start+3 { if len(path) >= start+3 {
fn(path.p[start:]) fn(path[start:])
} }
} }
@ -318,7 +318,7 @@ const (
// to the given rule // to the given rule
func (p *Path2D) IsPointInPath(x, y float64, rule pathRule) bool { func (p *Path2D) IsPointInPath(x, y float64, rule pathRule) bool {
inside := false inside := false
runSubPaths(p, func(sp []pathPoint) bool { runSubPaths(p.p, func(sp []pathPoint) bool {
num := 0 num := 0
prev := sp[len(sp)-1].pos prev := sp[len(sp)-1].pos
for _, pt := range p.p { for _, pt := range p.p {

View file

@ -339,7 +339,7 @@ func (cv *Canvas) FillPath(path *Path2D) {
var triBuf [500][2]float64 var triBuf [500][2]float64
tris := triBuf[:0] tris := triBuf[:0]
runSubPaths(path, func(sp []pathPoint) bool { runSubPaths(path.p, func(sp []pathPoint) bool {
tris = appendSubPathTriangles(tris, sp) tris = appendSubPathTriangles(tris, sp)
return false return false
}) })
@ -364,8 +364,10 @@ func appendSubPathTriangles(tris [][2]float64, path []pathPoint) [][2]float64 {
p1 = p2 p1 = p2
} }
} else if last.flags&pathSelfIntersects != 0 { } else if last.flags&pathSelfIntersects != 0 {
path = cutIntersections(path) selfIntersectingPathParts(path, func(sp []pathPoint) bool {
tris = triangulatePath(path, tris) tris = triangulatePath(sp, tris)
return false
})
} else { } else {
tris = triangulatePath(path, tris) tris = triangulatePath(path, tris)
} }
@ -385,7 +387,7 @@ func (cv *Canvas) clip(path *Path2D) {
var triBuf [500][2]float64 var triBuf [500][2]float64
tris := triBuf[:0] tris := triBuf[:0]
runSubPaths(path, func(sp []pathPoint) bool { runSubPaths(path.p, func(sp []pathPoint) bool {
tris = appendSubPathTriangles(tris, sp) tris = appendSubPathTriangles(tris, sp)
return false return false
}) })

View file

@ -5,6 +5,8 @@ import (
"sort" "sort"
) )
const samePointTolerance = 1e-20
func pointIsRightOfLine(a, b, p vec) (bool, bool) { func pointIsRightOfLine(a, b, p vec) (bool, bool) {
if a[1] == b[1] { if a[1] == b[1] {
return false, false return false, false
@ -23,6 +25,24 @@ func pointIsRightOfLine(a, b, p vec) (bool, bool) {
return p[0] > x, dir return p[0] > x, dir
} }
func pointIsBelowLine(a, b, p vec) (bool, bool) {
if a[0] == b[0] {
return false, false
}
dir := false
if a[0] > b[0] {
a, b = b, a
dir = !dir
}
if p[0] < a[0] || p[0] > b[0] {
return false, false
}
v := b.sub(a)
r := (p[0] - a[0]) / v[0]
x := a[1] + r*v[1]
return p[1] > x, dir
}
func triangleContainsPoint(a, b, c, p vec) bool { func triangleContainsPoint(a, b, c, p vec) bool {
// if point is outside triangle bounds, return false // if point is outside triangle bounds, return false
if p[0] < a[0] && p[0] < b[0] && p[0] < c[0] { if p[0] < a[0] && p[0] < b[0] && p[0] < c[0] {
@ -104,11 +124,52 @@ func triangulatePath(path []pathPoint, target [][2]float64) [][2]float64 {
return target return target
} }
func cutIntersections(path []pathPoint) []pathPoint { /*
tesselation strategy:
- cut the path at the intersections
- build a network of connected vertices and edges
- find out which side of each edge is inside the polygon
- pick an edge with only one side in the polygon, then follow it
along the side on which the polygon is, always picking the edge
with the smallest angle, until the path goes back to the starting
point. set each edge as no longer having the polygon on that side
- repeat until no more edges have a polygon on either side
*/
type tessNet struct {
verts []tessVert
edges []tessEdge
}
type tessVert struct {
pos vec
attached []int
count int
}
type tessEdge struct {
a, b int
leftInside bool
rightInside bool
}
func cutIntersections(path []pathPoint) tessNet {
// eliminate adjacent duplicate points
for i := 0; i < len(path); i++ {
a := path[i]
b := path[(i+1)%len(path)]
if isSamePoint(a.pos, b.pos, samePointTolerance) {
copy(path[i:], path[i+1:])
path = path[:len(path)-1]
i--
}
}
// find all the cuts
type cut struct { type cut struct {
from, to int from, to int
j int
b bool
ratio float64 ratio float64
point vec point vec
} }
@ -116,8 +177,8 @@ func cutIntersections(path []pathPoint) []pathPoint {
var cutBuf [50]cut var cutBuf [50]cut
cuts := cutBuf[:0] cuts := cutBuf[:0]
ip := len(path) - 1
for i := 0; i < len(path); i++ { for i := 0; i < len(path); i++ {
ip := (i + len(path) - 1) % len(path)
a0 := path[ip].pos a0 := path[ip].pos
a1 := path[i].pos a1 := path[i].pos
for j := i + 1; j < len(path); j++ { for j := i + 1; j < len(path); j++ {
@ -136,21 +197,19 @@ func cutIntersections(path []pathPoint) []pathPoint {
to: i, to: i,
ratio: r1, ratio: r1,
point: p, point: p,
j: j,
}) })
cuts = append(cuts, cut{ cuts = append(cuts, cut{
from: jp, from: jp,
to: j, to: j,
ratio: r2, ratio: r2,
point: p, point: p,
j: i,
b: true,
}) })
} }
ip = i
} }
if len(cuts) == 0 { if len(cuts) == 0 {
return path return tessNet{}
} }
sort.Slice(cuts, func(i, j int) bool { sort.Slice(cuts, func(i, j int) bool {
@ -158,14 +217,245 @@ func cutIntersections(path []pathPoint) []pathPoint {
return a.to > b.to || (a.to == b.to && a.ratio > b.ratio) return a.to > b.to || (a.to == b.to && a.ratio > b.ratio)
}) })
newPath := make([]pathPoint, len(path)+len(cuts)) // build vertex and edge lists
copy(newPath[:len(path)], path) verts := make([]tessVert, len(path)+len(cuts))
for i, pp := range path {
verts[i] = tessVert{
pos: pp.pos,
count: 2,
}
}
for _, cut := range cuts { for _, cut := range cuts {
copy(newPath[cut.to+1:], newPath[cut.to:]) copy(verts[cut.to+1:], verts[cut.to:])
newPath[cut.to].next = newPath[cut.to+1].pos verts[cut.to].pos = cut.point
newPath[cut.to].pos = cut.point }
edges := make([]tessEdge, 0, len(path)+len(cuts)*2)
for i := range verts {
next := (i + 1) % len(verts)
edges = append(edges, tessEdge{a: i, b: next})
} }
return newPath // eliminate duplicate points
for i := 0; i < len(verts); i++ {
a := verts[i]
for j := i + 1; j < len(verts); j++ {
b := verts[j]
if isSamePoint(a.pos, b.pos, samePointTolerance) {
copy(verts[j:], verts[j+1:])
verts = verts[:len(verts)-1]
for k, e := range edges {
if e.a == j {
edges[k].a = i
} else if e.a > j {
edges[k].a--
}
if e.b == j {
edges[k].b = i
} else if e.b > j {
edges[k].b--
}
}
verts[i].count += 2
j--
}
}
}
// build the attached edge lists on all vertices
total := 0
for _, v := range verts {
total += v.count
}
attachedBuf := make([]int, 0, total)
pos := 0
for i := range verts {
for j, e := range edges {
if e.a == i || e.b == i {
attachedBuf = append(attachedBuf, j)
}
}
verts[i].attached = attachedBuf[pos:len(attachedBuf)]
pos = len(attachedBuf)
}
return tessNet{verts: verts, edges: edges}
}
func setPathLeftRightInside(net *tessNet) {
for i, e1 := range net.edges {
a1, b1 := net.verts[e1.a], net.verts[e1.b]
diff := b1.pos.sub(a1.pos)
mid := a1.pos.add(diff.mulf(0.5))
num := 0
if math.Abs(diff[1]) < math.Abs(diff[0]) {
edir := diff[1] > 0
for j, e2 := range net.edges {
if i == j {
continue
}
a2, b2 := net.verts[e2.a], net.verts[e2.b]
r, dir := pointIsRightOfLine(a2.pos, b2.pos, mid)
if !r {
continue
}
if dir {
num++
} else {
num--
}
}
if edir {
net.edges[i].leftInside = (num - 1) != 0
net.edges[i].rightInside = num != 0
} else {
net.edges[i].leftInside = num != 0
net.edges[i].rightInside = (num + 1) != 0
}
} else {
edir := diff[0] > 0
for j, e2 := range net.edges {
if i == j {
continue
}
a2, b2 := net.verts[e2.a], net.verts[e2.b]
b, dir := pointIsBelowLine(a2.pos, b2.pos, mid)
if !b {
continue
}
if dir {
num++
} else {
num--
}
}
if edir {
net.edges[i].leftInside = num != 0
net.edges[i].rightInside = (num - 1) != 0
} else {
net.edges[i].leftInside = (num + 1) != 0
net.edges[i].rightInside = num != 0
}
}
}
}
func selfIntersectingPathParts(p []pathPoint, partFn func(sp []pathPoint) bool) {
runSubPaths(p, func(sp1 []pathPoint) bool {
net := cutIntersections(sp1)
if net.verts == nil {
return false
}
setPathLeftRightInside(&net)
var sp2Buf [50]pathPoint
sp2 := sp2Buf[:0]
for {
var start, from, cur, count int
var left bool
for i, e := range net.edges {
if e.leftInside != e.rightInside {
count++
start = e.a
from = i
cur = e.b
if e.leftInside {
left = true
net.edges[i].leftInside = false
} else {
net.edges[i].rightInside = false
}
break
}
}
if count == 0 {
break
}
// fmt.Println("start", start, from, cur, net.verts[cur], left)
sp2 = append(sp2, pathPoint{
pos: net.verts[cur].pos,
flags: pathMove,
})
for limit := 0; limit < len(net.edges); limit++ {
ecur := net.edges[from]
acur, bcur := net.verts[ecur.a], net.verts[ecur.b]
dir := bcur.pos.sub(acur.pos)
dirAngle := math.Atan2(dir[1], dir[0])
minAngleDiff := math.Pi * 2
var next, nextEdge int
any := false
for _, ei := range net.verts[cur].attached {
if ei == from {
continue
}
e := net.edges[ei]
if (left && !e.leftInside) || (!left && !e.rightInside) {
continue
}
na, nb := net.verts[e.a], net.verts[e.b]
if e.b == cur {
na, nb = nb, na
}
ndir := nb.pos.sub(na.pos)
nextAngle := math.Atan2(ndir[1], ndir[0]) + math.Pi
if nextAngle < dirAngle {
nextAngle += math.Pi * 2
} else if nextAngle > dirAngle+math.Pi*2 {
nextAngle -= math.Pi * 2
}
var angleDiff float64
if left {
angleDiff = nextAngle - dirAngle
} else {
angleDiff = dirAngle - nextAngle
}
if angleDiff < minAngleDiff {
minAngleDiff = angleDiff
nextEdge = ei
if e.a == cur {
next = e.b
} else {
next = e.a
}
any = true
// fmt.Println("-", e, nextEdge, next)
}
}
if !any {
break
}
// fmt.Println(start, from, cur, net.verts[cur], nextEdge, next, net.verts[next])
if left {
net.edges[nextEdge].leftInside = false
} else {
net.edges[nextEdge].rightInside = false
}
sp2 = append(sp2, pathPoint{
pos: net.verts[next].pos,
})
from = nextEdge
cur = next
if next == start {
break
}
}
stop := partFn(sp2)
if stop {
return true
}
sp2 = sp2[:0]
}
return false
})
} }