307 lines
9.4 KiB
Go
307 lines
9.4 KiB
Go
|
// geomx adds some geometry functions needed by rasterx
|
||
|
// Copyright 2017 by the rasterx Authors. All rights reserved.
|
||
|
// Created: 2/12/2017 by S.R.Wiley
|
||
|
|
||
|
package rasterx
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"math"
|
||
|
|
||
|
"golang.org/x/image/math/fixed"
|
||
|
)
|
||
|
|
||
|
// Invert returns the point inverted around the origin
|
||
|
func Invert(v fixed.Point26_6) fixed.Point26_6 {
|
||
|
return fixed.Point26_6{X: -v.X, Y: -v.Y}
|
||
|
}
|
||
|
|
||
|
// turnStarboard90 returns the vector 90 degrees starboard (right in direction heading)
|
||
|
func turnStarboard90(v fixed.Point26_6) fixed.Point26_6 {
|
||
|
return fixed.Point26_6{X: -v.Y, Y: v.X}
|
||
|
}
|
||
|
|
||
|
// turnPort90 returns the vector 90 degrees port (left in direction heading)
|
||
|
func turnPort90(v fixed.Point26_6) fixed.Point26_6 {
|
||
|
return fixed.Point26_6{X: v.Y, Y: -v.X}
|
||
|
}
|
||
|
|
||
|
// DotProd returns the inner product of p and q
|
||
|
func DotProd(p fixed.Point26_6, q fixed.Point26_6) fixed.Int52_12 {
|
||
|
return fixed.Int52_12(int64(p.X)*int64(q.X) + int64(p.Y)*int64(q.Y))
|
||
|
}
|
||
|
|
||
|
// Length is the distance from the origin of the point
|
||
|
func Length(v fixed.Point26_6) fixed.Int26_6 {
|
||
|
vx, vy := float64(v.X), float64(v.Y)
|
||
|
return fixed.Int26_6(math.Sqrt(vx*vx + vy*vy))
|
||
|
}
|
||
|
|
||
|
//PathCommand is the type for the path command token
|
||
|
type PathCommand fixed.Int26_6
|
||
|
|
||
|
// Human readable path constants
|
||
|
const (
|
||
|
PathMoveTo PathCommand = iota
|
||
|
PathLineTo
|
||
|
PathQuadTo
|
||
|
PathCubicTo
|
||
|
PathClose
|
||
|
)
|
||
|
|
||
|
// A Path starts with a PathCommand value followed by zero to three fixed
|
||
|
// int points.
|
||
|
type Path []fixed.Int26_6
|
||
|
|
||
|
// ToSVGPath returns a string representation of the path
|
||
|
func (p Path) ToSVGPath() string {
|
||
|
s := ""
|
||
|
for i := 0; i < len(p); {
|
||
|
if i != 0 {
|
||
|
s += " "
|
||
|
}
|
||
|
switch PathCommand(p[i]) {
|
||
|
case PathMoveTo:
|
||
|
s += fmt.Sprintf("M%4.3f,%4.3f", float32(p[i+1])/64, float32(p[i+2])/64)
|
||
|
i += 3
|
||
|
case PathLineTo:
|
||
|
s += fmt.Sprintf("L%4.3f,%4.3f", float32(p[i+1])/64, float32(p[i+2])/64)
|
||
|
i += 3
|
||
|
case PathQuadTo:
|
||
|
s += fmt.Sprintf("Q%4.3f,%4.3f,%4.3f,%4.3f", float32(p[i+1])/64, float32(p[i+2])/64,
|
||
|
float32(p[i+3])/64, float32(p[i+4])/64)
|
||
|
i += 5
|
||
|
case PathCubicTo:
|
||
|
s += "C" + fmt.Sprintf("C%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f", float32(p[i+1])/64, float32(p[i+2])/64,
|
||
|
float32(p[i+3])/64, float32(p[i+4])/64, float32(p[i+5])/64, float32(p[i+6])/64)
|
||
|
i += 7
|
||
|
case PathClose:
|
||
|
s += "Z"
|
||
|
i++
|
||
|
default:
|
||
|
panic("freetype/rasterx: bad pather")
|
||
|
}
|
||
|
}
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// String returns a readable representation of a Path.
|
||
|
func (p Path) String() string {
|
||
|
return p.ToSVGPath()
|
||
|
}
|
||
|
|
||
|
// Clear zeros the path slice
|
||
|
func (p *Path) Clear() {
|
||
|
*p = (*p)[:0]
|
||
|
}
|
||
|
|
||
|
// Start starts a new curve at the given point.
|
||
|
func (p *Path) Start(a fixed.Point26_6) {
|
||
|
*p = append(*p, fixed.Int26_6(PathMoveTo), a.X, a.Y)
|
||
|
}
|
||
|
|
||
|
// Line adds a linear segment to the current curve.
|
||
|
func (p *Path) Line(b fixed.Point26_6) {
|
||
|
*p = append(*p, fixed.Int26_6(PathLineTo), b.X, b.Y)
|
||
|
}
|
||
|
|
||
|
// QuadBezier adds a quadratic segment to the current curve.
|
||
|
func (p *Path) QuadBezier(b, c fixed.Point26_6) {
|
||
|
*p = append(*p, fixed.Int26_6(PathQuadTo), b.X, b.Y, c.X, c.Y)
|
||
|
}
|
||
|
|
||
|
// CubeBezier adds a cubic segment to the current curve.
|
||
|
func (p *Path) CubeBezier(b, c, d fixed.Point26_6) {
|
||
|
*p = append(*p, fixed.Int26_6(PathCubicTo), b.X, b.Y, c.X, c.Y, d.X, d.Y)
|
||
|
}
|
||
|
|
||
|
// Stop joins the ends of the path
|
||
|
func (p *Path) Stop(closeLoop bool) {
|
||
|
if closeLoop {
|
||
|
*p = append(*p, fixed.Int26_6(PathClose))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// AddTo adds the Path p to q.
|
||
|
func (p Path) AddTo(q Adder) {
|
||
|
for i := 0; i < len(p); {
|
||
|
switch PathCommand(p[i]) {
|
||
|
case PathMoveTo:
|
||
|
q.Stop(false) // Fixes issues #1 by described by Djadala; implicit close if currently in path.
|
||
|
q.Start(fixed.Point26_6{X: p[i+1], Y: p[i+2]})
|
||
|
i += 3
|
||
|
case PathLineTo:
|
||
|
q.Line(fixed.Point26_6{X: p[i+1], Y: p[i+2]})
|
||
|
i += 3
|
||
|
case PathQuadTo:
|
||
|
q.QuadBezier(fixed.Point26_6{X: p[i+1], Y: p[i+2]}, fixed.Point26_6{X: p[i+3], Y: p[i+4]})
|
||
|
i += 5
|
||
|
case PathCubicTo:
|
||
|
q.CubeBezier(fixed.Point26_6{X: p[i+1], Y: p[i+2]},
|
||
|
fixed.Point26_6{X: p[i+3], Y: p[i+4]}, fixed.Point26_6{X: p[i+5], Y: p[i+6]})
|
||
|
i += 7
|
||
|
case PathClose:
|
||
|
q.Stop(true)
|
||
|
i++
|
||
|
default:
|
||
|
panic("AddTo: bad path")
|
||
|
}
|
||
|
}
|
||
|
q.Stop(false)
|
||
|
}
|
||
|
|
||
|
// ToLength scales the point to the length indicated by ln
|
||
|
func ToLength(p fixed.Point26_6, ln fixed.Int26_6) (q fixed.Point26_6) {
|
||
|
if ln == 0 || (p.X == 0 && p.Y == 0) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
pX, pY := float64(p.X), float64(p.Y)
|
||
|
lnF := float64(ln)
|
||
|
pLen := math.Sqrt(pX*pX + pY*pY)
|
||
|
|
||
|
qX, qY := pX*lnF/pLen, pY*lnF/pLen
|
||
|
q.X, q.Y = fixed.Int26_6(qX), fixed.Int26_6(qY)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// ClosestPortside returns the closest of p1 or p2 on the port side of the
|
||
|
// line from the bow to the stern. (port means left side of the direction you are heading)
|
||
|
// isIntersecting is just convienice to reduce code, and if false returns false, because p1 and p2 are not valid
|
||
|
func ClosestPortside(bow, stern, p1, p2 fixed.Point26_6, isIntersecting bool) (xt fixed.Point26_6, intersects bool) {
|
||
|
if isIntersecting == false {
|
||
|
return
|
||
|
}
|
||
|
dir := bow.Sub(stern)
|
||
|
dp1 := p1.Sub(stern)
|
||
|
dp2 := p2.Sub(stern)
|
||
|
cp1 := dir.X*dp1.Y - dp1.X*dir.Y
|
||
|
cp2 := dir.X*dp2.Y - dp2.X*dir.Y
|
||
|
switch {
|
||
|
case cp1 < 0 && cp2 < 0:
|
||
|
return
|
||
|
case cp1 < 0 && cp2 >= 0:
|
||
|
return p2, true
|
||
|
case cp1 >= 0 && cp2 < 0:
|
||
|
return p1, true
|
||
|
default: // both points on port side
|
||
|
dirdot := DotProd(dir, dir)
|
||
|
// calculate vector rejections of dp1 and dp2 onto dir
|
||
|
h1 := dp1.Sub(dir.Mul(fixed.Int26_6((DotProd(dp1, dir) << 6) / dirdot)))
|
||
|
h2 := dp2.Sub(dir.Mul(fixed.Int26_6((DotProd(dp2, dir) << 6) / dirdot)))
|
||
|
// return point with smallest vector rejection; i.e. closest to dir line
|
||
|
if (h1.X*h1.X + h1.Y*h1.Y) > (h2.X*h2.X + h2.Y*h2.Y) {
|
||
|
return p2, true
|
||
|
}
|
||
|
return p1, true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// RadCurvature returns the curvature of a Bezier curve end point,
|
||
|
// given an end point, the two adjacent control points and the degree.
|
||
|
// The sign of the value indicates if the center of the osculating circle
|
||
|
// is left or right (port or starboard) of the curve in the forward direction.
|
||
|
func RadCurvature(p0, p1, p2 fixed.Point26_6, dm fixed.Int52_12) fixed.Int26_6 {
|
||
|
a, b := p2.Sub(p1), p1.Sub(p0)
|
||
|
abdot, bbdot := DotProd(a, b), DotProd(b, b)
|
||
|
h := a.Sub(b.Mul(fixed.Int26_6((abdot << 6) / bbdot))) // h is the vector rejection of a onto b
|
||
|
if h.X == 0 && h.Y == 0 { // points are co-linear
|
||
|
return 0
|
||
|
}
|
||
|
radCurve := fixed.Int26_6((fixed.Int52_12(a.X*a.X+a.Y*a.Y) * dm / fixed.Int52_12(Length(h)<<6)) >> 6)
|
||
|
if a.X*b.Y > b.X*a.Y { // xprod sign
|
||
|
return radCurve
|
||
|
}
|
||
|
return -radCurve
|
||
|
}
|
||
|
|
||
|
// CircleCircleIntersection calculates the points of intersection of
|
||
|
// two circles or returns with intersects == false if no such points exist.
|
||
|
func CircleCircleIntersection(ct, cl fixed.Point26_6, rt, rl fixed.Int26_6) (xt1, xt2 fixed.Point26_6, intersects bool) {
|
||
|
dc := cl.Sub(ct)
|
||
|
d := Length(dc)
|
||
|
|
||
|
// Check for solvability.
|
||
|
if d > (rt + rl) {
|
||
|
return // No solution. Circles do not intersect.
|
||
|
}
|
||
|
// check if d < abs(rt-rl)
|
||
|
if da := rt - rl; (da > 0 && d < da) || (da < 0 && d < -da) {
|
||
|
return // No solution. One circle is contained by the other.
|
||
|
}
|
||
|
|
||
|
rlf, rtf, df := float64(rl), float64(rt), float64(d)
|
||
|
af := (rtf*rtf - rlf*rlf + df*df) / df / 2.0
|
||
|
hfd := math.Sqrt(rtf*rtf-af*af) / df
|
||
|
afd := af / df
|
||
|
|
||
|
rOffx, rOffy := float64(-dc.Y)*hfd, float64(dc.X)*hfd
|
||
|
p2x := float64(ct.X) + float64(dc.X)*afd
|
||
|
p2y := float64(ct.Y) + float64(dc.Y)*afd
|
||
|
xt1x, xt1y := p2x+rOffx, p2y+rOffy
|
||
|
xt2x, xt2y := p2x-rOffx, p2y-rOffy
|
||
|
return fixed.Point26_6{X: fixed.Int26_6(xt1x), Y: fixed.Int26_6(xt1y)},
|
||
|
fixed.Point26_6{X: fixed.Int26_6(xt2x), Y: fixed.Int26_6(xt2y)}, true
|
||
|
}
|
||
|
|
||
|
// CalcIntersect calculates the points of intersection of two fixed point lines
|
||
|
// and panics if the determinate is zero. You have been warned.
|
||
|
func CalcIntersect(a1, a2, b1, b2 fixed.Point26_6) (x fixed.Point26_6) {
|
||
|
da, db, ds := a2.Sub(a1), b2.Sub(b1), a1.Sub(b1)
|
||
|
det := float32(da.X*db.Y - db.X*da.Y) // Determinate
|
||
|
t := float32(ds.Y*db.X-ds.X*db.Y) / det
|
||
|
x = a1.Add(fixed.Point26_6{X: fixed.Int26_6(float32(da.X) * t), Y: fixed.Int26_6(float32(da.Y) * t)})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// RayCircleIntersection calculates the points of intersection of
|
||
|
// a ray starting at s2 passing through s1 and a circle in fixed point.
|
||
|
// Returns intersects == false if no solution is possible. If two
|
||
|
// solutions are possible, the point closest to s2 is returned
|
||
|
func RayCircleIntersection(s1, s2, c fixed.Point26_6, r fixed.Int26_6) (x fixed.Point26_6, intersects bool) {
|
||
|
fx, fy, intersects := RayCircleIntersectionF(float64(s1.X), float64(s1.Y),
|
||
|
float64(s2.X), float64(s2.Y), float64(c.X), float64(c.Y), float64(r))
|
||
|
return fixed.Point26_6{X: fixed.Int26_6(fx),
|
||
|
Y: fixed.Int26_6(fy)}, intersects
|
||
|
|
||
|
}
|
||
|
|
||
|
// RayCircleIntersectionF calculates in floating point the points of intersection of
|
||
|
// a ray starting at s2 passing through s1 and a circle in fixed point.
|
||
|
// Returns intersects == false if no solution is possible. If two
|
||
|
// solutions are possible, the point closest to s2 is returned
|
||
|
func RayCircleIntersectionF(s1X, s1Y, s2X, s2Y, cX, cY, r float64) (x, y float64, intersects bool) {
|
||
|
n := s2X - cX // Calculating using 64* rather than divide
|
||
|
m := s2Y - cY
|
||
|
|
||
|
e := s2X - s1X
|
||
|
d := s2Y - s1Y
|
||
|
|
||
|
// Quadratic normal form coefficients
|
||
|
A, B, C := e*e+d*d, -2*(e*n+m*d), n*n+m*m-r*r
|
||
|
|
||
|
D := B*B - 4*A*C
|
||
|
|
||
|
if D <= 0 {
|
||
|
return // No intersection or is tangent
|
||
|
}
|
||
|
|
||
|
D = math.Sqrt(D)
|
||
|
t1, t2 := (-B+D)/(2*A), (-B-D)/(2*A)
|
||
|
p1OnSide := t1 > 0
|
||
|
p2OnSide := t2 > 0
|
||
|
|
||
|
switch {
|
||
|
case p1OnSide && p2OnSide:
|
||
|
if t2 < t1 { // both on ray, use closest to s2
|
||
|
t1 = t2
|
||
|
}
|
||
|
case p2OnSide: // Only p2 on ray
|
||
|
t1 = t2
|
||
|
case p1OnSide: // only p1 on ray
|
||
|
default: // Neither solution is on the ray
|
||
|
return
|
||
|
}
|
||
|
return (n - e*t1) + cX, (m - d*t1) + cY, true
|
||
|
}
|