From 6846c9dfc70b8eb6b326529908ef18f29e4a2a30 Mon Sep 17 00:00:00 2001 From: kercylan98 Date: Wed, 21 Feb 2024 16:58:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20geometry=20=E5=8C=85=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20SimpleCircle=20=E7=BB=93=E6=9E=84=E4=BD=93=EF=BC=8C=E7=94=A8?= =?UTF-8?q?=E4=BA=8E=E8=A1=A8=E7=A4=BA=E4=BB=85=E7=94=B1=E5=9C=86=E5=BF=83?= =?UTF-8?q?=E5=8F=8A=E5=8D=8A=E5=BE=84=E7=BB=84=E6=88=90=E7=9A=84=E5=9C=86?= =?UTF-8?q?=E5=BD=A2=EF=BC=8C=E5=8C=85=E5=90=AB=E6=8A=95=E5=BD=B1=E3=80=81?= =?UTF-8?q?=E8=B7=9D=E7=A6=BB=E7=AD=89=E5=B8=B8=E7=94=A8=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E3=80=82=E4=BC=98=E5=8C=96=20geometry=20=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E5=87=BD=E6=95=B0=EF=BC=8C=E6=89=80=E6=9C=89?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E5=85=A5=E5=8F=82=E5=9D=87=E4=BC=9A=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E4=B8=BA=20float64=20=E8=BF=90=E7=AE=97=EF=BC=8C?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E6=97=B6=E8=BD=AC=E6=8D=A2=E5=9B=9E=E5=8E=9F?= =?UTF-8?q?=E6=9C=89=E7=9A=84=E6=B3=9B=E5=9E=8B=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/geometry/circle.go | 2 +- utils/geometry/geometry.go | 35 +++--- utils/geometry/line.go | 2 +- utils/geometry/position.go | 12 ++ utils/geometry/rectangle.go | 52 ++++---- utils/geometry/shape.go | 7 +- utils/geometry/simple_circle.go | 175 +++++++++++++++++++++++++++ utils/geometry/simple_circle_test.go | 17 +++ 8 files changed, 253 insertions(+), 49 deletions(-) create mode 100644 utils/geometry/simple_circle.go create mode 100644 utils/geometry/simple_circle_test.go diff --git a/utils/geometry/circle.go b/utils/geometry/circle.go index c2d725b..c43ddcd 100644 --- a/utils/geometry/circle.go +++ b/utils/geometry/circle.go @@ -5,7 +5,7 @@ import ( "math" ) -// Circle 圆形 +// Circle 由多个点组成的圆形数据结构 type Circle[V generic.SignedNumber] struct { Shape[V] } diff --git a/utils/geometry/geometry.go b/utils/geometry/geometry.go index 4ab1fb4..9e76228 100644 --- a/utils/geometry/geometry.go +++ b/utils/geometry/geometry.go @@ -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 根据给定的位置和角度生成射线,检测射线是否与多边形发生碰撞 diff --git a/utils/geometry/line.go b/utils/geometry/line.go index e1283a6..61b2753 100644 --- a/utils/geometry/line.go +++ b/utils/geometry/line.go @@ -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 计算线段的截距 diff --git a/utils/geometry/position.go b/utils/geometry/position.go index a82ea64..c50caca 100644 --- a/utils/geometry/position.go +++ b/utils/geometry/position.go @@ -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() diff --git a/utils/geometry/rectangle.go b/utils/geometry/rectangle.go index 0e288fe..c0a13ed 100644 --- a/utils/geometry/rectangle.go +++ b/utils/geometry/rectangle.go @@ -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)) } diff --git a/utils/geometry/shape.go b/utils/geometry/shape.go index 97e3271..f88b90a 100644 --- a/utils/geometry/shape.go +++ b/utils/geometry/shape.go @@ -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 diff --git a/utils/geometry/simple_circle.go b/utils/geometry/simple_circle.go new file mode 100644 index 0000000..9cc93a7 --- /dev/null +++ b/utils/geometry/simple_circle.go @@ -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)) +} diff --git a/utils/geometry/simple_circle_test.go b/utils/geometry/simple_circle_test.go new file mode 100644 index 0000000..f8b9657 --- /dev/null +++ b/utils/geometry/simple_circle_test.go @@ -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)) + } +}