added new version of road generation with dynamic road numbers based on number of buildings
This commit is contained in:
@@ -31,10 +31,10 @@ A remake in go of a program that generates maps of rpg like towns. Inspied by Ro
|
|||||||
| **Min River Width** | The minimum width of a generated river, as a percentage of the smaller of the map's width or height. | `1%` to `100%` |
|
| **Min River Width** | The minimum width of a generated river, as a percentage of the smaller of the map's width or height. | `1%` to `100%` |
|
||||||
| **Max River Width** | The maximum width of a generated river, as a percentage of the smaller of the map's width or height. | `1%` to `100%` |
|
| **Max River Width** | The maximum width of a generated river, as a percentage of the smaller of the map's width or height. | `1%` to `100%` |
|
||||||
| **River Curvyness** | How curvy the rivers are. At 100%, rivers will meander significantly. At 0%, they will be perfectly straight lines. | `0%` (straight) to `100%` (very curvy) |
|
| **River Curvyness** | How curvy the rivers are. At 100%, rivers will meander significantly. At 0%, they will be perfectly straight lines. | `0%` (straight) to `100%` (very curvy) |
|
||||||
| **Num Roads** | The number of roads to generate. | `0` to `1000` |
|
|
||||||
| **Min Road Width** | The minimum width of a generated road in pixels. | `1` to `100` |
|
| **Min Road Width** | The minimum width of a generated road in pixels. | `1` to `100` |
|
||||||
| **Max Road Width** | The maximum width of a generated road in pixels. | `1` to `100` |
|
| **Max Road Width** | The maximum width of a generated road in pixels. | `1` to `100` |
|
||||||
| **Road Exits** | The number of roads that start at the edge of the map and extend inwards. | `0` to `100` |
|
| **Road Exits** | The number of roads that start at the edge of the map and extend inwards. | `0` to `100` |
|
||||||
|
| **Minimum Road Angle** | The minimum angle allowed between two roads at a junction. Higher values reduce tightly packed, nearly parallel branches. | `0°` to `180°` |
|
||||||
| **Road Curvyness** | How curvy the roads are. At 100%, roads will have many twists and turns. At 0%, they will be perfectly straight. | `0%` (straight) to `100%` (very curvy) |
|
| **Road Curvyness** | How curvy the roads are. At 100%, roads will have many twists and turns. At 0%, they will be perfectly straight. | `0%` (straight) to `100%` (very curvy) |
|
||||||
| **Road Distribution** | Controls the distribution of roads. At 100%, roads will be spread out across the entire map. At 0%, they will be clustered in the center. | `0%` (centered) to `100%` (spread out) |
|
| **Road Distribution** | Controls the distribution of roads. At 100%, roads will be spread out across the entire map. At 0%, they will be clustered in the center. | `0%` (centered) to `100%` (spread out) |
|
||||||
| **Num Buildings** | The number of buildings to generate. | `0` to `1000` |
|
| **Num Buildings** | The number of buildings to generate. | `0` to `1000` |
|
||||||
|
|||||||
@@ -107,8 +107,11 @@ func newNumericInputSlider(min, max float64, initialValue float64, format string
|
|||||||
|
|
||||||
// validate checks text entry for valid numeric input within the defined range
|
// validate checks text entry for valid numeric input within the defined range
|
||||||
func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
text = strings.TrimSuffix(text, "px")
|
text = strings.TrimSuffix(text, "px")
|
||||||
text = strings.TrimSuffix(text, "%")
|
text = strings.TrimSuffix(text, "%")
|
||||||
|
text = strings.TrimSuffix(text, "°")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
val, err := strconv.ParseFloat(text, 64)
|
val, err := strconv.ParseFloat(text, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.errorLabel.SetText("Not a number")
|
s.errorLabel.SetText("Not a number")
|
||||||
|
|||||||
@@ -429,18 +429,6 @@ func main() {
|
|||||||
val, _ := treeClumpinessSlider.value.Get()
|
val, _ := treeClumpinessSlider.value.Get()
|
||||||
settings.TreeClumpiness = val
|
settings.TreeClumpiness = val
|
||||||
}))
|
}))
|
||||||
numRoadsSlider := newNumericInputSlider(0, 2000, float64(settings.NumRoads), "%.0f", "Number of Roads")
|
|
||||||
numRoadsSlider.entry.OnChanged = func(s string) {
|
|
||||||
numRoadsSlider.validate(s, func(hasError bool) {
|
|
||||||
errorStates["numRoads"] = hasError
|
|
||||||
updateGenerateBtnState()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
numRoadsSlider.value.AddListener(binding.NewDataListener(func() {
|
|
||||||
val, _ := numRoadsSlider.value.Get()
|
|
||||||
settings.NumRoads = int(val)
|
|
||||||
}))
|
|
||||||
|
|
||||||
minRoadWidthSlider := newNumericInputSlider(1, 150, settings.MinRoadWidth, "%.0fpx", "Min Road Width")
|
minRoadWidthSlider := newNumericInputSlider(1, 150, settings.MinRoadWidth, "%.0fpx", "Min Road Width")
|
||||||
minRoadWidthSlider.entry.OnChanged = func(s string) {
|
minRoadWidthSlider.entry.OnChanged = func(s string) {
|
||||||
minRoadWidthSlider.validate(s, func(hasError bool) {
|
minRoadWidthSlider.validate(s, func(hasError bool) {
|
||||||
@@ -501,6 +489,18 @@ func main() {
|
|||||||
settings.RoadDistribution = val
|
settings.RoadDistribution = val
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
minRoadAngleSlider := newNumericInputSlider(0, 180, settings.MinRoadAngle, "%.0f°", "Minimum Road Angle")
|
||||||
|
minRoadAngleSlider.entry.OnChanged = func(s string) {
|
||||||
|
minRoadAngleSlider.validate(s, func(hasError bool) {
|
||||||
|
errorStates["minRoadAngle"] = hasError
|
||||||
|
updateGenerateBtnState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
minRoadAngleSlider.value.AddListener(binding.NewDataListener(func() {
|
||||||
|
val, _ := minRoadAngleSlider.value.Get()
|
||||||
|
settings.MinRoadAngle = val
|
||||||
|
}))
|
||||||
|
|
||||||
// Create UI elements for error display and action buttons
|
// Create UI elements for error display and action buttons
|
||||||
errorLabel := widget.NewLabel("")
|
errorLabel := widget.NewLabel("")
|
||||||
errorLabel.Wrapping = fyne.TextWrapWord
|
errorLabel.Wrapping = fyne.TextWrapWord
|
||||||
@@ -734,10 +734,10 @@ func main() {
|
|||||||
))
|
))
|
||||||
|
|
||||||
roadsTab := container.NewTabItem("Roads", container.NewVBox(
|
roadsTab := container.NewTabItem("Roads", container.NewVBox(
|
||||||
numRoadsSlider,
|
|
||||||
minRoadWidthSlider,
|
minRoadWidthSlider,
|
||||||
maxRoadWidthSlider,
|
maxRoadWidthSlider,
|
||||||
roadExitsSlider,
|
roadExitsSlider,
|
||||||
|
minRoadAngleSlider,
|
||||||
roadCurvynessSlider,
|
roadCurvynessSlider,
|
||||||
roadDistributionSlider,
|
roadDistributionSlider,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
|
||||||
"unsafe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PointOfInterest represents a location where roads may start, end, or intersect
|
// PointOfInterest represents a location where roads may start, end, or intersect.
|
||||||
type PointOfInterest struct {
|
type PointOfInterest struct {
|
||||||
X, Y int
|
X, Y int
|
||||||
Connections int
|
Connections int
|
||||||
IsExit bool
|
TargetDegree int
|
||||||
|
IsExit bool
|
||||||
|
ArterialWeight float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// PathPoint represents a single point in a road's path with bridge flag
|
// PathPoint represents a single point in a road's path with bridge flag.
|
||||||
type PathPoint struct {
|
type PathPoint struct {
|
||||||
Point image.Point
|
Point image.Point
|
||||||
IsBridge bool
|
IsBridge bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Road represents a connection between two Points of Interest
|
// Road represents a connection between two points of interest.
|
||||||
type Road struct {
|
type Road struct {
|
||||||
Start, End *PointOfInterest
|
Start, End *PointOfInterest
|
||||||
Width int
|
Width int
|
||||||
@@ -32,29 +31,32 @@ type Road struct {
|
|||||||
Importance int
|
Importance int
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateRoads creates roads on the map
|
// GenerateRoads creates roads on the map.
|
||||||
func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image, allWaterPixels []image.Point, seed int64) ([]image.Point, []image.Point, *image.RGBA) {
|
func GenerateRoads(width, height int, settings *Settings, _ image.Image, allWaterPixels []image.Point, seed int64) ([]image.Point, []image.Point, *image.RGBA) {
|
||||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
img.Set(x, y, color.Transparent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
randSrc := rand.New(rand.NewSource(seed))
|
randSrc := rand.New(rand.NewSource(seed))
|
||||||
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
|
roadColor := color.RGBA{R: 139, G: 69, B: 19, A: 255}
|
||||||
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255}
|
bridgeColor := color.RGBA{R: 60, G: 42, B: 33, A: 255}
|
||||||
|
waterMap := make(map[image.Point]bool, len(allWaterPixels))
|
||||||
|
for _, p := range allWaterPixels {
|
||||||
|
waterMap[p] = true
|
||||||
|
}
|
||||||
|
|
||||||
pois := generatePOIs(width, height, settings, allWaterPixels, randSrc)
|
roadTarget := estimateRoadTarget(settings, randSrc)
|
||||||
if len(pois) == 0 {
|
pois := generatePOIs(width, height, settings, waterMap, randSrc, roadTarget)
|
||||||
|
if len(pois) < 2 {
|
||||||
return nil, nil, img
|
return nil, nil, img
|
||||||
}
|
}
|
||||||
|
|
||||||
roads := connectPOIs(pois, width, height, settings, randSrc, allWaterPixels)
|
roads := connectPOIs(pois, width, height, settings, randSrc, waterMap, roadTarget)
|
||||||
assignRoadWidths(roads, settings)
|
roads = appendExitRoads(roads, pois, width, height, settings, randSrc, waterMap)
|
||||||
|
if len(roads) == 0 {
|
||||||
|
return nil, nil, img
|
||||||
|
}
|
||||||
|
assignRoadWidths(roads, settings, randSrc)
|
||||||
|
|
||||||
var allRoadPixels []image.Point
|
allRoadPixels := make([]image.Point, 0, len(roads)*64)
|
||||||
var allBridgePixels []image.Point
|
allBridgePixels := make([]image.Point, 0, len(roads)*16)
|
||||||
for _, road := range roads {
|
for _, road := range roads {
|
||||||
roadPixels, bridgePixels := drawRoad(img, road.Points, roadColor, bridgeColor, road.Width)
|
roadPixels, bridgePixels := drawRoad(img, road.Points, roadColor, bridgeColor, road.Width)
|
||||||
allRoadPixels = append(allRoadPixels, roadPixels...)
|
allRoadPixels = append(allRoadPixels, roadPixels...)
|
||||||
@@ -64,236 +66,537 @@ func GenerateRoads(width, height int, settings *Settings, noiseImg image.Image,
|
|||||||
return allRoadPixels, allBridgePixels, img
|
return allRoadPixels, allBridgePixels, img
|
||||||
}
|
}
|
||||||
|
|
||||||
// generatePOIs creates initial points where roads will originate
|
func generatePOIs(width, height int, settings *Settings, waterMap map[image.Point]bool, randSrc *rand.Rand, roadTarget int) []*PointOfInterest {
|
||||||
func generatePOIs(width, height int, settings *Settings, allWaterPixels []image.Point, randSrc *rand.Rand) []*PointOfInterest {
|
distribution := clamp01(settings.RoadDistribution / 100.0)
|
||||||
numPOIs := settings.NumRoads / 2
|
avgBuildingSize := (settings.MinBuildingSize + settings.MaxBuildingSize) / 2.0
|
||||||
if numPOIs == 0 {
|
if avgBuildingSize < 1 {
|
||||||
|
avgBuildingSize = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
coreNodes := estimateCoreNodeCount(width, height, distribution, avgBuildingSize, settings.NumBuildings)
|
||||||
|
if coreNodes < 2 {
|
||||||
|
coreNodes = 2
|
||||||
|
}
|
||||||
|
// Keep node count compatible with the requested road segment budget so a connected graph is feasible.
|
||||||
|
maxTotalNodes := max(2, roadTarget+1)
|
||||||
|
if coreNodes > maxTotalNodes {
|
||||||
|
coreNodes = maxTotalNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
centerX := width / 2
|
||||||
|
centerY := height / 2
|
||||||
|
maxRadius := math.Min(float64(width), float64(height)) * 0.48
|
||||||
|
minRadius := math.Min(float64(width), float64(height)) * 0.10
|
||||||
|
radius := minRadius + (maxRadius-minRadius)*distribution
|
||||||
|
|
||||||
|
pois := make([]*PointOfInterest, 0, coreNodes)
|
||||||
|
for len(pois) < coreNodes {
|
||||||
|
x, y, ok := sampleCorePOI(centerX, centerY, radius, width, height, randSrc)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p := image.Point{X: x, Y: y}
|
||||||
|
// Keep larger spacing between intersections so buildings have room.
|
||||||
|
if waterMap[p] || isTooCloseToExisting(pois, x, y, avgBuildingSize*1.1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pois = append(pois, &PointOfInterest{X: x, Y: y, TargetDegree: sampleTargetDegree(randSrc)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pois) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
waterMap := make(map[image.Point]bool)
|
for _, poi := range pois {
|
||||||
for _, p := range allWaterPixels {
|
centerDist := math.Hypot(float64(poi.X-centerX), float64(poi.Y-centerY))
|
||||||
waterMap[p] = true
|
centerFactor := 1.0 - clamp01(centerDist/(radius+1))
|
||||||
}
|
sizeFactor := clamp01((avgBuildingSize - 4.0) / 40.0)
|
||||||
|
poi.ArterialWeight = clamp01(0.60*centerFactor + 0.40*sizeFactor)
|
||||||
numExits := settings.RoadExits
|
|
||||||
if numExits > settings.NumRoads {
|
|
||||||
numExits = settings.NumRoads
|
|
||||||
}
|
|
||||||
|
|
||||||
pois := make([]*PointOfInterest, 0, numPOIs)
|
|
||||||
centerX := width / 2
|
|
||||||
centerY := height / 2
|
|
||||||
|
|
||||||
maxRadius := math.Min(float64(width)/2, float64(height)/2)
|
|
||||||
radius := maxRadius * (settings.RoadDistribution / 100.0)
|
|
||||||
|
|
||||||
for i := 0; i < numPOIs; i++ {
|
|
||||||
var x, y int
|
|
||||||
found := false
|
|
||||||
for j := 0; j < 100; j++ {
|
|
||||||
if i < numExits {
|
|
||||||
side := randSrc.Intn(4)
|
|
||||||
switch side {
|
|
||||||
case 0:
|
|
||||||
x = randSrc.Intn(width)
|
|
||||||
y = 0
|
|
||||||
case 1:
|
|
||||||
x = randSrc.Intn(width)
|
|
||||||
y = height - 1
|
|
||||||
case 2:
|
|
||||||
x = 0
|
|
||||||
y = randSrc.Intn(height)
|
|
||||||
case 3:
|
|
||||||
x = width - 1
|
|
||||||
y = randSrc.Intn(height)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
angle := randSrc.Float64() * 2 * math.Pi
|
|
||||||
r := math.Sqrt(randSrc.Float64()) * radius
|
|
||||||
x = int(float64(centerX) + r*math.Cos(angle))
|
|
||||||
y = int(float64(centerY) + r*math.Sin(angle))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !waterMap[image.Point{X: x, Y: y}] {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
isExit := i < numExits
|
|
||||||
pois = append(pois, &PointOfInterest{X: x, Y: y, IsExit: isExit})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pois
|
return pois
|
||||||
}
|
}
|
||||||
|
|
||||||
// connectPOIs creates roads by connecting Points of Interest
|
func estimateCoreNodeCount(width, height int, distribution, avgBuildingSize float64, numBuildings int) int {
|
||||||
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, allWaterPixels []image.Point) []*Road {
|
targetArea := float64(width*height) * (0.10 + 0.90*distribution)
|
||||||
if len(pois) < 2 {
|
spacing := avgBuildingSize * (1.4 - 0.5*distribution)
|
||||||
|
if spacing < 6 {
|
||||||
|
spacing = 6
|
||||||
|
}
|
||||||
|
byArea := int((targetArea / (spacing * spacing)) * 0.18)
|
||||||
|
buildingPressure := int(math.Sqrt(float64(max(numBuildings, 1))) * (0.7 + distribution*0.9))
|
||||||
|
nodes := byArea + buildingPressure
|
||||||
|
if nodes < 8 {
|
||||||
|
nodes = 8
|
||||||
|
}
|
||||||
|
maxNodes := int(clamp(float64(width*height)/50000.0, 80, 550))
|
||||||
|
if nodes > maxNodes {
|
||||||
|
nodes = maxNodes
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleCorePOI(centerX, centerY int, radius float64, width, height int, randSrc *rand.Rand) (int, int, bool) {
|
||||||
|
for i := 0; i < 60; i++ {
|
||||||
|
t := randSrc.Float64() * 2 * math.Pi
|
||||||
|
r := radius * math.Sqrt(randSrc.Float64())
|
||||||
|
x := centerX + int(math.Round(r*math.Cos(t)))
|
||||||
|
y := centerY + int(math.Round(r*math.Sin(t)))
|
||||||
|
if x >= 0 && x < width && y >= 0 && y < height {
|
||||||
|
return x, y, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTooCloseToExisting(pois []*PointOfInterest, x, y int, minDist float64) bool {
|
||||||
|
minDist2 := minDist * minDist
|
||||||
|
for _, p := range pois {
|
||||||
|
dx := float64(p.X - x)
|
||||||
|
dy := float64(p.Y - y)
|
||||||
|
if dx*dx+dy*dy < minDist2 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleEdgePOI(width, height int, randSrc *rand.Rand) *PointOfInterest {
|
||||||
|
side := randSrc.Intn(4)
|
||||||
|
switch side {
|
||||||
|
case 0:
|
||||||
|
return &PointOfInterest{X: randSrc.Intn(width), Y: 0}
|
||||||
|
case 1:
|
||||||
|
return &PointOfInterest{X: randSrc.Intn(width), Y: height - 1}
|
||||||
|
case 2:
|
||||||
|
return &PointOfInterest{X: 0, Y: randSrc.Intn(height)}
|
||||||
|
default:
|
||||||
|
return &PointOfInterest{X: width - 1, Y: randSrc.Intn(height)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleTargetDegree(randSrc *rand.Rand) int {
|
||||||
|
r := randSrc.Float64()
|
||||||
|
switch {
|
||||||
|
case r < 0.03:
|
||||||
|
return 1
|
||||||
|
case r < 0.17:
|
||||||
|
return 2
|
||||||
|
case r < 0.40:
|
||||||
|
return 3
|
||||||
|
case r < 0.85:
|
||||||
|
return 4
|
||||||
|
default:
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectPOIs(pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMap map[image.Point]bool, roadTarget int) []*Road {
|
||||||
|
minAngle := settings.MinRoadAngle * math.Pi / 180.0
|
||||||
|
if minAngle < 0 {
|
||||||
|
minAngle = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeDist := math.Min(float64(width), float64(height)) * 0.30
|
||||||
|
if roadTarget < len(pois)-1 {
|
||||||
|
roadTarget = len(pois) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
type edgeCandidate struct {
|
||||||
|
a, b int
|
||||||
|
score float64
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates := make([]edgeCandidate, 0, len(pois)*6)
|
||||||
|
for i := 0; i < len(pois); i++ {
|
||||||
|
for j := i + 1; j < len(pois); j++ {
|
||||||
|
a := pois[i]
|
||||||
|
b := pois[j]
|
||||||
|
if a.IsExit && b.IsExit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dx := float64(a.X - b.X)
|
||||||
|
dy := float64(a.Y - b.Y)
|
||||||
|
d := math.Hypot(dx, dy)
|
||||||
|
if !a.IsExit && !b.IsExit && d > edgeDist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (a.IsExit || b.IsExit) && d > edgeDist*1.6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
arterialBias := 1.0 - math.Abs(a.ArterialWeight-b.ArterialWeight)
|
||||||
|
distanceBias := 1.0 - clamp01(d/(edgeDist*1.6))
|
||||||
|
score := arterialBias*0.65 + distanceBias*0.35 + randSrc.Float64()*0.08
|
||||||
|
candidates = append(candidates, edgeCandidate{a: i, b: j, score: score})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var roads []*Road
|
sort.Slice(candidates, func(i, j int) bool {
|
||||||
var roadChan = make(chan *Road)
|
return candidates[i].score > candidates[j].score
|
||||||
var wg sync.WaitGroup
|
})
|
||||||
|
|
||||||
visited := make(map[*PointOfInterest]bool)
|
selected := make(map[uint64]bool, roadTarget)
|
||||||
existingRoads := make(map[string]bool)
|
adjAngles := make([][]float64, len(pois))
|
||||||
|
selectedEdges := make([]edgeCandidate, 0, roadTarget)
|
||||||
|
|
||||||
centerX := width / 2
|
addEdge := func(pick edgeCandidate) {
|
||||||
centerY := height / 2
|
key := edgeKey(pick.a, pick.b)
|
||||||
var startNode *PointOfInterest
|
selected[key] = true
|
||||||
minDist := -1.0
|
selectedEdges = append(selectedEdges, pick)
|
||||||
|
a := pois[pick.a]
|
||||||
for _, poi := range pois {
|
b := pois[pick.b]
|
||||||
if poi == nil {
|
angAB := math.Atan2(float64(b.Y-a.Y), float64(b.X-a.X))
|
||||||
continue
|
angBA := normalizeAngle(angAB + math.Pi)
|
||||||
}
|
a.Connections++
|
||||||
dist := math.Sqrt(math.Pow(float64(poi.X-centerX), 2) + math.Pow(float64(poi.Y-centerY), 2))
|
b.Connections++
|
||||||
if startNode == nil || dist < minDist {
|
adjAngles[pick.a] = append(adjAngles[pick.a], angAB)
|
||||||
minDist = dist
|
adjAngles[pick.b] = append(adjAngles[pick.b], angBA)
|
||||||
startNode = poi
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if startNode == nil {
|
canUseEdge := func(pick edgeCandidate) bool {
|
||||||
return nil
|
key := edgeKey(pick.a, pick.b)
|
||||||
|
if selected[key] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
a := pois[pick.a]
|
||||||
|
b := pois[pick.b]
|
||||||
|
if a.Connections >= max(1, a.TargetDegree+1) || b.Connections >= max(1, b.TargetDegree+1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
angAB := math.Atan2(float64(b.Y-a.Y), float64(b.X-a.X))
|
||||||
|
angBA := normalizeAngle(angAB + math.Pi)
|
||||||
|
if !angleAllowed(adjAngles[pick.a], angAB, minAngle) || !angleAllowed(adjAngles[pick.b], angBA, minAngle) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return pick.score-degreePenalty(a, b) >= -0.4
|
||||||
}
|
}
|
||||||
|
|
||||||
visited[startNode] = true
|
// Phase 1: enforce one connected backbone.
|
||||||
|
start := 0
|
||||||
|
bestWeight := pois[0].ArterialWeight
|
||||||
|
for i := 1; i < len(pois); i++ {
|
||||||
|
if pois[i].ArterialWeight > bestWeight {
|
||||||
|
start = i
|
||||||
|
bestWeight = pois[i].ArterialWeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connected := make([]bool, len(pois))
|
||||||
|
connected[start] = true
|
||||||
|
connectedCount := 1
|
||||||
|
|
||||||
avgDim := float64(width+height) / 2.0
|
for connectedCount < len(pois) && len(selectedEdges) < roadTarget {
|
||||||
numControlPoints := max(int(avgDim*0.03), 60)
|
bestIdx := -1
|
||||||
|
bestScore := -1.0
|
||||||
for len(visited) < len(pois) {
|
for idx, c := range candidates {
|
||||||
var closest *PointOfInterest
|
aConn := connected[c.a]
|
||||||
var fromNode *PointOfInterest
|
bConn := connected[c.b]
|
||||||
minDist := -1.0
|
if aConn == bConn {
|
||||||
|
continue
|
||||||
for poi := range visited {
|
}
|
||||||
for _, other := range pois {
|
if !canUseEdge(c) {
|
||||||
if poi == nil || other == nil {
|
continue
|
||||||
continue
|
}
|
||||||
}
|
if c.score > bestScore {
|
||||||
if !visited[other] {
|
bestScore = c.score
|
||||||
dist := math.Sqrt(math.Pow(float64(poi.X-other.X), 2) + math.Pow(float64(poi.Y-other.Y), 2))
|
bestIdx = idx
|
||||||
|
|
||||||
key := fmt.Sprintf("%p-%p", poi, other)
|
|
||||||
if uintptr(unsafe.Pointer(poi)) > uintptr(unsafe.Pointer(other)) {
|
|
||||||
key = fmt.Sprintf("%p-%p", other, poi)
|
|
||||||
}
|
|
||||||
if existingRoads[key] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if poi.IsExit && other.IsExit {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if closest == nil || dist < minDist {
|
|
||||||
minDist = dist
|
|
||||||
closest = other
|
|
||||||
fromNode = poi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if bestIdx == -1 {
|
||||||
if closest != nil {
|
|
||||||
visited[closest] = true
|
|
||||||
fromNode.Connections++
|
|
||||||
closest.Connections++
|
|
||||||
|
|
||||||
key := fmt.Sprintf("%p-%p", fromNode, closest)
|
|
||||||
if uintptr(unsafe.Pointer(fromNode)) > uintptr(unsafe.Pointer(closest)) {
|
|
||||||
key = fmt.Sprintf("%p-%p", closest, fromNode)
|
|
||||||
}
|
|
||||||
existingRoads[key] = true
|
|
||||||
wg.Add(1)
|
|
||||||
go func(fromNode, closest *PointOfInterest) {
|
|
||||||
defer wg.Done()
|
|
||||||
localRand := rand.New(rand.NewSource(randSrc.Int63()))
|
|
||||||
path := calculateRoadPath(fromNode, closest, settings.RoadCurvyness/100.0, avgDim, localRand, numControlPoints, allWaterPixels)
|
|
||||||
roadChan <- &Road{
|
|
||||||
Start: fromNode,
|
|
||||||
End: closest,
|
|
||||||
Points: path,
|
|
||||||
}
|
|
||||||
}(fromNode, closest)
|
|
||||||
} else {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
pick := candidates[bestIdx]
|
||||||
|
addEdge(pick)
|
||||||
|
if !connected[pick.a] {
|
||||||
|
connected[pick.a] = true
|
||||||
|
connectedCount++
|
||||||
|
}
|
||||||
|
if !connected[pick.b] {
|
||||||
|
connected[pick.b] = true
|
||||||
|
connectedCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
// Phase 2: add extra links up to the target.
|
||||||
wg.Wait()
|
for _, pick := range candidates {
|
||||||
close(roadChan)
|
if len(selectedEdges) >= roadTarget {
|
||||||
}()
|
break
|
||||||
|
}
|
||||||
for road := range roadChan {
|
if !canUseEdge(pick) {
|
||||||
roads = append(roads, road)
|
continue
|
||||||
|
}
|
||||||
|
addEdge(pick)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, road := range roads {
|
roads := make([]*Road, 0, len(selectedEdges))
|
||||||
road.Importance = road.Start.Connections + road.End.Connections
|
avgDim := float64(width+height) / 2
|
||||||
|
for _, e := range selectedEdges {
|
||||||
|
a := pois[e.a]
|
||||||
|
b := pois[e.b]
|
||||||
|
path := calculateRoadPath(a, b, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMap)
|
||||||
|
imp := a.Connections + b.Connections + int(math.Round((a.ArterialWeight+b.ArterialWeight)*4))
|
||||||
|
roads = append(roads, &Road{Start: a, End: b, Points: path, Importance: imp})
|
||||||
}
|
}
|
||||||
|
|
||||||
return roads
|
return roads
|
||||||
}
|
}
|
||||||
|
|
||||||
// assignRoadWidths sets road width based on importance
|
func appendExitRoads(roads []*Road, pois []*PointOfInterest, width, height int, settings *Settings, randSrc *rand.Rand, waterMap map[image.Point]bool) []*Road {
|
||||||
func assignRoadWidths(roads []*Road, settings *Settings) {
|
if settings.RoadExits <= 0 || len(pois) == 0 {
|
||||||
|
return roads
|
||||||
|
}
|
||||||
|
|
||||||
|
exitRoadsAdded := 0
|
||||||
|
avgDim := float64(width+height) / 2
|
||||||
|
usedEdgePoints := make([]image.Point, 0, settings.RoadExits)
|
||||||
|
|
||||||
|
for i := 0; i < settings.RoadExits; i++ {
|
||||||
|
edgeNode, ok := sampleNonWaterEdgePOI(width, height, randSrc, waterMap, usedEdgePoints)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
anchor := chooseExitAnchor(pois, usedEdgePoints, randSrc)
|
||||||
|
if anchor == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
anchor.Connections++
|
||||||
|
edgeNode.IsExit = true
|
||||||
|
edgeNode.TargetDegree = 1
|
||||||
|
edgeNode.Connections = 1
|
||||||
|
|
||||||
|
path := calculateRoadPath(anchor, edgeNode, settings.RoadCurvyness/100.0, avgDim, randSrc, waterMap)
|
||||||
|
importance := anchor.Connections + edgeNode.Connections + int(math.Round(anchor.ArterialWeight*3))
|
||||||
|
roads = append(roads, &Road{
|
||||||
|
Start: anchor,
|
||||||
|
End: edgeNode,
|
||||||
|
Points: path,
|
||||||
|
Importance: importance,
|
||||||
|
})
|
||||||
|
usedEdgePoints = append(usedEdgePoints, image.Point{X: edgeNode.X, Y: edgeNode.Y})
|
||||||
|
exitRoadsAdded++
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = exitRoadsAdded
|
||||||
|
return roads
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleNonWaterEdgePOI(width, height int, randSrc *rand.Rand, waterMap map[image.Point]bool, used []image.Point) (*PointOfInterest, bool) {
|
||||||
|
minSpacing := math.Min(float64(width), float64(height)) * 0.08
|
||||||
|
minSpacing2 := minSpacing * minSpacing
|
||||||
|
|
||||||
|
for tries := 0; tries < 120; tries++ {
|
||||||
|
p := sampleEdgePOI(width, height, randSrc)
|
||||||
|
pt := image.Point{X: p.X, Y: p.Y}
|
||||||
|
if waterMap[pt] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tooClose := false
|
||||||
|
for _, u := range used {
|
||||||
|
dx := float64(u.X - p.X)
|
||||||
|
dy := float64(u.Y - p.Y)
|
||||||
|
if dx*dx+dy*dy < minSpacing2 {
|
||||||
|
tooClose = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tooClose {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return p, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func chooseExitAnchor(pois []*PointOfInterest, usedExits []image.Point, randSrc *rand.Rand) *PointOfInterest {
|
||||||
|
if len(pois) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(usedExits) == 0 {
|
||||||
|
best := pois[0]
|
||||||
|
for i := 1; i < len(pois); i++ {
|
||||||
|
if pois[i].ArterialWeight > best.ArterialWeight {
|
||||||
|
best = pois[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
target := usedExits[len(usedExits)-1]
|
||||||
|
best := pois[randSrc.Intn(len(pois))]
|
||||||
|
bestScore := -1.0
|
||||||
|
for _, p := range pois {
|
||||||
|
d := math.Hypot(float64(p.X-target.X), float64(p.Y-target.Y))
|
||||||
|
score := p.ArterialWeight*2.0 + clamp(1.0-d/2000.0, 0, 1)
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
best = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
func estimateRoadTarget(settings *Settings, randSrc *rand.Rand) int {
|
||||||
|
// Two random numbers in [1,10], averaged -> triangular distribution centered at 10.5.
|
||||||
|
divisor := float64((randSrc.Intn(10)+1)+(randSrc.Intn(10)+1)) / 2.0
|
||||||
|
roads := int(math.Round(float64(max(settings.NumBuildings, 1)) / divisor))
|
||||||
|
if roads < 4 {
|
||||||
|
roads = 4
|
||||||
|
}
|
||||||
|
// Keep exits connectable and cap by graph size.
|
||||||
|
if roads < settings.RoadExits {
|
||||||
|
roads = settings.RoadExits
|
||||||
|
}
|
||||||
|
return roads
|
||||||
|
}
|
||||||
|
|
||||||
|
func edgeKey(a, b int) uint64 {
|
||||||
|
if a > b {
|
||||||
|
a, b = b, a
|
||||||
|
}
|
||||||
|
return (uint64(uint32(a)) << 32) | uint64(uint32(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func degreePenalty(a, b *PointOfInterest) float64 {
|
||||||
|
penalty := 0.0
|
||||||
|
if a.Connections >= a.TargetDegree {
|
||||||
|
penalty += 0.20 + float64(a.Connections-a.TargetDegree)*0.12
|
||||||
|
}
|
||||||
|
if b.Connections >= b.TargetDegree {
|
||||||
|
penalty += 0.20 + float64(b.Connections-b.TargetDegree)*0.12
|
||||||
|
}
|
||||||
|
return penalty
|
||||||
|
}
|
||||||
|
|
||||||
|
func angleAllowed(existing []float64, candidate, minAngle float64) bool {
|
||||||
|
if minAngle <= 0 || len(existing) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, ang := range existing {
|
||||||
|
d := math.Abs(normalizeAngle(candidate - ang))
|
||||||
|
if d > math.Pi {
|
||||||
|
d = 2*math.Pi - d
|
||||||
|
}
|
||||||
|
if d < minAngle {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAngle(a float64) float64 {
|
||||||
|
for a <= -math.Pi {
|
||||||
|
a += 2 * math.Pi
|
||||||
|
}
|
||||||
|
for a > math.Pi {
|
||||||
|
a -= 2 * math.Pi
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignRoadWidths(roads []*Road, settings *Settings, randSrc *rand.Rand) {
|
||||||
if len(roads) == 0 {
|
if len(roads) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(roads, func(i, j int) bool {
|
|
||||||
return roads[i].Importance > roads[j].Importance
|
|
||||||
})
|
|
||||||
|
|
||||||
minWidth := settings.MinRoadWidth
|
minWidth := settings.MinRoadWidth
|
||||||
maxWidth := settings.MaxRoadWidth
|
maxWidth := settings.MaxRoadWidth
|
||||||
widthStep := 0.0
|
if maxWidth < minWidth {
|
||||||
if len(roads) > 1 {
|
minWidth, maxWidth = maxWidth, minWidth
|
||||||
widthStep = (maxWidth - minWidth) / float64(len(roads)-1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, road := range roads {
|
maxImportance := 1
|
||||||
road.Width = int(maxWidth - float64(i)*widthStep)
|
for _, road := range roads {
|
||||||
|
if road.Importance > maxImportance {
|
||||||
|
maxImportance = road.Importance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
widths := make(map[*Road]float64, len(roads))
|
||||||
|
adj := make(map[*PointOfInterest][]*Road)
|
||||||
|
for _, r := range roads {
|
||||||
|
n := float64(r.Importance) / float64(maxImportance)
|
||||||
|
jitter := (randSrc.Float64() - 0.5) * 0.16
|
||||||
|
base := minWidth + (maxWidth-minWidth)*clamp01(n+jitter)
|
||||||
|
widths[r] = base
|
||||||
|
adj[r.Start] = append(adj[r.Start], r)
|
||||||
|
adj[r.End] = append(adj[r.End], r)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
next := make(map[*Road]float64, len(widths))
|
||||||
|
for r, w := range widths {
|
||||||
|
total := w
|
||||||
|
count := 1.0
|
||||||
|
for _, n := range []*PointOfInterest{r.Start, r.End} {
|
||||||
|
for _, nbr := range adj[n] {
|
||||||
|
if nbr == r {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total += widths[nbr]
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next[r] = w*0.55 + (total/count)*0.45
|
||||||
|
}
|
||||||
|
widths = next
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range roads {
|
||||||
|
w := clamp(widths[r], minWidth, maxWidth)
|
||||||
|
r.Width = max(1, int(math.Round(w)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawRoad draws a single road on the image including bridges
|
// drawRoad draws a single road on the image including bridges.
|
||||||
func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int) ([]image.Point, []image.Point) {
|
func drawRoad(img *image.RGBA, points []PathPoint, roadColor, bridgeColor color.Color, width int) ([]image.Point, []image.Point) {
|
||||||
var roadPixels []image.Point
|
var roadPixels []image.Point
|
||||||
var bridgePixels []image.Point
|
var bridgePixels []image.Point
|
||||||
for i := 0; i < len(points)-1; i++ {
|
bridgeWidth := int(math.Ceil(float64(width) * 1.15))
|
||||||
|
if bridgeWidth < 1 {
|
||||||
|
bridgeWidth = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(points)-1; {
|
||||||
p1 := points[i]
|
p1 := points[i]
|
||||||
p2 := points[i+1]
|
p2 := points[i+1]
|
||||||
c := roadColor
|
|
||||||
isBridge := p1.IsBridge && p2.IsBridge
|
isBridge := p1.IsBridge && p2.IsBridge
|
||||||
if isBridge {
|
if !isBridge {
|
||||||
c = bridgeColor
|
linePoints := drawLine(img, p1.Point.X, p1.Point.Y, p2.Point.X, p2.Point.Y, roadColor, width)
|
||||||
}
|
|
||||||
linePoints := drawLine(img, p1.Point.X, p1.Point.Y, p2.Point.X, p2.Point.Y, c, width)
|
|
||||||
if isBridge {
|
|
||||||
bridgePixels = append(bridgePixels, linePoints...)
|
|
||||||
} else {
|
|
||||||
roadPixels = append(roadPixels, linePoints...)
|
roadPixels = append(roadPixels, linePoints...)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw each contiguous bridge run as one straight span.
|
||||||
|
start := i
|
||||||
|
end := i + 1
|
||||||
|
for end < len(points)-1 && points[end].IsBridge && points[end+1].IsBridge {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
linePoints := drawLine(
|
||||||
|
img,
|
||||||
|
points[start].Point.X, points[start].Point.Y,
|
||||||
|
points[end].Point.X, points[end].Point.Y,
|
||||||
|
bridgeColor,
|
||||||
|
bridgeWidth,
|
||||||
|
)
|
||||||
|
bridgePixels = append(bridgePixels, linePoints...)
|
||||||
|
i = end
|
||||||
}
|
}
|
||||||
return roadPixels, bridgePixels
|
return roadPixels, bridgePixels
|
||||||
}
|
}
|
||||||
|
|
||||||
// bresenhamRoad creates a path between control points using Bresenham's algorithm
|
|
||||||
func bresenhamRoad(path []image.Point) []image.Point {
|
func bresenhamRoad(path []image.Point) []image.Point {
|
||||||
if len(path) < 2 {
|
if len(path) < 2 {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
var fullPath []image.Point
|
fullPath := make([]image.Point, 0, len(path)*8)
|
||||||
for i := 0; i < len(path)-1; i++ {
|
for i := 0; i < len(path)-1; i++ {
|
||||||
p1, p2 := path[i], path[i+1]
|
p1, p2 := path[i], path[i+1]
|
||||||
dx, dy := p2.X-p1.X, p2.Y-p1.Y
|
dx, dy := p2.X-p1.X, p2.Y-p1.Y
|
||||||
@@ -327,82 +630,92 @@ func bresenhamRoad(path []image.Point) []image.Point {
|
|||||||
return fullPath
|
return fullPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateRoadPath computes the path for a road including curves and bridges
|
// calculateRoadPath computes the path for a road including curves and bridges.
|
||||||
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, numControlPoints int, allWaterPixels []image.Point) []PathPoint {
|
func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, randSrc *rand.Rand, waterMap map[image.Point]bool) []PathPoint {
|
||||||
dx := end.X - start.X
|
dx := end.X - start.X
|
||||||
dy := end.Y - start.Y
|
dy := end.Y - start.Y
|
||||||
dist := math.Sqrt(float64(dx*dx + dy*dy))
|
dist := math.Hypot(float64(dx), float64(dy))
|
||||||
|
|
||||||
waterMap := make(map[image.Point]bool)
|
|
||||||
for _, p := range allWaterPixels {
|
|
||||||
waterMap[p] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if dist == 0 {
|
if dist == 0 {
|
||||||
return []PathPoint{{Point: image.Point{X: start.X, Y: start.Y}, IsBridge: waterMap[image.Point{X: start.X, Y: start.Y}]}}
|
p := image.Point{X: start.X, Y: start.Y}
|
||||||
|
return []PathPoint{{Point: p, IsBridge: waterMap[p]}}
|
||||||
}
|
}
|
||||||
|
|
||||||
distanceFactor := math.Min(1.0, dist/(avgDim*0.5))
|
curve := clamp(curvyness, 0, 1)
|
||||||
adjustedCurvyness := curvyness * distanceFactor
|
if curve <= 0 {
|
||||||
|
|
||||||
if adjustedCurvyness == 0 {
|
|
||||||
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
|
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
|
||||||
pathPoints := make([]PathPoint, len(points))
|
return toPathPoints(points, waterMap)
|
||||||
for i, p := range points {
|
|
||||||
pathPoints[i] = PathPoint{Point: p, IsBridge: waterMap[p]}
|
|
||||||
}
|
|
||||||
return pathPoints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-linear scaling: low values stay fairly straight, high values become very winding.
|
||||||
|
strength := math.Pow(curve, 1.35)
|
||||||
|
if strength < 0.001 {
|
||||||
|
points := bresenhamRoad([]image.Point{{X: start.X, Y: start.Y}, {X: end.X, Y: end.Y}})
|
||||||
|
return toPathPoints(points, waterMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseControls := int(math.Max(12, dist/(22.0-14.0*strength)))
|
||||||
|
controlPoints := make([]image.Point, baseControls+1)
|
||||||
|
perpX, perpY := -float64(dy)/dist, float64(dx)/dist
|
||||||
|
lengthScale := clamp(dist/(avgDim*0.55), 0.45, 2.4)
|
||||||
|
|
||||||
|
ampBase := clamp(dist*(0.01+0.13*strength*strength), 2, avgDim*0.16)
|
||||||
|
amp1 := ampBase * (0.9 + randSrc.Float64()*0.25)
|
||||||
|
amp2 := ampBase * (0.45 + randSrc.Float64()*0.20)
|
||||||
|
amp3 := ampBase * (0.20 + randSrc.Float64()*0.15)
|
||||||
|
|
||||||
|
w1 := clamp(dist*(1.10-0.70*strength), 30, avgDim*0.95)
|
||||||
|
w2 := clamp(dist*(0.55-0.30*strength), 16, avgDim*0.55)
|
||||||
|
w3 := clamp(dist*(0.26-0.12*strength), 8, avgDim*0.30)
|
||||||
|
|
||||||
type wave struct {
|
type wave struct {
|
||||||
amplitude float64
|
amplitude float64
|
||||||
numWaves float64
|
wavelength float64
|
||||||
phase float64
|
phase float64
|
||||||
}
|
}
|
||||||
|
|
||||||
waves := make([]wave, 2)
|
waves := []wave{
|
||||||
amp := (avgDim / 10.0) * adjustedCurvyness
|
{
|
||||||
mainWavelength := avgDim / 4.0
|
amplitude: amp1,
|
||||||
if mainWavelength < 1 {
|
wavelength: w1,
|
||||||
mainWavelength = 1
|
phase: randSrc.Float64() * 2 * math.Pi,
|
||||||
}
|
},
|
||||||
baseNumWaves := (dist / mainWavelength) * adjustedCurvyness
|
{
|
||||||
|
amplitude: amp2,
|
||||||
waves[0] = wave{
|
wavelength: w2,
|
||||||
amplitude: amp,
|
phase: randSrc.Float64() * 2 * math.Pi,
|
||||||
numWaves: baseNumWaves * (0.75 + randSrc.Float64()*0.5),
|
},
|
||||||
phase: randSrc.Float64() * 2 * math.Pi,
|
{
|
||||||
|
amplitude: amp3,
|
||||||
|
wavelength: w3,
|
||||||
|
phase: randSrc.Float64() * 2 * math.Pi,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
waves[1] = wave{
|
for i := 0; i <= baseControls; i++ {
|
||||||
amplitude: amp / 4,
|
t := float64(i) / float64(baseControls)
|
||||||
numWaves: baseNumWaves * 4 * (0.75 + randSrc.Float64()*0.5),
|
|
||||||
phase: randSrc.Float64() * 2 * math.Pi,
|
|
||||||
}
|
|
||||||
|
|
||||||
controlPoints := make([]image.Point, numControlPoints+1)
|
|
||||||
for i := 0; i <= numControlPoints; i++ {
|
|
||||||
t := float64(i) / float64(numControlPoints)
|
|
||||||
x := float64(start.X) + t*float64(dx)
|
x := float64(start.X) + t*float64(dx)
|
||||||
y := float64(start.Y) + t*float64(dy)
|
y := float64(start.Y) + t*float64(dy)
|
||||||
|
|
||||||
p := image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
|
// Keep endpoints fixed while allowing large mid-segment deflection.
|
||||||
if !waterMap[p] {
|
envelope := math.Pow(math.Sin(t*math.Pi), 0.78)
|
||||||
perpX, perpY := -float64(dy)/dist, float64(dx)/dist
|
offset := 0.0
|
||||||
|
for _, w := range waves {
|
||||||
totalOffset := 0.0
|
angle := (dist*t/w.wavelength)*2*math.Pi + w.phase
|
||||||
for _, w := range waves {
|
offset += math.Sin(angle) * w.amplitude
|
||||||
totalOffset += math.Sin(t*w.numWaves*2*math.Pi+w.phase) * w.amplitude
|
|
||||||
}
|
|
||||||
totalOffset *= math.Sin(t * math.Pi)
|
|
||||||
|
|
||||||
x += totalOffset * perpX
|
|
||||||
y += totalOffset * perpY
|
|
||||||
}
|
}
|
||||||
|
offset *= envelope * lengthScale
|
||||||
|
|
||||||
|
x += offset * perpX
|
||||||
|
y += offset * perpY
|
||||||
controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
|
controlPoints[i] = image.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
|
||||||
}
|
}
|
||||||
|
|
||||||
points := bresenhamRoad(controlPoints)
|
points := bresenhamRoad(controlPoints)
|
||||||
|
return toPathPoints(points, waterMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPathPoints(points []image.Point, waterMap map[image.Point]bool) []PathPoint {
|
||||||
pathPoints := make([]PathPoint, len(points))
|
pathPoints := make([]PathPoint, len(points))
|
||||||
for i, p := range points {
|
for i, p := range points {
|
||||||
pathPoints[i] = PathPoint{Point: p, IsBridge: waterMap[p]}
|
pathPoints[i] = PathPoint{Point: p, IsBridge: waterMap[p]}
|
||||||
@@ -410,7 +723,7 @@ func calculateRoadPath(start, end *PointOfInterest, curvyness, avgDim float64, r
|
|||||||
return pathPoints
|
return pathPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawLine draws a line with specified width on the image
|
// drawLine draws a line with specified width on the image.
|
||||||
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) []image.Point {
|
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) []image.Point {
|
||||||
var points []image.Point
|
var points []image.Point
|
||||||
dx := abs(x1 - x0)
|
dx := abs(x1 - x0)
|
||||||
@@ -453,7 +766,17 @@ func drawLine(img *image.RGBA, x0, y0, x1, y1 int, col color.Color, width int) [
|
|||||||
return points
|
return points
|
||||||
}
|
}
|
||||||
|
|
||||||
// abs returns the absolute value of an integer
|
func clamp(v, lo, hi float64) float64 {
|
||||||
|
if v < lo {
|
||||||
|
return lo
|
||||||
|
}
|
||||||
|
if v > hi {
|
||||||
|
return hi
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// abs returns the absolute value of an integer.
|
||||||
func abs(x int) int {
|
func abs(x int) int {
|
||||||
if x < 0 {
|
if x < 0 {
|
||||||
return -x
|
return -x
|
||||||
|
|||||||
+13
-5
@@ -35,12 +35,12 @@ type Settings struct {
|
|||||||
TreeClumpiness float64 `json:"tree_clumpiness"`
|
TreeClumpiness float64 `json:"tree_clumpiness"`
|
||||||
|
|
||||||
// Road settings
|
// Road settings
|
||||||
NumRoads int `json:"num_roads"`
|
|
||||||
MinRoadWidth float64 `json:"min_road_width"`
|
MinRoadWidth float64 `json:"min_road_width"`
|
||||||
MaxRoadWidth float64 `json:"max_road_width"`
|
MaxRoadWidth float64 `json:"max_road_width"`
|
||||||
RoadExits int `json:"road_exits"`
|
RoadExits int `json:"road_exits"`
|
||||||
RoadCurvyness float64 `json:"road_curvyness"`
|
RoadCurvyness float64 `json:"road_curvyness"`
|
||||||
RoadDistribution float64 `json:"road_distribution"`
|
RoadDistribution float64 `json:"road_distribution"`
|
||||||
|
MinRoadAngle float64 `json:"min_road_angle"`
|
||||||
|
|
||||||
// Building settings
|
// Building settings
|
||||||
NumBuildings int `json:"num_buildings"`
|
NumBuildings int `json:"num_buildings"`
|
||||||
@@ -130,12 +130,12 @@ func LoadSettings() (*Settings, error) {
|
|||||||
RiverCurvyness: 50,
|
RiverCurvyness: 50,
|
||||||
RiverWidthVariability: 50,
|
RiverWidthVariability: 50,
|
||||||
RiverEdgeRoughness: 50,
|
RiverEdgeRoughness: 50,
|
||||||
NumRoads: 100,
|
|
||||||
MinRoadWidth: 2,
|
MinRoadWidth: 2,
|
||||||
MaxRoadWidth: 8,
|
MaxRoadWidth: 8,
|
||||||
RoadExits: 5,
|
RoadExits: 5,
|
||||||
RoadCurvyness: 50,
|
RoadCurvyness: 50,
|
||||||
RoadDistribution: 50,
|
RoadDistribution: 50,
|
||||||
|
MinRoadAngle: 18,
|
||||||
NumBuildings: 200,
|
NumBuildings: 200,
|
||||||
MinBuildingSize: 10,
|
MinBuildingSize: 10,
|
||||||
MaxBuildingSize: 30,
|
MaxBuildingSize: 30,
|
||||||
@@ -157,12 +157,17 @@ func LoadSettings() (*Settings, error) {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Decode the JSON data into a Settings struct
|
// Decode through a wrapper so we can tell whether newer fields were present.
|
||||||
var settings Settings
|
type settingsDisk struct {
|
||||||
|
Settings
|
||||||
|
MinRoadAngle *float64 `json:"min_road_angle"`
|
||||||
|
}
|
||||||
|
var disk settingsDisk
|
||||||
decoder := json.NewDecoder(file)
|
decoder := json.NewDecoder(file)
|
||||||
if err := decoder.Decode(&settings); err != nil {
|
if err := decoder.Decode(&disk); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
settings := disk.Settings
|
||||||
|
|
||||||
if settings.LakeShape == "" {
|
if settings.LakeShape == "" {
|
||||||
settings.LakeShape = "circle"
|
settings.LakeShape = "circle"
|
||||||
@@ -187,6 +192,9 @@ func LoadSettings() (*Settings, error) {
|
|||||||
if settings.BuildingComplexityRatio == 0 {
|
if settings.BuildingComplexityRatio == 0 {
|
||||||
settings.BuildingComplexityRatio = 50
|
settings.BuildingComplexityRatio = 50
|
||||||
}
|
}
|
||||||
|
if disk.MinRoadAngle == nil {
|
||||||
|
settings.MinRoadAngle = 18
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure LastExportPath is set to a default value if it's empty
|
// Ensure LastExportPath is set to a default value if it's empty
|
||||||
if settings.LastExportPath == "" {
|
if settings.LastExportPath == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user