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" {
|
if shape == "mixed" {
|
||||||
shape = chooseShape(randSrc, settings.BuildingShapeRatios)
|
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 ok {
|
||||||
// If successful, draw the building and update data structures
|
// If successful, draw the building and update data structures
|
||||||
for _, p := range pixels {
|
for _, p := range pixels {
|
||||||
@@ -138,6 +147,205 @@ func GenerateBuildings(img *image.RGBA, width, height int, settings *Settings, r
|
|||||||
return buildingPixels
|
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.
|
// chooseShape selects a building shape based on the provided ratios.
|
||||||
func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string {
|
func chooseShape(randSrc *rand.Rand, ratios map[string]float64) string {
|
||||||
// Create a slice of shapes and their cumulative weights
|
// Create a slice of shapes and their cumulative weights
|
||||||
|
|||||||
@@ -650,7 +650,7 @@ func main() {
|
|||||||
buildingDistributionSlider.SetValue(settings.BuildingDistribution)
|
buildingDistributionSlider.SetValue(settings.BuildingDistribution)
|
||||||
|
|
||||||
buildingShapeLabel := widget.NewLabel("Building Shape:")
|
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
|
settings.BuildingShape = s
|
||||||
})
|
})
|
||||||
buildingShapeSelect.SetSelected(settings.BuildingShape)
|
buildingShapeSelect.SetSelected(settings.BuildingShape)
|
||||||
@@ -677,6 +677,46 @@ func main() {
|
|||||||
rectangleRatioSlider,
|
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() {
|
updateRatioSliders := func() {
|
||||||
squareRatioSlider.SetValue(settings.BuildingShapeRatios["squares"])
|
squareRatioSlider.SetValue(settings.BuildingShapeRatios["squares"])
|
||||||
circleRatioSlider.SetValue(settings.BuildingShapeRatios["circles"])
|
circleRatioSlider.SetValue(settings.BuildingShapeRatios["circles"])
|
||||||
@@ -687,7 +727,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
squareRatioSlider.OnChanged = func(val float64) {
|
squareRatioSlider.OnChanged = func(val float64) {
|
||||||
if settings.BuildingShape != "mixed" {
|
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oldVal := settings.BuildingShapeRatios["squares"]
|
oldVal := settings.BuildingShapeRatios["squares"]
|
||||||
@@ -718,7 +758,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
circleRatioSlider.OnChanged = func(val float64) {
|
circleRatioSlider.OnChanged = func(val float64) {
|
||||||
if settings.BuildingShape != "mixed" {
|
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oldVal := settings.BuildingShapeRatios["circles"]
|
oldVal := settings.BuildingShapeRatios["circles"]
|
||||||
@@ -748,7 +788,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rectangleRatioSlider.OnChanged = func(val float64) {
|
rectangleRatioSlider.OnChanged = func(val float64) {
|
||||||
if settings.BuildingShape != "mixed" {
|
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oldVal := settings.BuildingShapeRatios["rectangles"]
|
oldVal := settings.BuildingShapeRatios["rectangles"]
|
||||||
@@ -781,13 +821,25 @@ func main() {
|
|||||||
buildingShapeSelect.OnChanged = func(s string) {
|
buildingShapeSelect.OnChanged = func(s string) {
|
||||||
settings.BuildingShape = s
|
settings.BuildingShape = s
|
||||||
if s == "mixed" {
|
if s == "mixed" {
|
||||||
|
proceduralContainer.Hide()
|
||||||
|
ratioContainer.Show()
|
||||||
|
} else if s == "procedural" {
|
||||||
|
proceduralContainer.Show()
|
||||||
ratioContainer.Show()
|
ratioContainer.Show()
|
||||||
} else {
|
} else {
|
||||||
|
proceduralContainer.Hide()
|
||||||
ratioContainer.Hide()
|
ratioContainer.Hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Initial visibility check
|
// 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()
|
ratioContainer.Hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,6 +854,7 @@ func main() {
|
|||||||
buildingDistributionSlider,
|
buildingDistributionSlider,
|
||||||
buildingShapeLabel,
|
buildingShapeLabel,
|
||||||
buildingShapeSelect,
|
buildingShapeSelect,
|
||||||
|
proceduralContainer,
|
||||||
ratioContainer,
|
ratioContainer,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
+20
-1
@@ -48,6 +48,11 @@ type Settings struct {
|
|||||||
// Ratios for mixed building shapes
|
// Ratios for mixed building shapes
|
||||||
BuildingShapeRatios map[string]float64 `json:"building_shape_ratios"`
|
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
|
// General settings
|
||||||
Seed int64 `json:"seed"`
|
Seed int64 `json:"seed"`
|
||||||
LastExportPath string `json:"last_export_path"`
|
LastExportPath string `json:"last_export_path"`
|
||||||
@@ -132,7 +137,10 @@ func LoadSettings() (*Settings, error) {
|
|||||||
"circles": 30,
|
"circles": 30,
|
||||||
"rectangles": 30,
|
"rectangles": 30,
|
||||||
},
|
},
|
||||||
LastExportPath: homeDir,
|
MinBuildingComplexity: 1,
|
||||||
|
MaxBuildingComplexity: 3,
|
||||||
|
BuildingComplexityRatio: 50,
|
||||||
|
LastExportPath: homeDir,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
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
|
// Ensure LastExportPath is set to a default value if it's empty
|
||||||
if settings.LastExportPath == "" {
|
if settings.LastExportPath == "" {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
|
|||||||
Reference in New Issue
Block a user