added procedural building generation
This commit is contained in:
+209
-1
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user