vRp.CD2g_test/utils/geometry/shape.go

483 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package geometry
import (
"bytes"
"fmt"
"github.com/kercylan98/minotaur/utils/collection"
"github.com/kercylan98/minotaur/utils/generic"
"math"
"sort"
"strings"
)
var (
shapeStringHasBorder = false // 控制 Shape.String 是否拥有边界
)
// SetShapeStringHasBorder 设置 Shape.String 是拥有边界的
func SetShapeStringHasBorder() {
shapeStringHasBorder = true
}
// SetShapeStringNotHasBorder 设置 Shape.String 是没有边界的
func SetShapeStringNotHasBorder() {
shapeStringHasBorder = false
}
// NewShape 通过多个点生成一个形状进行返回
func NewShape[V generic.SignedNumber](points ...Point[V]) Shape[V] {
return points
}
// NewShapeWithString 通过字符串将指定 rune 转换为点位置生成形状进行返回
// - 每个点的顺序从上到下,从左到右
func NewShapeWithString[V generic.SignedNumber](rows []string, point rune) (shape Shape[V]) {
var width int
for _, row := range rows {
length := len(row)
if length > width {
width = length
}
}
for y := 0; y < len(rows); y++ {
runes := bytes.Runes([]byte(rows[y]))
for x := 0; x < width; x++ {
if x >= len(runes) {
break
}
if point == runes[x] {
shape = append(shape, NewPoint(V(x), V(y)))
}
}
}
return shape
}
// Shape 通过多个点表示了一个形状
type Shape[V generic.SignedNumber] []Point[V]
// Points 获取这个形状的所有点
func (slf Shape[V]) Points() []Point[V] {
return slf
}
// PointCount 获取这个形状的点数量
func (slf Shape[V]) PointCount() int {
return len(slf)
}
// Contains 返回该形状中是否包含点
func (slf Shape[V]) Contains(point Point[V]) bool {
x, y := point.GetXY()
inside := false
for i, j := 0, len(slf)-1; i < len(slf); i, j = i+1, i {
ix := slf[i].GetX()
iy := slf[i].GetY()
jx := slf[j].GetX()
jy := slf[j].GetY()
if ((iy <= y && y < jy) || (jy <= y && y < iy)) && x < ((jx-ix)*(y-iy))/(jy-iy)+ix {
inside = !inside
}
}
return inside
}
// ToCircle 将形状转换为圆形进行处理
// - 当形状非圆形时将会产生意外情况
func (slf Shape[V]) ToCircle() Circle[V] {
return Circle[V]{Shape: slf}
}
// String 将该形状转换为可视化的字符串进行返回
func (slf Shape[V]) String() string {
var result string
left, right, top, bottom := GetShapeCoverageAreaWithPoint(slf.Points()...)
width := right - left + 1
height := bottom - top + 1
if !shapeStringHasBorder {
for y := top; y < top+height; y++ {
for x := left; x < left+width; x++ {
exist := false
for _, p := range slf {
if int(x) == int(p.GetX()) && int(y) == int(p.GetY()) {
exist = true
break
}
}
if exist {
result += "X "
} else {
result += "# "
}
}
result = result[:len(result)-1]
result += "\n"
}
} else {
if left < 0 {
left += -left
right += -left
}
if top < 0 {
top += -top
bottom += -top
}
for y := V(0); y < top+height; y++ {
for x := V(0); x < left+width; x++ {
exist := false
for _, p := range slf {
if x == p.GetX() && y == p.GetY() {
exist = true
break
}
}
if exist {
result += "X "
} else {
result += "# "
}
}
result = result[:len(result)-1]
result += "\n"
}
}
return fmt.Sprintf("%v\n%s", slf.Points(), strings.TrimSuffix(result, "\n"))
}
// ShapeSearch 获取该形状中包含的所有图形组合及其位置
// - 需要注意的是,即便图形最终表示为相同的,但是只要位置组合顺序不同,那么也将被认定为一种图形组合
// - [[1 0] [1 1] [1 2]] 和 [[1 1] [1 0] [1 2]] 可以被视为两个图形组合
// - 返回的坐标为原始形状的坐标
//
// 可通过可选项对搜索结果进行过滤
func (slf Shape[V]) ShapeSearch(options ...ShapeSearchOption) (result []Shape[V]) {
opt := newShapeSearchOptions()
opt.directionCountUpper = map[Direction]int{}
for _, d := range DirectionUDLR {
opt.directionCountUpper[d] = math.MaxInt
}
for _, option := range options {
option(opt)
}
var shapes []Shape[V]
switch opt.sort {
case 1:
shapes = slf.getAllGraphicCompositionWithAsc(opt)
case -1:
shapes = slf.getAllGraphicCompositionWithDesc(opt)
default:
shapes = slf.getAllGraphicComposition(opt)
}
result = shapes
if opt.deduplication {
deduplication := make(map[V]struct{})
w := V(len(slf.Points()))
var notRepeat = make([]Shape[V], 0, len(result))
for _, points := range result {
count := len(points)
if count < opt.lowerLimit || count > opt.upperLimit {
continue
}
var match = true
for _, point := range points {
pos := point.GetPos(w)
if _, exist := deduplication[pos]; exist {
match = false
break
}
deduplication[pos] = struct{}{}
}
if match {
notRepeat = append(notRepeat, points)
}
}
result = notRepeat
} else {
limit := make([]Shape[V], 0, len(result))
for _, shape := range result {
count := len(shape.Points())
if count < opt.lowerLimit || count > opt.upperLimit {
continue
}
limit = append(limit, shape)
}
result = limit
}
return
}
// getAllGraphicComposition 获取该形状中包含的所有图形组合及其位置
// - 需要注意的是,即便图形最终表示为相同的,但是只要位置组合顺序不同,那么也将被认定为一种图形组合
// - [[1 0] [1 1] [1 2]] 和 [[1 1] [1 0] [1 2]] 可以被视为两个图形组合
// - 返回的坐标为原始形状的坐标
func (slf Shape[V]) getAllGraphicComposition(opt *shapeSearchOptions) (result []Shape[V]) {
left, right, top, bottom := GetShapeCoverageAreaWithPoint(slf.Points()...)
width := right - left + 1
height := bottom - top + 1
areaWidth := width + left
areaHeight := height + top
rectangleShape := GenerateShapeOnRectangleWithCoordinate(slf.Points()...)
records := make(map[V]struct{})
var match = func(links Shape[V], directionCount map[Direction]int, count int) bool {
if opt.rectangle {
return false
}
match := true
for _, direction := range DirectionUDLR {
c := directionCount[direction]
if c < opt.directionCountLower[direction] || c > opt.directionCountUpper[direction] {
match = false
break
}
}
if opt.directionCount > 0 && len(directionCount) != opt.directionCount {
match = false
}
if directionCount[GetOppositionDirection(opt.oppositionDirection)] > 0 {
match = false
}
if opt.ra {
match = false
if directionCount[DirectionLeft] > 0 && directionCount[DirectionUp] > 0 && count == directionCount[DirectionLeft]+directionCount[DirectionUp] {
match = true
} else if directionCount[DirectionUp] > 0 && directionCount[DirectionRight] > 0 && count == directionCount[DirectionUp]+directionCount[DirectionRight] {
match = true
} else if directionCount[DirectionRight] > 0 && directionCount[DirectionDown] > 0 && count == directionCount[DirectionRight]+directionCount[DirectionDown] {
match = true
} else if directionCount[DirectionDown] > 0 && directionCount[DirectionLeft] > 0 && count == directionCount[DirectionDown]+directionCount[DirectionLeft] {
match = true
}
}
if match {
result = append(result, links)
}
return match
}
if opt.rectangle {
l, r, t, b := GetShapeCoverageAreaWithPoint(slf.Points()...)
rs := GenerateShapeOnRectangleWithCoordinate(slf.Points()...)
w := r - l + 1
h := b - t + 1
shapes := GetExpressibleRectangleBySize(w, h, V(opt.rectangleMinWidth), V(opt.rectangleMinHeight))
for _, s := range shapes {
x, y := 0, 0
for {
if V(x)+s.GetX() >= w {
x = 0
y++
}
if V(y)+s.GetY() >= h {
break
}
points := GetRectangleFullPoints(s[0]+1, s[1]+1)
find := 0
for _, point := range points {
px, py := PointToCoordinate(point)
ox, oy := px+V(x), py+V(y)
if !rs[int(ox)][int(oy)] {
find = 0
break
}
find++
}
if find == len(points) {
sw := s.GetX() + 1
sh := s.GetY() + 1
if !(sw < V(opt.rectangleMinWidth) || sw > V(opt.rectangleMaxWidth) || sh < V(opt.rectangleMinHeight) || sh > V(opt.rectangleMaxHeight)) {
result = append(result, points)
}
}
x++
}
}
} else {
for _, point := range slf.Points() {
// 搜索四个方向
var next = -1
var directionPoint = point
var links = Shape[V]{}
var linkRecord = map[V]struct{}{}
var directionCount = map[Direction]int{}
var count = 0
for i, directions := range [][]Direction{DirectionUDLR, DirectionLRUD} {
var direction Direction
for {
next, direction = collection.FindLoopedNextInSlice(directions, next)
for {
directionPoint = GetDirectionNextWithPoint(direction, directionPoint)
if px, py := directionPoint.GetXY(); px < 0 || px >= areaWidth || py < 0 || py >= areaHeight {
break
}
offset := directionPoint.GetOffset(-left, -top)
if offset.OutOf(V(0), V(0), width, height) || !rectangleShape[int(offset.GetX())][int(offset.GetY())] {
break
}
recordPos := directionPoint.GetPos(areaWidth)
if _, exist := linkRecord[recordPos]; !exist {
linkRecord[recordPos] = struct{}{}
links = append(links, directionPoint)
directionCount[direction]++
count++
match(links, directionCount, count)
}
pos := directionPoint.GetPos(areaWidth)
if _, exist := records[pos]; !exist && opt.directionCount < 1 {
result = append(result, Shape[V]{directionPoint})
records[pos] = struct{}{}
}
}
finish := false
switch i {
case 0:
if direction == DirectionRight {
finish = true
}
case 1:
if direction == DirectionDown {
finish = true
}
}
if finish {
break
}
directionPoint = point
}
}
}
}
return result
}
// Edges 获取该形状每一条边
// - 该形状需要最少由3个点组成否则将不会返回任意一边
func (slf Shape[V]) Edges() (edges []LineSegment[V]) {
if len(slf) < 3 {
return
}
for i := 1; i < slf.PointCount(); i++ {
before := slf[i-1]
edges = append(edges, NewLineSegment(before, slf[i]))
}
edges = append(edges, NewLineSegment(slf[0], slf[len(slf)-1]))
return edges
}
// IsPointOnEdge 检查点是否在该形状的一条边上
func (slf Shape[V]) IsPointOnEdge(point Point[V]) bool {
for _, edge := range slf.Edges() {
if PointOnLineSegmentWithPointInBounds(edge.GetStart(), edge.GetEnd(), point) {
return true
}
}
return false
}
// getAllGraphicCompositionWithAsc 通过升序的方式获取该形状中包含的所有图形组合及其位置
// - 升序指标为图形包含的点数量
// - 其余内容可参考 getAllGraphicComposition
func (slf Shape[V]) getAllGraphicCompositionWithAsc(opt *shapeSearchOptions) (result []Shape[V]) {
result = slf.getAllGraphicComposition(opt)
sort.Slice(result, func(i, j int) bool {
return len(result[i].Points()) < len(result[j].Points())
})
return
}
// getAllGraphicCompositionWithDesc 通过降序的方式获取该形状中包含的所有图形组合及其位置
// - 降序指标为图形包含的点数量
// - 其余内容可参考 GetAllGraphicComposition
func (slf Shape[V]) getAllGraphicCompositionWithDesc(opt *shapeSearchOptions) (result []Shape[V]) {
result = slf.getAllGraphicComposition(opt)
sort.Slice(result, func(i, j int) bool {
return len(result[i].Points()) > len(result[j].Points())
})
return
}
// CalcBoundingRadius 计算多边形转换为圆的半径,即外接圆的半径
func CalcBoundingRadius[V generic.SignedNumber](shape Shape[V]) V {
var boundingRadius V
var centroid = CalcRectangleCentroid(shape)
for _, point := range shape.Points() {
distance := CalcDistanceWithCoordinate(DoublePointToCoordinate(centroid, point))
if distance > boundingRadius {
boundingRadius = distance
}
}
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
for _, point := range shape.Points() {
distance := CalcDistanceWithCoordinate(DoublePointToCoordinate(centroid, point))
if distance > boundingRadius {
boundingRadius = distance
}
}
return boundingRadius
}
// CalcTriangleTwiceArea 计算由 a、b、c 三个点组成的三角形的面积的两倍
func CalcTriangleTwiceArea[V generic.SignedNumber](a, b, c Point[V]) V {
ax := b.GetX() - a.GetX()
ay := b.GetY() - a.GetY()
bx := c.GetX() - a.GetX()
by := c.GetY() - a.GetY()
return bx*ay - ax*by
}
// IsPointOnEdge 检查点是否在 edges 的任意一条边上
func IsPointOnEdge[V generic.SignedNumber](edges []LineSegment[V], point Point[V]) bool {
for _, edge := range edges {
if PointOnLineSegmentWithPointInBounds(edge.GetStart(), edge.GetEnd(), point) {
return true
}
}
return false
}
// ProjectionPointToShape 将一个点投影到一个多边形上,找到离该点最近的投影点,并返回投影点和距离
func ProjectionPointToShape[V generic.SignedNumber](point Point[V], shape Shape[V]) (Point[V], V) {
var closestProjection Point[V]
var hasClosestProjection bool
var closestDistance V
for _, edge := range shape.Edges() {
projectedPoint := CalcProjectionPoint(edge, point)
distance := CalcDistanceWithCoordinate(DoublePointToCoordinate(point, projectedPoint))
if !hasClosestProjection || distance < closestDistance {
closestDistance = distance
closestProjection = projectedPoint
hasClosestProjection = true
}
}
return closestProjection, closestDistance
}