From 230ed5aa13aec4716b9fc78f8ebc1831ccde995b Mon Sep 17 00:00:00 2001 From: Grimsace Date: Fri, 6 Feb 2026 10:21:40 -0600 Subject: [PATCH] added procedural building generation --- buildings.go | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++- main.go | 63 ++++++++++++++-- settings.go | 21 +++++- 3 files changed, 287 insertions(+), 7 deletions(-) diff --git a/buildings.go b/buildings.go index c5d6146..347fd86 100644 --- a/buildings.go +++ b/buildings.go @@ -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 diff --git a/main.go b/main.go index 0c486c3..7473d4b 100644 --- a/main.go +++ b/main.go @@ -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, )) diff --git a/settings.go b/settings.go index 86c3779..cb64e79 100644 --- a/settings.go +++ b/settings.go @@ -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,7 +137,10 @@ func LoadSettings() (*Settings, error) { "circles": 30, "rectangles": 30, }, - LastExportPath: homeDir, + MinBuildingComplexity: 1, + MaxBuildingComplexity: 3, + BuildingComplexityRatio: 50, + LastExportPath: homeDir, }, nil } return nil, err @@ -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()