Files
RPG_City_Maker_Reborn/main.go
T

1384 lines
44 KiB
Go

package main
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"github.com/chai2010/webp"
)
func main() {
// Initialize the Fyne application and window
a := app.NewWithID("com.example.rpgcitymaker")
w := a.NewWindow("RPG City Maker")
w.Resize(fyne.NewSize(800, 600))
// Load settings from file, or use defaults if loading fails
settings, err := LoadSettings()
if err != nil {
log.Println("Error loading settings:", err)
// Use default settings if loading fails
settings = &Settings{Detail: 1, Roughness: 0, Width: 300, Height: 300, Lakes: 0, LakeSizeLower: 1, LakeSizeUpper: 5}
}
// Create canvas objects for displaying the generated images
canvasImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
heightmapImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
bumpmapImg := &canvas.Image{
FillMode: canvas.ImageFillContain,
}
// Initialize slices to store generated map features
var lakes [][]image.Point
var riverPixels []image.Point
var treePixels []image.Point
var buildingPixels []image.Point
var buildings [][]image.Point
var roadPixels []image.Point
var bridgePixels []image.Point
// Set up application configuration directory
configDir, err := os.UserConfigDir()
if err != nil {
log.Fatal("Failed to get user config dir:", err)
}
appConfigDir := filepath.Join(configDir, "rpgcitymakerreborn")
canvasPath := filepath.Join(appConfigDir, "canvas.png")
heightmapPath := filepath.Join(appConfigDir, "heightmap.png")
bumpmapPath := filepath.Join(appConfigDir, "bumpmap.png")
// Save settings and images on window close
w.SetOnClosed(func() {
if err := settings.Save(); err != nil {
log.Println("Error saving settings:", err)
}
if canvasImg.Image != nil {
canvasFile, err := os.Create(canvasPath)
if err != nil {
log.Println("Failed to create canvas file:", err)
} else {
defer canvasFile.Close()
if err := png.Encode(canvasFile, canvasImg.Image); err != nil {
log.Println("Failed to encode canvas image:", err)
}
}
}
if heightmapImg.Image != nil {
heightmapFile, err := os.Create(heightmapPath)
if err != nil {
log.Println("Failed to create heightmap file:", err)
} else {
defer heightmapFile.Close()
if err := png.Encode(heightmapFile, heightmapImg.Image); err != nil {
log.Println("Failed to encode heightmap image:", err)
}
}
}
if bumpmapImg.Image != nil {
bumpmapFile, err := os.Create(bumpmapPath)
if err != nil {
log.Println("Failed to create bumpmap file:", err)
} else {
defer bumpmapFile.Close()
if err := png.Encode(bumpmapFile, bumpmapImg.Image); err != nil {
log.Println("Failed to encode bumpmap image:", err)
}
}
}
})
// Load previously saved images
canvasFile, err := os.Open(canvasPath)
if err == nil {
defer canvasFile.Close()
img, err := png.Decode(canvasFile)
if err == nil {
canvasImg.Image = img
}
}
heightmapFile, err := os.Open(heightmapPath)
if err == nil {
defer heightmapFile.Close()
img, err := png.Decode(heightmapFile)
if err == nil {
heightmapImg.Image = img
}
}
bumpmapFile, err := os.Open(bumpmapPath)
if err == nil {
defer bumpmapFile.Close()
img, err := png.Decode(bumpmapFile)
if err == nil {
bumpmapImg.Image = img
}
}
// Generate initial images if none are loaded
if canvasImg.Image == nil || heightmapImg.Image == nil {
// Step 1: Generating Heightmap
seedProvider := NewSeedProvider(settings.Seed)
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
// Step 2: Generating Lakes
var lakeImage image.Image
lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, seedProvider.Next(), settings.LakeEdgeRoughness, settings.LakeShape)
// Step 3: Generating Rivers
var riverImage image.Image
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness)
finalImage := riverImage.(*image.RGBA)
var roadImage *image.RGBA
var flatLakePixels []image.Point
for _, lake := range lakes {
flatLakePixels = append(flatLakePixels, lake...)
}
allWaterPixels := append(flatLakePixels, riverPixels...)
// Step 4: Generating Roads
roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next())
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
for x := 0; x < roadImage.Bounds().Max.X; x++ {
r, g, b, a := roadImage.At(x, y).RGBA()
if a > 0 {
finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)})
}
}
}
// Step 5: Generating Buildings
buildings, buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next())
// Step 6: Generating Trees
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
// Step 7: Darkening Water Areas
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels)
// Step 8: Flattening Building Areas
flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height)
// Step 9: Flattening Road Areas
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadPixels)
// Step 10: Applying Roughness
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
// Step 11: Generating Bump Map
bumpMap := GenerateBumpMap(compositeImg.(*image.RGBA), settings.Width, settings.Height, 0.10)
heightmapImg.Image = compositeImg
bumpmapImg.Image = bumpMap
canvasImg.Image = finalImage
}
var generateBtn *widget.Button
errorStates := make(map[string]bool)
// ... (other code)
// Main generation button and logic
generateBtn = widget.NewButton("Generate", func() {
// ... (generation logic)
})
// Function to update the generate button state
updateGenerateBtnState := func() {
for _, hasError := range errorStates {
if hasError {
generateBtn.Disable()
return
}
}
generateBtn.Enable()
}
detailSlider := newNumericInputSlider(1, 16, settings.Detail, "%.0f", "Detail")
detailSlider.entry.OnChanged = func(s string) {
detailSlider.validate(s, func(hasError bool) {
errorStates["detail"] = hasError
updateGenerateBtnState()
})
}
detailSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := detailSlider.value.Get()
settings.Detail = val
}))
roughnessSlider := newNumericInputSlider(0, 100, settings.Roughness, "%.0f%%", "Roughness")
roughnessSlider.entry.OnChanged = func(s string) {
roughnessSlider.validate(s, func(hasError bool) {
errorStates["roughness"] = hasError
updateGenerateBtnState()
})
}
roughnessSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := roughnessSlider.value.Get()
settings.Roughness = val
}))
// Create UI elements for controlling lake generation settings
lakesSlider := newNumericInputSlider(0, 15, float64(settings.Lakes), "%.0f", "Lakes")
lakesSlider.entry.OnChanged = func(s string) {
lakesSlider.validate(s, func(hasError bool) {
errorStates["lakes"] = hasError
updateGenerateBtnState()
})
}
lakesSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := lakesSlider.value.Get()
settings.Lakes = int(val)
}))
lakeSizeLowerSlider := newNumericInputSlider(1, 100, settings.LakeSizeLower, "%.0f%%", "Min Lake Size")
lakeSizeLowerSlider.entry.OnChanged = func(s string) {
lakeSizeLowerSlider.validate(s, func(hasError bool) {
errorStates["lakeSizeLower"] = hasError
updateGenerateBtnState()
})
}
lakeSizeLowerSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := lakeSizeLowerSlider.value.Get()
settings.LakeSizeLower = val
}))
lakeSizeUpperSlider := newNumericInputSlider(1, 100, settings.LakeSizeUpper, "%.0f%%", "Max Lake Size")
lakeSizeUpperSlider.entry.OnChanged = func(s string) {
lakeSizeUpperSlider.validate(s, func(hasError bool) {
errorStates["lakeSizeUpper"] = hasError
updateGenerateBtnState()
})
}
lakeSizeUpperSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := lakeSizeUpperSlider.value.Get()
settings.LakeSizeUpper = val
}))
lakeEdgeRoughnessSlider := newNumericInputSlider(0, 100, settings.LakeEdgeRoughness, "%.0f%%", "Lake Edge Roughness")
lakeEdgeRoughnessSlider.entry.OnChanged = func(s string) {
lakeEdgeRoughnessSlider.validate(s, func(hasError bool) {
errorStates["lakeEdgeRoughness"] = hasError
updateGenerateBtnState()
})
}
lakeEdgeRoughnessSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := lakeEdgeRoughnessSlider.value.Get()
settings.LakeEdgeRoughness = val
}))
lakeShapeLabel := widget.NewLabel("Lake Shape:")
lakeShapeSelect := widget.NewSelect([]string{"circle", "oval", "procedural"}, func(s string) {
settings.LakeShape = s
})
lakeShapeSelect.SetSelected(settings.LakeShape)
riversSlider := newNumericInputSlider(0, 5, float64(settings.Rivers), "%.0f", "Rivers")
riversSlider.entry.OnChanged = func(s string) {
riversSlider.validate(s, func(hasError bool) {
errorStates["rivers"] = hasError
updateGenerateBtnState()
})
}
riversSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := riversSlider.value.Get()
settings.Rivers = int(val)
}))
minRiverWidthSlider := newNumericInputSlider(1, 100, settings.MinRiverWidth, "%.0f%%", "Min River Width")
minRiverWidthSlider.entry.OnChanged = func(s string) {
minRiverWidthSlider.validate(s, func(hasError bool) {
errorStates["minRiverWidth"] = hasError
updateGenerateBtnState()
})
}
minRiverWidthSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := minRiverWidthSlider.value.Get()
settings.MinRiverWidth = val
}))
maxRiverWidthSlider := newNumericInputSlider(1, 100, settings.MaxRiverWidth, "%.0f%%", "Max River Width")
maxRiverWidthSlider.entry.OnChanged = func(s string) {
maxRiverWidthSlider.validate(s, func(hasError bool) {
errorStates["maxRiverWidth"] = hasError
updateGenerateBtnState()
})
}
maxRiverWidthSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := maxRiverWidthSlider.value.Get()
settings.MaxRiverWidth = val
}))
riverCurvynessSlider := newNumericInputSlider(0, 100, settings.RiverCurvyness, "%.0f%%", "River Curvyness")
riverCurvynessSlider.entry.OnChanged = func(s string) {
riverCurvynessSlider.validate(s, func(hasError bool) {
errorStates["riverCurvyness"] = hasError
updateGenerateBtnState()
})
}
riverCurvynessSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := riverCurvynessSlider.value.Get()
settings.RiverCurvyness = val
}))
riverWidthVariabilitySlider := newNumericInputSlider(0, 100, settings.RiverWidthVariability, "%.0f%%", "River Width Variability")
riverWidthVariabilitySlider.entry.OnChanged = func(s string) {
riverWidthVariabilitySlider.validate(s, func(hasError bool) {
errorStates["riverWidthVariability"] = hasError
updateGenerateBtnState()
})
}
riverWidthVariabilitySlider.value.AddListener(binding.NewDataListener(func() {
val, _ := riverWidthVariabilitySlider.value.Get()
settings.RiverWidthVariability = val
}))
riverEdgeRoughnessSlider := newNumericInputSlider(0, 100, settings.RiverEdgeRoughness, "%.0f%%", "River Edge Roughness")
riverEdgeRoughnessSlider.entry.OnChanged = func(s string) {
riverEdgeRoughnessSlider.validate(s, func(hasError bool) {
errorStates["riverEdgeRoughness"] = hasError
updateGenerateBtnState()
})
}
riverEdgeRoughnessSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := riverEdgeRoughnessSlider.value.Get()
settings.RiverEdgeRoughness = val
}))
minTreeSizeSlider := newNumericInputSlider(1, 150, settings.MinTreeSize, "%.0fpx", "Min Tree Size")
minTreeSizeSlider.entry.OnChanged = func(s string) {
minTreeSizeSlider.validate(s, func(hasError bool) {
errorStates["minTreeSize"] = hasError
updateGenerateBtnState()
})
}
minTreeSizeSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := minTreeSizeSlider.value.Get()
settings.MinTreeSize = val
}))
maxTreeSizeSlider := newNumericInputSlider(1, 150, settings.MaxTreeSize, "%.0fpx", "Max Tree Size")
maxTreeSizeSlider.entry.OnChanged = func(s string) {
maxTreeSizeSlider.validate(s, func(hasError bool) {
errorStates["maxTreeSize"] = hasError
updateGenerateBtnState()
})
}
maxTreeSizeSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := maxTreeSizeSlider.value.Get()
settings.MaxTreeSize = val
}))
treeCoverageSlider := newNumericInputSlider(1, 100, settings.TreeCoverage, "%.0f%%", "Tree Coverage")
treeCoverageSlider.entry.OnChanged = func(s string) {
treeCoverageSlider.validate(s, func(hasError bool) {
errorStates["treeCoverage"] = hasError
updateGenerateBtnState()
})
}
treeCoverageSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := treeCoverageSlider.value.Get()
settings.TreeCoverage = val
}))
treeClumpinessSlider := newNumericInputSlider(0, 100, settings.TreeClumpiness, "%.0f%%", "Tree Clumpiness")
treeClumpinessSlider.entry.OnChanged = func(s string) {
treeClumpinessSlider.validate(s, func(hasError bool) {
errorStates["treeClumpiness"] = hasError
updateGenerateBtnState()
})
}
treeClumpinessSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := treeClumpinessSlider.value.Get()
settings.TreeClumpiness = val
}))
minRoadWidthSlider := newNumericInputSlider(1, 150, settings.MinRoadWidth, "%.0fpx", "Min Road Width")
minRoadWidthSlider.entry.OnChanged = func(s string) {
minRoadWidthSlider.validate(s, func(hasError bool) {
errorStates["minRoadWidth"] = hasError
updateGenerateBtnState()
})
}
minRoadWidthSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := minRoadWidthSlider.value.Get()
settings.MinRoadWidth = val
}))
maxRoadWidthSlider := newNumericInputSlider(1, 150, settings.MaxRoadWidth, "%.0fpx", "Max Road Width")
maxRoadWidthSlider.entry.OnChanged = func(s string) {
maxRoadWidthSlider.validate(s, func(hasError bool) {
errorStates["maxRoadWidth"] = hasError
updateGenerateBtnState()
})
}
maxRoadWidthSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := maxRoadWidthSlider.value.Get()
settings.MaxRoadWidth = val
}))
roadExitsSlider := newNumericInputSlider(0, 100, float64(settings.RoadExits), "%.0f", "Road Exits")
roadExitsSlider.entry.OnChanged = func(s string) {
roadExitsSlider.validate(s, func(hasError bool) {
errorStates["roadExits"] = hasError
updateGenerateBtnState()
})
}
roadExitsSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := roadExitsSlider.value.Get()
settings.RoadExits = int(val)
}))
roadCurvynessSlider := newNumericInputSlider(0, 100, settings.RoadCurvyness, "%.0f%%", "Road Curvyness")
roadCurvynessSlider.entry.OnChanged = func(s string) {
roadCurvynessSlider.validate(s, func(hasError bool) {
errorStates["roadCurvyness"] = hasError
updateGenerateBtnState()
})
}
roadCurvynessSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := roadCurvynessSlider.value.Get()
settings.RoadCurvyness = val
}))
roadDistributionSlider := newNumericInputSlider(0, 100, settings.RoadDistribution, "%.0f%%", "Distribution")
roadDistributionSlider.entry.OnChanged = func(s string) {
roadDistributionSlider.validate(s, func(hasError bool) {
errorStates["roadDistribution"] = hasError
updateGenerateBtnState()
})
}
roadDistributionSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := roadDistributionSlider.value.Get()
settings.RoadDistribution = val
}))
minRoadAngleSlider := newNumericInputSlider(0, 180, settings.MinRoadAngle, "%.0f°", "Minimum Road Angle")
minRoadAngleSlider.entry.OnChanged = func(s string) {
minRoadAngleSlider.validate(s, func(hasError bool) {
errorStates["minRoadAngle"] = hasError
updateGenerateBtnState()
})
}
minRoadAngleSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := minRoadAngleSlider.value.Get()
settings.MinRoadAngle = val
}))
// Create UI elements for error display and action buttons
errorLabel := widget.NewLabel("")
errorLabel.Wrapping = fyne.TextWrapWord
errorLabel.Hide()
progressLabel := widget.NewLabel("")
progressBar := widget.NewProgressBar()
progressContainer := container.NewVBox(progressLabel, progressBar)
progressContainer.Hide()
exportCanvasBtn := widget.NewButton("Export Canvas", func() {
showSaveDialog(w, canvasImg.Image, settings)
})
exportHeightmapBtn := widget.NewButton("Export Heightmap", func() {
showSaveDialog(w, heightmapImg.Image, settings)
})
exportBumpmapBtn := widget.NewButton("Export Bump Map", func() {
showSaveDialog(w, bumpmapImg.Image, settings)
})
exportMasksBtn := widget.NewButton("Export Masks", func() {
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels, buildings)
})
// Main generation button and logic
generateBtn = widget.NewButton("Generate", func() {
go func() {
// Disable button and show progress bar during generation
fyne.Do(func() {
generateBtn.Disable()
progressContainer.Show()
})
defer func() {
// Re-enable button and hide progress bar after generation
fyne.Do(func() {
generateBtn.Enable()
progressContainer.Hide()
})
}()
seedProvider := NewSeedProvider(settings.Seed)
// Step 1: Generating Heightmap
fyne.Do(func() {
progressLabel.SetText("Step 1 of 11: Generating Heightmap")
progressBar.SetValue(1.0 / 11.0)
})
noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, seedProvider.Next())
// Step 2: Generating Lakes
fyne.Do(func() {
progressLabel.SetText("Step 2 of 11: Generating Lakes")
progressBar.SetValue(2.0 / 11.0)
})
var lakeImage image.Image
lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, seedProvider.Next(), settings.LakeEdgeRoughness, settings.LakeShape)
// Step 3: Generating Rivers
fyne.Do(func() {
progressLabel.SetText("Step 3 of 11: Generating Rivers")
progressBar.SetValue(3.0 / 11.0)
})
var riverImage image.Image
riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness)
finalImage := riverImage.(*image.RGBA)
var flatLakePixels []image.Point
for _, lake := range lakes {
flatLakePixels = append(flatLakePixels, lake...)
}
allWaterPixels := append(flatLakePixels, riverPixels...)
// Step 4: Generating Roads
fyne.Do(func() {
progressLabel.SetText("Step 4 of 11: Generating Roads")
progressBar.SetValue(4.0 / 11.0)
})
var roadImage *image.RGBA
roadPixels, bridgePixels, roadImage = GenerateRoads(settings.Width, settings.Height, settings, noiseImg, allWaterPixels, seedProvider.Next()) // Draw roadImage over finalImage
for y := 0; y < roadImage.Bounds().Max.Y; y++ {
for x := 0; x < roadImage.Bounds().Max.X; x++ {
r, g, b, a := roadImage.At(x, y).RGBA()
if a > 0 {
finalImage.Set(x, y, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)})
}
}
}
// Step 5: Generating Buildings
fyne.Do(func() {
progressLabel.SetText("Step 5 of 11: Generating Buildings")
progressBar.SetValue(5.0 / 11.0)
})
buildings, buildingPixels = GenerateBuildings(finalImage, settings.Width, settings.Height, settings, roadPixels, allWaterPixels, seedProvider.Next())
// Step 6: Darkening Water Areas
fyne.Do(func() {
progressLabel.SetText("Step 6 of 11: Darkening Water Areas")
progressBar.SetValue(6.0 / 11.0)
})
darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels)
// Step 7: Flattening Building Areas
fyne.Do(func() {
progressLabel.SetText("Step 7 of 11: Flattening Building Areas")
progressBar.SetValue(7.0 / 11.0)
})
flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height)
fyne.Do(func() {
progressLabel.SetText("Step 8 of 11: Flattening Road Areas")
progressBar.SetValue(8.0 / 11.0)
})
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, roadPixels)
// Step 8: Applying Roughness
fyne.Do(func() {
progressLabel.SetText("Step 9 of 11: Applying Roughness")
progressBar.SetValue(9.0 / 11.0)
})
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
// Step 9: Generating Trees
fyne.Do(func() {
progressLabel.SetText("Step 10 of 11: Generating Trees")
progressBar.SetValue(10.0 / 11.0)
})
treePixels = GenerateTrees(finalImage, allWaterPixels, roadPixels, buildingPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
// Step 10: Generating Bump Map
fyne.Do(func() {
progressLabel.SetText("Step 11 of 11: Generating Bump Map")
progressBar.SetValue(1.0)
})
bumpMap := GenerateBumpMap(compositeImg.(*image.RGBA), settings.Width, settings.Height, 0.10)
// Step 11: Finalizing Images
fyne.Do(func() {
heightmapImg.Image = compositeImg
heightmapImg.Refresh()
bumpmapImg.Image = bumpMap
bumpmapImg.Refresh()
canvasImg.Image = finalImage
canvasImg.Refresh()
})
}()
})
// Create UI elements for image dimensions and seed
widthEntry := widget.NewEntry()
widthEntry.SetText(strconv.Itoa(settings.Width))
widthEntry.OnChanged = func(s string) {
val, err := strconv.Atoi(s)
if err != nil {
errorLabel.Text = "Width must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Width = val
}
heightEntry := widget.NewEntry()
heightEntry.SetText(strconv.Itoa(settings.Height))
heightEntry.OnChanged = func(s string) {
val, err := strconv.Atoi(s)
if err != nil {
errorLabel.Text = "Height must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Height = val
}
seedEntry := widget.NewEntry()
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
seedEntry.OnChanged = func(s string) {
val, err := strconv.ParseInt(s, 10, 64)
if err != nil {
errorLabel.Text = "Seed must be a number"
errorLabel.Show()
generateBtn.Disable()
return
}
errorLabel.Hide()
generateBtn.Enable()
settings.Seed = val
}
randomizeBtn := widget.NewButton("Randomize", func() {
settings.Seed = time.Now().UnixNano()
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
})
// Create tabs for organizing settings
terrainTab := container.NewTabItem("Terrain", container.NewVBox(
detailSlider,
roughnessSlider,
widget.NewLabel(""), // Spacer
minTreeSizeSlider,
maxTreeSizeSlider,
treeCoverageSlider,
treeClumpinessSlider,
))
waterTab := container.NewTabItem("Water", container.NewVBox(
lakesSlider,
lakeSizeLowerSlider,
lakeSizeUpperSlider,
lakeEdgeRoughnessSlider,
lakeShapeLabel,
lakeShapeSelect,
widget.NewLabel(""), // Spacer
riversSlider,
minRiverWidthSlider,
maxRiverWidthSlider,
riverCurvynessSlider,
riverWidthVariabilitySlider,
riverEdgeRoughnessSlider,
))
roadsTab := container.NewTabItem("Roads", container.NewVBox(
minRoadWidthSlider,
maxRoadWidthSlider,
roadExitsSlider,
minRoadAngleSlider,
roadCurvynessSlider,
roadDistributionSlider,
))
numBuildingsSlider := newNumericInputSlider(0, 10000, float64(settings.NumBuildings), "%.0f", "Number of Buildings")
numBuildingsSlider.entry.OnChanged = func(s string) {
numBuildingsSlider.validate(s, func(hasError bool) {
errorStates["numBuildings"] = hasError
updateGenerateBtnState()
})
}
numBuildingsSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := numBuildingsSlider.value.Get()
settings.NumBuildings = int(val)
}))
minBuildingSizeSlider := newNumericInputSlider(1, 150, settings.MinBuildingSize, "%.0fpx", "Min Building Size")
minBuildingSizeSlider.entry.OnChanged = func(s string) {
minBuildingSizeSlider.validate(s, func(hasError bool) {
errorStates["minBuildingSize"] = hasError
updateGenerateBtnState()
})
}
minBuildingSizeSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := minBuildingSizeSlider.value.Get()
settings.MinBuildingSize = val
}))
maxBuildingSizeSlider := newNumericInputSlider(1, 150, settings.MaxBuildingSize, "%.0fpx", "Max Building Size")
maxBuildingSizeSlider.entry.OnChanged = func(s string) {
maxBuildingSizeSlider.validate(s, func(hasError bool) {
errorStates["maxBuildingSize"] = hasError
updateGenerateBtnState()
})
}
maxBuildingSizeSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := maxBuildingSizeSlider.value.Get()
settings.MaxBuildingSize = val
}))
buildingDistributionSlider := newNumericInputSlider(0, 100, settings.BuildingDistribution, "%.0f%%", "Building Distribution")
buildingDistributionSlider.entry.OnChanged = func(s string) {
buildingDistributionSlider.validate(s, func(hasError bool) {
errorStates["buildingDistribution"] = hasError
updateGenerateBtnState()
})
}
buildingDistributionSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := buildingDistributionSlider.value.Get()
settings.BuildingDistribution = val
}))
buildingShapeLabel := widget.NewLabel("Building Shape:")
buildingShapeSelect := widget.NewSelect([]string{"squares", "circles", "rectangles", "mixed", "procedural"}, func(s string) {
settings.BuildingShape = s
})
buildingShapeSelect.SetSelected(settings.BuildingShape)
// Create sliders for building shape ratios (visible only when "mixed" is selected)
squareRatioSlider := widget.NewSlider(0, 100)
circleRatioSlider := widget.NewSlider(0, 100)
rectangleRatioSlider := widget.NewSlider(0, 100)
squareRatioLabel := widget.NewLabel(fmt.Sprintf("Squares: %.0f%%", settings.BuildingShapeRatios["squares"]))
circleRatioLabel := widget.NewLabel(fmt.Sprintf("Circles: %.0f%%", settings.BuildingShapeRatios["circles"]))
rectangleRatioLabel := widget.NewLabel(fmt.Sprintf("Rectangles: %.0f%%", settings.BuildingShapeRatios["rectangles"]))
squareRatioSlider.SetValue(settings.BuildingShapeRatios["squares"])
circleRatioSlider.SetValue(settings.BuildingShapeRatios["circles"])
rectangleRatioSlider.SetValue(settings.BuildingShapeRatios["rectangles"])
ratioContainer := container.NewVBox(
squareRatioLabel,
squareRatioSlider,
circleRatioLabel,
circleRatioSlider,
rectangleRatioLabel,
rectangleRatioSlider,
)
minBuildingComplexitySlider := newNumericInputSlider(1, 6, float64(settings.MinBuildingComplexity), "%.0f", "Min Building Complexity")
minBuildingComplexitySlider.entry.OnChanged = func(s string) {
minBuildingComplexitySlider.validate(s, func(hasError bool) {
errorStates["minBuildingComplexity"] = hasError
updateGenerateBtnState()
})
}
minBuildingComplexitySlider.value.AddListener(binding.NewDataListener(func() {
val, _ := minBuildingComplexitySlider.value.Get()
settings.MinBuildingComplexity = int(val)
}))
maxBuildingComplexitySlider := newNumericInputSlider(1, 6, float64(settings.MaxBuildingComplexity), "%.0f", "Max Building Complexity")
maxBuildingComplexitySlider.entry.OnChanged = func(s string) {
maxBuildingComplexitySlider.validate(s, func(hasError bool) {
errorStates["maxBuildingComplexity"] = hasError
updateGenerateBtnState()
})
}
maxBuildingComplexitySlider.value.AddListener(binding.NewDataListener(func() {
val, _ := maxBuildingComplexitySlider.value.Get()
settings.MaxBuildingComplexity = int(val)
}))
buildingComplexityRatioSlider := newNumericInputSlider(0, 100, settings.BuildingComplexityRatio, "%.0f%%", "Building Complexity Ratio")
buildingComplexityRatioSlider.entry.OnChanged = func(s string) {
buildingComplexityRatioSlider.validate(s, func(hasError bool) {
errorStates["buildingComplexityRatio"] = hasError
updateGenerateBtnState()
})
}
buildingComplexityRatioSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := buildingComplexityRatioSlider.value.Get()
settings.BuildingComplexityRatio = val
}))
proceduralContainer := container.NewVBox(
minBuildingComplexitySlider,
maxBuildingComplexitySlider,
buildingComplexityRatioSlider,
)
updateRatioSliders := func() {
squareRatioSlider.SetValue(settings.BuildingShapeRatios["squares"])
circleRatioSlider.SetValue(settings.BuildingShapeRatios["circles"])
rectangleRatioSlider.SetValue(settings.BuildingShapeRatios["rectangles"])
squareRatioLabel.SetText(fmt.Sprintf("Squares: %.0f%%", settings.BuildingShapeRatios["squares"]))
circleRatioLabel.SetText(fmt.Sprintf("Circles: %.0f%%", settings.BuildingShapeRatios["circles"]))
rectangleRatioLabel.SetText(fmt.Sprintf("Rectangles: %.0f%%", settings.BuildingShapeRatios["rectangles"]))
}
squareRatioSlider.OnChanged = func(val float64) {
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
return
}
oldVal := settings.BuildingShapeRatios["squares"]
diff := val - oldVal
settings.BuildingShapeRatios["squares"] = val
settings.BuildingShapeRatios["circles"] -= diff / 2
settings.BuildingShapeRatios["rectangles"] -= diff / 2
// Clamp values to 0-100 range
if settings.BuildingShapeRatios["circles"] < 0 {
settings.BuildingShapeRatios["rectangles"] += settings.BuildingShapeRatios["circles"]
settings.BuildingShapeRatios["circles"] = 0
}
if settings.BuildingShapeRatios["rectangles"] < 0 {
settings.BuildingShapeRatios["circles"] += settings.BuildingShapeRatios["rectangles"]
settings.BuildingShapeRatios["rectangles"] = 0
}
if settings.BuildingShapeRatios["circles"] > 100 {
settings.BuildingShapeRatios["rectangles"] += settings.BuildingShapeRatios["circles"] - 100
settings.BuildingShapeRatios["circles"] = 100
}
if settings.BuildingShapeRatios["rectangles"] > 100 {
settings.BuildingShapeRatios["circles"] += settings.BuildingShapeRatios["rectangles"] - 100
settings.BuildingShapeRatios["rectangles"] = 100
}
updateRatioSliders()
}
circleRatioSlider.OnChanged = func(val float64) {
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
return
}
oldVal := settings.BuildingShapeRatios["circles"]
diff := val - oldVal
settings.BuildingShapeRatios["circles"] = val
settings.BuildingShapeRatios["squares"] -= diff / 2
settings.BuildingShapeRatios["rectangles"] -= diff / 2
// Clamp values to 0-100 range
if settings.BuildingShapeRatios["squares"] < 0 {
settings.BuildingShapeRatios["rectangles"] += settings.BuildingShapeRatios["squares"]
settings.BuildingShapeRatios["squares"] = 0
}
if settings.BuildingShapeRatios["rectangles"] < 0 {
settings.BuildingShapeRatios["squares"] += settings.BuildingShapeRatios["rectangles"]
settings.BuildingShapeRatios["rectangles"] = 0
}
if settings.BuildingShapeRatios["squares"] > 100 {
settings.BuildingShapeRatios["rectangles"] += settings.BuildingShapeRatios["squares"] - 100
settings.BuildingShapeRatios["squares"] = 100
}
if settings.BuildingShapeRatios["rectangles"] > 100 {
settings.BuildingShapeRatios["squares"] += settings.BuildingShapeRatios["rectangles"] - 100
settings.BuildingShapeRatios["rectangles"] = 100
}
updateRatioSliders()
}
rectangleRatioSlider.OnChanged = func(val float64) {
if settings.BuildingShape != "mixed" && settings.BuildingShape != "procedural" {
return
}
oldVal := settings.BuildingShapeRatios["rectangles"]
diff := val - oldVal
settings.BuildingShapeRatios["rectangles"] = val
settings.BuildingShapeRatios["squares"] -= diff / 2
settings.BuildingShapeRatios["circles"] -= diff / 2
// Clamp values to 0-100 range
if settings.BuildingShapeRatios["squares"] < 0 {
settings.BuildingShapeRatios["circles"] += settings.BuildingShapeRatios["squares"]
settings.BuildingShapeRatios["squares"] = 0
}
if settings.BuildingShapeRatios["circles"] < 0 {
settings.BuildingShapeRatios["squares"] += settings.BuildingShapeRatios["circles"]
settings.BuildingShapeRatios["circles"] = 0
}
if settings.BuildingShapeRatios["squares"] > 100 {
settings.BuildingShapeRatios["circles"] += settings.BuildingShapeRatios["squares"] - 100
settings.BuildingShapeRatios["squares"] = 100
}
if settings.BuildingShapeRatios["circles"] > 100 {
settings.BuildingShapeRatios["squares"] += settings.BuildingShapeRatios["circles"] - 100
settings.BuildingShapeRatios["circles"] = 100
}
updateRatioSliders()
}
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" {
proceduralContainer.Hide()
ratioContainer.Show()
} else if settings.BuildingShape == "procedural" {
proceduralContainer.Show()
ratioContainer.Show()
} else {
proceduralContainer.Hide()
ratioContainer.Hide()
}
buildingsTab := container.NewTabItem("Buildings", container.NewVBox(
numBuildingsSlider,
minBuildingSizeSlider,
maxBuildingSizeSlider,
buildingDistributionSlider,
buildingShapeLabel,
buildingShapeSelect,
proceduralContainer,
ratioContainer,
))
imageTab := container.NewTabItem("Image", container.NewVBox(
widget.NewLabel("Width:"),
widthEntry, widget.NewLabel("Height:"),
heightEntry,
widget.NewLabel("Seed:"),
seedEntry,
randomizeBtn,
generateBtn,
errorLabel,
progressContainer,
widget.NewSeparator(),
exportCanvasBtn,
exportHeightmapBtn,
exportBumpmapBtn,
exportMasksBtn,
))
// Create the main layout using a horizontal split
tabs := container.NewAppTabs(
imageTab,
terrainTab,
waterTab,
roadsTab,
buildingsTab,
)
left := container.NewVBox(
widget.NewLabel("RPG City Maker Reborn"),
tabs,
)
right := container.NewMax()
var tappableCanvas, tappableHeightmap, tappableBumpmap *tappableImage
updateRightPanel := func() {
var top, bottom fyne.CanvasObject
switch settings.ImageViewState {
case 1:
top = tappableCanvas
bottom = container.NewGridWithColumns(2, tappableHeightmap, tappableBumpmap)
case 2:
top = tappableHeightmap
bottom = container.NewGridWithColumns(2, tappableCanvas, tappableBumpmap)
case 3:
top = tappableBumpmap
bottom = container.NewGridWithColumns(2, tappableCanvas, tappableHeightmap)
default:
right.Objects = []fyne.CanvasObject{container.NewGridWithRows(3, tappableCanvas, tappableHeightmap, tappableBumpmap)}
right.Refresh()
return
}
split := container.NewVSplit(top, bottom)
split.Offset = 0.8
right.Objects = []fyne.CanvasObject{split}
right.Refresh()
}
tappableCanvas = newTappableImage(container.NewMax(canvasImg), func() {
if settings.ImageViewState == 1 {
settings.ImageViewState = 0
} else {
settings.ImageViewState = 1
}
updateRightPanel()
})
tappableHeightmap = newTappableImage(container.NewMax(heightmapImg), func() {
if settings.ImageViewState == 2 {
settings.ImageViewState = 0
} else {
settings.ImageViewState = 2
}
updateRightPanel()
})
tappableBumpmap = newTappableImage(container.NewMax(bumpmapImg), func() {
if settings.ImageViewState == 3 {
settings.ImageViewState = 0
} else {
settings.ImageViewState = 3
}
updateRightPanel()
})
updateRightPanel()
split := container.NewHSplit(
left,
right,
)
split.SetOffset(0.3)
// Set the window content and start the application
w.SetContent(split)
w.ShowAndRun()
}
// getImageData encodes an image to the specified format and returns the data as a byte buffer.
func getImageData(img image.Image, format string) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
var err error
switch format {
case "PNG":
err = png.Encode(buf, img)
case "JPG":
err = jpeg.Encode(buf, img, nil)
case "WEBP":
err = webp.Encode(buf, img, &webp.Options{Lossless: true})
}
return buf, err
}
// showMasksSaveDialog displays a dialog for saving the generated masks.
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels, roadPixels, bridgePixels, buildingPixels []image.Point, buildings [][]image.Point) {
// Create UI elements for the save dialog
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("masks_folder")
formatSelect := widget.NewSelect([]string{"PNG", "JPG", "WEBP"}, nil)
formatSelect.SetSelected("PNG")
packageSelect := widget.NewSelect([]string{"Folder", "tar.gz", "zip"}, nil)
packageSelect.SetSelected("Folder")
pathLabel := widget.NewLabel(settings.LastExportPath)
browseBtn := widget.NewButton("Browse", func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
log.Println("Error opening folder dialog:", err)
return
}
if uri == nil {
return
}
settings.LastExportPath = uri.Path()
pathLabel.SetText(settings.LastExportPath)
}, win)
})
content := container.NewVBox(
widget.NewLabel("Save to:"),
container.NewBorder(nil, nil, nil, browseBtn, pathLabel),
widget.NewLabel("Folder Name:"),
fileNameEntry,
widget.NewLabel("Format:"),
formatSelect,
widget.NewLabel("Packaging:"),
packageSelect,
)
saveDialog := dialog.NewCustom("Export Masks", "Cancel", content, win)
saveBtn := widget.NewButton("Save", func() {
folderName := fileNameEntry.Text
if folderName == "" {
return
}
imgFormat := strings.ToLower(formatSelect.Selected)
bounds := canvasImg.Bounds()
// Create mask images from the generated data
lakeMask := image.NewGray(bounds)
for _, lake := range lakes {
for _, p := range lake {
lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
}
riverMask := image.NewGray(bounds)
for _, p := range riverPixels {
riverMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
treeMask := image.NewGray(bounds)
for _, p := range treePixels {
treeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
roadMask := image.NewGray(bounds)
for _, p := range roadPixels {
roadMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
bridgeMask := image.NewGray(bounds)
for _, p := range bridgePixels {
bridgeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
buildingMask := image.NewGray(bounds)
for _, building := range buildings {
for _, p := range building {
buildingMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
}
imagesToSave := map[string]image.Image{
"canvas." + imgFormat: canvasImg,
"heightmap." + imgFormat: heightmapImg,
"bump_map." + imgFormat: bumpmapImg,
"lakes_mask." + imgFormat: lakeMask,
"rivers_mask." + imgFormat: riverMask,
"trees_mask." + imgFormat: treeMask,
"roads_mask." + imgFormat: roadMask,
"bridges_mask." + imgFormat: bridgeMask,
"buildings_mask." + imgFormat: buildingMask,
}
// Save the images based on the selected packaging option
switch packageSelect.Selected {
case "Folder":
exportPath := filepath.Join(pathLabel.Text, folderName)
if err := os.MkdirAll(exportPath, 0755); err != nil {
log.Println("Error creating directory:", err)
return
}
settings.LastExportPath = exportPath
for name, img := range imagesToSave {
saveImage(img, filepath.Join(exportPath, name))
}
case "tar.gz":
filePath := filepath.Join(pathLabel.Text, folderName+".tar.gz")
settings.LastExportPath = filepath.Dir(filePath)
file, err := os.Create(filePath)
if err != nil {
log.Println("Error creating archive:", err)
return
}
defer file.Close()
gw := gzip.NewWriter(file)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
for name, img := range imagesToSave {
buf, err := getImageData(img, formatSelect.Selected)
if err != nil {
log.Printf("Error encoding image %s: %v\n", name, err)
continue
}
hdr := &tar.Header{
Name: name,
Mode: 0644,
Size: int64(buf.Len()),
}
if err := tw.WriteHeader(hdr); err != nil {
log.Printf("Error writing tar header for %s: %v\n", name, err)
continue
}
if _, err := tw.Write(buf.Bytes()); err != nil {
log.Printf("Error writing tar data for %s: %v\n", name, err)
}
}
case "zip":
filePath := filepath.Join(pathLabel.Text, folderName+".zip")
settings.LastExportPath = filepath.Dir(filePath)
file, err := os.Create(filePath)
if err != nil {
log.Println("Error creating archive:", err)
return
}
defer file.Close()
zw := zip.NewWriter(file)
defer zw.Close()
for name, img := range imagesToSave {
buf, err := getImageData(img, formatSelect.Selected)
if err != nil {
log.Printf("Error encoding image %s: %v\n", name, err)
continue
}
f, err := zw.Create(name)
if err != nil {
log.Printf("Error creating zip entry for %s: %v\n", name, err)
continue
}
if _, err := f.Write(buf.Bytes()); err != nil {
log.Printf("Error writing zip data for %s: %v\n", name, err)
}
}
}
saveDialog.Hide()
})
saveBtn.Disable()
fileNameEntry.OnChanged = func(text string) {
if text == "" {
saveBtn.Disable()
} else {
saveBtn.Enable()
}
}
saveDialog.SetButtons([]fyne.CanvasObject{
widget.NewButton("Cancel", func() {
saveDialog.Hide()
}),
saveBtn,
})
saveDialog.Show()
}
// saveImage saves an image to the specified path.
func saveImage(img image.Image, path string) {
file, err := os.Create(path)
if err != nil {
log.Println("Error creating file:", err)
return
}
defer file.Close()
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".png":
err = png.Encode(file, img)
case ".jpg":
err = jpeg.Encode(file, img, nil)
case ".webp":
err = webp.Encode(file, img, &webp.Options{Lossless: true})
}
if err != nil {
log.Println("Failed to encode image:", err)
}
}
// showSaveDialog displays a dialog for saving an image.
func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) {
// Create UI elements for the save dialog
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("image")
formatSelect := widget.NewSelect([]string{"PNG", "JPG", "WEBP"}, nil)
formatSelect.SetSelected("PNG")
pathLabel := widget.NewLabel(settings.LastExportPath)
browseBtn := widget.NewButton("Browse", func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
log.Println("Error opening folder dialog:", err)
return
}
if uri == nil {
return
}
settings.LastExportPath = uri.Path()
pathLabel.SetText(settings.LastExportPath)
}, win)
})
content := container.NewVBox(
widget.NewLabel("Save to:"),
container.NewBorder(nil, nil, nil, browseBtn, pathLabel),
widget.NewLabel("Filename:"),
fileNameEntry,
widget.NewLabel("Format:"),
formatSelect,
)
saveDialog := dialog.NewCustom("Export Image", "Cancel", content, win)
saveBtn := widget.NewButton("Save", func() {
fileName := fileNameEntry.Text
if fileName == "" {
return
}
filePath := filepath.Join(pathLabel.Text, fileName+"."+strings.ToLower(formatSelect.Selected))
settings.LastExportPath = filepath.Dir(filePath)
saveImage(img, filePath)
saveDialog.Hide()
})
saveBtn.Disable()
fileNameEntry.OnChanged = func(text string) {
if text == "" {
saveBtn.Disable()
} else {
saveBtn.Enable()
}
}
saveDialog.SetButtons([]fyne.CanvasObject{
widget.NewButton("Cancel", func() {
saveDialog.Hide()
}),
saveBtn,
})
saveDialog.Show()
}