scaled buildings and roads based on image dimensions
This commit is contained in:
@@ -31,14 +31,14 @@ 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) |
|
||||||
| **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 as a percentage of the average image dimension (`(width + height) / 2`). | `0.1%` to `5%` in `0.1%` steps |
|
||||||
| **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 as a percentage of the average image dimension (`(width + height) / 2`). | `0.1%` to `5%` in `0.1%` steps |
|
||||||
| **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°` |
|
| **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` |
|
||||||
| **Min Building Size** | The minimum size of a generated building in pixels. | `1` to `100` |
|
| **Min Building Size** | The minimum size of a generated building as a percentage of the average image dimension (`(width + height) / 2`). | `0.5%` to `25%` in `0.5%` steps |
|
||||||
| **Max Building Size** | The maximum size of a generated building in pixels. | `1` to `100` |
|
| **Max Building Size** | The maximum size of a generated building as a percentage of the average image dimension (`(width + height) / 2`). | `0.5%` to `25%` in `0.5%` steps |
|
||||||
| **Building Distro** | Controls the distribution of buildings. At 100%, buildings will be spread out across the entire map. At 0%, they will be always near a road. | `0%` (centered) to `100%` (spread out) |
|
| **Building Distro** | Controls the distribution of buildings. At 100%, buildings will be spread out across the entire map. At 0%, they will be always near a road. | `0%` (centered) to `100%` (spread out) |
|
||||||
| **Building Shape** | The shape of the buildings. | `squares` or `circles` |
|
| **Building Shape** | The shape of the buildings. | `squares` or `circles` |
|
||||||
|
|||||||
+54
-1
@@ -9,6 +9,58 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minBuildingSizePercent = 0.5
|
||||||
|
maxBuildingSizePercent = 25.0
|
||||||
|
buildingSizePercentStep = 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
func averageImageDimension(width, height int) float64 {
|
||||||
|
return (float64(width) + float64(height)) / 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampBuildingSizePercent(v float64) float64 {
|
||||||
|
if v < minBuildingSizePercent {
|
||||||
|
return minBuildingSizePercent
|
||||||
|
}
|
||||||
|
if v > maxBuildingSizePercent {
|
||||||
|
return maxBuildingSizePercent
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapBuildingSizePercent(v float64) float64 {
|
||||||
|
v = clampBuildingSizePercent(v)
|
||||||
|
steps := math.Round((v - minBuildingSizePercent) / buildingSizePercentStep)
|
||||||
|
return clampBuildingSizePercent(minBuildingSizePercent + steps*buildingSizePercentStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBuildingSizePercentRange(minPercent, maxPercent float64) (float64, float64) {
|
||||||
|
minPercent = snapBuildingSizePercent(minPercent)
|
||||||
|
maxPercent = snapBuildingSizePercent(maxPercent)
|
||||||
|
if minPercent > maxPercent {
|
||||||
|
minPercent, maxPercent = maxPercent, minPercent
|
||||||
|
}
|
||||||
|
return minPercent, maxPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuildingSizeRangePixels(settings *Settings, width, height int) (float64, float64) {
|
||||||
|
minPercent, maxPercent := normalizeBuildingSizePercentRange(settings.MinBuildingSize, settings.MaxBuildingSize)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateBuildings creates and places buildings on the map.
|
// GenerateBuildings creates and places buildings on the map.
|
||||||
func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, roadPixels, allWaterPixels []image.Point, seed int64) ([][]image.Point, []image.Point) {
|
func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, roadPixels, allWaterPixels []image.Point, seed int64) ([][]image.Point, []image.Point) {
|
||||||
// Early exit if no buildings are to be generated
|
// Early exit if no buildings are to be generated
|
||||||
@@ -87,6 +139,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
|||||||
buildingsPlaced := 0
|
buildingsPlaced := 0
|
||||||
searchTries := 100 // Number of attempts to find a spot for a building around an anchor
|
searchTries := 100 // Number of attempts to find a spot for a building around an anchor
|
||||||
maxPlacementAttempts := settings.NumBuildings * 5 // To prevent infinite loops
|
maxPlacementAttempts := settings.NumBuildings * 5 // To prevent infinite loops
|
||||||
|
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
|
||||||
|
|
||||||
for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 {
|
for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 {
|
||||||
maxPlacementAttempts--
|
maxPlacementAttempts--
|
||||||
@@ -133,7 +186,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to create a building at the selected center
|
// Attempt to create a building at the selected center
|
||||||
size := settings.MinBuildingSize + randSrc.Float64()*(settings.MaxBuildingSize-settings.MinBuildingSize)
|
size := minBuildingSizePx + randSrc.Float64()*(maxBuildingSizePx-minBuildingSizePx)
|
||||||
shape := settings.BuildingShape
|
shape := settings.BuildingShape
|
||||||
if shape == "mixed" {
|
if shape == "mixed" {
|
||||||
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
|
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
|
||||||
|
|||||||
+30
-1
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ type numericInputSlider struct {
|
|||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
value binding.Float
|
value binding.Float
|
||||||
min, max float64
|
min, max float64
|
||||||
|
step float64
|
||||||
slider *widget.Slider
|
slider *widget.Slider
|
||||||
entry *widget.Entry
|
entry *widget.Entry
|
||||||
format string
|
format string
|
||||||
@@ -83,6 +85,7 @@ func newNumericInputSlider(min, max float64, initialValue float64, format string
|
|||||||
min: min,
|
min: min,
|
||||||
max: max,
|
max: max,
|
||||||
format: format,
|
format: format,
|
||||||
|
step: 0,
|
||||||
}
|
}
|
||||||
s.ExtendBaseWidget(s)
|
s.ExtendBaseWidget(s)
|
||||||
|
|
||||||
@@ -105,6 +108,17 @@ func newNumericInputSlider(min, max float64, initialValue float64, format string
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newNumericInputSliderWithStep(min, max, initialValue, step float64, format string, labelText string) *numericInputSlider {
|
||||||
|
s := newNumericInputSlider(min, max, initialValue, format, labelText)
|
||||||
|
if step > 0 {
|
||||||
|
s.step = step
|
||||||
|
s.slider.Step = step
|
||||||
|
rounded := min + math.Round((initialValue-min)/step)*step
|
||||||
|
s.value.Set(rounded)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// 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.TrimSpace(text)
|
||||||
@@ -121,11 +135,26 @@ func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if val < s.min || val > s.max {
|
if val < s.min || val > s.max {
|
||||||
s.errorLabel.SetText(fmt.Sprintf("Out of range (%.0f-%.0f)", s.min, s.max))
|
s.errorLabel.SetText(fmt.Sprintf(
|
||||||
|
"Out of range (%s-%s)",
|
||||||
|
strconv.FormatFloat(s.min, 'f', -1, 64),
|
||||||
|
strconv.FormatFloat(s.max, 'f', -1, 64),
|
||||||
|
))
|
||||||
s.errorLabel.Show()
|
s.errorLabel.Show()
|
||||||
onError(true)
|
onError(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if s.step > 0 {
|
||||||
|
steps := math.Round((val - s.min) / s.step)
|
||||||
|
snapped := s.min + steps*s.step
|
||||||
|
if math.Abs(val-snapped) > 1e-9 {
|
||||||
|
s.errorLabel.SetText(fmt.Sprintf("Use increments of %s", strconv.FormatFloat(s.step, 'f', -1, 64)))
|
||||||
|
s.errorLabel.Show()
|
||||||
|
onError(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val = snapped
|
||||||
|
}
|
||||||
|
|
||||||
s.errorLabel.Hide()
|
s.errorLabel.Hide()
|
||||||
onError(false)
|
onError(false)
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ func main() {
|
|||||||
val, _ := treeClumpinessSlider.value.Get()
|
val, _ := treeClumpinessSlider.value.Get()
|
||||||
settings.TreeClumpiness = val
|
settings.TreeClumpiness = val
|
||||||
}))
|
}))
|
||||||
minRoadWidthSlider := newNumericInputSlider(1, 150, settings.MinRoadWidth, "%.0fpx", "Min Road Width")
|
minRoadWidthSlider := newNumericInputSliderWithStep(minRoadWidthPercent, maxRoadWidthPercent, settings.MinRoadWidth, roadWidthPercentStep, "%.1f%%", "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) {
|
||||||
errorStates["minRoadWidth"] = hasError
|
errorStates["minRoadWidth"] = hasError
|
||||||
@@ -441,7 +441,7 @@ func main() {
|
|||||||
settings.MinRoadWidth = val
|
settings.MinRoadWidth = val
|
||||||
}))
|
}))
|
||||||
|
|
||||||
maxRoadWidthSlider := newNumericInputSlider(1, 150, settings.MaxRoadWidth, "%.0fpx", "Max Road Width")
|
maxRoadWidthSlider := newNumericInputSliderWithStep(minRoadWidthPercent, maxRoadWidthPercent, settings.MaxRoadWidth, roadWidthPercentStep, "%.1f%%", "Max Road Width")
|
||||||
maxRoadWidthSlider.entry.OnChanged = func(s string) {
|
maxRoadWidthSlider.entry.OnChanged = func(s string) {
|
||||||
maxRoadWidthSlider.validate(s, func(hasError bool) {
|
maxRoadWidthSlider.validate(s, func(hasError bool) {
|
||||||
errorStates["maxRoadWidth"] = hasError
|
errorStates["maxRoadWidth"] = hasError
|
||||||
@@ -753,7 +753,7 @@ func main() {
|
|||||||
settings.NumBuildings = int(val)
|
settings.NumBuildings = int(val)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
minBuildingSizeSlider := newNumericInputSlider(1, 150, settings.MinBuildingSize, "%.0fpx", "Min Building Size")
|
minBuildingSizeSlider := newNumericInputSliderWithStep(minBuildingSizePercent, maxBuildingSizePercent, settings.MinBuildingSize, buildingSizePercentStep, "%.1f%%", "Min Building Size")
|
||||||
minBuildingSizeSlider.entry.OnChanged = func(s string) {
|
minBuildingSizeSlider.entry.OnChanged = func(s string) {
|
||||||
minBuildingSizeSlider.validate(s, func(hasError bool) {
|
minBuildingSizeSlider.validate(s, func(hasError bool) {
|
||||||
errorStates["minBuildingSize"] = hasError
|
errorStates["minBuildingSize"] = hasError
|
||||||
@@ -765,7 +765,7 @@ func main() {
|
|||||||
settings.MinBuildingSize = val
|
settings.MinBuildingSize = val
|
||||||
}))
|
}))
|
||||||
|
|
||||||
maxBuildingSizeSlider := newNumericInputSlider(1, 150, settings.MaxBuildingSize, "%.0fpx", "Max Building Size")
|
maxBuildingSizeSlider := newNumericInputSliderWithStep(minBuildingSizePercent, maxBuildingSizePercent, settings.MaxBuildingSize, buildingSizePercentStep, "%.1f%%", "Max Building Size")
|
||||||
maxBuildingSizeSlider.entry.OnChanged = func(s string) {
|
maxBuildingSizeSlider.entry.OnChanged = func(s string) {
|
||||||
maxBuildingSizeSlider.validate(s, func(hasError bool) {
|
maxBuildingSizeSlider.validate(s, func(hasError bool) {
|
||||||
errorStates["maxBuildingSize"] = hasError
|
errorStates["maxBuildingSize"] = hasError
|
||||||
|
|||||||
@@ -33,6 +33,54 @@ type Road struct {
|
|||||||
|
|
||||||
var lastExitRoadPixels []image.Point
|
var lastExitRoadPixels []image.Point
|
||||||
|
|
||||||
|
const (
|
||||||
|
minRoadWidthPercent = 0.1
|
||||||
|
maxRoadWidthPercent = 5.0
|
||||||
|
roadWidthPercentStep = 0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
func clampRoadWidthPercent(v float64) float64 {
|
||||||
|
if v < minRoadWidthPercent {
|
||||||
|
return minRoadWidthPercent
|
||||||
|
}
|
||||||
|
if v > maxRoadWidthPercent {
|
||||||
|
return maxRoadWidthPercent
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapRoadWidthPercent(v float64) float64 {
|
||||||
|
v = clampRoadWidthPercent(v)
|
||||||
|
steps := math.Round((v - minRoadWidthPercent) / roadWidthPercentStep)
|
||||||
|
return clampRoadWidthPercent(minRoadWidthPercent + steps*roadWidthPercentStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRoadWidthPercentRange(minPercent, maxPercent float64) (float64, float64) {
|
||||||
|
minPercent = snapRoadWidthPercent(minPercent)
|
||||||
|
maxPercent = snapRoadWidthPercent(maxPercent)
|
||||||
|
if minPercent > maxPercent {
|
||||||
|
minPercent, maxPercent = maxPercent, minPercent
|
||||||
|
}
|
||||||
|
return minPercent, maxPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoadWidthRangePixels(settings *Settings, width, height int) (float64, float64) {
|
||||||
|
minPercent, maxPercent := normalizeRoadWidthPercentRange(settings.MinRoadWidth, settings.MaxRoadWidth)
|
||||||
|
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 getExitRoadPixels() []image.Point {
|
func getExitRoadPixels() []image.Point {
|
||||||
out := make([]image.Point, len(lastExitRoadPixels))
|
out := make([]image.Point, len(lastExitRoadPixels))
|
||||||
copy(out, lastExitRoadPixels)
|
copy(out, lastExitRoadPixels)
|
||||||
@@ -61,7 +109,7 @@ func GenerateRoads(width, height int, settings *Settings, _ image.Image, allWate
|
|||||||
if len(roads) == 0 {
|
if len(roads) == 0 {
|
||||||
return nil, nil, img
|
return nil, nil, img
|
||||||
}
|
}
|
||||||
assignRoadWidths(roads, settings, randSrc)
|
assignRoadWidths(roads, settings, randSrc, width, height)
|
||||||
|
|
||||||
allRoadPixels := make([]image.Point, 0, len(roads)*64)
|
allRoadPixels := make([]image.Point, 0, len(roads)*64)
|
||||||
allBridgePixels := make([]image.Point, 0, len(roads)*16)
|
allBridgePixels := make([]image.Point, 0, len(roads)*16)
|
||||||
@@ -82,7 +130,8 @@ func GenerateRoads(width, height int, settings *Settings, _ image.Image, allWate
|
|||||||
|
|
||||||
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, waterMap map[image.Point]bool, randSrc *rand.Rand, roadTarget int) []*PointOfInterest {
|
||||||
distribution := clamp01(settings.RoadDistribution / 100.0)
|
distribution := clamp01(settings.RoadDistribution / 100.0)
|
||||||
avgBuildingSize := (settings.MinBuildingSize + settings.MaxBuildingSize) / 2.0
|
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
|
||||||
|
avgBuildingSize := (minBuildingSizePx + maxBuildingSizePx) / 2.0
|
||||||
if avgBuildingSize < 1 {
|
if avgBuildingSize < 1 {
|
||||||
avgBuildingSize = 1
|
avgBuildingSize = 1
|
||||||
}
|
}
|
||||||
@@ -512,13 +561,12 @@ func normalizeAngle(a float64) float64 {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func assignRoadWidths(roads []*Road, settings *Settings, randSrc *rand.Rand) {
|
func assignRoadWidths(roads []*Road, settings *Settings, randSrc *rand.Rand, width, height int) {
|
||||||
if len(roads) == 0 {
|
if len(roads) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
minWidth := settings.MinRoadWidth
|
minWidth, maxWidth := getRoadWidthRangePixels(settings, width, height)
|
||||||
maxWidth := settings.MaxRoadWidth
|
|
||||||
if maxWidth < minWidth {
|
if maxWidth < minWidth {
|
||||||
minWidth, maxWidth = maxWidth, minWidth
|
minWidth, maxWidth = maxWidth, minWidth
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-13
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -130,15 +131,15 @@ func LoadSettings() (*Settings, error) {
|
|||||||
RiverCurvyness: 50,
|
RiverCurvyness: 50,
|
||||||
RiverWidthVariability: 50,
|
RiverWidthVariability: 50,
|
||||||
RiverEdgeRoughness: 50,
|
RiverEdgeRoughness: 50,
|
||||||
MinRoadWidth: 2,
|
MinRoadWidth: 0.7,
|
||||||
MaxRoadWidth: 8,
|
MaxRoadWidth: 2.7,
|
||||||
RoadExits: 5,
|
RoadExits: 5,
|
||||||
RoadCurvyness: 50,
|
RoadCurvyness: 50,
|
||||||
RoadDistribution: 50,
|
RoadDistribution: 50,
|
||||||
MinRoadAngle: 18,
|
MinRoadAngle: 18,
|
||||||
NumBuildings: 200,
|
NumBuildings: 200,
|
||||||
MinBuildingSize: 10,
|
MinBuildingSize: 3.5,
|
||||||
MaxBuildingSize: 30,
|
MaxBuildingSize: 10.0,
|
||||||
BuildingDistribution: 20,
|
BuildingDistribution: 20,
|
||||||
BuildingShape: "mixed",
|
BuildingShape: "mixed",
|
||||||
BuildingShapeRatios: map[string]float64{
|
BuildingShapeRatios: map[string]float64{
|
||||||
@@ -157,17 +158,19 @@ func LoadSettings() (*Settings, error) {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Decode through a wrapper so we can tell whether newer fields were present.
|
// Decode once into Settings, and separately inspect raw keys for field-presence checks.
|
||||||
type settingsDisk struct {
|
raw, err := io.ReadAll(file)
|
||||||
Settings
|
if err != nil {
|
||||||
MinRoadAngle *float64 `json:"min_road_angle"`
|
return nil, err
|
||||||
}
|
}
|
||||||
var disk settingsDisk
|
var settings Settings
|
||||||
decoder := json.NewDecoder(file)
|
if err := json.Unmarshal(raw, &settings); err != nil {
|
||||||
if err := decoder.Decode(&disk); err != nil {
|
return nil, err
|
||||||
|
}
|
||||||
|
var rawKeys map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw, &rawKeys); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
settings := disk.Settings
|
|
||||||
|
|
||||||
if settings.LakeShape == "" {
|
if settings.LakeShape == "" {
|
||||||
settings.LakeShape = "circle"
|
settings.LakeShape = "circle"
|
||||||
@@ -192,10 +195,34 @@ func LoadSettings() (*Settings, error) {
|
|||||||
if settings.BuildingComplexityRatio == 0 {
|
if settings.BuildingComplexityRatio == 0 {
|
||||||
settings.BuildingComplexityRatio = 50
|
settings.BuildingComplexityRatio = 50
|
||||||
}
|
}
|
||||||
if disk.MinRoadAngle == nil {
|
if _, ok := rawKeys["min_road_angle"]; !ok {
|
||||||
settings.MinRoadAngle = 18
|
settings.MinRoadAngle = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Road widths are percentages of average image dimension.
|
||||||
|
// Migrate older pixel-based values when they exceed the valid percentage range.
|
||||||
|
if settings.MinRoadWidth > maxRoadWidthPercent || settings.MaxRoadWidth > maxRoadWidthPercent {
|
||||||
|
avgDim := averageImageDimension(settings.Width, settings.Height)
|
||||||
|
if avgDim < 1 {
|
||||||
|
avgDim = 1
|
||||||
|
}
|
||||||
|
settings.MinRoadWidth = (settings.MinRoadWidth / avgDim) * 100.0
|
||||||
|
settings.MaxRoadWidth = (settings.MaxRoadWidth / avgDim) * 100.0
|
||||||
|
}
|
||||||
|
settings.MinRoadWidth, settings.MaxRoadWidth = normalizeRoadWidthPercentRange(settings.MinRoadWidth, settings.MaxRoadWidth)
|
||||||
|
|
||||||
|
// Building sizes are percentages of average image dimension.
|
||||||
|
// Migrate older pixel-based values when they exceed the valid percentage range.
|
||||||
|
if settings.MinBuildingSize > maxBuildingSizePercent || settings.MaxBuildingSize > maxBuildingSizePercent {
|
||||||
|
avgDim := averageImageDimension(settings.Width, settings.Height)
|
||||||
|
if avgDim < 1 {
|
||||||
|
avgDim = 1
|
||||||
|
}
|
||||||
|
settings.MinBuildingSize = (settings.MinBuildingSize / avgDim) * 100.0
|
||||||
|
settings.MaxBuildingSize = (settings.MaxBuildingSize / avgDim) * 100.0
|
||||||
|
}
|
||||||
|
settings.MinBuildingSize, settings.MaxBuildingSize = normalizeBuildingSizePercentRange(settings.MinBuildingSize, settings.MaxBuildingSize)
|
||||||
|
|
||||||
// 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 == "" {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
|
|||||||
Reference in New Issue
Block a user