Files
RPG_City_Maker_Reborn/main.go
T
2026-03-11 13:45:38 -05:00

2138 lines
68 KiB
Go

package main
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"embed"
"encoding/json"
"fmt"
"image"
"image/color"
"image/draw"
"image/jpeg"
"image/png"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"sort"
"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"
)
const generationStepTimeout = time.Minute
//go:embed Presets/*.json
var embeddedPresets embed.FS
func runWithTimeout[T any](timeout time.Duration, fn func() T) (T, bool) {
ch := make(chan T, 1)
go func() {
ch <- fn()
}()
select {
case out := <-ch:
return out, true
case <-time.After(timeout):
var zero T
return zero, false
}
}
func cloneToRGBA(src image.Image, width, height int) *image.RGBA {
if src == nil {
return image.NewRGBA(image.Rect(0, 0, width, height))
}
if rgba, ok := src.(*image.RGBA); ok {
out := image.NewRGBA(rgba.Bounds())
copy(out.Pix, rgba.Pix)
return out
}
out := image.NewRGBA(src.Bounds())
draw.Draw(out, out.Bounds(), src, image.Point{}, draw.Src)
return out
}
type presetEntry struct {
Name string
Path string
}
func listEmbeddedPresets() ([]presetEntry, error) {
entries, err := fs.ReadDir(embeddedPresets, "Presets")
if err != nil {
return nil, err
}
var presets []presetEntry
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !strings.HasSuffix(strings.ToLower(name), ".json") {
continue
}
displayName := strings.TrimSuffix(name, filepath.Ext(name))
presets = append(presets, presetEntry{
Name: displayName,
Path: filepath.Join("Presets", name),
})
}
sort.Slice(presets, func(i, j int) bool {
return presets[i].Name < presets[j].Name
})
return presets, nil
}
func loadEmbeddedPreset(path string) (*Settings, error) {
raw, err := embeddedPresets.ReadFile(path)
if err != nil {
return nil, err
}
return decodeSettingsJSON(raw)
}
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 buildings [][]image.Point
var waterMask *PixelMask
var riverMask *PixelMask
var treeMask *PixelMask
var buildingMask *PixelMask
var roadMask *PixelMask
var bridgeMask *PixelMask
var exitRoadMask *PixelMask
var wallMask *PixelMask
var turretMask *PixelMask
// 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())
prevBuildings := settings.NumBuildings
cappedBuildings := capRequestedBuildingsToFit(settings, settings.Width, settings.Height)
if cappedBuildings < prevBuildings {
log.Printf("Building count capped from %d to %d based on image size and average building size.\n", prevBuildings, cappedBuildings)
}
// Step 2: Generating Lakes (timeout: 1 minute)
var lakeImage image.Image = image.NewRGBA(image.Rect(0, 0, settings.Width, settings.Height))
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
img image.Image
lks [][]image.Point
} {
img, lks := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, seedProvider.Next(), settings.LakeEdgeRoughness, settings.LakeShape)
return struct {
img image.Image
lks [][]image.Point
}{img: img, lks: lks}
}); ok {
lakeImage = out.img
lakes = out.lks
} else {
log.Println("GenerateLakes timed out after 1 minute; continuing.")
lakes = nil
}
// Step 3: Generating Rivers (timeout: 1 minute)
riverBase := cloneToRGBA(lakeImage, settings.Width, settings.Height)
var finalImage *image.RGBA = riverBase
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
img image.Image
rmsk *PixelMask
} {
img, rmsk := GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, riverBase, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness)
return struct {
img image.Image
rmsk *PixelMask
}{img: img, rmsk: rmsk}
}); ok {
riverMask = out.rmsk
finalImage = cloneToRGBA(out.img, settings.Width, settings.Height)
} else {
log.Println("GenerateRivers timed out after 1 minute; continuing.")
riverMask = NewPixelMask(settings.Width, settings.Height)
}
waterMask = BuildMaskFromLakes(settings.Width, settings.Height, lakes)
if riverMask != nil {
waterMask.Merge(riverMask)
}
// Step 4: Preparing Road Nodes
var roadNodes []*PointOfInterest
var roadTarget int
var edgeToEdgeOnly bool
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
pois []*PointOfInterest
target int
edgeToEdge bool
} {
pois, target, edgeToEdge := PrepareRoadNodes(settings.Width, settings.Height, settings, waterMask, seedProvider.Next())
return struct {
pois []*PointOfInterest
target int
edgeToEdge bool
}{pois: pois, target: target, edgeToEdge: edgeToEdge}
}); ok {
roadNodes, roadTarget, edgeToEdgeOnly = out.pois, out.target, out.edgeToEdge
} else {
log.Println("PrepareRoadNodes timed out after 1 minute; continuing.")
roadNodes = nil
roadTarget = 0
edgeToEdgeOnly = false
}
// Step 5: Generating Fortifications
var wallLayout *FortificationLayout
fortBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
layout *FortificationLayout
} {
layout, _ := GenerateFortifications(fortBase, settings.Width, settings.Height, settings, waterMask, roadNodes, seedProvider.Next())
return struct {
layout *FortificationLayout
}{layout: layout}
}); ok {
wallLayout = out.layout
if wallLayout != nil {
wallMask = wallLayout.Mask
} else {
wallMask = NewPixelMask(settings.Width, settings.Height)
}
finalImage = fortBase
} else {
log.Println("GenerateFortifications timed out after 1 minute; continuing.")
wallLayout = &FortificationLayout{Mask: NewPixelMask(settings.Width, settings.Height)}
wallMask = wallLayout.Mask
}
// Step 6: Generating Roads
var roadAnchors []image.Point
var roadList []*Road
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
rd *PixelMask
br *PixelMask
ex *PixelMask
anc []image.Point
rl []*Road
} {
rd, br, ex, anc, rl := GenerateRoadsWithPOIs(roadBase, settings.Width, settings.Height, settings, waterMask, wallLayout, roadNodes, roadTarget, edgeToEdgeOnly, seedProvider.Next())
return struct {
rd *PixelMask
br *PixelMask
ex *PixelMask
anc []image.Point
rl []*Road
}{rd: rd, br: br, ex: ex, anc: anc, rl: rl}
}); ok {
roadMask, bridgeMask, exitRoadMask, roadAnchors, roadList = out.rd, out.br, out.ex, out.anc, out.rl
drawWallMask(roadBase, wallMask)
turretMask = GenerateTurrets(roadBase, settings.Width, settings.Height, settings, wallLayout, waterMask, roadMask, roadList)
finalImage = roadBase
} else {
log.Println("GenerateRoads timed out after 1 minute; continuing.")
roadMask = NewPixelMask(settings.Width, settings.Height)
bridgeMask = NewPixelMask(settings.Width, settings.Height)
exitRoadMask = NewPixelMask(settings.Width, settings.Height)
turretMask = NewPixelMask(settings.Width, settings.Height)
roadAnchors = nil
}
placementMask := cloneMask(roadMask)
if placementMask == nil {
placementMask = NewPixelMask(settings.Width, settings.Height)
}
if wallMask != nil {
placementMask.Merge(wallMask)
}
if turretMask != nil {
placementMask.Merge(turretMask)
}
// Step 7: Generating Buildings
buildingBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
blds [][]image.Point
bmsk *PixelMask
} {
blds, bmsk := GenerateBuildings(buildingBase, settings.Width, settings.Height, settings, roadAnchors, waterMask, placementMask, exitRoadMask, seedProvider.Next())
return struct {
blds [][]image.Point
bmsk *PixelMask
}{blds: blds, bmsk: bmsk}
}); ok {
buildings, buildingMask = out.blds, out.bmsk
finalImage = buildingBase
} else {
log.Println("GenerateBuildings timed out after 1 minute; continuing.")
buildings = nil
buildingMask = NewPixelMask(settings.Width, settings.Height)
}
// Step 8: Generating Trees
treeBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
if out, ok := runWithTimeout(generationStepTimeout, func() *PixelMask {
return GenerateTrees(treeBase, waterMask, placementMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
}); ok {
treeMask = out
finalImage = treeBase
} else {
log.Println("GenerateTrees timed out after 1 minute; continuing.")
treeMask = NewPixelMask(settings.Width, settings.Height)
}
// Step 7: Darkening Water Areas
darkenedHeightmap := DarkenLakeAreas(noiseImg, waterMask)
// Step 8: Flattening Building Areas
flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height)
// Step 9: Flattening Road and Wall Areas
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, placementMask)
// 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 := newNumericInputSliderWithStep(minTreeSizePercent, maxTreeSizePercent, settings.MinTreeSize, treeSizePercentStep, "%.1f%%", "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 := newNumericInputSliderWithStep(minTreeSizePercent, maxTreeSizePercent, settings.MaxTreeSize, treeSizePercentStep, "%.1f%%", "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 := newNumericInputSliderWithStep(minRoadWidthPercent, maxRoadWidthPercent, settings.MinRoadWidth, roadWidthPercentStep, "%.1f%%", "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 := newNumericInputSliderWithStep(minRoadWidthPercent, maxRoadWidthPercent, settings.MaxRoadWidth, roadWidthPercentStep, "%.1f%%", "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
}))
buildingsPerRoadSlider := newNumericInputSlider(1, 20, float64(settings.BuildingsPerRoad), "%.0f", "Buildings Per Road")
buildingsPerRoadSlider.entry.OnChanged = func(s string) {
buildingsPerRoadSlider.validate(s, func(hasError bool) {
errorStates["buildingsPerRoad"] = hasError
updateGenerateBtnState()
})
}
buildingsPerRoadSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := buildingsPerRoadSlider.value.Get()
settings.BuildingsPerRoad = int(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
}))
minWallWidthSlider := newNumericInputSliderWithStep(minWallWidthPercent, maxWallWidthPercent, settings.MinWallWidth, wallWidthPercentStep, "%.1f%%", "Min Wall Width")
minWallWidthSlider.entry.OnChanged = func(s string) {
minWallWidthSlider.validate(s, func(hasError bool) {
errorStates["minWallWidth"] = hasError
updateGenerateBtnState()
})
}
minWallWidthSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := minWallWidthSlider.value.Get()
settings.MinWallWidth = val
}))
maxWallWidthSlider := newNumericInputSliderWithStep(minWallWidthPercent, maxWallWidthPercent, settings.MaxWallWidth, wallWidthPercentStep, "%.1f%%", "Max Wall Width")
maxWallWidthSlider.entry.OnChanged = func(s string) {
maxWallWidthSlider.validate(s, func(hasError bool) {
errorStates["maxWallWidth"] = hasError
updateGenerateBtnState()
})
}
maxWallWidthSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := maxWallWidthSlider.value.Get()
settings.MaxWallWidth = val
}))
numWallsSlider := newNumericInputSlider(0, 5, float64(settings.NumWalls), "%.0f", "Number of Walls")
numWallsSlider.entry.OnChanged = func(s string) {
numWallsSlider.validate(s, func(hasError bool) {
errorStates["numWalls"] = hasError
updateGenerateBtnState()
})
}
numWallsSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := numWallsSlider.value.Get()
settings.NumWalls = int(val)
}))
cityCoverageSlider := newNumericInputSlider(1, 100, settings.CityCoverage, "%.0f%%", "City Coverage")
cityCoverageSlider.entry.OnChanged = func(s string) {
cityCoverageSlider.validate(s, func(hasError bool) {
errorStates["cityCoverage"] = hasError
updateGenerateBtnState()
})
}
cityCoverageSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := cityCoverageSlider.value.Get()
settings.CityCoverage = val
}))
wallCurvynessSlider := newNumericInputSlider(0, 100, settings.WallCurvyness, "%.0f%%", "Wall Curvyness")
wallCurvynessSlider.entry.OnChanged = func(s string) {
wallCurvynessSlider.validate(s, func(hasError bool) {
errorStates["wallCurvyness"] = hasError
updateGenerateBtnState()
})
}
wallCurvynessSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := wallCurvynessSlider.value.Get()
settings.WallCurvyness = val
}))
showTurretsCheck := widget.NewCheck("Show Turrets", func(v bool) {
settings.ShowTurrets = v
})
showTurretsCheck.SetChecked(settings.ShowTurrets)
turretSizeSlider := newNumericInputSliderWithStep(minTurretSizePercent, maxTurretSizePercent, settings.TurretSize, turretSizePercentStep, "%.1f%%", "Turret Size")
turretSizeSlider.entry.OnChanged = func(s string) {
turretSizeSlider.validate(s, func(hasError bool) {
errorStates["turretSize"] = hasError
updateGenerateBtnState()
})
}
turretSizeSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := turretSizeSlider.value.Get()
settings.TurretSize = val
}))
turretShapeLabel := widget.NewLabel("Turret Shape:")
turretShapeSelect := widget.NewSelect([]string{"circular", "square"}, func(s string) {
settings.TurretShape = s
})
turretShapeSelect.SetSelected(settings.TurretShape)
turretSpacingSlider := newNumericInputSlider(0, 100, settings.TurretSpacing, "%.0f%%", "Turret Spacing")
turretSpacingSlider.entry.OnChanged = func(s string) {
turretSpacingSlider.validate(s, func(hasError bool) {
errorStates["turretSpacing"] = hasError
updateGenerateBtnState()
})
}
turretSpacingSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := turretSpacingSlider.value.Get()
settings.TurretSpacing = val
}))
gateSpacingSlider := newNumericInputSlider(0, 100, settings.GateSpacing, "%.0f%%", "Gate Spacing")
gateSpacingSlider.entry.OnChanged = func(s string) {
gateSpacingSlider.validate(s, func(hasError bool) {
errorStates["gateSpacing"] = hasError
updateGenerateBtnState()
})
}
gateSpacingSlider.value.AddListener(binding.NewDataListener(func() {
val, _ := gateSpacingSlider.value.Get()
settings.GateSpacing = val
}))
turretControls := container.NewVBox(
turretSizeSlider,
turretShapeLabel,
turretShapeSelect,
turretSpacingSlider,
)
if !settings.ShowTurrets {
turretControls.Hide()
}
showTurretsCheck.OnChanged = func(v bool) {
settings.ShowTurrets = v
if v {
turretControls.Show()
} else {
turretControls.Hide()
}
}
// Create UI elements for error display and action buttons
var applySettingsToUI func(*Settings)
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, riverMask, treeMask, roadMask, bridgeMask, wallMask, turretMask, buildingMask)
})
exportSettingsBtn := widget.NewButton("Export Settings", func() {
showSettingsExportDialog(w, settings)
})
importSettingsBtn := widget.NewButton("Import Settings", func() {
showSettingsImportDialog(w, settings, applySettingsToUI)
})
// Main generation button and logic
generateBtn = widget.NewButton("Generate", func() {
go func() {
var timedOutSteps []string
addTimeout := func(step string) {
timedOutSteps = append(timedOutSteps, step)
}
// 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)
prevBuildings := settings.NumBuildings
cappedBuildings := capRequestedBuildingsToFit(settings, settings.Width, settings.Height)
if cappedBuildings < prevBuildings {
log.Printf("Building count capped from %d to %d based on image size and average building size.\n", prevBuildings, cappedBuildings)
}
// Step 1: Generating Heightmap
fyne.Do(func() {
progressLabel.SetText("Step 1 of 13: Generating Heightmap")
progressBar.SetValue(1.0 / 13.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 13: Generating Lakes")
progressBar.SetValue(2.0 / 13.0)
})
var lakeImage image.Image = image.NewRGBA(image.Rect(0, 0, settings.Width, settings.Height))
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
img image.Image
lks [][]image.Point
} {
img, lks := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, seedProvider.Next(), settings.LakeEdgeRoughness, settings.LakeShape)
return struct {
img image.Image
lks [][]image.Point
}{img: img, lks: lks}
}); ok {
lakeImage = out.img
lakes = out.lks
} else {
log.Println("GenerateLakes timed out after 1 minute; continuing.")
addTimeout("Lakes")
lakes = nil
}
// Step 3: Generating Rivers
fyne.Do(func() {
progressLabel.SetText("Step 3 of 13: Generating Rivers")
progressBar.SetValue(3.0 / 13.0)
})
riverBase := cloneToRGBA(lakeImage, settings.Width, settings.Height)
finalImage := riverBase
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
img image.Image
rmsk *PixelMask
} {
img, rmsk := GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, riverBase, lakes, seedProvider.Next(), noiseImg, settings.RiverWidthVariability, settings.RiverEdgeRoughness)
return struct {
img image.Image
rmsk *PixelMask
}{img: img, rmsk: rmsk}
}); ok {
riverMask = out.rmsk
finalImage = cloneToRGBA(out.img, settings.Width, settings.Height)
} else {
log.Println("GenerateRivers timed out after 1 minute; continuing.")
addTimeout("Rivers")
riverMask = NewPixelMask(settings.Width, settings.Height)
}
waterMask = BuildMaskFromLakes(settings.Width, settings.Height, lakes)
if riverMask != nil {
waterMask.Merge(riverMask)
}
// Step 4: Preparing Road Nodes
fyne.Do(func() {
progressLabel.SetText("Step 4 of 13: Preparing Road Nodes")
progressBar.SetValue(4.0 / 13.0)
})
var roadNodes []*PointOfInterest
var roadTarget int
var edgeToEdgeOnly bool
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
pois []*PointOfInterest
target int
edgeToEdge bool
} {
pois, target, edgeToEdge := PrepareRoadNodes(settings.Width, settings.Height, settings, waterMask, seedProvider.Next())
return struct {
pois []*PointOfInterest
target int
edgeToEdge bool
}{pois: pois, target: target, edgeToEdge: edgeToEdge}
}); ok {
roadNodes, roadTarget, edgeToEdgeOnly = out.pois, out.target, out.edgeToEdge
} else {
log.Println("PrepareRoadNodes timed out after 1 minute; continuing.")
addTimeout("Road Nodes")
roadNodes = nil
roadTarget = 0
edgeToEdgeOnly = false
}
// Step 5: Generating Fortifications
fyne.Do(func() {
progressLabel.SetText("Step 5 of 13: Generating Fortifications")
progressBar.SetValue(5.0 / 13.0)
})
var wallLayout *FortificationLayout
fortBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
layout *FortificationLayout
} {
layout, _ := GenerateFortifications(fortBase, settings.Width, settings.Height, settings, waterMask, roadNodes, seedProvider.Next())
return struct {
layout *FortificationLayout
}{layout: layout}
}); ok {
wallLayout = out.layout
if wallLayout != nil {
wallMask = wallLayout.Mask
} else {
wallMask = NewPixelMask(settings.Width, settings.Height)
}
finalImage = fortBase
} else {
log.Println("GenerateFortifications timed out after 1 minute; continuing.")
addTimeout("Fortifications")
wallLayout = &FortificationLayout{Mask: NewPixelMask(settings.Width, settings.Height)}
wallMask = wallLayout.Mask
}
// Step 6: Generating Roads
fyne.Do(func() {
progressLabel.SetText("Step 6 of 13: Generating Roads")
progressBar.SetValue(6.0 / 13.0)
})
var roadAnchors []image.Point
var roadList []*Road
roadBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
rd *PixelMask
br *PixelMask
ex *PixelMask
anc []image.Point
rl []*Road
} {
rd, br, ex, anc, rl := GenerateRoadsWithPOIs(roadBase, settings.Width, settings.Height, settings, waterMask, wallLayout, roadNodes, roadTarget, edgeToEdgeOnly, seedProvider.Next())
return struct {
rd *PixelMask
br *PixelMask
ex *PixelMask
anc []image.Point
rl []*Road
}{rd: rd, br: br, ex: ex, anc: anc, rl: rl}
}); ok {
roadMask, bridgeMask, exitRoadMask, roadAnchors, roadList = out.rd, out.br, out.ex, out.anc, out.rl
drawWallMask(roadBase, wallMask)
turretMask = GenerateTurrets(roadBase, settings.Width, settings.Height, settings, wallLayout, waterMask, roadMask, roadList)
finalImage = roadBase
} else {
log.Println("GenerateRoads timed out after 1 minute; continuing.")
addTimeout("Roads")
roadMask = NewPixelMask(settings.Width, settings.Height)
bridgeMask = NewPixelMask(settings.Width, settings.Height)
exitRoadMask = NewPixelMask(settings.Width, settings.Height)
turretMask = NewPixelMask(settings.Width, settings.Height)
roadAnchors = nil
}
placementMask := cloneMask(roadMask)
if placementMask == nil {
placementMask = NewPixelMask(settings.Width, settings.Height)
}
if wallMask != nil {
placementMask.Merge(wallMask)
}
if turretMask != nil {
placementMask.Merge(turretMask)
}
// Step 7: Generating Buildings
fyne.Do(func() {
progressLabel.SetText("Step 7 of 13: Generating Buildings")
progressBar.SetValue(7.0 / 13.0)
})
buildingBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
if out, ok := runWithTimeout(generationStepTimeout, func() struct {
blds [][]image.Point
bmsk *PixelMask
} {
blds, bmsk := GenerateBuildings(buildingBase, settings.Width, settings.Height, settings, roadAnchors, waterMask, placementMask, exitRoadMask, seedProvider.Next())
return struct {
blds [][]image.Point
bmsk *PixelMask
}{blds: blds, bmsk: bmsk}
}); ok {
buildings, buildingMask = out.blds, out.bmsk
finalImage = buildingBase
} else {
log.Println("GenerateBuildings timed out after 1 minute; continuing.")
addTimeout("Buildings")
buildings = nil
buildingMask = NewPixelMask(settings.Width, settings.Height)
}
// Step 8: Darkening Water Areas
fyne.Do(func() {
progressLabel.SetText("Step 8 of 13: Darkening Water Areas")
progressBar.SetValue(8.0 / 13.0)
})
darkenedHeightmap := DarkenLakeAreas(noiseImg, waterMask)
// Step 9: Flattening Building Areas
fyne.Do(func() {
progressLabel.SetText("Step 9 of 13: Flattening Building Areas")
progressBar.SetValue(9.0 / 13.0)
})
flattenedBuildingHeightmap := FlattenBuildingAreas(darkenedHeightmap.(*image.RGBA), buildings, settings.Width, settings.Height)
fyne.Do(func() {
progressLabel.SetText("Step 10 of 13: Flattening Road and Wall Areas")
progressBar.SetValue(10.0 / 13.0)
})
flattenedHeightmap := FlattenRoadAreas(flattenedBuildingHeightmap, placementMask)
// Step 11: Applying Roughness
fyne.Do(func() {
progressLabel.SetText("Step 11 of 13: Applying Roughness")
progressBar.SetValue(11.0 / 13.0)
})
compositeImg := ApplyRoughness(flattenedHeightmap, settings.Roughness)
// Step 12: Generating Trees
fyne.Do(func() {
progressLabel.SetText("Step 12 of 13: Generating Trees")
progressBar.SetValue(12.0 / 13.0)
})
treeBase := cloneToRGBA(finalImage, settings.Width, settings.Height)
if out, ok := runWithTimeout(generationStepTimeout, func() *PixelMask {
return GenerateTrees(treeBase, waterMask, placementMask, buildingMask, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, seedProvider.Next())
}); ok {
treeMask = out
finalImage = treeBase
} else {
log.Println("GenerateTrees timed out after 1 minute; continuing.")
addTimeout("Trees")
treeMask = NewPixelMask(settings.Width, settings.Height)
}
// Step 13: Generating Bump Map
fyne.Do(func() {
progressLabel.SetText("Step 13 of 13: 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()
if len(timedOutSteps) > 0 {
errorLabel.SetText("Generation timed out after 1 minute for: " + strings.Join(timedOutSteps, ", ") + ". Partial results were used.")
errorLabel.Show()
} else {
errorLabel.Hide()
}
})
}()
})
// 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,
buildingsPerRoadSlider,
roadExitsSlider,
minRoadAngleSlider,
roadCurvynessSlider,
roadDistributionSlider,
))
fortificationsTab := container.NewTabItem("Fortifications", container.NewVBox(
minWallWidthSlider,
maxWallWidthSlider,
numWallsSlider,
cityCoverageSlider,
wallCurvynessSlider,
gateSpacingSlider,
showTurretsCheck,
turretControls,
))
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 := newNumericInputSliderWithStep(minBuildingSizePercent, maxBuildingSizePercent, settings.MinBuildingSize, buildingSizePercentStep, "%.1f%%", "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 := newNumericInputSliderWithStep(minBuildingSizePercent, maxBuildingSizePercent, settings.MaxBuildingSize, buildingSizePercentStep, "%.1f%%", "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()
}
applySettingsToUI = func(imported *Settings) {
lastExportPath := settings.LastExportPath
imageViewState := settings.ImageViewState
*settings = *imported
settings.LastExportPath = lastExportPath
settings.ImageViewState = imageViewState
if settings.BuildingShapeRatios == nil {
settings.BuildingShapeRatios = map[string]float64{
"squares": 40,
"circles": 30,
"rectangles": 30,
}
}
widthEntry.SetText(strconv.Itoa(settings.Width))
heightEntry.SetText(strconv.Itoa(settings.Height))
seedEntry.SetText(strconv.FormatInt(settings.Seed, 10))
detailSlider.value.Set(settings.Detail)
roughnessSlider.value.Set(settings.Roughness)
lakesSlider.value.Set(float64(settings.Lakes))
lakeSizeLowerSlider.value.Set(settings.LakeSizeLower)
lakeSizeUpperSlider.value.Set(settings.LakeSizeUpper)
lakeEdgeRoughnessSlider.value.Set(settings.LakeEdgeRoughness)
lakeShapeSelect.SetSelected(settings.LakeShape)
riversSlider.value.Set(float64(settings.Rivers))
minRiverWidthSlider.value.Set(settings.MinRiverWidth)
maxRiverWidthSlider.value.Set(settings.MaxRiverWidth)
riverCurvynessSlider.value.Set(settings.RiverCurvyness)
riverWidthVariabilitySlider.value.Set(settings.RiverWidthVariability)
riverEdgeRoughnessSlider.value.Set(settings.RiverEdgeRoughness)
minTreeSizeSlider.value.Set(settings.MinTreeSize)
maxTreeSizeSlider.value.Set(settings.MaxTreeSize)
treeCoverageSlider.value.Set(settings.TreeCoverage)
treeClumpinessSlider.value.Set(settings.TreeClumpiness)
minRoadWidthSlider.value.Set(settings.MinRoadWidth)
maxRoadWidthSlider.value.Set(settings.MaxRoadWidth)
buildingsPerRoadSlider.value.Set(float64(settings.BuildingsPerRoad))
roadExitsSlider.value.Set(float64(settings.RoadExits))
roadCurvynessSlider.value.Set(settings.RoadCurvyness)
roadDistributionSlider.value.Set(settings.RoadDistribution)
minRoadAngleSlider.value.Set(settings.MinRoadAngle)
minWallWidthSlider.value.Set(settings.MinWallWidth)
maxWallWidthSlider.value.Set(settings.MaxWallWidth)
numWallsSlider.value.Set(float64(settings.NumWalls))
cityCoverageSlider.value.Set(settings.CityCoverage)
wallCurvynessSlider.value.Set(settings.WallCurvyness)
gateSpacingSlider.value.Set(settings.GateSpacing)
showTurretsCheck.SetChecked(settings.ShowTurrets)
turretSizeSlider.value.Set(settings.TurretSize)
turretShapeSelect.SetSelected(settings.TurretShape)
turretSpacingSlider.value.Set(settings.TurretSpacing)
numBuildingsSlider.value.Set(float64(settings.NumBuildings))
minBuildingSizeSlider.value.Set(settings.MinBuildingSize)
maxBuildingSizeSlider.value.Set(settings.MaxBuildingSize)
buildingDistributionSlider.value.Set(settings.BuildingDistribution)
buildingShapeSelect.SetSelected(settings.BuildingShape)
updateRatioSliders()
minBuildingComplexitySlider.value.Set(float64(settings.MinBuildingComplexity))
maxBuildingComplexitySlider.value.Set(float64(settings.MaxBuildingComplexity))
buildingComplexityRatioSlider.value.Set(settings.BuildingComplexityRatio)
for key := range errorStates {
errorStates[key] = false
}
errorLabel.Hide()
updateGenerateBtnState()
}
presetEntries, err := listEmbeddedPresets()
if err != nil {
log.Println("Error loading embedded presets:", err)
}
presetNames := []string{"No Preset"}
presetPathByName := map[string]string{}
for _, preset := range presetEntries {
presetNames = append(presetNames, preset.Name)
presetPathByName[preset.Name] = preset.Path
}
presetSelect := widget.NewSelect(presetNames, func(name string) {
if name == "" || name == "No Preset" {
return
}
path, ok := presetPathByName[name]
if !ok {
return
}
imported, err := loadEmbeddedPreset(path)
if err != nil {
dialog.ShowError(err, w)
return
}
imported.Seed = settings.Seed
applySettingsToUI(imported)
})
presetSelect.SetSelected("No Preset")
buildingsTab := container.NewTabItem("Buildings", container.NewVBox(
numBuildingsSlider,
minBuildingSizeSlider,
maxBuildingSizeSlider,
buildingDistributionSlider,
buildingShapeLabel,
buildingShapeSelect,
proceduralContainer,
ratioContainer,
))
imageTab := container.NewTabItem("Image", container.NewVBox(
widget.NewLabel("Preset:"),
presetSelect,
widget.NewSeparator(),
widget.NewLabel("Width:"),
widthEntry, widget.NewLabel("Height:"),
heightEntry,
widget.NewLabel("Seed:"),
seedEntry,
randomizeBtn,
generateBtn,
errorLabel,
progressContainer,
widget.NewSeparator(),
exportCanvasBtn,
exportHeightmapBtn,
exportBumpmapBtn,
exportMasksBtn,
exportSettingsBtn,
importSettingsBtn,
))
// Create the main layout using a horizontal split
tabs := container.NewAppTabs(
imageTab,
terrainTab,
waterTab,
roadsTab,
fortificationsTab,
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()
}
func encodeImageToWriter(w io.Writer, img image.Image, format string) error {
switch format {
case "PNG":
return png.Encode(w, img)
case "JPG":
return jpeg.Encode(w, img, nil)
case "WEBP":
return webp.Encode(w, img, &webp.Options{Lossless: true})
}
return fmt.Errorf("unsupported format: %s", format)
}
// showMasksSaveDialog displays a dialog for saving the generated masks.
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverMask, treeMask, roadMask, bridgeMask, wallMask, turretMask, buildingMask *PixelMask) {
// 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()
maskToGray := func(mask *PixelMask) image.Image {
out := image.NewGray(bounds)
if mask == nil {
return out
}
for y := 0; y < mask.Height; y++ {
row := y * mask.Width
for x := 0; x < mask.Width; x++ {
if mask.Data[row+x] != 0 {
out.SetGray(x, y, color.Gray{Y: 255})
}
}
}
return out
}
buildLakeMask := func() image.Image {
lakeMask := image.NewGray(bounds)
for _, lake := range lakes {
for _, p := range lake {
lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255})
}
}
return lakeMask
}
type exportItem struct {
name string
make func() image.Image
}
items := []exportItem{
{name: "canvas." + imgFormat, make: func() image.Image { return canvasImg }},
{name: "heightmap." + imgFormat, make: func() image.Image { return heightmapImg }},
{name: "bump_map." + imgFormat, make: func() image.Image { return bumpmapImg }},
{name: "lakes_mask." + imgFormat, make: buildLakeMask},
{name: "rivers_mask." + imgFormat, make: func() image.Image { return maskToGray(riverMask) }},
{name: "trees_mask." + imgFormat, make: func() image.Image { return maskToGray(treeMask) }},
{name: "roads_mask." + imgFormat, make: func() image.Image { return maskToGray(roadMask) }},
{name: "bridges_mask." + imgFormat, make: func() image.Image { return maskToGray(bridgeMask) }},
{name: "walls_mask." + imgFormat, make: func() image.Image { return maskToGray(wallMask) }},
{name: "turrets_mask." + imgFormat, make: func() image.Image { return maskToGray(turretMask) }},
{name: "buildings_mask." + imgFormat, make: func() image.Image { return maskToGray(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 _, item := range items {
saveImage(item.make(), filepath.Join(exportPath, item.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()
var buf bytes.Buffer
for _, item := range items {
buf.Reset()
if err := encodeImageToWriter(&buf, item.make(), formatSelect.Selected); err != nil {
log.Printf("Error encoding image %s: %v\n", item.name, err)
continue
}
hdr := &tar.Header{
Name: item.name,
Mode: 0644,
Size: int64(buf.Len()),
}
if err := tw.WriteHeader(hdr); err != nil {
log.Printf("Error writing tar header for %s: %v\n", item.name, err)
continue
}
if _, err := tw.Write(buf.Bytes()); err != nil {
log.Printf("Error writing tar data for %s: %v\n", item.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 _, item := range items {
f, err := zw.Create(item.name)
if err != nil {
log.Printf("Error creating zip entry for %s: %v\n", item.name, err)
continue
}
if err := encodeImageToWriter(f, item.make(), formatSelect.Selected); err != nil {
log.Printf("Error writing zip data for %s: %v\n", item.name, err)
continue
}
}
}
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)
}
}
func decodeSettingsJSON(raw []byte) (*Settings, error) {
var imported Settings
if err := json.Unmarshal(raw, &imported); err != nil {
return nil, err
}
var rawKeys map[string]json.RawMessage
if err := json.Unmarshal(raw, &rawKeys); err != nil {
return nil, err
}
normalizeSettings(&imported, rawKeys)
return &imported, nil
}
func saveSettingsToFile(path string, settings *Settings) error {
exportSettings := *settings
exportSettings.LastExportPath = ""
exportSettings.ImageViewState = 0
data, err := json.MarshalIndent(&exportSettings, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
func showSettingsExportDialog(win fyne.Window, settings *Settings) {
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("settings")
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,
)
saveDialog := dialog.NewCustom("Export Settings", "Cancel", content, win)
saveBtn := widget.NewButton("Save", func() {
fileName := strings.TrimSpace(fileNameEntry.Text)
if fileName == "" {
return
}
if !strings.HasSuffix(strings.ToLower(fileName), ".json") {
fileName += ".json"
}
filePath := filepath.Join(pathLabel.Text, fileName)
if err := saveSettingsToFile(filePath, settings); err != nil {
dialog.ShowError(err, win)
return
}
settings.LastExportPath = filepath.Dir(filePath)
saveDialog.Hide()
})
saveBtn.Disable()
fileNameEntry.OnChanged = func(text string) {
if strings.TrimSpace(text) == "" {
saveBtn.Disable()
} else {
saveBtn.Enable()
}
}
saveDialog.SetButtons([]fyne.CanvasObject{
widget.NewButton("Cancel", func() {
saveDialog.Hide()
}),
saveBtn,
})
saveDialog.Show()
}
func showSettingsImportDialog(win fyne.Window, settings *Settings, apply func(*Settings)) {
dialog.ShowFileOpen(func(rc fyne.URIReadCloser, err error) {
if err != nil {
dialog.ShowError(err, win)
return
}
if rc == nil {
return
}
defer rc.Close()
raw, err := io.ReadAll(rc)
if err != nil {
dialog.ShowError(err, win)
return
}
imported, err := decodeSettingsJSON(raw)
if err != nil {
dialog.ShowError(err, win)
return
}
if uri := rc.URI(); uri != nil {
settings.LastExportPath = filepath.Dir(uri.Path())
}
apply(imported)
}, win)
}
// 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()
}