changed how building distribution works

This commit is contained in:
grimsace
2026-04-06 09:50:05 -05:00
parent fdac5ef45a
commit 4888e10f73
3 changed files with 89 additions and 22 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ All settings below are currently implemented in the UI.
| **Number of Buildings** | Requested building count before fit-cap. | `0` to `10000` |
| **Min Building Size** | Minimum building size as % of average image dimension `((width+height)/2)`. | `0.5%` to `25%` in `0.5%` steps |
| **Max Building Size** | Maximum building size as % of average image dimension `((width+height)/2)`. | `0.5%` to `25%` in `0.5%` steps |
| **Building Distribution** | Placement preference from near roads toward random map-wide placement. | `0%` to `100%` |
| **Building Distribution** | How far buildings may spread away from roads: `0%` stays road-adjacent, `100%` becomes fully random map-wide placement. | `0%` to `100%` |
| **Building Shape** | Primary building generation mode. | `squares`, `circles`, `rectangles`, `mixed`, `procedural` |
| **Squares / Circles / Rectangles** | Shape ratio sliders used by `mixed` and `procedural` building modes. | `0%` to `100%` each (normalized together) |
| **Min Building Complexity** | Minimum number of shape components in procedural buildings. | `1` to `6` |
+80 -21
View File
@@ -170,18 +170,28 @@ func GenerateBuildings(
// Main loop for placing buildings
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
maxPlacementAttempts := settings.NumBuildings * 3 // To prevent infinite loops
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
avgBuildingSizePx := (minBuildingSizePx + maxBuildingSizePx) * 0.5
anchorUsage := make(map[image.Point]int, len(anchorPoints))
normalAnchorCap := 0
exitAnchorCap := 0
distributionFactor := clamp01(settings.BuildingDistribution / 100.0)
roadPlacementBias := 1.0 - distributionFactor
roadAnchorMinOffset := clamp(avgBuildingSizePx*0.55, 2, 16)
roadAnchorMaxOffset := roadAnchorMinOffset + math.Pow(distributionFactor, 1.05)*clamp(avgBuildingSizePx*13.0, 16, 180)
roadSearchTries := 42
randomSearchTries := 18
if len(normalRoadAnchors) > 0 {
normalAnchorCap = max(2, int(math.Ceil((float64(settings.NumBuildings)/float64(len(normalRoadAnchors)))*1.15)))
}
if len(exitRoadAnchors) > 0 {
exitAnchorCap = max(1, int(math.Ceil((float64(settings.NumBuildings)/float64(len(exitRoadAnchors)))*0.20)))
}
if settings.NumBuildings <= 80 {
normalAnchorCap = max(normalAnchorCap, 6)
exitAnchorCap = max(exitAnchorCap, 2)
}
pickAnchorWithCapacity := func(candidates []image.Point, capLimit int) (image.Point, bool) {
if len(candidates) == 0 {
@@ -209,13 +219,28 @@ func GenerateBuildings(
return image.Point{}, false
}
pickLeastUsedAnchor := func(candidates []image.Point) (image.Point, bool) {
if len(candidates) == 0 {
return image.Point{}, false
}
best := candidates[randSrc.Intn(len(candidates))]
bestCount := anchorUsage[best]
for _, candidate := range candidates {
if count := anchorUsage[candidate]; count < bestCount {
best = candidate
bestCount = count
}
}
return best, true
}
for buildingsPlaced < settings.NumBuildings && maxPlacementAttempts > 0 {
maxPlacementAttempts--
// Select an anchor point for the new building
var anchor image.Point
usedRoadAnchor := false
if randSrc.Float64() > settings.BuildingDistribution/100.0 {
if len(anchorPoints) > 0 && randSrc.Float64() < roadPlacementBias {
// Buildings should only rarely use exit-road anchors.
useExitAnchor := len(exitRoadAnchors) > 0 && randSrc.Float64() < 0.02
if useExitAnchor {
@@ -235,31 +260,72 @@ func GenerateBuildings(
}
}
if !usedRoadAnchor {
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
if !ok {
continue
if a, ok := pickLeastUsedAnchor(normalRoadAnchors); ok {
anchor = a
usedRoadAnchor = true
} else if a, ok := pickLeastUsedAnchor(anchorPoints); ok {
anchor = a
usedRoadAnchor = true
} else {
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
if !ok {
continue
}
anchor = p
}
anchor = p
}
} else {
}
if !usedRoadAnchor {
p, ok := sampleRandomLandPoint(width, height, waterMask, roadMask, randSrc)
if !ok {
continue // No land to place buildings on
if a, ok := pickLeastUsedAnchor(normalRoadAnchors); ok {
anchor = a
usedRoadAnchor = true
} else if a, ok := pickLeastUsedAnchor(anchorPoints); ok {
anchor = a
usedRoadAnchor = true
} else {
continue // No land to place buildings on
}
} else {
anchor = p
}
anchor = p
}
searchTries := randomSearchTries
if usedRoadAnchor {
searchTries = roadSearchTries
}
// Search for a valid building location around the anchor
for i := 0; i < searchTries; i++ {
searchRadius := float64(i) * 2.0 // Search in expanding circles
// Sample building shape/size first so road-adjacent placement can leave enough setback.
size := minBuildingSizePx + randSrc.Float64()*(maxBuildingSizePx-minBuildingSizePx)
shape := settings.BuildingShape
if shape == "mixed" {
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
}
angle := randSrc.Float64() * 2 * math.Pi
dist := searchRadius * randSrc.Float64()
dist := 0.0
if usedRoadAnchor {
minSetback := max(roadAnchorMinOffset, size*0.85+2.0)
progress := float64(i) / float64(max(searchTries-1, 1))
searchRadius := minSetback + progress*(roadAnchorMaxOffset-minSetback)
if searchRadius < minSetback {
searchRadius = minSetback
}
dist = minSetback + randSrc.Float64()*(searchRadius-minSetback)
} else {
searchRadius := clamp(float64(i+1)*avgBuildingSizePx*0.45, avgBuildingSizePx, avgBuildingSizePx*5.5)
dist = randSrc.Float64() * searchRadius
}
center := image.Point{
X: anchor.X + int(dist*math.Cos(angle)),
Y: anchor.Y + int(dist*math.Sin(angle)),
}
// For fully random distribution, pick any point on the map
if settings.BuildingDistribution == 100 {
// Non-road placement is fully random over the map.
if !usedRoadAnchor {
center = image.Point{
X: randSrc.Intn(width),
Y: randSrc.Intn(height),
@@ -271,13 +337,6 @@ func GenerateBuildings(
continue
}
// Attempt to create a building at the selected center
size := minBuildingSizePx + randSrc.Float64()*(maxBuildingSizePx-minBuildingSizePx)
shape := settings.BuildingShape
if shape == "mixed" {
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
}
var pixels []image.Point
var ok bool
+8
View File
@@ -1995,6 +1995,14 @@ func collectRoadAnchors(roads []*Road, settings *Settings, waterMask *PixelMask,
minBuildingSizePx, maxBuildingSizePx := getBuildingSizeRangePixels(settings, width, height)
spacing := int(math.Round(clamp((minBuildingSizePx+maxBuildingSizePx)*0.5, 8, 28)))
switch {
case settings.NumBuildings >= 2500:
spacing = int(math.Round(float64(spacing) * 0.72))
case settings.NumBuildings >= 1000:
spacing = int(math.Round(float64(spacing) * 0.80))
case settings.NumBuildings >= 300:
spacing = int(math.Round(float64(spacing) * 0.90))
}
if spacing < 6 {
spacing = 6
}