implemented self intersecting polygon support
This commit is contained in:
parent
7c4521b227
commit
70ece785e9
4 changed files with 323 additions and 26 deletions
|
@ -120,17 +120,22 @@ These features *should* work just like their HTML5 counterparts, but there are l
|
|||
- shadowOffset(X/Y)
|
||||
- shadowBlur
|
||||
- isPointInPath
|
||||
- self intersecting polygons
|
||||
|
||||
# Missing features
|
||||
|
||||
- globalCompositeOperation
|
||||
- isPointInStroke
|
||||
- textBaseline hanging and ideographic (currently work just like top and bottom)
|
||||
- full self intersecting polygon support
|
||||
- image patterns with repeat and transform
|
||||
|
||||
# Version history
|
||||
|
||||
v0.8.0
|
||||
|
||||
- Self intersecting polygon support
|
||||
- isPointInPath implemented
|
||||
|
||||
v0.7.2
|
||||
|
||||
- Fixed build tags for macOS and iOS
|
||||
|
|
12
path2d.go
12
path2d.go
|
@ -287,21 +287,21 @@ func (p *Path2D) Rect(x, y, w, h float64) {
|
|||
// func (p *Path2D) Ellipse(...) {
|
||||
// }
|
||||
|
||||
func runSubPaths(path *Path2D, fn func(subPath []pathPoint) bool) {
|
||||
func runSubPaths(path []pathPoint, fn func(subPath []pathPoint) bool) {
|
||||
start := 0
|
||||
for i, p := range path.p {
|
||||
for i, p := range path {
|
||||
if p.flags&pathMove == 0 {
|
||||
continue
|
||||
}
|
||||
if i >= start+3 {
|
||||
if fn(path.p[start:i]) {
|
||||
if fn(path[start:i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
start = i
|
||||
}
|
||||
if len(path.p) >= start+3 {
|
||||
fn(path.p[start:])
|
||||
if len(path) >= start+3 {
|
||||
fn(path[start:])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -318,7 +318,7 @@ const (
|
|||
// to the given rule
|
||||
func (p *Path2D) IsPointInPath(x, y float64, rule pathRule) bool {
|
||||
inside := false
|
||||
runSubPaths(p, func(sp []pathPoint) bool {
|
||||
runSubPaths(p.p, func(sp []pathPoint) bool {
|
||||
num := 0
|
||||
prev := sp[len(sp)-1].pos
|
||||
for _, pt := range p.p {
|
||||
|
|
10
paths.go
10
paths.go
|
@ -339,7 +339,7 @@ func (cv *Canvas) FillPath(path *Path2D) {
|
|||
|
||||
var triBuf [500][2]float64
|
||||
tris := triBuf[:0]
|
||||
runSubPaths(path, func(sp []pathPoint) bool {
|
||||
runSubPaths(path.p, func(sp []pathPoint) bool {
|
||||
tris = appendSubPathTriangles(tris, sp)
|
||||
return false
|
||||
})
|
||||
|
@ -364,8 +364,10 @@ func appendSubPathTriangles(tris [][2]float64, path []pathPoint) [][2]float64 {
|
|||
p1 = p2
|
||||
}
|
||||
} else if last.flags&pathSelfIntersects != 0 {
|
||||
path = cutIntersections(path)
|
||||
tris = triangulatePath(path, tris)
|
||||
selfIntersectingPathParts(path, func(sp []pathPoint) bool {
|
||||
tris = triangulatePath(sp, tris)
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
tris = triangulatePath(path, tris)
|
||||
}
|
||||
|
@ -385,7 +387,7 @@ func (cv *Canvas) clip(path *Path2D) {
|
|||
|
||||
var triBuf [500][2]float64
|
||||
tris := triBuf[:0]
|
||||
runSubPaths(path, func(sp []pathPoint) bool {
|
||||
runSubPaths(path.p, func(sp []pathPoint) bool {
|
||||
tris = appendSubPathTriangles(tris, sp)
|
||||
return false
|
||||
})
|
||||
|
|
320
triangulation.go
320
triangulation.go
|
@ -5,6 +5,8 @@ import (
|
|||
"sort"
|
||||
)
|
||||
|
||||
const samePointTolerance = 1e-20
|
||||
|
||||
func pointIsRightOfLine(a, b, p vec) (bool, bool) {
|
||||
if a[1] == b[1] {
|
||||
return false, false
|
||||
|
@ -23,6 +25,24 @@ func pointIsRightOfLine(a, b, p vec) (bool, bool) {
|
|||
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 {
|
||||
// if point is outside triangle bounds, return false
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
from, to int
|
||||
j int
|
||||
b bool
|
||||
ratio float64
|
||||
point vec
|
||||
}
|
||||
|
@ -116,8 +177,8 @@ func cutIntersections(path []pathPoint) []pathPoint {
|
|||
var cutBuf [50]cut
|
||||
cuts := cutBuf[:0]
|
||||
|
||||
ip := len(path) - 1
|
||||
for i := 0; i < len(path); i++ {
|
||||
ip := (i + len(path) - 1) % len(path)
|
||||
a0 := path[ip].pos
|
||||
a1 := path[i].pos
|
||||
for j := i + 1; j < len(path); j++ {
|
||||
|
@ -136,21 +197,19 @@ func cutIntersections(path []pathPoint) []pathPoint {
|
|||
to: i,
|
||||
ratio: r1,
|
||||
point: p,
|
||||
j: j,
|
||||
})
|
||||
cuts = append(cuts, cut{
|
||||
from: jp,
|
||||
to: j,
|
||||
ratio: r2,
|
||||
point: p,
|
||||
j: i,
|
||||
b: true,
|
||||
})
|
||||
}
|
||||
ip = i
|
||||
}
|
||||
|
||||
if len(cuts) == 0 {
|
||||
return path
|
||||
return tessNet{}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
newPath := make([]pathPoint, len(path)+len(cuts))
|
||||
copy(newPath[:len(path)], path)
|
||||
|
||||
// build vertex and edge lists
|
||||
verts := make([]tessVert, len(path)+len(cuts))
|
||||
for i, pp := range path {
|
||||
verts[i] = tessVert{
|
||||
pos: pp.pos,
|
||||
count: 2,
|
||||
}
|
||||
}
|
||||
for _, cut := range cuts {
|
||||
copy(newPath[cut.to+1:], newPath[cut.to:])
|
||||
newPath[cut.to].next = newPath[cut.to+1].pos
|
||||
newPath[cut.to].pos = cut.point
|
||||
copy(verts[cut.to+1:], verts[cut.to:])
|
||||
verts[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
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue