717 lines
16 KiB
Go
717 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"math/rand"
|
|
"sort"
|
|
)
|
|
|
|
const (
|
|
minWallWidthPercent = minBuildingSizePercent
|
|
maxWallWidthPercent = maxBuildingSizePercent
|
|
wallWidthPercentStep = buildingSizePercentStep
|
|
|
|
minTurretSizePercent = 0.2
|
|
maxTurretSizePercent = maxWallWidthPercent
|
|
turretSizePercentStep = 0.1
|
|
)
|
|
|
|
func clampWallWidthPercent(v float64) float64 {
|
|
if v < minWallWidthPercent {
|
|
return minWallWidthPercent
|
|
}
|
|
if v > maxWallWidthPercent {
|
|
return maxWallWidthPercent
|
|
}
|
|
return v
|
|
}
|
|
|
|
func snapWallWidthPercent(v float64) float64 {
|
|
v = clampWallWidthPercent(v)
|
|
steps := math.Round((v - minWallWidthPercent) / wallWidthPercentStep)
|
|
return clampWallWidthPercent(minWallWidthPercent + steps*wallWidthPercentStep)
|
|
}
|
|
|
|
func normalizeWallWidthPercentRange(minPercent, maxPercent float64) (float64, float64) {
|
|
minPercent = snapWallWidthPercent(minPercent)
|
|
maxPercent = snapWallWidthPercent(maxPercent)
|
|
if minPercent > maxPercent {
|
|
minPercent, maxPercent = maxPercent, minPercent
|
|
}
|
|
return minPercent, maxPercent
|
|
}
|
|
|
|
func getWallWidthRangePixels(settings *Settings, width, height int) (float64, float64) {
|
|
minPercent, maxPercent := normalizeWallWidthPercentRange(settings.MinWallWidth, settings.MaxWallWidth)
|
|
avgDim := averageImageDimension(width, height)
|
|
if avgDim < 1 {
|
|
avgDim = 1
|
|
}
|
|
minPx := (minPercent / 100.0) * avgDim
|
|
maxPx := (maxPercent / 100.0) * avgDim
|
|
if minPx < 1 {
|
|
minPx = 1
|
|
}
|
|
if maxPx < 1 {
|
|
maxPx = 1
|
|
}
|
|
return minPx, maxPx
|
|
}
|
|
|
|
func clampTurretSizePercent(v float64) float64 {
|
|
if v < minTurretSizePercent {
|
|
return minTurretSizePercent
|
|
}
|
|
if v > maxTurretSizePercent {
|
|
return maxTurretSizePercent
|
|
}
|
|
return v
|
|
}
|
|
|
|
func snapTurretSizePercent(v float64) float64 {
|
|
v = clampTurretSizePercent(v)
|
|
steps := math.Round((v - minTurretSizePercent) / turretSizePercentStep)
|
|
return clampTurretSizePercent(minTurretSizePercent + steps*turretSizePercentStep)
|
|
}
|
|
|
|
func getTurretSizePixels(settings *Settings, width, height int) float64 {
|
|
sizePercent := snapTurretSizePercent(settings.TurretSize)
|
|
avgDim := averageImageDimension(width, height)
|
|
if avgDim < 1 {
|
|
avgDim = 1
|
|
}
|
|
sizePx := (sizePercent / 100.0) * avgDim
|
|
if sizePx < 1 {
|
|
sizePx = 1
|
|
}
|
|
return sizePx
|
|
}
|
|
|
|
type FortificationLayout struct {
|
|
Mask *PixelMask
|
|
WallIDByPixel []int
|
|
Coverages []float64
|
|
}
|
|
|
|
func GenerateFortifications(
|
|
img *image.RGBA,
|
|
width, height int,
|
|
settings *Settings,
|
|
waterMask *PixelMask,
|
|
roadNodes []*PointOfInterest,
|
|
seed int64,
|
|
) (*FortificationLayout, [][]image.Point) {
|
|
layout := &FortificationLayout{
|
|
Mask: NewPixelMask(width, height),
|
|
WallIDByPixel: make([]int, width*height),
|
|
}
|
|
if settings.NumWalls <= 0 || settings.CityCoverage <= 0 {
|
|
return layout, nil
|
|
}
|
|
if waterMask == nil {
|
|
waterMask = NewPixelMask(width, height)
|
|
}
|
|
if img == nil {
|
|
img = image.NewRGBA(image.Rect(0, 0, width, height))
|
|
}
|
|
|
|
randSrc := rand.New(rand.NewSource(seed))
|
|
minWidthPx, maxWidthPx := getWallWidthRangePixels(settings, width, height)
|
|
wallColor := color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
|
|
|
walls := make([][]image.Point, 0, settings.NumWalls)
|
|
outerCoverage := clamp(settings.CityCoverage, 1, 100)
|
|
totalWalls := max(1, settings.NumWalls)
|
|
prevCoverage := 101.0
|
|
layout.Coverages = make([]float64, 0, totalWalls)
|
|
|
|
for i := 0; i < totalWalls; i++ {
|
|
baseCoverage := outerCoverage * float64(totalWalls-i) / float64(totalWalls)
|
|
coverage := baseCoverage
|
|
if i > 0 {
|
|
coverage += randSrc.Float64()*10.0 - 5.0
|
|
}
|
|
coverage = clamp(coverage, 1, 100)
|
|
if coverage >= prevCoverage {
|
|
coverage = prevCoverage - 1
|
|
if coverage < 1 {
|
|
coverage = 1
|
|
}
|
|
}
|
|
prevCoverage = coverage
|
|
layout.Coverages = append(layout.Coverages, coverage)
|
|
|
|
nodes := estimateWallNodeCount(coverage)
|
|
wallPath := generateWallLoop(width, height, coverage, settings.WallCurvyness, nodes, randSrc, roadNodes)
|
|
if len(wallPath) < 3 {
|
|
continue
|
|
}
|
|
|
|
wallWidthPx := minWidthPx
|
|
if maxWidthPx > minWidthPx {
|
|
wallWidthPx = minWidthPx + randSrc.Float64()*(maxWidthPx-minWidthPx)
|
|
}
|
|
wallWidth := int(math.Round(wallWidthPx))
|
|
if wallWidth < 1 {
|
|
wallWidth = 1
|
|
}
|
|
|
|
pixels := drawWallLoopWithWaterGaps(img, wallPath, wallColor, wallWidth, layout.Mask, waterMask, layout.WallIDByPixel, i+1)
|
|
if len(pixels) > 0 {
|
|
walls = append(walls, pixels)
|
|
}
|
|
}
|
|
|
|
return layout, walls
|
|
}
|
|
|
|
func estimateWallNodeCount(coverage float64) int {
|
|
n := int(math.Round(20 + coverage*0.7))
|
|
if n < 20 {
|
|
n = 20
|
|
}
|
|
if n > 96 {
|
|
n = 96
|
|
}
|
|
return n
|
|
}
|
|
|
|
func generateWallLoop(width, height int, coverage, curvyness float64, nodes int, randSrc *rand.Rand, roadNodes []*PointOfInterest) []image.Point {
|
|
if width <= 0 || height <= 0 || nodes < 3 {
|
|
return nil
|
|
}
|
|
|
|
centerX, centerY, baseRadiusX, baseRadiusY := wallEllipseFromRoadNodes(width, height, coverage, roadNodes)
|
|
curveScale := clamp(curvyness, 0, 100) / 100.0
|
|
warpAmp := 0.20 * curveScale
|
|
phaseA := randSrc.Float64() * 2 * math.Pi
|
|
phaseB := randSrc.Float64() * 2 * math.Pi
|
|
|
|
out := make([]image.Point, 0, nodes+1)
|
|
for i := 0; i < nodes; i++ {
|
|
t := (2 * math.Pi * float64(i)) / float64(nodes)
|
|
warp := 1.0 + warpAmp*(0.6*math.Sin(3*t+phaseA)+0.4*math.Sin(5*t+phaseB))
|
|
if warp < 0.7 {
|
|
warp = 0.7
|
|
}
|
|
rx := baseRadiusX * warp
|
|
ry := baseRadiusY * warp
|
|
x := int(math.Round(centerX + rx*math.Cos(t)))
|
|
y := int(math.Round(centerY + ry*math.Sin(t)))
|
|
if x < 0 {
|
|
x = 0
|
|
}
|
|
if x >= width {
|
|
x = width - 1
|
|
}
|
|
if y < 0 {
|
|
y = 0
|
|
}
|
|
if y >= height {
|
|
y = height - 1
|
|
}
|
|
out = append(out, image.Point{X: x, Y: y})
|
|
}
|
|
if len(out) > 0 {
|
|
out = append(out, out[0])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func wallEllipseFromRoadNodes(width, height int, coverage float64, roadNodes []*PointOfInterest) (centerX, centerY, radiusX, radiusY float64) {
|
|
centerX = float64(width-1) * 0.5
|
|
centerY = float64(height-1) * 0.5
|
|
coverageRadius := math.Sqrt(clamp(coverage, 1, 100) / 100.0)
|
|
radiusX = centerX * coverageRadius
|
|
radiusY = centerY * coverageRadius
|
|
|
|
if len(roadNodes) == 0 {
|
|
return centerX, centerY, radiusX, radiusY
|
|
}
|
|
|
|
sumX, sumY := 0.0, 0.0
|
|
for _, n := range roadNodes {
|
|
sumX += float64(n.X)
|
|
sumY += float64(n.Y)
|
|
}
|
|
centerX = sumX / float64(len(roadNodes))
|
|
centerY = sumY / float64(len(roadNodes))
|
|
|
|
dists := make([]float64, 0, len(roadNodes))
|
|
var sx, sy float64
|
|
for _, n := range roadNodes {
|
|
dx := float64(n.X) - centerX
|
|
dy := float64(n.Y) - centerY
|
|
dists = append(dists, math.Hypot(dx, dy))
|
|
sx += dx * dx
|
|
sy += dy * dy
|
|
}
|
|
sort.Float64s(dists)
|
|
q := clamp(coverage, 1, 100) / 100.0
|
|
idx := int(math.Ceil(q*float64(len(dists)))) - 1
|
|
if idx < 0 {
|
|
idx = 0
|
|
}
|
|
if idx >= len(dists) {
|
|
idx = len(dists) - 1
|
|
}
|
|
baseRadius := dists[idx]
|
|
if baseRadius < 10 {
|
|
baseRadius = 10
|
|
}
|
|
|
|
stdX := math.Sqrt(sx / float64(len(roadNodes)))
|
|
stdY := math.Sqrt(sy / float64(len(roadNodes)))
|
|
aspect := 1.0
|
|
if stdY > 0.001 {
|
|
aspect = stdX / stdY
|
|
}
|
|
aspect = clamp(aspect, 0.65, 1.55)
|
|
radiusX = baseRadius * aspect
|
|
radiusY = baseRadius / aspect
|
|
|
|
maxRadiusX := math.Max(5, math.Min(centerX, float64(width-1)-centerX))
|
|
maxRadiusY := math.Max(5, math.Min(centerY, float64(height-1)-centerY))
|
|
radiusX = clamp(radiusX, 5, maxRadiusX)
|
|
radiusY = clamp(radiusY, 5, maxRadiusY)
|
|
|
|
return centerX, centerY, radiusX, radiusY
|
|
}
|
|
|
|
func drawWallLoopWithWaterGaps(
|
|
img *image.RGBA,
|
|
loop []image.Point,
|
|
col color.RGBA,
|
|
width int,
|
|
wallMask *PixelMask,
|
|
waterMask *PixelMask,
|
|
wallIDByPixel []int,
|
|
wallID int,
|
|
) []image.Point {
|
|
if len(loop) < 2 || wallMask == nil {
|
|
return nil
|
|
}
|
|
seen := make(map[int]bool)
|
|
pixels := make([]image.Point, 0, len(loop)*8)
|
|
radius := max(1, width/2)
|
|
|
|
for i := 0; i < len(loop)-1; i++ {
|
|
a := loop[i]
|
|
b := loop[i+1]
|
|
drawSegmentSelective(a.X, a.Y, b.X, b.Y, func(x, y int) {
|
|
if !wallMask.InBounds(x, y) {
|
|
return
|
|
}
|
|
if waterMask != nil && waterMask.GetXY(x, y) {
|
|
return
|
|
}
|
|
for dy := -radius; dy <= radius; dy++ {
|
|
yy := y + dy
|
|
if yy < 0 || yy >= wallMask.Height {
|
|
continue
|
|
}
|
|
for dx := -radius; dx <= radius; dx++ {
|
|
if dx*dx+dy*dy > radius*radius {
|
|
continue
|
|
}
|
|
xx := x + dx
|
|
if xx < 0 || xx >= wallMask.Width {
|
|
continue
|
|
}
|
|
if waterMask != nil && waterMask.GetXY(xx, yy) {
|
|
continue
|
|
}
|
|
wallMask.SetXY(xx, yy)
|
|
if len(wallIDByPixel) == wallMask.Width*wallMask.Height {
|
|
wallIDByPixel[yy*wallMask.Width+xx] = wallID
|
|
}
|
|
img.Set(xx, yy, col)
|
|
idx := yy*wallMask.Width + xx
|
|
if !seen[idx] {
|
|
seen[idx] = true
|
|
pixels = append(pixels, image.Point{X: xx, Y: yy})
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
return pixels
|
|
}
|
|
|
|
func drawSegmentSelective(x0, y0, x1, y1 int, plot func(x, y int)) {
|
|
dx := abs(x1 - x0)
|
|
dy := abs(y1 - y0)
|
|
sx := -1
|
|
if x0 < x1 {
|
|
sx = 1
|
|
}
|
|
sy := -1
|
|
if y0 < y1 {
|
|
sy = 1
|
|
}
|
|
err := dx - dy
|
|
for {
|
|
plot(x0, y0)
|
|
if x0 == x1 && y0 == y1 {
|
|
break
|
|
}
|
|
e2 := 2 * err
|
|
if e2 > -dy {
|
|
err -= dy
|
|
x0 += sx
|
|
}
|
|
if e2 < dx {
|
|
err += dx
|
|
y0 += sy
|
|
}
|
|
}
|
|
}
|
|
|
|
func cloneMask(src *PixelMask) *PixelMask {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
dst := NewPixelMask(src.Width, src.Height)
|
|
copy(dst.Data, src.Data)
|
|
return dst
|
|
}
|
|
|
|
func drawWallMask(img *image.RGBA, wallMask *PixelMask) {
|
|
if img == nil || wallMask == nil {
|
|
return
|
|
}
|
|
black := color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
|
for y := 0; y < wallMask.Height; y++ {
|
|
row := y * wallMask.Width
|
|
for x := 0; x < wallMask.Width; x++ {
|
|
if wallMask.Data[row+x] != 0 {
|
|
img.Set(x, y, black)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func GenerateTurrets(
|
|
img *image.RGBA,
|
|
width, height int,
|
|
settings *Settings,
|
|
layout *FortificationLayout,
|
|
waterMask, roadMask *PixelMask,
|
|
roads []*Road,
|
|
) *PixelMask {
|
|
mask := NewPixelMask(width, height)
|
|
if !settings.ShowTurrets || layout == nil || layout.Mask == nil || len(layout.WallIDByPixel) != width*height {
|
|
return mask
|
|
}
|
|
if img == nil {
|
|
img = image.NewRGBA(image.Rect(0, 0, width, height))
|
|
}
|
|
if waterMask == nil {
|
|
waterMask = NewPixelMask(width, height)
|
|
}
|
|
if roadMask == nil {
|
|
roadMask = NewPixelMask(width, height)
|
|
}
|
|
|
|
sizePx := getTurretSizePixels(settings, width, height)
|
|
radius := int(math.Round(sizePx / 2.0))
|
|
if radius < 1 {
|
|
radius = 1
|
|
}
|
|
shape := settings.TurretShape
|
|
if shape != "square" {
|
|
shape = "circular"
|
|
}
|
|
colorRed := color.RGBA{R: 220, G: 25, B: 25, A: 255}
|
|
|
|
wallPoints := make(map[int][]image.Point)
|
|
waterMeetPoints := make(map[int][]image.Point)
|
|
for y := 0; y < height; y++ {
|
|
row := y * width
|
|
for x := 0; x < width; x++ {
|
|
wid := layout.WallIDByPixel[row+x]
|
|
if wid <= 0 {
|
|
continue
|
|
}
|
|
if !isBoundaryWallPixel(x, y, layout.Mask) {
|
|
continue
|
|
}
|
|
p := image.Point{X: x, Y: y}
|
|
wallPoints[wid] = append(wallPoints[wid], p)
|
|
if touchesWater(x, y, waterMask) {
|
|
waterMeetPoints[wid] = append(waterMeetPoints[wid], p)
|
|
}
|
|
}
|
|
}
|
|
|
|
occupied := make(map[int]bool)
|
|
addTurret := func(center image.Point) {
|
|
snapped, ok := snapPointToWall(center, layout.Mask, max(3, radius*4))
|
|
if !ok {
|
|
return
|
|
}
|
|
if nearbyTurretExists(mask, snapped, max(2, radius)) {
|
|
return
|
|
}
|
|
key := snapped.Y*width + snapped.X
|
|
if occupied[key] {
|
|
return
|
|
}
|
|
occupied[key] = true
|
|
drawTurret(img, mask, snapped, radius, shape, colorRed)
|
|
}
|
|
|
|
// Base spacing turrets along each wall ring.
|
|
for wid, pts := range wallPoints {
|
|
if len(pts) == 0 {
|
|
continue
|
|
}
|
|
centroid := averagePoint(pts)
|
|
sort.Slice(pts, func(i, j int) bool {
|
|
ai := math.Atan2(float64(pts[i].Y-centroid.Y), float64(pts[i].X-centroid.X))
|
|
aj := math.Atan2(float64(pts[j].Y-centroid.Y), float64(pts[j].X-centroid.X))
|
|
return ai < aj
|
|
})
|
|
// Spacing is "distance along wall as % of wall circumference", independent of turret size.
|
|
spacingPct := clamp(settings.TurretSpacing, 0, 100)
|
|
step := int(math.Round((spacingPct / 100.0) * float64(len(pts))))
|
|
if step < 1 {
|
|
step = 1
|
|
}
|
|
if step > len(pts) {
|
|
step = len(pts)
|
|
}
|
|
for i := 0; i < len(pts); i += step {
|
|
addTurret(pts[i])
|
|
}
|
|
|
|
// Always place turrets where wall meets water.
|
|
for _, p := range waterMeetPoints[wid] {
|
|
addTurret(p)
|
|
}
|
|
}
|
|
|
|
// Gate turrets: one on each side of each road crossing, spacing = 3x road width.
|
|
for _, r := range roads {
|
|
if r == nil || len(r.Points) < 2 {
|
|
continue
|
|
}
|
|
gates := roadGateCentersForRoad(r, layout)
|
|
if len(gates) == 0 {
|
|
continue
|
|
}
|
|
for _, g := range gates {
|
|
tx, ty, ok := fortEstimateWallTangent(g, layout.Mask)
|
|
if !ok {
|
|
continue
|
|
}
|
|
offset := 1.5 * float64(max(1, r.Width))
|
|
left := image.Point{
|
|
X: int(math.Round(float64(g.X) + tx*offset)),
|
|
Y: int(math.Round(float64(g.Y) + ty*offset)),
|
|
}
|
|
right := image.Point{
|
|
X: int(math.Round(float64(g.X) - tx*offset)),
|
|
Y: int(math.Round(float64(g.Y) - ty*offset)),
|
|
}
|
|
addTurret(left)
|
|
addTurret(right)
|
|
}
|
|
}
|
|
|
|
return mask
|
|
}
|
|
|
|
func isBoundaryWallPixel(x, y int, wallMask *PixelMask) bool {
|
|
if wallMask == nil || !wallMask.GetXY(x, y) {
|
|
return false
|
|
}
|
|
for dy := -1; dy <= 1; dy++ {
|
|
for dx := -1; dx <= 1; dx++ {
|
|
if dx == 0 && dy == 0 {
|
|
continue
|
|
}
|
|
nx, ny := x+dx, y+dy
|
|
if !wallMask.InBounds(nx, ny) || !wallMask.GetXY(nx, ny) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func touchesWater(x, y int, waterMask *PixelMask) bool {
|
|
if waterMask == nil {
|
|
return false
|
|
}
|
|
for dy := -1; dy <= 1; dy++ {
|
|
for dx := -1; dx <= 1; dx++ {
|
|
nx, ny := x+dx, y+dy
|
|
if waterMask.GetXY(nx, ny) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func averagePoint(points []image.Point) image.Point {
|
|
if len(points) == 0 {
|
|
return image.Point{}
|
|
}
|
|
var sx, sy int
|
|
for _, p := range points {
|
|
sx += p.X
|
|
sy += p.Y
|
|
}
|
|
return image.Point{X: sx / len(points), Y: sy / len(points)}
|
|
}
|
|
|
|
func drawTurret(img *image.RGBA, mask *PixelMask, center image.Point, radius int, shape string, col color.RGBA) {
|
|
for dy := -radius; dy <= radius; dy++ {
|
|
for dx := -radius; dx <= radius; dx++ {
|
|
if shape == "circular" && dx*dx+dy*dy > radius*radius {
|
|
continue
|
|
}
|
|
x, y := center.X+dx, center.Y+dy
|
|
if !mask.InBounds(x, y) {
|
|
continue
|
|
}
|
|
mask.SetXY(x, y)
|
|
img.Set(x, y, col)
|
|
}
|
|
}
|
|
}
|
|
|
|
func nearbyTurretExists(mask *PixelMask, center image.Point, radius int) bool {
|
|
if mask == nil {
|
|
return false
|
|
}
|
|
for dy := -radius; dy <= radius; dy++ {
|
|
for dx := -radius; dx <= radius; dx++ {
|
|
if dx*dx+dy*dy > radius*radius {
|
|
continue
|
|
}
|
|
if mask.GetXY(center.X+dx, center.Y+dy) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func snapPointToWall(center image.Point, wallMask *PixelMask, maxRadius int) (image.Point, bool) {
|
|
if wallMask == nil {
|
|
return image.Point{}, false
|
|
}
|
|
if wallMask.GetXY(center.X, center.Y) {
|
|
return center, true
|
|
}
|
|
if maxRadius < 1 {
|
|
maxRadius = 1
|
|
}
|
|
best := image.Point{}
|
|
bestD2 := math.MaxInt
|
|
found := false
|
|
for r := 1; r <= maxRadius; r++ {
|
|
minX := center.X - r
|
|
maxX := center.X + r
|
|
minY := center.Y - r
|
|
maxY := center.Y + r
|
|
for y := minY; y <= maxY; y++ {
|
|
for x := minX; x <= maxX; x++ {
|
|
if x != minX && x != maxX && y != minY && y != maxY {
|
|
continue
|
|
}
|
|
if !wallMask.GetXY(x, y) {
|
|
continue
|
|
}
|
|
dx := x - center.X
|
|
dy := y - center.Y
|
|
d2 := dx*dx + dy*dy
|
|
if d2 < bestD2 {
|
|
bestD2 = d2
|
|
best = image.Point{X: x, Y: y}
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
if found {
|
|
return best, true
|
|
}
|
|
}
|
|
return image.Point{}, false
|
|
}
|
|
|
|
func roadGateCentersForRoad(r *Road, layout *FortificationLayout) []image.Point {
|
|
out := make([]image.Point, 0, 2)
|
|
if r == nil || layout == nil || layout.Mask == nil || len(r.Points) < 2 {
|
|
return out
|
|
}
|
|
prevID := 0
|
|
if layout.Mask.InBounds(r.Points[0].Point.X, r.Points[0].Point.Y) {
|
|
prevID = layout.WallIDByPixel[r.Points[0].Point.Y*layout.Mask.Width+r.Points[0].Point.X]
|
|
}
|
|
for i := 1; i < len(r.Points); i++ {
|
|
p := r.Points[i].Point
|
|
currID := 0
|
|
if layout.Mask.InBounds(p.X, p.Y) {
|
|
currID = layout.WallIDByPixel[p.Y*layout.Mask.Width+p.X]
|
|
}
|
|
if (prevID == 0 && currID > 0) || (prevID > 0 && currID == 0) {
|
|
out = append(out, p)
|
|
}
|
|
prevID = currID
|
|
}
|
|
return out
|
|
}
|
|
|
|
func fortEstimateWallTangent(mid image.Point, wallMask *PixelMask) (float64, float64, bool) {
|
|
if wallMask == nil {
|
|
return 0, 0, false
|
|
}
|
|
const r = 4
|
|
var pts [][2]float64
|
|
for dy := -r; dy <= r; dy++ {
|
|
y := mid.Y + dy
|
|
if y < 0 || y >= wallMask.Height {
|
|
continue
|
|
}
|
|
for dx := -r; dx <= r; dx++ {
|
|
x := mid.X + dx
|
|
if x < 0 || x >= wallMask.Width {
|
|
continue
|
|
}
|
|
if wallMask.GetXY(x, y) {
|
|
pts = append(pts, [2]float64{float64(x), float64(y)})
|
|
}
|
|
}
|
|
}
|
|
if len(pts) < 3 {
|
|
return 0, 0, false
|
|
}
|
|
var mx, my float64
|
|
for _, p := range pts {
|
|
mx += p[0]
|
|
my += p[1]
|
|
}
|
|
mx /= float64(len(pts))
|
|
my /= float64(len(pts))
|
|
var sxx, syy, sxy float64
|
|
for _, p := range pts {
|
|
dx := p[0] - mx
|
|
dy := p[1] - my
|
|
sxx += dx * dx
|
|
syy += dy * dy
|
|
sxy += dx * dy
|
|
}
|
|
if sxx+syy < 0.001 {
|
|
return 0, 0, false
|
|
}
|
|
theta := 0.5 * math.Atan2(2*sxy, sxx-syy)
|
|
return math.Cos(theta), math.Sin(theta), true
|
|
}
|