added procedural building generation

This commit is contained in:
Grimsace
2026-02-06 10:21:40 -06:00
parent 6eb4dad5c8
commit 230ed5aa13
3 changed files with 287 additions and 7 deletions
+209 -1
View File
@@ -122,7 +122,16 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
if shape == "mixed" {
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
}
pixels, ok := getBuildingPixels(center, size, shape, isWater, isRoad, isBuilding, width, height, randSrc)
var pixels []image.Point
var ok bool
if shape == "procedural" {
pixels, ok = getProceduralBuildingPixels(center, size, settings, isWater, isRoad, isBuilding, width, height, randSrc)
} else {
pixels, ok = getBuildingPixels(center, size, shape, isWater, isRoad, isBuilding, width, height, randSrc)
}
if ok {
// If successful, draw the building and update data structures
for _, p := range pixels {
@@ -138,6 +147,205 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
return buildingPixels
}
// getProceduralBuildingPixels generates a complex building by connecting multiple shapes.
func getProceduralBuildingPixels(center image.Point, size float64, settings *Settings, isWater, isRoad, isBuilding map[image.Point]bool, width, height int, randSrc *rand.Rand) ([]image.Point, bool) {
// Determine complexity
complexity := settings.MinBuildingComplexity
if settings.BuildingComplexityRatio > randSrc.Float64()*100 {
complexity = settings.MinBuildingComplexity + randSrc.Intn(settings.MaxBuildingComplexity-settings.MinBuildingComplexity+1)
}
type shapeDescription struct {
shape string
center image.Point
size float64
}
var shapeDescriptions []shapeDescription
var buildingCenter image.Point
// Generate component shapes
for i := 0; i < complexity; i++ {
shape := chooseShape(randSrc, settings.BuildingShapeRatios)
componentSize := size * (0.5 + randSrc.Float64()*0.5) // Components can be 50-100% of the building size
var newCenter image.Point
if i == 0 {
newCenter = center
buildingCenter = center
} else {
// Place subsequent components near existing ones
prevShape := shapeDescriptions[randSrc.Intn(len(shapeDescriptions))]
angle := randSrc.Float64() * 2 * math.Pi
dist := componentSize * (0.25 + randSrc.Float64()*0.5) // Overlap between 25% and 75%
newCenter = image.Point{
X: prevShape.center.X + int(dist*math.Cos(angle)),
Y: prevShape.center.Y + int(dist*math.Sin(angle)),
}
}
shapeDescriptions = append(shapeDescriptions, shapeDescription{shape, newCenter, componentSize})
}
// Find the bounding box of the unscaled building
var minX, minY, maxX, maxY int
for i, sd := range shapeDescriptions {
halfSize := int(sd.size / 2)
if i == 0 {
minX, minY = sd.center.X-halfSize, sd.center.Y-halfSize
maxX, maxY = sd.center.X+halfSize, sd.center.Y+halfSize
} else {
if sd.center.X-halfSize < minX {
minX = sd.center.X - halfSize
}
if sd.center.Y-halfSize < minY {
minY = sd.center.Y - halfSize
}
if sd.center.X+halfSize > maxX {
maxX = sd.center.X + halfSize
}
if sd.center.Y+halfSize > maxY {
maxY = sd.center.Y + halfSize
}
}
}
// Calculate scaling factor
currentWidth := float64(maxX - minX)
currentHeight := float64(maxY - minY)
scale := size / math.Max(currentWidth, currentHeight)
// Generate final pixels
var finalPixels []image.Point
pixelMap := make(map[image.Point]bool)
for _, sd := range shapeDescriptions {
scaledSize := sd.size * scale
scaledCenterX := buildingCenter.X + int((float64(sd.center.X)-float64(minX)-currentWidth/2)*scale)
scaledCenterY := buildingCenter.Y + int((float64(sd.center.Y)-float64(minY)-currentHeight/2)*scale)
pixels, ok := getComponentPixels(image.Point{X: scaledCenterX, Y: scaledCenterY}, scaledSize, sd.shape, randSrc)
if !ok {
continue
}
for _, p := range pixels {
if p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height || isWater[p] || isRoad[p] || isBuilding[p] {
return nil, false
}
if !pixelMap[p] {
finalPixels = append(finalPixels, p)
pixelMap[p] = true
}
}
}
if len(finalPixels) == 0 {
return nil, false
}
return finalPixels, true
}
// scalePixels scales the building to the final size.
func scalePixels(pixels []image.Point, finalSize float64) []image.Point {
if len(pixels) == 0 {
return pixels
}
// Find the bounding box of the pixels
minX, minY := pixels[0].X, pixels[0].Y
maxX, maxY := pixels[0].X, pixels[0].Y
for _, p := range pixels {
if p.X < minX {
minX = p.X
}
if p.Y < minY {
minY = p.Y
}
if p.X > maxX {
maxX = p.X
}
if p.Y > maxY {
maxY = p.Y
}
}
// Calculate the current dimensions
currentWidth := float64(maxX - minX)
currentHeight := float64(maxY - minY)
// Determine the scaling factor
scale := finalSize / math.Max(currentWidth, currentHeight)
// Calculate the center of the bounding box
centerX := float64(minX) + currentWidth/2
centerY := float64(minY) + currentHeight/2
// Scale and translate the pixels
var scaledPixels []image.Point
pixelMap := make(map[image.Point]bool) // To avoid duplicate pixels
for _, p := range pixels {
// Translate to origin
translatedX := float64(p.X) - centerX
translatedY := float64(p.Y) - centerY
// Scale
scaledX := translatedX * scale
scaledY := translatedY * scale
// Translate back to the center
finalX := int(math.Round(scaledX + centerX))
finalY := int(math.Round(scaledY + centerY))
newPoint := image.Point{X: finalX, Y: finalY}
if !pixelMap[newPoint] {
scaledPixels = append(scaledPixels, newPoint)
pixelMap[newPoint] = true
}
}
return scaledPixels
}
// getComponentPixels generates the pixels for a single shape component without collision checks.
func getComponentPixels(center image.Point, size float64, shape string, randSrc *rand.Rand) ([]image.Point, bool) {
var pixels []image.Point
var halfSize = int(size / 2)
switch shape {
case "squares":
for y := center.Y - halfSize; y <= center.Y+halfSize; y++ {
for x := center.X - halfSize; x <= center.X+halfSize; x++ {
pixels = append(pixels, image.Point{X: x, Y: y})
}
}
case "circles":
r2 := (size / 2) * (size / 2)
for y := center.Y - halfSize; y <= center.Y+halfSize; y++ {
for x := center.X - halfSize; x <= center.X+halfSize; x++ {
dx, dy := float64(x-center.X), float64(y-center.Y)
if dx*dx+dy*dy <= r2 {
pixels = append(pixels, image.Point{X: x, Y: y})
}
}
}
case "rectangles":
longSide := size
shortSide := randSrc.Float64()*(size-float64(halfSize)) + float64(halfSize)
var w, h int
if randSrc.Intn(2) == 0 {
w, h = int(longSide), int(shortSide)
} else {
w, h = int(shortSide), int(longSide)
}
halfW, halfH := w/2, h/2
for y := center.Y - halfH; y <= center.Y+halfH; y++ {
for x := center.X - halfW; x <= center.X+halfW; x++ {
pixels = append(pixels, image.Point{X: x, Y: y})
}
}
}
return pixels, len(pixels) > 0
}
// chooseShape selects a building shape based on the provided ratios.
func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string {
// Create a slice of shapes and their cumulative weights
+58 -5
View File
@@ -650,7 +650,7 @@ func main() {
buildingDistributionSlider.SetValue(settings.BuildingDistribution)
buildingShapeLabel := widget.NewLabel("Building Shape:")
buildingShapeSelect := widget.NewSelect([]string{"squares", "circles", "rectangles", "mixed"}, func(s string) {
buildingShapeSelect := widget.NewSelect([]string{"squares", "circles", "rectangles", "mixed", "procedural"}, func(s string) {
settings.BuildingShape = s
})
buildingShapeSelect.SetSelected(settings.BuildingShape)
@@ -677,6 +677,46 @@ func main() {
rectangleRatioSlider,
)
// Create sliders for procedural building complexity
minBuildingComplexityLabel := widget.NewLabel(fmt.Sprintf("Min Building Complexity: %d", settings.MinBuildingComplexity))
minBuildingComplexitySlider := widget.NewSlider(1, 6)
minBuildingComplexitySlider.OnChanged = func(val float64) {
settings.MinBuildingComplexity = int(val)
if float64(settings.MinBuildingComplexity) > float64(settings.MaxBuildingComplexity) {
settings.MaxBuildingComplexity = settings.MinBuildingComplexity
}
minBuildingComplexityLabel.SetText(fmt.Sprintf("Min Building Complexity: %d", settings.MinBuildingComplexity))
}
minBuildingComplexitySlider.SetValue(float64(settings.MinBuildingComplexity))
maxBuildingComplexityLabel := widget.NewLabel(fmt.Sprintf("Max Building Complexity: %d", settings.MaxBuildingComplexity))
maxBuildingComplexitySlider := widget.NewSlider(1, 6)
maxBuildingComplexitySlider.OnChanged = func(val float64) {
settings.MaxBuildingComplexity = int(val)
if float64(settings.MaxBuildingComplexity) < float64(settings.MinBuildingComplexity) {
settings.MinBuildingComplexity = settings.MaxBuildingComplexity
}
maxBuildingComplexityLabel.SetText(fmt.Sprintf("Max Building Complexity: %d", settings.MaxBuildingComplexity))
}
maxBuildingComplexitySlider.SetValue(float64(settings.MaxBuildingComplexity))
buildingComplexityRatioLabel := widget.NewLabel(fmt.Sprintf("Building Complexity Ratio: %.0f%%", settings.BuildingComplexityRatio))
buildingComplexityRatioSlider := widget.NewSlider(0, 100)
buildingComplexityRatioSlider.OnChanged = func(val float64) {
settings.BuildingComplexityRatio = val
buildingComplexityRatioLabel.SetText(fmt.Sprintf("Building Complexity Ratio: %.0f%%", settings.BuildingComplexityRatio))
}
buildingComplexityRatioSlider.SetValue(settings.BuildingComplexityRatio)
proceduralContainer := container.NewVBox(
minBuildingComplexityLabel,
minBuildingComplexitySlider,
maxBuildingComplexityLabel,
maxBuildingComplexitySlider,
buildingComplexityRatioLabel,
buildingComplexityRatioSlider,
)
updateRatioSliders := func() {
squareRatioSlider.SetValue(settings.BuildingShapeRatios["squares"])
circleRatioSlider.SetValue(settings.BuildingShapeRatios["circles"])
@@ -687,7 +727,7 @@ func main() {
}
squareRatioSlider.OnChanged = func(val float64) {
if settings.BuildingShape != "mixed" {
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
return
}
oldVal := settings.BuildingShapeRatios["squares"]
@@ -718,7 +758,7 @@ func main() {
}
circleRatioSlider.OnChanged = func(val float64) {
if settings.BuildingShape != "mixed" {
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
return
}
oldVal := settings.BuildingShapeRatios["circles"]
@@ -748,7 +788,7 @@ func main() {
}
rectangleRatioSlider.OnChanged = func(val float64) {
if settings.BuildingShape != "mixed" {
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
return
}
oldVal := settings.BuildingShapeRatios["rectangles"]
@@ -781,13 +821,25 @@ func main() {
buildingShapeSelect.OnChanged = func(s string) {
settings.BuildingShape = s
if s == "mixed" {
proceduralContainer.Hide()
ratioContainer.Show()
} else if s == "procedural" {
proceduralContainer.Show()
ratioContainer.Show()
} else {
proceduralContainer.Hide()
ratioContainer.Hide()
}
}
// Initial visibility check
if settings.BuildingShape != "mixed" {
if settings.BuildingShape == "mixed" {
proceduralContainer.Hide()
ratioContainer.Show()
} else if settings.BuildingShape == "procedural" {
proceduralContainer.Show()
ratioContainer.Show()
} else {
proceduralContainer.Hide()
ratioContainer.Hide()
}
@@ -802,6 +854,7 @@ func main() {
buildingDistributionSlider,
buildingShapeLabel,
buildingShapeSelect,
proceduralContainer,
ratioContainer,
))
+19
View File
@@ -48,6 +48,11 @@ type Settings struct {
// Ratios for mixed building shapes
BuildingShapeRatios map[string]float64 `json:"building_shape_ratios"`
// Procedural building settings
MinBuildingComplexity int `json:"min_building_complexity"`
MaxBuildingComplexity int `json:"max_building_complexity"`
BuildingComplexityRatio float64 `json:"building_complexity_ratio"`
// General settings
Seed int64 `json:"seed"`
LastExportPath string `json:"last_export_path"`
@@ -132,6 +137,9 @@ func LoadSettings() (*Settings, error) {
"circles": 30,
"rectangles": 30,
},
MinBuildingComplexity: 1,
MaxBuildingComplexity: 3,
BuildingComplexityRatio: 50,
LastExportPath: homeDir,
}, nil
}
@@ -155,6 +163,17 @@ func LoadSettings() (*Settings, error) {
}
}
// Ensure procedural building settings are initialized
if settings.MinBuildingComplexity == 0 {
settings.MinBuildingComplexity = 1
}
if settings.MaxBuildingComplexity == 0 {
settings.MaxBuildingComplexity = 3
}
if settings.BuildingComplexityRatio == 0 {
settings.BuildingComplexityRatio = 50
}
// Ensure LastExportPath is set to a default value if it's empty
if settings.LastExportPath == "" {
homeDir, err := os.UserHomeDir()