feat: geometry 包新增 SimpleCircle 结构体,用于表示仅由圆心及半径组成的圆形,包含投影、距离等常用函数。优化 geometry 中的计算函数,所有计算入参均会转换为 float64 运算,输出时转换回原有的泛型类型

This commit is contained in:
kercylan98 2024-02-21 16:58:00 +08:00
parent 7fa0e68636
commit 6846c9dfc7
8 changed files with 253 additions and 49 deletions

View File

@ -5,7 +5,7 @@ import (
"math"
)
// Circle 圆形
// Circle 由多个点组成的圆形数据结构
type Circle[V generic.SignedNumber] struct {
Shape[V]
}

View File

@ -32,6 +32,8 @@ func GetOppositionDirection(direction Direction) Direction {
return DirectionRight
case DirectionRight:
return DirectionLeft
case DirectionUnknown:
return DirectionUnknown
}
return DirectionUnknown
}
@ -89,21 +91,14 @@ func GetDirectionNextWithPos[V generic.SignedNumber](direction Direction, width,
// CalcDirection 计算点2位于点1的方向
func CalcDirection[V generic.SignedNumber](x1, y1, x2, y2 V) Direction {
var oneEighty = 180
var fortyFive = 45
var oneThirtyFive = 135
var twoTwentyFive = 225
var threeFifteen = 315
var end = 360
var start = 0
angle := CalcAngle(x1, y1, x2, y2) + V(oneEighty)
if angle > V(oneThirtyFive) && angle <= V(twoTwentyFive) {
angle := CalcAngle(float64(x1), float64(y1), float64(x2), float64(y2)) + 180
if angle > 45 && angle <= 225 {
return DirectionRight
} else if (angle > V(threeFifteen) && angle <= V(end)) || (angle >= V(start) && angle <= V(fortyFive)) {
} else if (angle > 315 && angle <= 360) || (angle >= 0 && angle <= 45) {
return DirectionLeft
} else if angle > V(twoTwentyFive) && angle <= V(threeFifteen) {
} else if angle > 225 && angle <= 315 {
return DirectionUp
} else if angle > V(fortyFive) && angle <= V(oneThirtyFive) {
} else if angle > 45 && angle <= 134 {
return DirectionDown
}
return DirectionUnknown
@ -141,20 +136,18 @@ func CalcNewCoordinate[V generic.SignedNumber](x, y, angle, distance V) (newX, n
// CalcRadianWithAngle 根据角度 angle 计算弧度
func CalcRadianWithAngle[V generic.SignedNumber](angle V) V {
var pi = math.Pi
var dividend = 180.0
return angle * V(pi) / V(dividend)
return V(float64(angle) * math.Pi / 180)
}
// CalcAngleDifference 计算两个角度之间的最小角度差
func CalcAngleDifference[V generic.SignedNumber](angleA, angleB V) V {
pi := math.Pi
t := angleA - angleB
a := t + V(pi)
b := V(pi) * 2
t = V(math.Floor(float64(a/b))) * b
t -= V(pi)
return t
t := float64(angleA) - float64(angleB)
a := t + math.Pi
b := math.Pi * 2
t = math.Floor(a/b) * b
t -= pi
return V(t)
}
// CalcRayIsIntersect 根据给定的位置和角度生成射线,检测射线是否与多边形发生碰撞

View File

@ -172,7 +172,7 @@ func CalcLineSegmentIsIntersect[V generic.SignedNumber](line1, line2 LineSegment
// CalcLineSegmentSlope 计算线段的斜率
func CalcLineSegmentSlope[V generic.SignedNumber](line LineSegment[V]) V {
return (line.GetEnd().GetY() - line.GetStart().GetY()) / (line.GetEnd().GetX() - line.GetStart().GetX())
return V((float64(line.GetEnd().GetY()) - float64(line.GetStart().GetY())) / (float64(line.GetEnd().GetX()) - float64(line.GetStart().GetX())))
}
// CalcLineSegmentIntercept 计算线段的截距

View File

@ -84,6 +84,18 @@ func (slf Point[V]) Abs() Point[V] {
return NewPoint(V(math.Abs(float64(slf.GetX()))), V(math.Abs(float64(slf.GetY()))))
}
// Distance 返回两个点之间的距离
func (slf Point[V]) Distance(point Point[V]) float64 {
return math.Sqrt(float64(slf.DistanceSquared(point)))
}
// DistanceSquared 返回两个点之间的距离的平方
func (slf Point[V]) DistanceSquared(point Point[V]) V {
x, y := slf.GetXY()
px, py := point.GetXY()
return (x-px)*(x-px) + (y-py)*(y-py)
}
// Max 返回两个位置中每个维度的最大值组成的新的位置
func (slf Point[V]) Max(point Point[V]) Point[V] {
x, y := slf.GetXY()

View File

@ -4,19 +4,20 @@ import "github.com/kercylan98/minotaur/utils/generic"
// GetAdjacentTranslatePos 获取一个连续位置的矩阵中,特定位置相邻的最多四个平移方向(上下左右)的位置
func GetAdjacentTranslatePos[T any, P generic.SignedNumber](matrix []T, width, pos P) (result []P) {
size := P(len(matrix))
currentRow := pos / width
if up := pos - width; up >= 0 {
result = append(result, up)
wf, pf := float64(width), float64(pos)
size := float64(len(matrix))
currentRow := pf / wf
if up := -wf; up >= 0 {
result = append(result, P(up))
}
if down := pos + width; down < size {
result = append(result, down)
if down := pf + wf; down < size {
result = append(result, P(down))
}
if left := pos - 1; left >= 0 && currentRow == (left/width) {
result = append(result, left)
if left := pf - 1; left >= 0 && currentRow == (left/wf) {
result = append(result, P(left))
}
if right := pos + 1; right < size && currentRow == (right/width) {
result = append(result, right)
if right := pf + 1; right < size && currentRow == (right/wf) {
result = append(result, P(right))
}
return
}
@ -61,19 +62,20 @@ func GetAdjacentTranslateCoordinateYX[T any, P generic.SignedNumber](matrix [][]
// GetAdjacentDiagonalsPos 获取一个连续位置的矩阵中,特定位置相邻的对角线最多四个方向的位置
func GetAdjacentDiagonalsPos[T any, P generic.SignedNumber](matrix []T, width, pos P) (result []P) {
size := P(len(matrix))
currentRow := pos / width
if topLeft := pos - width - 1; topLeft >= 0 && currentRow-1 == (topLeft/width) {
result = append(result, topLeft)
size := float64(len(matrix))
wf, pf := float64(width), float64(pos)
currentRow := pf / wf
if topLeft := pf - wf - 1; topLeft >= 0 && currentRow-1 == (topLeft/wf) {
result = append(result, P(topLeft))
}
if topRight := pos - width + 1; topRight >= 0 && currentRow-1 == (topRight/width) {
result = append(result, topRight)
if topRight := pf - wf + 1; topRight >= 0 && currentRow-1 == (topRight/wf) {
result = append(result, P(topRight))
}
if bottomLeft := pos + width - 1; bottomLeft < size && currentRow+1 == (bottomLeft/width) {
result = append(result, bottomLeft)
if bottomLeft := pf + wf - 1; bottomLeft < size && currentRow+1 == (bottomLeft/wf) {
result = append(result, P(bottomLeft))
}
if bottomRight := pos + width + 1; bottomRight < size && currentRow+1 == (bottomRight/width) {
result = append(result, bottomRight)
if bottomRight := pf + wf + 1; bottomRight < size && currentRow+1 == (bottomRight/wf) {
result = append(result, P(bottomRight))
}
return
}
@ -326,13 +328,13 @@ func GetRectangleFullPos[V generic.SignedNumber](width, height V) (result []V) {
// CalcRectangleCentroid 计算矩形质心
// - 非多边形质心计算,仅为顶点的平均值 - 该区域中多边形因子的适当质心
func CalcRectangleCentroid[V generic.SignedNumber](shape Shape[V]) Point[V] {
x, y := V(0), V(0)
length := V(shape.PointCount())
var x, y float64
length := float64(shape.PointCount())
for _, point := range shape.Points() {
x += point.GetX()
y += point.GetY()
x += float64(point.GetX())
y += float64(point.GetY())
}
x /= length
y /= length
return NewPoint(x, y)
return NewPoint(V(x), V(x))
}

View File

@ -413,7 +413,7 @@ func (slf Shape[V]) getAllGraphicCompositionWithDesc(opt *shapeSearchOptions) (r
return
}
// CalcBoundingRadius 计算多边形转换为圆的半径
// CalcBoundingRadius 计算多边形转换为圆的半径,即外接圆的半径
func CalcBoundingRadius[V generic.SignedNumber](shape Shape[V]) V {
var boundingRadius V
var centroid = CalcRectangleCentroid(shape)
@ -426,6 +426,11 @@ func CalcBoundingRadius[V generic.SignedNumber](shape Shape[V]) V {
return boundingRadius
}
// CalcBoundingRadiusWithWidthAndHeight 计算多边形转换为圆的半径,即外接圆的半径
func CalcBoundingRadiusWithWidthAndHeight[V generic.SignedNumber](width, height V) V {
return V(math.Sqrt(float64(width*width+height*height)) / 2)
}
// CalcBoundingRadiusWithCentroid 计算多边形在特定质心下圆的半径
func CalcBoundingRadiusWithCentroid[V generic.SignedNumber](shape Shape[V], centroid Point[V]) V {
var boundingRadius V

View File

@ -0,0 +1,175 @@
package geometry
import (
"fmt"
"github.com/kercylan98/minotaur/utils/generic"
"github.com/kercylan98/minotaur/utils/random"
"math"
)
// NewSimpleCircle 通过传入圆的半径和圆心位置,生成一个圆
func NewSimpleCircle[V generic.SignedNumber](radius V, centroid Point[V]) SimpleCircle[V] {
if radius <= 0 {
panic(fmt.Errorf("radius must be greater than 0, but got %v", radius))
}
return SimpleCircle[V]{
centroid: centroid,
radius: radius,
}
}
// SimpleCircle 仅由位置和半径组成的圆形数据结构
type SimpleCircle[V generic.SignedNumber] struct {
centroid Point[V] // 圆心位置
radius V // 半径
}
// String 获取圆形的字符串表示
func (sc SimpleCircle[V]) String() string {
return fmt.Sprintf("SimpleCircle{centroid: %v, %v, radius: %v}", sc.centroid.GetX(), sc.centroid.GetY(), sc.radius)
}
// Centroid 获取圆形质心位置
func (sc SimpleCircle[V]) Centroid() Point[V] {
return sc.centroid
}
// CentroidX 获取圆形质心位置的 X 坐标
func (sc SimpleCircle[V]) CentroidX() V {
return sc.centroid.GetX()
}
// CentroidY 获取圆形质心位置的 Y 坐标
func (sc SimpleCircle[V]) CentroidY() V {
return sc.centroid.GetY()
}
// CentroidXY 获取圆形质心位置的 X、Y 坐标
func (sc SimpleCircle[V]) CentroidXY() (V, V) {
return sc.centroid.GetXY()
}
// PointIsIn 检查特定点是否位于圆内
func (sc SimpleCircle[V]) PointIsIn(pos Point[V]) bool {
return V(pos.Distance(sc.centroid)) <= sc.radius
}
// CentroidDistance 计算与另一个圆的质心距离
func (sc SimpleCircle[V]) CentroidDistance(circle SimpleCircle[V]) float64 {
return sc.centroid.Distance(circle.centroid)
}
// Radius 获取圆形半径
func (sc SimpleCircle[V]) Radius() V {
return sc.radius
}
// ZoomRadius 获取缩放后的半径
func (sc SimpleCircle[V]) ZoomRadius(zoom float64) V {
return V(float64(sc.radius) * zoom)
}
// Area 获取圆形面积
func (sc SimpleCircle[V]) Area() V {
return sc.radius * sc.radius
}
// Projection 获取圆形投影到另一个圆形的特定比例下的位置和半径
func (sc SimpleCircle[V]) Projection(circle SimpleCircle[V], ratio float64) SimpleCircle[V] {
// 计算圆心朝目标按比例移动后的位置
distance := float64(sc.Centroid().Distance(circle.centroid))
moveDistance := distance * ratio
newX := float64(sc.CentroidX()) + moveDistance*(float64(circle.CentroidX())-float64(sc.CentroidX()))/distance
newY := float64(sc.CentroidY()) + moveDistance*(float64(circle.CentroidY())-float64(sc.CentroidY()))/distance
return NewSimpleCircle(V(float64(sc.radius)*ratio), NewPoint(V(newX), V(newY)))
}
// Length 获取圆的周长
func (sc SimpleCircle[V]) Length() V {
return 2 * sc.radius
}
// Overlap 与另一个圆是否发生重叠
func (sc SimpleCircle[V]) Overlap(circle SimpleCircle[V]) bool {
return sc.centroid.Distance(circle.centroid) < float64(sc.radius+circle.radius)
}
// RandomPoint 获取圆内随机点
func (sc SimpleCircle[V]) RandomPoint() Point[V] {
rx := V(random.Float64() * float64(sc.radius))
ry := V(random.Float64() * float64(sc.radius))
if random.Bool() {
rx = -rx
}
if random.Bool() {
ry = -ry
}
return sc.centroid.GetOffset(rx, ry)
}
// RandomPointWithinCircle 获取圆内随机点,且该圆在 radius 小于父圆时不会超出父圆
func (sc SimpleCircle[V]) RandomPointWithinCircle(radius V) Point[V] {
// 生成随机角度
angle := random.Float64() * 2 * math.Pi
// 限制坐标随机范围
var rx, ry float64
if radius < sc.radius {
r := float64(sc.radius - radius)
rx = random.Float64() * r
ry = random.Float64() * r
} else {
r := float64(sc.radius)
rx = random.Float64() * r
ry = random.Float64() * r
}
// 生成随机点
return sc.centroid.GetOffset(V(rx*math.Cos(angle)), V(ry*math.Sin(angle)))
}
// RandomPointWithinRadius 获取圆内随机点,且该点与圆心的距离小于等于指定半径
func (sc SimpleCircle[V]) RandomPointWithinRadius(radius V) Point[V] {
if radius > sc.radius {
panic("radius must be less than or equal to the circle radius")
}
rx := V(random.Float64() * float64(radius))
ry := V(random.Float64() * float64(radius))
if random.Bool() {
rx = -rx
}
if random.Bool() {
ry = -ry
}
return sc.centroid.GetOffset(rx, ry)
}
// RandomPointWithinRadiusAndSector 获取圆内随机点,且距离圆心小于等于指定半径,且角度在指定范围内
// - startAngle: 起始角度,取值范围为 0 到 360 度
// - endAngle: 结束角度,取值范围为 0 到 360 度,且大于起始角度
func (sc SimpleCircle[V]) RandomPointWithinRadiusAndSector(radius, startAngle, endAngle V) Point[V] {
var full = 360
if radius > sc.radius {
panic("radius must be less than or equal to the circle radius")
}
if startAngle < 0 || startAngle > V(full) {
panic("startAngle must be in the range 0 to 360 degrees")
}
if endAngle < 0 || endAngle > V(full) {
panic("endAngle must be in the range 0 to 360 degrees")
}
if startAngle > endAngle {
panic("startAngle must be less than or equal to endAngle")
}
angle := V(random.Float64() * float64(endAngle-startAngle))
return sc.centroid.GetOffset(radius*V(math.Cos(float64(angle))), radius*V(math.Sin(float64(angle))))
}
// RandomCircleWithinParent 根据指定半径,生成一个圆内随机子圆,该圆不会超出父圆
func (sc SimpleCircle[V]) RandomCircleWithinParent(radius V) SimpleCircle[V] {
if radius > sc.radius {
panic("radius must be less than or equal to the circle radius")
}
return NewSimpleCircle(radius, sc.RandomPointWithinCircle(radius))
}

View File

@ -0,0 +1,17 @@
package geometry_test
import (
"github.com/kercylan98/minotaur/utils/geometry"
"testing"
)
func TestSimpleCircle_RandomSubCircle(t *testing.T) {
for i := 0; i < 10; i++ {
sc := geometry.NewSimpleCircle(10, geometry.NewPoint(0, 0))
sub := sc.RandomCircleWithinParent(8)
t.Log(sc)
t.Log(sub)
t.Log(sc.CentroidDistance(sub))
}
}