diff --git a/main.go b/main.go index cfd89a5..28b8bcf 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,21 @@ package main import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" "fmt" "image" "image/color" - "log" - "strconv" - "time" - + "image/jpeg" "image/png" + "log" "os" "path/filepath" - - "image/jpeg" + "strconv" "strings" + "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" @@ -46,15 +48,17 @@ func main() { FillMode: canvas.ImageFillContain, } + var lakes [][]image.Point + var riverPixels []image.Point + var treePixels []image.Point + 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") - w.SetOnClosed(func() { if err := settings.Save(); err != nil { log.Println("Error saving settings:", err) @@ -84,7 +88,6 @@ func main() { } } }) - canvasFile, err := os.Open(canvasPath) if err == nil { defer canvasFile.Close() @@ -106,8 +109,10 @@ func main() { if canvasImg.Image == nil || heightmapImg.Image == nil { // Initial image generation noiseImg := GenerateHeightmap(settings.Width, settings.Height, int(settings.Detail), 100.0, settings.Seed) - lakeImage, lakes := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed) - riverImage, riverPixels := GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, settings.Seed, noiseImg) + var lakeImage image.Image + lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed) + var riverImage image.Image + riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, settings.Seed, noiseImg) finalImage := riverImage.(*image.RGBA) var flatLakePixels []image.Point @@ -116,7 +121,7 @@ func main() { } allWaterPixels := append(flatLakePixels, riverPixels...) - GenerateTrees(finalImage, allWaterPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) + treePixels = GenerateTrees(finalImage, allWaterPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) darkenedHeightmap := DarkenLakeAreas(noiseImg, allWaterPixels) compositeImg := ApplyRoughness(darkenedHeightmap, settings.Roughness) heightmapImg.Image = compositeImg @@ -266,6 +271,9 @@ func main() { exportHeightmapBtn := widget.NewButton("Export Heightmap", func() { showSaveDialog(w, heightmapImg.Image, settings) }) + exportMasksBtn := widget.NewButton("Export Masks", func() { + showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, settings, lakes, riverPixels, treePixels) + }) generateBtn = widget.NewButton("Generate", func() { go func() { @@ -295,7 +303,8 @@ func main() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Lakes", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) - lakeImage, lakes := GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed) + var lakeImage image.Image + lakeImage, lakes = GenerateLakes(settings.Width, settings.Height, settings.Lakes, settings.LakeSizeLower, settings.LakeSizeUpper, noiseImg, settings.Seed) // Step 3: Generating Rivers currentStep++ @@ -303,7 +312,8 @@ func main() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Rivers", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) - riverImage, riverPixels := GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, settings.Seed, noiseImg) + var riverImage image.Image + riverImage, riverPixels = GenerateRivers(settings.Width, settings.Height, settings.Rivers, settings.MinRiverWidth, settings.MaxRiverWidth, settings.RiverCurvyness, lakeImage, lakes, settings.Seed, noiseImg) finalImage := riverImage.(*image.RGBA) var flatLakePixels []image.Point @@ -334,7 +344,7 @@ func main() { progressBar.SetText(fmt.Sprintf("Step %d/%d: Generating Trees", currentStep, steps)) progressBar.SetValue(float64(currentStep) / float64(steps)) }) - GenerateTrees(finalImage, allWaterPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) + treePixels = GenerateTrees(finalImage, allWaterPixels, settings.MinTreeSize, settings.MaxTreeSize, settings.TreeCoverage, settings.TreeClumpiness, settings.Seed) // Step 7: Finalizing Images currentStep++ @@ -452,6 +462,7 @@ func main() { widget.NewSeparator(), exportCanvasBtn, exportHeightmapBtn, + exportMasksBtn, )) tabs := container.NewAppTabs( @@ -479,6 +490,209 @@ func main() { w.SetContent(split) w.ShowAndRun() } +func getImageData(img image.Image, format string) (*bytes.Buffer, error) { + buf := new(bytes.Buffer) + var err error + switch format { + case "PNG": + err = png.Encode(buf, img) + case "JPG": + err = jpeg.Encode(buf, img, nil) + case "WEBP": + err = webp.Encode(buf, img, &webp.Options{Lossless: true}) + } + return buf, err +} + +func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg image.Image, settings *Settings, lakes [][]image.Point, riverPixels, treePixels []image.Point) { + fileNameEntry := widget.NewEntry() + fileNameEntry.SetPlaceHolder("masks_folder") + + formatSelect := widget.NewSelect([]string{"PNG", "JPG", "WEBP"}, nil) + formatSelect.SetSelected("PNG") + + packageSelect := widget.NewSelect([]string{"Folder", "tar.gz", "zip"}, nil) + packageSelect.SetSelected("Folder") + + pathLabel := widget.NewLabel(settings.LastExportPath) + + browseBtn := widget.NewButton("Browse", func() { + dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { + if err != nil { + log.Println("Error opening folder dialog:", err) + return + } + if uri == nil { + return + } + settings.LastExportPath = uri.Path() + pathLabel.SetText(settings.LastExportPath) + }, win) + }) + + content := container.NewVBox( + widget.NewLabel("Save to:"), + container.NewBorder(nil, nil, nil, browseBtn, pathLabel), + widget.NewLabel("Folder Name:"), + fileNameEntry, + widget.NewLabel("Format:"), + formatSelect, + widget.NewLabel("Packaging:"), + packageSelect, + ) + + saveDialog := dialog.NewCustom("Export Masks", "Cancel", content, win) + + saveBtn := widget.NewButton("Save", func() { + folderName := fileNameEntry.Text + if folderName == "" { + return + } + imgFormat := strings.ToLower(formatSelect.Selected) + bounds := canvasImg.Bounds() + + // Create mask images + lakeMask := image.NewGray(bounds) + for _, lake := range lakes { + for _, p := range lake { + lakeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) + } + } + riverMask := image.NewGray(bounds) + for _, p := range riverPixels { + riverMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) + } + treeMask := image.NewGray(bounds) + for _, p := range treePixels { + treeMask.SetGray(p.X, p.Y, color.Gray{Y: 255}) + } + + imagesToSave := map[string]image.Image{ + "canvas." + imgFormat: canvasImg, + "heightmap." + imgFormat: heightmapImg, + "lakes_mask." + imgFormat: lakeMask, + "rivers_mask." + imgFormat: riverMask, + "trees_mask." + imgFormat: treeMask, + } + + switch packageSelect.Selected { + case "Folder": + exportPath := filepath.Join(pathLabel.Text, folderName) + if err := os.MkdirAll(exportPath, 0755); err != nil { + log.Println("Error creating directory:", err) + return + } + settings.LastExportPath = exportPath + for name, img := range imagesToSave { + saveImage(img, filepath.Join(exportPath, name)) + } + case "tar.gz": + filePath := filepath.Join(pathLabel.Text, folderName+".tar.gz") + settings.LastExportPath = filepath.Dir(filePath) + file, err := os.Create(filePath) + if err != nil { + log.Println("Error creating archive:", err) + return + } + defer file.Close() + + gw := gzip.NewWriter(file) + defer gw.Close() + tw := tar.NewWriter(gw) + defer tw.Close() + + for name, img := range imagesToSave { + buf, err := getImageData(img, formatSelect.Selected) + if err != nil { + log.Printf("Error encoding image %s: %v\n", name, err) + continue + } + hdr := &tar.Header{ + Name: name, + Mode: 0644, + Size: int64(buf.Len()), + } + if err := tw.WriteHeader(hdr); err != nil { + log.Printf("Error writing tar header for %s: %v\n", name, err) + continue + } + if _, err := tw.Write(buf.Bytes()); err != nil { + log.Printf("Error writing tar data for %s: %v\n", name, err) + } + } + case "zip": + filePath := filepath.Join(pathLabel.Text, folderName+".zip") + settings.LastExportPath = filepath.Dir(filePath) + file, err := os.Create(filePath) + if err != nil { + log.Println("Error creating archive:", err) + return + } + defer file.Close() + + zw := zip.NewWriter(file) + defer zw.Close() + + for name, img := range imagesToSave { + buf, err := getImageData(img, formatSelect.Selected) + if err != nil { + log.Printf("Error encoding image %s: %v\n", name, err) + continue + } + f, err := zw.Create(name) + if err != nil { + log.Printf("Error creating zip entry for %s: %v\n", name, err) + continue + } + if _, err := f.Write(buf.Bytes()); err != nil { + log.Printf("Error writing zip data for %s: %v\n", name, err) + } + } + } + + saveDialog.Hide() + }) + saveBtn.Disable() + + fileNameEntry.OnChanged = func(text string) { + if text == "" { + saveBtn.Disable() + } else { + saveBtn.Enable() + } + } + + saveDialog.SetButtons([]fyne.CanvasObject{ + widget.NewButton("Cancel", func() { + saveDialog.Hide() + }), + saveBtn, + }) + + saveDialog.Show() +} + +func saveImage(img image.Image, path string) { + file, err := os.Create(path) + if err != nil { + log.Println("Error creating file:", err) + return + } + defer file.Close() + + ext := strings.ToLower(filepath.Ext(path)) + switch ext { + case ".png": + err = png.Encode(file, img) + case ".jpg": + err = jpeg.Encode(file, img, nil) + case ".webp": + err = webp.Encode(file, img, &webp.Options{Lossless: true}) + } + if err != nil { + log.Println("Failed to encode image:", err) + } +} func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) { fileNameEntry := widget.NewEntry() @@ -522,25 +736,7 @@ func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) { filePath := filepath.Join(pathLabel.Text, fileName+"."+strings.ToLower(formatSelect.Selected)) settings.LastExportPath = filepath.Dir(filePath) - - file, err := os.Create(filePath) - if err != nil { - log.Println("Error creating file:", err) - return - } - defer file.Close() - - switch formatSelect.Selected { - 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) - } + saveImage(img, filePath) saveDialog.Hide() }) saveBtn.Disable() diff --git a/terrain.go b/terrain.go index 69ed68c..deb48af 100644 --- a/terrain.go +++ b/terrain.go @@ -85,25 +85,25 @@ func DarkenLakeAreas(heightmap image.Image, lakePixels []image.Point) image.Imag return composite } -func GenerateTrees(img *image.RGBA, lakePixels []image.Point, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) { +func GenerateTrees(img *image.RGBA, lakePixels []image.Point, minTreeSize, maxTreeSize, treeCoverage, treeClumpiness float64, seed int64) []image.Point { width := img.Bounds().Dx() height := img.Bounds().Dy() // 1. Calculate number of trees to place from coverage %. avgTreeSize := (minTreeSize + maxTreeSize) / 2 if avgTreeSize <= 0 { - return + return nil } avgRadius := avgTreeSize / 2 avgTreeArea := math.Pi * avgRadius * avgRadius if avgTreeArea == 0 { - return + return nil } totalArea := float64(width * height) targetTreePixels := totalArea * (treeCoverage / 100.0) numTreesToPlace := int(targetTreePixels / avgTreeArea) if numTreesToPlace == 0 { - return + return nil } // 2. Generate a simplex noise map for tree placement. @@ -146,6 +146,7 @@ func GenerateTrees(img *image.RGBA, lakePixels []image.Point, minTreeSize, maxTr return treeNoiseMap.GrayAt(p.X, p.Y).Y >= threshold && !isLake[p] }, seed) + var treePixels []image.Point // 5. Draw the trees. for _, p := range allPoints { size := minTreeSize + randSrc.Float64()*(maxTreeSize-minTreeSize) @@ -165,10 +166,12 @@ func GenerateTrees(img *image.RGBA, lakePixels []image.Point, minTreeSize, maxTr // Blend the tree color with the background // For simplicity, we just set a solid color for now. img.Set(x, y, color.RGBA{R: 0, G: 100, B: 0, A: 255}) + treePixels = append(treePixels, pt) } } } } + return treePixels } func poissonDiscSampling(width, height int, minRadius float64, k int, initialPoints []image.Point, isValid func(image.Point) bool, seed int64) []image.Point {