2026-01-26 14:29:05 -06:00
package main
import (
2026-02-02 13:26:28 -06:00
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
2026-03-11 13:45:38 -05:00
"embed"
2026-03-11 13:08:40 -05:00
"encoding/json"
2026-01-26 14:29:05 -06:00
"fmt"
2026-01-27 15:16:23 -06:00
"image"
"image/color"
2026-02-26 13:21:04 -06:00
"image/draw"
2026-02-02 13:26:28 -06:00
"image/jpeg"
2026-01-27 15:16:23 -06:00
"image/png"
2026-02-26 13:39:39 -06:00
"io"
2026-03-11 13:45:38 -05:00
"io/fs"
2026-02-02 13:26:28 -06:00
"log"
2026-01-27 15:16:23 -06:00
"os"
"path/filepath"
2026-03-11 13:45:38 -05:00
"sort"
2026-02-02 13:26:28 -06:00
"strconv"
2026-02-02 12:24:14 -06:00
"strings"
2026-02-02 13:26:28 -06:00
"time"
2026-02-02 12:24:14 -06:00
2026-01-26 14:29:05 -06:00
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
2026-02-12 14:33:47 -06:00
"fyne.io/fyne/v2/data/binding"
2026-02-02 12:24:14 -06:00
"fyne.io/fyne/v2/dialog"
2026-01-26 14:29:05 -06:00
"fyne.io/fyne/v2/widget"
2026-02-02 12:24:14 -06:00
"github.com/chai2010/webp"
2026-01-26 14:29:05 -06:00
)
2026-02-26 13:21:04 -06:00
const generationStepTimeout = time . Minute
2026-03-11 13:45:38 -05:00
//go:embed Presets/*.json
var embeddedPresets embed . FS
2026-02-26 13:21:04 -06:00
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
}
2026-03-11 13:45:38 -05:00
type presetEntry struct {
Name string
Path string
}
2026-03-11 14:24:18 -05:00
type presetInfo struct {
Name string
Path string
Source string
}
2026-03-11 13:45:38 -05:00
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 )
}
2026-03-11 14:24:18 -05:00
func listUserPresets ( dir string ) ([] presetEntry , error ) {
entries , err := os . ReadDir ( dir )
if err != nil {
if os . IsNotExist ( err ) {
return [] presetEntry {}, 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 ( dir , name ),
})
}
sort . Slice ( presets , func ( i , j int ) bool {
return presets [ i ]. Name < presets [ j ]. Name
})
return presets , nil
}
func buildPresetOptions ( settings * Settings , userPresetDir string ) ([] string , map [ string ] presetInfo ) {
hidden := map [ string ] bool {}
for _ , name := range settings . HiddenPresets {
hidden [ name ] = true
}
info := map [ string ] presetInfo {}
var names [] string
embedded , err := listEmbeddedPresets ()
if err != nil {
log . Println ( "Error loading embedded presets:" , err )
} else {
for _ , preset := range embedded {
if hidden [ preset . Name ] {
continue
}
info [ preset . Name ] = presetInfo {
Name : preset . Name ,
Path : preset . Path ,
Source : "embedded" ,
}
names = append ( names , preset . Name )
}
}
userPresets , err := listUserPresets ( userPresetDir )
if err != nil {
log . Println ( "Error loading user presets:" , err )
} else {
for _ , preset := range userPresets {
if _ , exists := info [ preset . Name ]; exists {
continue
}
info [ preset . Name ] = presetInfo {
Name : preset . Name ,
Path : preset . Path ,
Source : "user" ,
}
names = append ( names , preset . Name )
}
}
sort . Strings ( names )
return append ([] string { "No Preset" }, names ... ), info
}
func savePresetToFile ( path string , settings * Settings ) error {
raw , err := json . Marshal ( settings )
if err != nil {
return err
}
var data map [ string ] any
if err := json . Unmarshal ( raw , & data ); err != nil {
return err
}
delete ( data , "seed" )
delete ( data , "last_export_path" )
delete ( data , "image_view_state" )
pretty , err := json . MarshalIndent ( data , "" , " " )
if err != nil {
return err
}
return os . WriteFile ( path , pretty , 0644 )
}
2026-01-26 14:29:05 -06:00
func main () {
2026-02-05 17:50:55 -06:00
// Initialize the Fyne application and window
2026-02-02 12:24:14 -06:00
a := app . NewWithID ( "com.example.rpgcitymaker" )
2026-01-26 14:29:05 -06:00
w := a . NewWindow ( "RPG City Maker" )
w . Resize ( fyne . NewSize ( 800 , 600 ))
2026-02-05 17:50:55 -06:00
// Load settings from file, or use defaults if loading fails
2026-01-26 14:29:05 -06:00
settings , err := LoadSettings ()
if err != nil {
log . Println ( "Error loading settings:" , err )
// Use default settings if loading fails
2026-01-27 12:00:19 -06:00
settings = & Settings { Detail : 1 , Roughness : 0 , Width : 300 , Height : 300 , Lakes : 0 , LakeSizeLower : 1 , LakeSizeUpper : 5 }
2026-01-26 14:29:05 -06:00
}
2026-02-05 17:50:55 -06:00
// Create canvas objects for displaying the generated images
2026-01-26 14:29:05 -06:00
canvasImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
heightmapImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
2026-02-09 10:13:36 -06:00
bumpmapImg := & canvas . Image {
FillMode : canvas . ImageFillContain ,
}
2026-01-26 14:29:05 -06:00
2026-02-05 17:50:55 -06:00
// Initialize slices to store generated map features
2026-02-02 13:26:28 -06:00
var lakes [][] image . Point
2026-02-09 09:41:35 -06:00
var buildings [][] image . Point
2026-02-26 13:03:48 -06:00
var waterMask * PixelMask
var riverMask * PixelMask
var treeMask * PixelMask
var buildingMask * PixelMask
var roadMask * PixelMask
var bridgeMask * PixelMask
var exitRoadMask * PixelMask
2026-03-02 10:40:14 -06:00
var wallMask * PixelMask
2026-03-02 12:50:57 -06:00
var turretMask * PixelMask
2026-02-02 13:26:28 -06:00
2026-02-05 17:50:55 -06:00
// Set up application configuration directory
2026-01-27 15:16:23 -06:00
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" )
2026-02-09 10:13:36 -06:00
bumpmapPath := filepath . Join ( appConfigDir , "bumpmap.png" )
2026-02-05 17:50:55 -06:00
// Save settings and images on window close
2026-01-27 15:16:23 -06:00
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 )
}
}
}
2026-02-09 10:13:36 -06:00
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 )
}
}
}
2026-01-27 15:16:23 -06:00
})
2026-02-05 17:50:55 -06:00
// Load previously saved images
2026-01-27 15:16:23 -06:00
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
}
}
2026-02-09 10:13:36 -06:00
bumpmapFile , err := os . Open ( bumpmapPath )
if err == nil {
defer bumpmapFile . Close ()
img , err := png . Decode ( bumpmapFile )
if err == nil {
bumpmapImg . Image = img
}
}
2026-02-05 17:50:55 -06:00
// Generate initial images if none are loaded
2026-01-27 15:16:23 -06:00
if canvasImg . Image == nil || heightmapImg . Image == nil {
2026-02-04 14:19:18 -06:00
2026-02-05 17:50:55 -06:00
// Step 1: Generating Heightmap
2026-02-04 15:15:47 -06:00
seedProvider := NewSeedProvider ( settings . Seed )
noiseImg := GenerateHeightmap ( settings . Width , settings . Height , int ( settings . Detail ), 100.0 , seedProvider . Next ())
2026-02-26 13:21:04 -06:00
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 )
}
2026-02-04 14:19:18 -06:00
2026-02-26 13:21:04 -06:00
// 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
}
2026-02-04 14:19:18 -06:00
2026-02-26 13:21:04 -06:00
// 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 )
}
2026-02-26 13:03:48 -06:00
waterMask = BuildMaskFromLakes ( settings . Width , settings . Height , lakes )
if riverMask != nil {
waterMask . Merge ( riverMask )
2026-02-04 14:19:18 -06:00
}
2026-03-02 10:40:14 -06:00
// 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
2026-02-26 13:03:48 -06:00
var roadAnchors [] image . Point
2026-03-02 12:50:57 -06:00
var roadList [] * Road
2026-02-26 13:21:04 -06:00
roadBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
rd * PixelMask
br * PixelMask
ex * PixelMask
anc [] image . Point
2026-03-02 12:50:57 -06:00
rl [] * Road
2026-02-26 13:21:04 -06:00
} {
2026-03-02 12:50:57 -06:00
rd , br , ex , anc , rl := GenerateRoadsWithPOIs ( roadBase , settings . Width , settings . Height , settings , waterMask , wallLayout , roadNodes , roadTarget , edgeToEdgeOnly , seedProvider . Next ())
2026-02-26 13:21:04 -06:00
return struct {
rd * PixelMask
br * PixelMask
ex * PixelMask
anc [] image . Point
2026-03-02 12:50:57 -06:00
rl [] * Road
}{ rd : rd , br : br , ex : ex , anc : anc , rl : rl }
2026-02-26 13:21:04 -06:00
}); ok {
2026-03-02 12:50:57 -06:00
roadMask , bridgeMask , exitRoadMask , roadAnchors , roadList = out . rd , out . br , out . ex , out . anc , out . rl
2026-03-02 11:20:57 -06:00
drawWallMask ( roadBase , wallMask )
2026-03-02 12:50:57 -06:00
turretMask = GenerateTurrets ( roadBase , settings . Width , settings . Height , settings , wallLayout , waterMask , roadMask , roadList )
2026-02-26 13:21:04 -06:00
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 )
2026-03-02 12:50:57 -06:00
turretMask = NewPixelMask ( settings . Width , settings . Height )
2026-02-26 13:21:04 -06:00
roadAnchors = nil
}
2026-01-30 10:55:15 -06:00
2026-03-02 10:40:14 -06:00
placementMask := cloneMask ( roadMask )
if placementMask == nil {
placementMask = NewPixelMask ( settings . Width , settings . Height )
}
if wallMask != nil {
placementMask . Merge ( wallMask )
}
2026-03-10 12:42:13 -05:00
if turretMask != nil {
placementMask . Merge ( turretMask )
}
2026-03-02 10:40:14 -06:00
// Step 7: Generating Buildings
2026-02-26 13:21:04 -06:00
buildingBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
blds [][] image . Point
bmsk * PixelMask
} {
2026-03-02 10:40:14 -06:00
blds , bmsk := GenerateBuildings ( buildingBase , settings . Width , settings . Height , settings , roadAnchors , waterMask , placementMask , exitRoadMask , seedProvider . Next ())
2026-02-26 13:21:04 -06:00
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 )
}
2026-02-05 15:11:55 -06:00
2026-03-02 10:40:14 -06:00
// Step 8: Generating Trees
2026-02-26 13:21:04 -06:00
treeBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () * PixelMask {
2026-03-02 10:40:14 -06:00
return GenerateTrees ( treeBase , waterMask , placementMask , buildingMask , settings . MinTreeSize , settings . MaxTreeSize , settings . TreeCoverage , settings . TreeClumpiness , seedProvider . Next ())
2026-02-26 13:21:04 -06:00
}); ok {
treeMask = out
finalImage = treeBase
} else {
log . Println ( "GenerateTrees timed out after 1 minute; continuing." )
treeMask = NewPixelMask ( settings . Width , settings . Height )
}
2026-02-04 14:19:18 -06:00
2026-02-05 17:50:55 -06:00
// Step 7: Darkening Water Areas
2026-02-26 13:03:48 -06:00
darkenedHeightmap := DarkenLakeAreas ( noiseImg , waterMask )
2026-02-04 14:19:18 -06:00
2026-02-09 09:41:35 -06:00
// Step 8: Flattening Building Areas
flattenedBuildingHeightmap := FlattenBuildingAreas ( darkenedHeightmap .( * image . RGBA ), buildings , settings . Width , settings . Height )
2026-02-04 16:13:06 -06:00
2026-03-02 10:40:14 -06:00
// Step 9: Flattening Road and Wall Areas
flattenedHeightmap := FlattenRoadAreas ( flattenedBuildingHeightmap , placementMask )
2026-02-09 09:41:35 -06:00
// Step 10: Applying Roughness
2026-02-04 16:13:06 -06:00
compositeImg := ApplyRoughness ( flattenedHeightmap , settings . Roughness )
2026-02-04 14:19:18 -06:00
2026-02-09 10:13:36 -06:00
// Step 11: Generating Bump Map
bumpMap := GenerateBumpMap ( compositeImg .( * image . RGBA ), settings . Width , settings . Height , 0.10 )
2026-01-27 15:16:23 -06:00
heightmapImg . Image = compositeImg
2026-02-09 10:13:36 -06:00
bumpmapImg . Image = bumpMap
2026-02-04 14:19:18 -06:00
2026-01-27 15:16:23 -06:00
canvasImg . Image = finalImage
2026-02-04 14:19:18 -06:00
2026-01-27 15:16:23 -06:00
}
2026-02-12 14:33:47 -06:00
var generateBtn * widget . Button
errorStates := make ( map [ string ] bool )
2026-03-11 14:00:49 -05:00
var applyingPreset bool
var clearPresetSelection func ()
markPresetDirty := func () {
if applyingPreset {
return
}
if clearPresetSelection != nil {
clearPresetSelection ()
}
}
2026-02-12 14:33:47 -06:00
// ... (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 ()
2026-01-26 14:29:05 -06:00
settings . Detail = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-01-26 14:29:05 -06:00
2026-02-12 14:33:47 -06:00
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 ()
2026-01-26 14:29:05 -06:00
settings . Roughness = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-05 17:50:55 -06:00
// Create UI elements for controlling lake generation settings
2026-02-12 14:33:47 -06:00
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 ()
2026-01-26 14:29:05 -06:00
settings . Lakes = int ( val )
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
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 ()
})
2026-01-26 14:29:05 -06:00
}
2026-02-12 14:33:47 -06:00
lakeSizeLowerSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := lakeSizeLowerSlider . value . Get ()
2026-01-27 12:00:19 -06:00
settings . LakeSizeLower = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-01-27 12:00:19 -06:00
2026-02-12 14:33:47 -06:00
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 ()
2026-01-27 12:00:19 -06:00
settings . LakeSizeUpper = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-18 15:14:47 -06:00
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-18 15:14:47 -06:00
}))
2026-02-19 10:29:17 -06:00
lakeShapeLabel := widget . NewLabel ( "Lake Shape:" )
lakeShapeSelect := widget . NewSelect ([] string { "circle" , "oval" , "procedural" }, func ( s string ) {
settings . LakeShape = s
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-19 10:29:17 -06:00
})
lakeShapeSelect . SetSelected ( settings . LakeShape )
2026-02-12 14:33:47 -06:00
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 ()
})
2026-01-27 12:00:19 -06:00
}
2026-02-12 14:33:47 -06:00
riversSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := riversSlider . value . Get ()
2026-01-30 10:38:00 -06:00
settings . Rivers = int ( val )
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
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 ()
})
2026-01-30 10:38:00 -06:00
}
2026-02-12 14:33:47 -06:00
minRiverWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minRiverWidthSlider . value . Get ()
2026-01-30 10:38:00 -06:00
settings . MinRiverWidth = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-01-30 10:38:00 -06:00
2026-02-12 14:33:47 -06:00
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 ()
2026-01-30 10:38:00 -06:00
settings . MaxRiverWidth = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-01-30 10:38:00 -06:00
2026-02-12 14:33:47 -06:00
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 ()
2026-01-30 10:38:00 -06:00
settings . RiverCurvyness = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-18 14:33:09 -06:00
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-18 14:33:09 -06:00
}))
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-18 14:33:09 -06:00
}))
2026-02-26 14:52:19 -06:00
minTreeSizeSlider := newNumericInputSliderWithStep ( minTreeSizePercent , maxTreeSizePercent , settings . MinTreeSize , treeSizePercentStep , "%.1f%%" , "Min Tree Size" )
2026-02-12 14:33:47 -06:00
minTreeSizeSlider . entry . OnChanged = func ( s string ) {
minTreeSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minTreeSize" ] = hasError
updateGenerateBtnState ()
})
2026-01-30 10:38:00 -06:00
}
2026-02-12 14:33:47 -06:00
minTreeSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minTreeSizeSlider . value . Get ()
2026-01-27 15:16:23 -06:00
settings . MinTreeSize = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-01-27 15:16:23 -06:00
2026-02-26 14:52:19 -06:00
maxTreeSizeSlider := newNumericInputSliderWithStep ( minTreeSizePercent , maxTreeSizePercent , settings . MaxTreeSize , treeSizePercentStep , "%.1f%%" , "Max Tree Size" )
2026-02-12 14:33:47 -06:00
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 ()
2026-01-27 15:16:23 -06:00
settings . MaxTreeSize = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-01-27 15:16:23 -06:00
2026-02-12 14:33:47 -06:00
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 ()
2026-01-27 15:16:23 -06:00
settings . TreeCoverage = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-01-27 15:16:23 -06:00
2026-02-12 14:33:47 -06:00
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 ()
2026-01-27 15:16:23 -06:00
settings . TreeClumpiness = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-26 12:49:01 -06:00
minRoadWidthSlider := newNumericInputSliderWithStep ( minRoadWidthPercent , maxRoadWidthPercent , settings . MinRoadWidth , roadWidthPercentStep , "%.1f%%" , "Min Road Width" )
2026-02-12 14:33:47 -06:00
minRoadWidthSlider . entry . OnChanged = func ( s string ) {
minRoadWidthSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minRoadWidth" ] = hasError
updateGenerateBtnState ()
})
2026-02-04 14:19:18 -06:00
}
2026-02-12 14:33:47 -06:00
minRoadWidthSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minRoadWidthSlider . value . Get ()
2026-02-04 14:19:18 -06:00
settings . MinRoadWidth = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-26 12:49:01 -06:00
maxRoadWidthSlider := newNumericInputSliderWithStep ( minRoadWidthPercent , maxRoadWidthPercent , settings . MaxRoadWidth , roadWidthPercentStep , "%.1f%%" , "Max Road Width" )
2026-02-12 14:33:47 -06:00
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 ()
2026-02-04 14:19:18 -06:00
settings . MaxRoadWidth = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-27 09:31:42 -06:00
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 )
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-27 09:31:42 -06:00
}))
2026-02-12 14:33:47 -06:00
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 ()
2026-02-04 14:19:18 -06:00
settings . RoadExits = int ( val )
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-12 14:33:47 -06:00
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 ()
2026-02-04 14:19:18 -06:00
settings . RoadCurvyness = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-12 14:33:47 -06:00
roadDistributionSlider := newNumericInputSlider ( 0 , 100 , settings . RoadDistribution , "%.0f%%" , "Distribution" )
roadDistributionSlider . entry . OnChanged = func ( s string ) {
roadDistributionSlider . validate ( s , func ( hasError bool ) {
errorStates [ "roadDistribution" ] = hasError
updateGenerateBtnState ()
})
2026-02-04 14:19:18 -06:00
}
2026-02-12 14:33:47 -06:00
roadDistributionSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := roadDistributionSlider . value . Get ()
settings . RoadDistribution = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-04 14:19:18 -06:00
2026-02-26 11:05:38 -06:00
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-26 11:05:38 -06:00
}))
2026-03-02 10:40:14 -06:00
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 10:40:14 -06:00
}))
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 10:40:14 -06:00
}))
2026-03-11 12:43:41 -05:00
numWallsSlider := newNumericInputSlider ( 0 , 5 , float64 ( settings . NumWalls ), "%.0f" , "Number of Walls" )
2026-03-02 10:40:14 -06:00
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 )
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 10:40:14 -06:00
}))
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 10:40:14 -06:00
}))
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 10:40:14 -06:00
}))
2026-03-02 12:50:57 -06:00
showTurretsCheck := widget . NewCheck ( "Show Turrets" , func ( v bool ) {
settings . ShowTurrets = v
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 12:50:57 -06:00
})
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 12:50:57 -06:00
}))
turretShapeLabel := widget . NewLabel ( "Turret Shape:" )
turretShapeSelect := widget . NewSelect ([] string { "circular" , "square" }, func ( s string ) {
settings . TurretShape = s
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 12:50:57 -06:00
})
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 12:50:57 -06:00
}))
2026-03-11 12:43:41 -05:00
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
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-11 12:43:41 -05:00
}))
2026-03-02 12:50:57 -06:00
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 ()
}
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-03-02 12:50:57 -06:00
}
2026-02-05 17:50:55 -06:00
// Create UI elements for error display and action buttons
2026-03-11 13:08:40 -05:00
var applySettingsToUI func ( * Settings )
2026-02-12 14:33:47 -06:00
errorLabel := widget . NewLabel ( "" )
errorLabel . Wrapping = fyne . TextWrapWord
2026-01-27 15:16:23 -06:00
errorLabel . Hide ()
2026-02-13 09:29:06 -06:00
progressLabel := widget . NewLabel ( "" )
2026-02-12 14:33:47 -06:00
progressBar := widget . NewProgressBar ()
2026-02-13 09:29:06 -06:00
progressContainer := container . NewVBox ( progressLabel , progressBar )
progressContainer . Hide ()
2026-02-02 12:40:25 -06:00
exportCanvasBtn := widget . NewButton ( "Export Canvas" , func () {
2026-02-02 12:24:14 -06:00
showSaveDialog ( w , canvasImg . Image , settings )
})
2026-02-02 12:40:25 -06:00
exportHeightmapBtn := widget . NewButton ( "Export Heightmap" , func () {
showSaveDialog ( w , heightmapImg . Image , settings )
})
2026-02-09 10:13:36 -06:00
exportBumpmapBtn := widget . NewButton ( "Export Bump Map" , func () {
showSaveDialog ( w , bumpmapImg . Image , settings )
})
2026-02-02 13:26:28 -06:00
exportMasksBtn := widget . NewButton ( "Export Masks" , func () {
2026-03-02 12:50:57 -06:00
showMasksSaveDialog ( w , canvasImg . Image , heightmapImg . Image , bumpmapImg . Image , settings , lakes , riverMask , treeMask , roadMask , bridgeMask , wallMask , turretMask , buildingMask )
2026-02-02 13:26:28 -06:00
})
2026-03-11 13:08:40 -05:00
exportSettingsBtn := widget . NewButton ( "Export Settings" , func () {
showSettingsExportDialog ( w , settings )
})
importSettingsBtn := widget . NewButton ( "Import Settings" , func () {
showSettingsImportDialog ( w , settings , applySettingsToUI )
})
2026-02-05 17:50:55 -06:00
// Main generation button and logic
2026-01-28 12:35:10 -06:00
generateBtn = widget . NewButton ( "Generate" , func () {
go func () {
2026-02-26 14:22:23 -06:00
var timedOutSteps [] string
addTimeout := func ( step string ) {
timedOutSteps = append ( timedOutSteps , step )
}
2026-02-13 09:29:06 -06:00
// Disable button and show progress bar during generation
2026-02-02 10:54:09 -06:00
fyne . Do ( func () {
generateBtn . Disable ()
2026-02-13 09:29:06 -06:00
progressContainer . Show ()
2026-02-02 10:54:09 -06:00
})
defer func () {
2026-02-13 09:29:06 -06:00
// Re-enable button and hide progress bar after generation
2026-02-02 10:54:09 -06:00
fyne . Do ( func () {
generateBtn . Enable ()
2026-02-13 09:29:06 -06:00
progressContainer . Hide ()
2026-02-02 10:54:09 -06:00
})
}()
2026-02-04 15:15:47 -06:00
seedProvider := NewSeedProvider ( settings . Seed )
2026-02-26 13:21:04 -06:00
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 )
}
2026-01-28 12:35:10 -06:00
// Step 1: Generating Heightmap
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 1 of 13: Generating Heightmap" )
progressBar . SetValue ( 1.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-02-04 15:15:47 -06:00
noiseImg := GenerateHeightmap ( settings . Width , settings . Height , int ( settings . Detail ), 100.0 , seedProvider . Next ())
2026-01-28 12:35:10 -06:00
// Step 2: Generating Lakes
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 2 of 13: Generating Lakes" )
progressBar . SetValue ( 2.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-02-26 13:21:04 -06:00
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." )
2026-02-26 14:22:23 -06:00
addTimeout ( "Lakes" )
2026-02-26 13:21:04 -06:00
lakes = nil
}
2026-01-28 12:35:10 -06:00
2026-01-30 10:38:00 -06:00
// Step 3: Generating Rivers
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 3 of 13: Generating Rivers" )
progressBar . SetValue ( 3.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-02-26 13:21:04 -06:00
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." )
2026-02-26 14:22:23 -06:00
addTimeout ( "Rivers" )
2026-02-26 13:21:04 -06:00
riverMask = NewPixelMask ( settings . Width , settings . Height )
}
2026-02-26 13:03:48 -06:00
waterMask = BuildMaskFromLakes ( settings . Width , settings . Height , lakes )
if riverMask != nil {
waterMask . Merge ( riverMask )
2026-02-04 14:50:36 -06:00
}
2026-03-02 10:40:14 -06:00
// Step 4: Preparing Road Nodes
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
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 )
2026-02-13 09:29:06 -06:00
})
2026-02-26 13:03:48 -06:00
var roadAnchors [] image . Point
2026-03-02 12:50:57 -06:00
var roadList [] * Road
2026-02-26 13:21:04 -06:00
roadBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
rd * PixelMask
br * PixelMask
ex * PixelMask
anc [] image . Point
2026-03-02 12:50:57 -06:00
rl [] * Road
2026-02-26 13:21:04 -06:00
} {
2026-03-02 12:50:57 -06:00
rd , br , ex , anc , rl := GenerateRoadsWithPOIs ( roadBase , settings . Width , settings . Height , settings , waterMask , wallLayout , roadNodes , roadTarget , edgeToEdgeOnly , seedProvider . Next ())
2026-02-26 13:21:04 -06:00
return struct {
rd * PixelMask
br * PixelMask
ex * PixelMask
anc [] image . Point
2026-03-02 12:50:57 -06:00
rl [] * Road
}{ rd : rd , br : br , ex : ex , anc : anc , rl : rl }
2026-02-26 13:21:04 -06:00
}); ok {
2026-03-02 12:50:57 -06:00
roadMask , bridgeMask , exitRoadMask , roadAnchors , roadList = out . rd , out . br , out . ex , out . anc , out . rl
2026-03-02 11:20:57 -06:00
drawWallMask ( roadBase , wallMask )
2026-03-02 12:50:57 -06:00
turretMask = GenerateTurrets ( roadBase , settings . Width , settings . Height , settings , wallLayout , waterMask , roadMask , roadList )
2026-02-26 13:21:04 -06:00
finalImage = roadBase
} else {
log . Println ( "GenerateRoads timed out after 1 minute; continuing." )
2026-02-26 14:22:23 -06:00
addTimeout ( "Roads" )
2026-02-26 13:21:04 -06:00
roadMask = NewPixelMask ( settings . Width , settings . Height )
bridgeMask = NewPixelMask ( settings . Width , settings . Height )
exitRoadMask = NewPixelMask ( settings . Width , settings . Height )
2026-03-02 12:50:57 -06:00
turretMask = NewPixelMask ( settings . Width , settings . Height )
2026-02-26 13:21:04 -06:00
roadAnchors = nil
}
2026-03-02 10:40:14 -06:00
placementMask := cloneMask ( roadMask )
if placementMask == nil {
placementMask = NewPixelMask ( settings . Width , settings . Height )
}
if wallMask != nil {
placementMask . Merge ( wallMask )
}
2026-03-10 12:42:13 -05:00
if turretMask != nil {
placementMask . Merge ( turretMask )
}
2026-02-04 14:19:18 -06:00
2026-03-02 10:40:14 -06:00
// Step 7: Generating Buildings
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 7 of 13: Generating Buildings" )
progressBar . SetValue ( 7.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-02-26 13:21:04 -06:00
buildingBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () struct {
blds [][] image . Point
bmsk * PixelMask
} {
2026-03-02 10:40:14 -06:00
blds , bmsk := GenerateBuildings ( buildingBase , settings . Width , settings . Height , settings , roadAnchors , waterMask , placementMask , exitRoadMask , seedProvider . Next ())
2026-02-26 13:21:04 -06:00
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." )
2026-02-26 14:22:23 -06:00
addTimeout ( "Buildings" )
2026-02-26 13:21:04 -06:00
buildings = nil
buildingMask = NewPixelMask ( settings . Width , settings . Height )
}
2026-02-05 15:11:55 -06:00
2026-03-02 10:40:14 -06:00
// Step 8: Darkening Water Areas
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 8 of 13: Darkening Water Areas" )
progressBar . SetValue ( 8.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-02-26 13:03:48 -06:00
darkenedHeightmap := DarkenLakeAreas ( noiseImg , waterMask )
2026-01-30 10:38:00 -06:00
2026-03-02 10:40:14 -06:00
// Step 9: Flattening Building Areas
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 9 of 13: Flattening Building Areas" )
progressBar . SetValue ( 9.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-02-09 09:41:35 -06:00
flattenedBuildingHeightmap := FlattenBuildingAreas ( darkenedHeightmap .( * image . RGBA ), buildings , settings . Width , settings . Height )
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 10 of 13: Flattening Road and Wall Areas" )
progressBar . SetValue ( 10.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-03-02 10:40:14 -06:00
flattenedHeightmap := FlattenRoadAreas ( flattenedBuildingHeightmap , placementMask )
2026-02-09 09:41:35 -06:00
2026-03-02 10:40:14 -06:00
// Step 11: Applying Roughness
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 11 of 13: Applying Roughness" )
progressBar . SetValue ( 11.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-02-04 16:13:06 -06:00
compositeImg := ApplyRoughness ( flattenedHeightmap , settings . Roughness )
2026-01-28 12:35:10 -06:00
2026-03-02 10:40:14 -06:00
// Step 12: Generating Trees
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 12 of 13: Generating Trees" )
progressBar . SetValue ( 12.0 / 13.0 )
2026-02-13 09:29:06 -06:00
})
2026-02-26 13:21:04 -06:00
treeBase := cloneToRGBA ( finalImage , settings . Width , settings . Height )
if out , ok := runWithTimeout ( generationStepTimeout , func () * PixelMask {
2026-03-02 10:40:14 -06:00
return GenerateTrees ( treeBase , waterMask , placementMask , buildingMask , settings . MinTreeSize , settings . MaxTreeSize , settings . TreeCoverage , settings . TreeClumpiness , seedProvider . Next ())
2026-02-26 13:21:04 -06:00
}); ok {
treeMask = out
finalImage = treeBase
} else {
log . Println ( "GenerateTrees timed out after 1 minute; continuing." )
2026-02-26 14:22:23 -06:00
addTimeout ( "Trees" )
2026-02-26 13:21:04 -06:00
treeMask = NewPixelMask ( settings . Width , settings . Height )
}
2026-01-28 13:06:01 -06:00
2026-03-02 10:40:14 -06:00
// Step 13: Generating Bump Map
2026-02-12 14:33:47 -06:00
2026-02-13 09:29:06 -06:00
fyne . Do ( func () {
2026-03-02 10:40:14 -06:00
progressLabel . SetText ( "Step 13 of 13: Generating Bump Map" )
2026-02-13 09:29:06 -06:00
progressBar . SetValue ( 1.0 )
})
2026-02-09 10:13:36 -06:00
bumpMap := GenerateBumpMap ( compositeImg .( * image . RGBA ), settings . Width , settings . Height , 0.10 )
// Step 11: Finalizing Images
2026-02-12 14:33:47 -06:00
2026-02-02 10:54:09 -06:00
fyne . Do ( func () {
heightmapImg . Image = compositeImg
heightmapImg . Refresh ()
2026-02-09 10:13:36 -06:00
bumpmapImg . Image = bumpMap
bumpmapImg . Refresh ()
2026-02-02 10:54:09 -06:00
canvasImg . Image = finalImage
canvasImg . Refresh ()
2026-02-26 14:22:23 -06:00
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 ()
}
2026-02-02 10:54:09 -06:00
})
2026-01-28 12:35:10 -06:00
}()
2026-01-26 14:29:05 -06:00
})
2026-02-05 17:50:55 -06:00
// Create UI elements for image dimensions and seed
2026-01-27 15:16:23 -06:00
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 ))
})
2026-02-05 17:50:55 -06:00
// Create tabs for organizing settings
2026-01-26 14:29:05 -06:00
terrainTab := container . NewTabItem ( "Terrain" , container . NewVBox (
detailSlider ,
roughnessSlider ,
2026-02-02 10:15:26 -06:00
widget . NewLabel ( "" ), // Spacer
minTreeSizeSlider ,
maxTreeSizeSlider ,
treeCoverageSlider ,
treeClumpinessSlider ,
))
waterTab := container . NewTabItem ( "Water" , container . NewVBox (
2026-01-26 14:29:05 -06:00
lakesSlider ,
2026-01-27 12:00:19 -06:00
lakeSizeLowerSlider ,
lakeSizeUpperSlider ,
2026-02-18 15:14:47 -06:00
lakeEdgeRoughnessSlider ,
2026-02-19 10:29:17 -06:00
lakeShapeLabel ,
lakeShapeSelect ,
2026-01-30 10:38:00 -06:00
widget . NewLabel ( "" ), // Spacer
riversSlider ,
minRiverWidthSlider ,
maxRiverWidthSlider ,
riverCurvynessSlider ,
2026-02-18 14:33:09 -06:00
riverWidthVariabilitySlider ,
riverEdgeRoughnessSlider ,
2026-01-26 14:29:05 -06:00
))
2026-02-04 14:19:18 -06:00
roadsTab := container . NewTabItem ( "Roads" , container . NewVBox (
minRoadWidthSlider ,
maxRoadWidthSlider ,
2026-02-27 09:31:42 -06:00
buildingsPerRoadSlider ,
2026-02-04 14:19:18 -06:00
roadExitsSlider ,
2026-02-26 11:05:38 -06:00
minRoadAngleSlider ,
2026-02-04 14:19:18 -06:00
roadCurvynessSlider ,
roadDistributionSlider ,
))
2026-03-02 10:40:14 -06:00
fortificationsTab := container . NewTabItem ( "Fortifications" , container . NewVBox (
minWallWidthSlider ,
maxWallWidthSlider ,
numWallsSlider ,
cityCoverageSlider ,
wallCurvynessSlider ,
2026-03-11 12:43:41 -05:00
gateSpacingSlider ,
2026-03-02 12:50:57 -06:00
showTurretsCheck ,
turretControls ,
2026-03-02 10:40:14 -06:00
))
2026-02-12 14:33:47 -06:00
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 ()
2026-02-05 15:11:55 -06:00
settings . NumBuildings = int ( val )
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-26 12:49:01 -06:00
minBuildingSizeSlider := newNumericInputSliderWithStep ( minBuildingSizePercent , maxBuildingSizePercent , settings . MinBuildingSize , buildingSizePercentStep , "%.1f%%" , "Min Building Size" )
2026-02-12 14:33:47 -06:00
minBuildingSizeSlider . entry . OnChanged = func ( s string ) {
minBuildingSizeSlider . validate ( s , func ( hasError bool ) {
errorStates [ "minBuildingSize" ] = hasError
updateGenerateBtnState ()
})
2026-02-05 15:11:55 -06:00
}
2026-02-12 14:33:47 -06:00
minBuildingSizeSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := minBuildingSizeSlider . value . Get ()
2026-02-05 15:11:55 -06:00
settings . MinBuildingSize = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-05 15:11:55 -06:00
2026-02-26 12:49:01 -06:00
maxBuildingSizeSlider := newNumericInputSliderWithStep ( minBuildingSizePercent , maxBuildingSizePercent , settings . MaxBuildingSize , buildingSizePercentStep , "%.1f%%" , "Max Building Size" )
2026-02-12 14:33:47 -06:00
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 ()
2026-02-05 15:11:55 -06:00
settings . MaxBuildingSize = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-05 15:11:55 -06:00
2026-02-12 14:33:47 -06:00
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 ()
})
2026-02-05 15:11:55 -06:00
}
2026-02-12 14:33:47 -06:00
buildingDistributionSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := buildingDistributionSlider . value . Get ()
settings . BuildingDistribution = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-05 15:11:55 -06:00
buildingShapeLabel := widget . NewLabel ( "Building Shape:" )
2026-02-06 10:21:40 -06:00
buildingShapeSelect := widget . NewSelect ([] string { "squares" , "circles" , "rectangles" , "mixed" , "procedural" }, func ( s string ) {
2026-02-05 15:11:55 -06:00
settings . BuildingShape = s
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-05 15:11:55 -06:00
})
buildingShapeSelect . SetSelected ( settings . BuildingShape )
2026-02-06 09:01:09 -06:00
// 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 ,
)
2026-02-12 14:33:47 -06:00
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 ()
2026-02-06 10:21:40 -06:00
settings . MinBuildingComplexity = int ( val )
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-06 10:21:40 -06:00
2026-02-12 14:33:47 -06:00
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 ()
2026-02-06 10:21:40 -06:00
settings . MaxBuildingComplexity = int ( val )
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-06 10:21:40 -06:00
2026-02-12 14:33:47 -06:00
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 ()
})
2026-02-06 10:21:40 -06:00
}
2026-02-12 14:33:47 -06:00
buildingComplexityRatioSlider . value . AddListener ( binding . NewDataListener ( func () {
val , _ := buildingComplexityRatioSlider . value . Get ()
settings . BuildingComplexityRatio = val
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-12 14:33:47 -06:00
}))
2026-02-06 10:21:40 -06:00
proceduralContainer := container . NewVBox (
minBuildingComplexitySlider ,
maxBuildingComplexitySlider ,
buildingComplexityRatioSlider ,
)
2026-02-06 09:01:09 -06:00
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 ) {
2026-02-06 10:21:40 -06:00
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
2026-02-06 09:01:09 -06:00
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 ()
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-06 09:01:09 -06:00
}
circleRatioSlider . OnChanged = func ( val float64 ) {
2026-02-06 10:21:40 -06:00
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
2026-02-06 09:01:09 -06:00
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 ()
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-06 09:01:09 -06:00
}
rectangleRatioSlider . OnChanged = func ( val float64 ) {
2026-02-06 10:21:40 -06:00
if settings . BuildingShape != "mixed" && settings . BuildingShape != "procedural" {
2026-02-06 09:01:09 -06:00
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 ()
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-06 09:01:09 -06:00
}
buildingShapeSelect . OnChanged = func ( s string ) {
settings . BuildingShape = s
if s == "mixed" {
2026-02-06 10:21:40 -06:00
proceduralContainer . Hide ()
ratioContainer . Show ()
} else if s == "procedural" {
proceduralContainer . Show ()
2026-02-06 09:01:09 -06:00
ratioContainer . Show ()
} else {
2026-02-06 10:21:40 -06:00
proceduralContainer . Hide ()
2026-02-06 09:01:09 -06:00
ratioContainer . Hide ()
}
2026-03-11 14:00:49 -05:00
markPresetDirty ()
2026-02-06 09:01:09 -06:00
}
// Initial visibility check
2026-02-06 10:21:40 -06:00
if settings . BuildingShape == "mixed" {
proceduralContainer . Hide ()
ratioContainer . Show ()
} else if settings . BuildingShape == "procedural" {
proceduralContainer . Show ()
ratioContainer . Show ()
} else {
proceduralContainer . Hide ()
2026-02-06 09:01:09 -06:00
ratioContainer . Hide ()
}
2026-03-11 13:08:40 -05:00
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 ()
}
2026-03-11 14:24:18 -05:00
presetDir := filepath . Join ( appConfigDir , "presets" )
var presetInfoByName map [ string ] presetInfo
var suppressPresetApply bool
var removePresetBtn * widget . Button
presetSelect := widget . NewSelect ([] string { "No Preset" }, func ( name string ) {
if suppressPresetApply {
2026-03-11 13:45:38 -05:00
return
}
2026-03-11 14:24:18 -05:00
if name == "" || name == "No Preset" {
if removePresetBtn != nil {
removePresetBtn . Disable ()
}
return
}
info , ok := presetInfoByName [ name ]
2026-03-11 13:45:38 -05:00
if ! ok {
return
}
2026-03-11 14:00:49 -05:00
applyingPreset = true
2026-03-11 14:24:18 -05:00
var imported * Settings
var err error
if info . Source == "embedded" {
imported , err = loadEmbeddedPreset ( info . Path )
} else {
raw , readErr := os . ReadFile ( info . Path )
if readErr != nil {
err = readErr
} else {
imported , err = decodeSettingsJSON ( raw )
}
}
2026-03-11 13:45:38 -05:00
if err != nil {
2026-03-11 14:00:49 -05:00
applyingPreset = false
2026-03-11 13:45:38 -05:00
dialog . ShowError ( err , w )
return
}
imported . Seed = settings . Seed
applySettingsToUI ( imported )
2026-03-11 14:00:49 -05:00
time . AfterFunc ( 50 * time . Millisecond , func () {
fyne . Do ( func () {
applyingPreset = false
})
})
2026-03-11 14:24:18 -05:00
if removePresetBtn != nil {
removePresetBtn . Enable ()
}
2026-03-11 13:45:38 -05:00
})
presetSelect . SetSelected ( "No Preset" )
2026-03-11 14:00:49 -05:00
clearPresetSelection = func () {
if presetSelect . Selected != "No Preset" {
presetSelect . SetSelected ( "No Preset" )
}
}
2026-03-11 13:45:38 -05:00
2026-03-11 14:24:18 -05:00
refreshPresetOptions := func ( selectName string ) {
names , info := buildPresetOptions ( settings , presetDir )
presetInfoByName = info
suppressPresetApply = true
presetSelect . Options = names
presetSelect . Refresh ()
if selectName == "" {
selectName = presetSelect . Selected
}
if _ , ok := presetInfoByName [ selectName ]; ! ok {
selectName = "No Preset"
}
presetSelect . SetSelected ( selectName )
if removePresetBtn != nil {
if selectName == "" || selectName == "No Preset" {
removePresetBtn . Disable ()
} else {
removePresetBtn . Enable ()
}
}
suppressPresetApply = false
}
addPresetBtn := widget . NewButton ( "Add Preset" , func () {
nameEntry := widget . NewEntry ()
nameEntry . SetPlaceHolder ( "Preset Name" )
content := container . NewVBox (
widget . NewLabel ( "Preset Name:" ),
nameEntry ,
)
var addDialog * dialog . CustomDialog
saveBtn := widget . NewButton ( "Save" , func () {
name := strings . TrimSpace ( nameEntry . Text )
if name == "" || name == "No Preset" {
return
}
if strings . ContainsAny ( name , "/\\" ) {
dialog . ShowError ( fmt . Errorf ( "preset name cannot contain path separators" ), w )
return
}
if _ , exists := presetInfoByName [ name ]; exists {
dialog . ShowError ( fmt . Errorf ( "preset name already exists" ), w )
return
}
if err := os . MkdirAll ( presetDir , 0755 ); err != nil {
dialog . ShowError ( err , w )
return
}
filePath := filepath . Join ( presetDir , name + ".json" )
if err := savePresetToFile ( filePath , settings ); err != nil {
dialog . ShowError ( err , w )
return
}
refreshPresetOptions ( name )
if addDialog != nil {
addDialog . Hide ()
}
})
saveBtn . Disable ()
nameEntry . OnChanged = func ( text string ) {
if strings . TrimSpace ( text ) == "" {
saveBtn . Disable ()
} else {
saveBtn . Enable ()
}
}
addDialog = dialog . NewCustom ( "Add Preset" , "Cancel" , content , w )
addDialog . SetButtons ([] fyne . CanvasObject {
widget . NewButton ( "Cancel" , func () {
addDialog . Hide ()
}),
saveBtn ,
})
addDialog . Show ()
})
removePresetBtn = widget . NewButton ( "Remove Preset" , func () {
name := presetSelect . Selected
if name == "" || name == "No Preset" {
return
}
info , ok := presetInfoByName [ name ]
if ! ok {
return
}
if info . Source == "embedded" {
found := false
for _ , existing := range settings . HiddenPresets {
if existing == name {
found = true
break
}
}
if ! found {
settings . HiddenPresets = append ( settings . HiddenPresets , name )
if err := settings . Save (); err != nil {
log . Println ( "Error saving hidden preset list:" , err )
}
}
} else {
if err := os . Remove ( info . Path ); err != nil {
dialog . ShowError ( err , w )
return
}
}
refreshPresetOptions ( "No Preset" )
})
removePresetBtn . Disable ()
refreshPresetOptions ( "No Preset" )
2026-02-05 15:11:55 -06:00
buildingsTab := container . NewTabItem ( "Buildings" , container . NewVBox (
numBuildingsSlider ,
minBuildingSizeSlider ,
maxBuildingSizeSlider ,
buildingDistributionSlider ,
buildingShapeLabel ,
buildingShapeSelect ,
2026-02-06 10:21:40 -06:00
proceduralContainer ,
2026-02-06 09:01:09 -06:00
ratioContainer ,
2026-02-05 15:11:55 -06:00
))
2026-01-26 14:29:05 -06:00
imageTab := container . NewTabItem ( "Image" , container . NewVBox (
2026-03-11 13:45:38 -05:00
widget . NewLabel ( "Preset:" ),
presetSelect ,
2026-03-11 14:24:18 -05:00
container . NewGridWithColumns ( 2 , addPresetBtn , removePresetBtn ),
2026-03-11 13:45:38 -05:00
widget . NewSeparator (),
2026-01-26 14:29:05 -06:00
widget . NewLabel ( "Width:" ),
2026-02-04 14:19:18 -06:00
widthEntry , widget . NewLabel ( "Height:" ),
2026-01-26 14:29:05 -06:00
heightEntry ,
2026-01-27 15:16:23 -06:00
widget . NewLabel ( "Seed:" ),
seedEntry ,
randomizeBtn ,
2026-01-26 14:29:05 -06:00
generateBtn ,
2026-01-27 15:16:23 -06:00
errorLabel ,
2026-02-13 09:29:06 -06:00
progressContainer ,
2026-02-02 12:24:14 -06:00
widget . NewSeparator (),
2026-02-02 12:40:25 -06:00
exportCanvasBtn ,
exportHeightmapBtn ,
2026-02-09 10:13:36 -06:00
exportBumpmapBtn ,
2026-02-02 13:26:28 -06:00
exportMasksBtn ,
2026-03-11 13:08:40 -05:00
exportSettingsBtn ,
importSettingsBtn ,
2026-01-26 14:29:05 -06:00
))
2026-02-05 17:50:55 -06:00
// Create the main layout using a horizontal split
2026-01-26 14:29:05 -06:00
tabs := container . NewAppTabs (
imageTab ,
terrainTab ,
2026-02-02 10:15:26 -06:00
waterTab ,
2026-02-04 14:19:18 -06:00
roadsTab ,
2026-03-02 10:40:14 -06:00
fortificationsTab ,
2026-02-05 15:11:55 -06:00
buildingsTab ,
2026-01-26 14:29:05 -06:00
)
left := container . NewVBox (
2026-02-02 10:15:26 -06:00
widget . NewLabel ( "RPG City Maker Reborn" ),
2026-01-26 14:29:05 -06:00
tabs ,
)
2026-02-09 10:35:28 -06:00
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 ()
2026-01-26 14:29:05 -06:00
split := container . NewHSplit (
left ,
right ,
)
split . SetOffset ( 0.3 )
2026-02-05 17:50:55 -06:00
// Set the window content and start the application
2026-01-26 14:29:05 -06:00
w . SetContent ( split )
w . ShowAndRun ()
}
2026-02-05 17:50:55 -06:00
2026-02-26 13:39:39 -06:00
func encodeImageToWriter ( w io . Writer , img image . Image , format string ) error {
2026-02-02 13:26:28 -06:00
switch format {
case "PNG" :
2026-02-26 13:39:39 -06:00
return png . Encode ( w , img )
2026-02-02 13:26:28 -06:00
case "JPG" :
2026-02-26 13:39:39 -06:00
return jpeg . Encode ( w , img , nil )
2026-02-02 13:26:28 -06:00
case "WEBP" :
2026-02-26 13:39:39 -06:00
return webp . Encode ( w , img , & webp . Options { Lossless : true })
2026-02-02 13:26:28 -06:00
}
2026-02-26 13:39:39 -06:00
return fmt . Errorf ( "unsupported format: %s" , format )
2026-02-02 13:26:28 -06:00
}
2026-02-05 17:50:55 -06:00
// showMasksSaveDialog displays a dialog for saving the generated masks.
2026-03-02 12:50:57 -06:00
func showMasksSaveDialog ( win fyne . Window , canvasImg , heightmapImg , bumpmapImg image . Image , settings * Settings , lakes [][] image . Point , riverMask , treeMask , roadMask , bridgeMask , wallMask , turretMask , buildingMask * PixelMask ) {
2026-02-05 17:50:55 -06:00
// Create UI elements for the save dialog
2026-02-02 13:26:28 -06:00
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 ()
2026-02-26 13:39:39 -06:00
maskToGray := func ( mask * PixelMask ) image . Image {
2026-02-26 13:03:48 -06:00
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
}
2026-02-26 13:39:39 -06:00
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 })
}
2026-02-02 13:26:28 -06:00
}
2026-02-26 13:39:39 -06:00
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 ) }},
2026-03-02 10:40:14 -06:00
{ name : "walls_mask." + imgFormat , make : func () image . Image { return maskToGray ( wallMask ) }},
2026-03-02 12:50:57 -06:00
{ name : "turrets_mask." + imgFormat , make : func () image . Image { return maskToGray ( turretMask ) }},
2026-02-26 13:39:39 -06:00
{ name : "buildings_mask." + imgFormat , make : func () image . Image { return maskToGray ( buildingMask ) }},
2026-02-02 13:26:28 -06:00
}
2026-02-05 17:50:55 -06:00
// Save the images based on the selected packaging option
2026-02-02 13:26:28 -06:00
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
2026-02-26 13:39:39 -06:00
for _ , item := range items {
saveImage ( item . make (), filepath . Join ( exportPath , item . name ))
2026-02-02 13:26:28 -06:00
}
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 ()
2026-02-26 13:39:39 -06:00
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 )
2026-02-02 13:26:28 -06:00
continue
}
hdr := & tar . Header {
2026-02-26 13:39:39 -06:00
Name : item . name ,
2026-02-02 13:26:28 -06:00
Mode : 0644 ,
Size : int64 ( buf . Len ()),
}
if err := tw . WriteHeader ( hdr ); err != nil {
2026-02-26 13:39:39 -06:00
log . Printf ( "Error writing tar header for %s: %v\n" , item . name , err )
2026-02-02 13:26:28 -06:00
continue
}
if _ , err := tw . Write ( buf . Bytes ()); err != nil {
2026-02-26 13:39:39 -06:00
log . Printf ( "Error writing tar data for %s: %v\n" , item . name , err )
2026-02-02 13:26:28 -06:00
}
}
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 ()
2026-02-26 13:39:39 -06:00
for _ , item := range items {
f , err := zw . Create ( item . name )
2026-02-02 13:26:28 -06:00
if err != nil {
2026-02-26 13:39:39 -06:00
log . Printf ( "Error creating zip entry for %s: %v\n" , item . name , err )
2026-02-02 13:26:28 -06:00
continue
}
2026-02-26 13:39:39 -06:00
if err := encodeImageToWriter ( f , item . make (), formatSelect . Selected ); err != nil {
log . Printf ( "Error writing zip data for %s: %v\n" , item . name , err )
2026-02-02 13:26:28 -06:00
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 ()
}
2026-02-05 17:50:55 -06:00
// saveImage saves an image to the specified path.
2026-02-02 13:26:28 -06:00
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 )
}
}
2026-02-02 12:24:14 -06:00
2026-03-11 13:08:40 -05:00
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 )
}
2026-02-05 17:50:55 -06:00
// showSaveDialog displays a dialog for saving an image.
2026-02-02 12:24:14 -06:00
func showSaveDialog ( win fyne . Window , img image . Image , settings * Settings ) {
2026-02-05 17:50:55 -06:00
// Create UI elements for the save dialog
2026-02-02 12:24:14 -06:00
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 )
2026-02-02 13:26:28 -06:00
saveImage ( img , filePath )
2026-02-02 12:24:14 -06:00
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 ()
}