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%` |
|
||||
| **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) |
|
||||
| **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` |
|
||||
| **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 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` |
|
||||
| **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 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` |
|
||||
| **Min Building Size** | The minimum size of a generated building in pixels. | `1` to `100` |
|
||||
| **Max Building Size** | The maximum 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 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 Shape** | The shape of the buildings. | `squares` or `circles` |
|
||||
|
||||
+54
-1
@@ -9,6 +9,58 @@ import (
|
||||
"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.
|
||||
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
|
||||
@@ -87,6 +139,7 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
||||
buildingsPlaced := 0
|
||||
searchTries := 100 // Number of attempts to find a spot for a building around an anchor
|
||||
maxPlacementAttempts := settings.NumBuildings * 5 // To prevent infinite loops
|
||||
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
|
||||
|
||||
for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 {
|
||||
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
|
||||
size := settings.MinBuildingSize + randSrc.Float64()*(settings.MaxBuildingSize-settings.MinBuildingSize)
|
||||
size := minBuildingSizePx + randSrc.Float64()*(maxBuildingSizePx-minBuildingSizePx)
|
||||
shape := settings.BuildingShape
|
||||
if shape == "mixed" {
|
||||
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
|
||||
|
||||
+30
-1
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -42,6 +43,7 @@ type numericInputSlider struct {
|
||||
widget.BaseWidget
|
||||
value binding.Float
|
||||
min, max float64
|
||||
step float64
|
||||
slider *widget.Slider
|
||||
entry *widget.Entry
|
||||
format string
|
||||
@@ -83,6 +85,7 @@ func newNumericInputSlider(min, max float64, initialValue float64, format string
|
||||
min: min,
|
||||
max: max,
|
||||
format: format,
|
||||
step: 0,
|
||||
}
|
||||
s.ExtendBaseWidget(s)
|
||||
|
||||
@@ -105,6 +108,17 @@ func newNumericInputSlider(min, max float64, initialValue float64, format string
|
||||
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
|
||||
func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
||||
text = strings.TrimSpace(text)
|
||||
@@ -121,11 +135,26 @@ func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
||||
}
|
||||
|
||||
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()
|
||||
onError(true)
|
||||
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()
|
||||
onError(false)
|
||||
|
||||
@@ -429,7 +429,7 @@ func main() {
|
||||
val, _ := treeClumpinessSlider.value.Get()
|
||||
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.validate(s, func(hasError bool) {
|
||||
errorStates["minRoadWidth"] = hasError
|
||||
@@ -441,7 +441,7 @@ func main() {
|
||||
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.validate(s, func(hasError bool) {
|
||||
errorStates["maxRoadWidth"] = hasError
|
||||
@@ -753,7 +753,7 @@ func main() {
|
||||
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.validate(s, func(hasError bool) {
|
||||
errorStates["minBuildingSize"] = hasError
|
||||
@@ -765,7 +765,7 @@ func main() {
|
||||
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.validate(s, func(hasError bool) {
|
||||
errorStates["maxBuildingSize"] = hasError
|
||||
|
||||
@@ -33,6 +33,54 @@ type Road struct {
|
||||
|
||||
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 {
|
||||
out := make([]image.Point, len(lastExitRoadPixels))
|
||||
copy(out, lastExitRoadPixels)
|
||||
@@ -61,7 +109,7 @@ func GenerateRoads(width, height int, settings *Settings, _ image.Image, allWate
|
||||
if len(roads) == 0 {
|
||||
return nil, nil, img
|
||||
}
|
||||
assignRoadWidths(roads, settings, randSrc)
|
||||
assignRoadWidths(roads, settings, randSrc, width, height)
|
||||
|
||||
allRoadPixels := make([]image.Point, 0, len(roads)*64)
|
||||
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 {
|
||||
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 {
|
||||
avgBuildingSize = 1
|
||||
}
|
||||
@@ -512,13 +561,12 @@ func normalizeAngle(a float64) float64 {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
minWidth := settings.MinRoadWidth
|
||||
maxWidth := settings.MaxRoadWidth
|
||||
minWidth, maxWidth := getRoadWidthRangePixels(settings, width, height)
|
||||
if maxWidth < minWidth {
|
||||
minWidth, maxWidth = maxWidth, minWidth
|
||||
}
|
||||
|
||||
+40
-13
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -130,15 +131,15 @@ func LoadSettings() (*Settings, error) {
|
||||
RiverCurvyness: 50,
|
||||
RiverWidthVariability: 50,
|
||||
RiverEdgeRoughness: 50,
|
||||
MinRoadWidth: 2,
|
||||
MaxRoadWidth: 8,
|
||||
MinRoadWidth: 0.7,
|
||||
MaxRoadWidth: 2.7,
|
||||
RoadExits: 5,
|
||||
RoadCurvyness: 50,
|
||||
RoadDistribution: 50,
|
||||
MinRoadAngle: 18,
|
||||
NumBuildings: 200,
|
||||
MinBuildingSize: 10,
|
||||
MaxBuildingSize: 30,
|
||||
MinBuildingSize: 3.5,
|
||||
MaxBuildingSize: 10.0,
|
||||
BuildingDistribution: 20,
|
||||
BuildingShape: "mixed",
|
||||
BuildingShapeRatios: map[string]float64{
|
||||
@@ -157,17 +158,19 @@ func LoadSettings() (*Settings, error) {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Decode through a wrapper so we can tell whether newer fields were present.
|
||||
type settingsDisk struct {
|
||||
Settings
|
||||
MinRoadAngle *float64 `json:"min_road_angle"`
|
||||
// Decode once into Settings, and separately inspect raw keys for field-presence checks.
|
||||
raw, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var disk settingsDisk
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&disk); err != nil {
|
||||
var settings Settings
|
||||
if err := json.Unmarshal(raw, &settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rawKeys map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &rawKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settings := disk.Settings
|
||||
|
||||
if settings.LakeShape == "" {
|
||||
settings.LakeShape = "circle"
|
||||
@@ -192,10 +195,34 @@ func LoadSettings() (*Settings, error) {
|
||||
if settings.BuildingComplexityRatio == 0 {
|
||||
settings.BuildingComplexityRatio = 50
|
||||
}
|
||||
if disk.MinRoadAngle == nil {
|
||||
if _, ok := rawKeys["min_road_angle"]; !ok {
|
||||
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
|
||||
if settings.LastExportPath == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
|
||||
Reference in New Issue
Block a user