added requested add text feature
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/font/opentype"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
const (
|
||||
minTextSizePercent = 0.5
|
||||
maxTextSizePercent = 12.0
|
||||
textSizePercentStep = 0.1
|
||||
)
|
||||
|
||||
type SystemFont struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
type TextPlacementRequest struct {
|
||||
Content string
|
||||
FontName string
|
||||
FontPath string
|
||||
SizePercent float64
|
||||
ColorName string
|
||||
}
|
||||
|
||||
type PlacedText struct {
|
||||
TextPlacementRequest
|
||||
Position image.Point
|
||||
}
|
||||
|
||||
type PendingTextPlacement struct {
|
||||
TextPlacementRequest
|
||||
Position image.Point
|
||||
Visible bool
|
||||
Preview *image.RGBA
|
||||
}
|
||||
|
||||
type TextLayer struct {
|
||||
Width int
|
||||
Height int
|
||||
Items []PlacedText
|
||||
Image *image.RGBA
|
||||
Mask *PixelMask
|
||||
}
|
||||
|
||||
var (
|
||||
systemFontsOnce sync.Once
|
||||
cachedFonts []SystemFont
|
||||
parsedFontMu sync.Mutex
|
||||
parsedFontByPath = map[string]*opentype.Font{}
|
||||
)
|
||||
|
||||
func loadSystemFonts() []SystemFont {
|
||||
systemFontsOnce.Do(func() {
|
||||
seen := map[string]bool{}
|
||||
for _, root := range fontSearchRoots() {
|
||||
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil || d == nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".ttf" && ext != ".otf" && ext != ".ttc" {
|
||||
return nil
|
||||
}
|
||||
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
key := strings.ToLower(name + "|" + path)
|
||||
if seen[key] {
|
||||
return nil
|
||||
}
|
||||
seen[key] = true
|
||||
cachedFonts = append(cachedFonts, SystemFont{Name: name, Path: path})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
sort.Slice(cachedFonts, func(i, j int) bool {
|
||||
return strings.ToLower(cachedFonts[i].Name) < strings.ToLower(cachedFonts[j].Name)
|
||||
})
|
||||
if len(cachedFonts) == 0 {
|
||||
cachedFonts = []SystemFont{{Name: "Default", Path: ""}}
|
||||
}
|
||||
})
|
||||
out := make([]SystemFont, len(cachedFonts))
|
||||
copy(out, cachedFonts)
|
||||
return out
|
||||
}
|
||||
|
||||
func fontSearchRoots() []string {
|
||||
home, _ := os.UserHomeDir()
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
winDir := os.Getenv("WINDIR")
|
||||
if winDir == "" {
|
||||
winDir = `C:\\Windows`
|
||||
}
|
||||
return []string{filepath.Join(winDir, "Fonts")}
|
||||
case "darwin":
|
||||
roots := []string{"/System/Library/Fonts", "/Library/Fonts"}
|
||||
if home != "" {
|
||||
roots = append(roots, filepath.Join(home, "Library", "Fonts"))
|
||||
}
|
||||
return roots
|
||||
default:
|
||||
roots := []string{"/usr/share/fonts", "/usr/local/share/fonts"}
|
||||
if home != "" {
|
||||
roots = append(roots, filepath.Join(home, ".fonts"), filepath.Join(home, ".local", "share", "fonts"))
|
||||
}
|
||||
return roots
|
||||
}
|
||||
}
|
||||
|
||||
func newTextLayer(width, height int) *TextLayer {
|
||||
return &TextLayer{
|
||||
Width: width,
|
||||
Height: height,
|
||||
Image: image.NewRGBA(image.Rect(0, 0, width, height)),
|
||||
Mask: NewPixelMask(width, height),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *TextLayer) Clear(width, height int) {
|
||||
l.Width = width
|
||||
l.Height = height
|
||||
l.Items = nil
|
||||
l.Image = image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
l.Mask = NewPixelMask(width, height)
|
||||
}
|
||||
|
||||
func (l *TextLayer) Add(item PlacedText) error {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
l.Items = append(l.Items, item)
|
||||
return l.rebuild()
|
||||
}
|
||||
|
||||
func (l *TextLayer) rebuild() error {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
l.Image = image.NewRGBA(image.Rect(0, 0, l.Width, l.Height))
|
||||
l.Mask = NewPixelMask(l.Width, l.Height)
|
||||
for _, item := range l.Items {
|
||||
if err := drawPlacedText(l.Image, l.Mask, item, 255, l.Width, l.Height); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func composeCanvasWithText(base image.Image, layer *TextLayer, pending *PendingTextPlacement) image.Image {
|
||||
if base == nil {
|
||||
return nil
|
||||
}
|
||||
bounds := base.Bounds()
|
||||
out := cloneToRGBA(base, bounds.Dx(), bounds.Dy())
|
||||
if layer != nil && layer.Image != nil {
|
||||
draw.Draw(out, out.Bounds(), layer.Image, image.Point{}, draw.Over)
|
||||
}
|
||||
if pending != nil && pending.Visible && pending.Preview == nil && strings.TrimSpace(pending.Content) != "" {
|
||||
_ = drawPlacedText(out, nil, PlacedText{TextPlacementRequest: pending.TextPlacementRequest, Position: pending.Position}, 128, bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func renderTextSprite(req TextPlacementRequest, alpha uint8, width, height int) (*image.RGBA, error) {
|
||||
if strings.TrimSpace(req.Content) == "" {
|
||||
return image.NewRGBA(image.Rect(0, 0, 1, 1)), nil
|
||||
}
|
||||
face, err := loadFontFace(req.FontPath, textSizePixels(req.SizePercent, width, height))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics := face.Metrics()
|
||||
spriteW := max(1, font.MeasureString(face, req.Content).Ceil())
|
||||
spriteH := max(1, (metrics.Ascent + metrics.Descent).Ceil())
|
||||
sprite := image.NewRGBA(image.Rect(0, 0, spriteW, spriteH))
|
||||
col := textColorByName(req.ColorName)
|
||||
col.A = alpha
|
||||
d := &font.Drawer{
|
||||
Dst: sprite,
|
||||
Src: image.NewUniform(col),
|
||||
Face: face,
|
||||
Dot: fixed.P(0, metrics.Ascent.Ceil()),
|
||||
}
|
||||
d.DrawString(req.Content)
|
||||
return sprite, nil
|
||||
}
|
||||
|
||||
func drawPlacedText(dst *image.RGBA, mask *PixelMask, item PlacedText, alpha uint8, width, height int) error {
|
||||
if dst == nil || strings.TrimSpace(item.Content) == "" {
|
||||
return nil
|
||||
}
|
||||
face, err := loadFontFace(item.FontPath, textSizePixels(item.SizePercent, width, height))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
temp := image.NewRGBA(dst.Bounds())
|
||||
col := textColorByName(item.ColorName)
|
||||
col.A = alpha
|
||||
d := &font.Drawer{
|
||||
Dst: temp,
|
||||
Src: image.NewUniform(col),
|
||||
Face: face,
|
||||
Dot: fixed.P(item.Position.X, item.Position.Y+face.Metrics().Ascent.Ceil()),
|
||||
}
|
||||
d.DrawString(item.Content)
|
||||
draw.Draw(dst, dst.Bounds(), temp, image.Point{}, draw.Over)
|
||||
|
||||
if mask != nil {
|
||||
for y := 0; y < temp.Bounds().Dy(); y++ {
|
||||
for x := 0; x < temp.Bounds().Dx(); x++ {
|
||||
_, _, _, a := temp.At(x, y).RGBA()
|
||||
if a != 0 {
|
||||
mask.SetXY(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func textSizePixels(sizePercent float64, width, height int) float64 {
|
||||
sizePercent = clamp(sizePercent, minTextSizePercent, maxTextSizePercent)
|
||||
avgDim := averageImageDimension(width, height)
|
||||
if avgDim < 1 {
|
||||
avgDim = 1
|
||||
}
|
||||
sizePx := (sizePercent / 100.0) * avgDim
|
||||
if sizePx < 1 {
|
||||
sizePx = 1
|
||||
}
|
||||
return sizePx
|
||||
}
|
||||
|
||||
func loadFontFace(path string, size float64) (font.Face, error) {
|
||||
if size < 1 {
|
||||
size = 1
|
||||
}
|
||||
if path == "" {
|
||||
return basicfont.Face7x13, nil
|
||||
}
|
||||
parsedFontMu.Lock()
|
||||
parsed := parsedFontByPath[path]
|
||||
parsedFontMu.Unlock()
|
||||
if parsed == nil {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return basicfont.Face7x13, nil
|
||||
}
|
||||
fontFile, err := opentype.Parse(data)
|
||||
if err != nil {
|
||||
return basicfont.Face7x13, nil
|
||||
}
|
||||
parsedFontMu.Lock()
|
||||
parsedFontByPath[path] = fontFile
|
||||
parsed = fontFile
|
||||
parsedFontMu.Unlock()
|
||||
}
|
||||
face, err := opentype.NewFace(parsed, &opentype.FaceOptions{Size: size, DPI: 72, Hinting: font.HintingFull})
|
||||
if err != nil {
|
||||
return basicfont.Face7x13, nil
|
||||
}
|
||||
return face, nil
|
||||
}
|
||||
|
||||
func textColorByName(name string) color.RGBA {
|
||||
switch strings.ToLower(name) {
|
||||
case "white":
|
||||
return color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
case "grey", "gray":
|
||||
return color.RGBA{R: 140, G: 140, B: 140, A: 255}
|
||||
default:
|
||||
return color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
||||
}
|
||||
}
|
||||
|
||||
func mapCanvasPositionToImagePoint(pos fyne.Position, widgetSize fyne.Size, img image.Image) (image.Point, bool) {
|
||||
if img == nil {
|
||||
return image.Point{}, false
|
||||
}
|
||||
offsetX, offsetY, drawW, drawH, ok := containedImageRect(widgetSize, img)
|
||||
if !ok {
|
||||
return image.Point{}, false
|
||||
}
|
||||
if pos.X < offsetX || pos.Y < offsetY || pos.X > offsetX+drawW || pos.Y > offsetY+drawH {
|
||||
return image.Point{}, false
|
||||
}
|
||||
relX := (pos.X - offsetX) / drawW
|
||||
relY := (pos.Y - offsetY) / drawH
|
||||
bounds := img.Bounds()
|
||||
x := int(math.Round(float64(relX * float32(bounds.Dx()))))
|
||||
y := int(math.Round(float64(relY * float32(bounds.Dy()))))
|
||||
if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
if y < 0 {
|
||||
y = 0
|
||||
}
|
||||
if x >= bounds.Dx() {
|
||||
x = bounds.Dx() - 1
|
||||
}
|
||||
if y >= bounds.Dy() {
|
||||
y = bounds.Dy() - 1
|
||||
}
|
||||
return image.Point{X: x, Y: y}, true
|
||||
}
|
||||
|
||||
func mapImagePointToCanvasRect(point image.Point, sprite image.Image, widgetSize fyne.Size, base image.Image) (fyne.Position, fyne.Size, bool) {
|
||||
offsetX, offsetY, drawW, drawH, ok := containedImageRect(widgetSize, base)
|
||||
if !ok || sprite == nil {
|
||||
return fyne.Position{}, fyne.Size{}, false
|
||||
}
|
||||
baseBounds := base.Bounds()
|
||||
scaleX := drawW / float32(baseBounds.Dx())
|
||||
scaleY := drawH / float32(baseBounds.Dy())
|
||||
pos := fyne.NewPos(offsetX+float32(point.X)*scaleX, offsetY+float32(point.Y)*scaleY)
|
||||
size := fyne.NewSize(float32(sprite.Bounds().Dx())*scaleX, float32(sprite.Bounds().Dy())*scaleY)
|
||||
return pos, size, true
|
||||
}
|
||||
|
||||
func containedImageRect(widgetSize fyne.Size, img image.Image) (float32, float32, float32, float32, bool) {
|
||||
if img == nil {
|
||||
return 0, 0, 0, 0, false
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
imgW := float32(bounds.Dx())
|
||||
imgH := float32(bounds.Dy())
|
||||
if imgW <= 0 || imgH <= 0 || widgetSize.Width <= 0 || widgetSize.Height <= 0 {
|
||||
return 0, 0, 0, 0, false
|
||||
}
|
||||
imgAspect := imgW / imgH
|
||||
widgetAspect := widgetSize.Width / widgetSize.Height
|
||||
drawW := widgetSize.Width
|
||||
drawH := widgetSize.Height
|
||||
offsetX := float32(0)
|
||||
offsetY := float32(0)
|
||||
if widgetAspect > imgAspect {
|
||||
drawW = widgetSize.Height * imgAspect
|
||||
offsetX = (widgetSize.Width - drawW) * 0.5
|
||||
} else {
|
||||
drawH = widgetSize.Width / imgAspect
|
||||
offsetY = (widgetSize.Height - drawH) * 0.5
|
||||
}
|
||||
return offsetX, offsetY, drawW, drawH, true
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/data/binding"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
@@ -242,11 +243,14 @@ func estimateWallTangent(mid image.Point, wallMask *PixelMask) (float64, float64
|
||||
type tappableImage struct {
|
||||
widget.BaseWidget
|
||||
image *fyne.Container
|
||||
onTapped func()
|
||||
onTapped func(*fyne.PointEvent)
|
||||
onSecondaryTap func(*fyne.PointEvent)
|
||||
onHover func(fyne.Position)
|
||||
onHoverEnd func()
|
||||
}
|
||||
|
||||
// newTappableImage wraps a container so taps can trigger image interactions.
|
||||
func newTappableImage(img *fyne.Container, tapped func()) *tappableImage {
|
||||
func newTappableImage(img *fyne.Container, tapped func(*fyne.PointEvent)) *tappableImage {
|
||||
ti := &tappableImage{
|
||||
image: img,
|
||||
onTapped: tapped,
|
||||
@@ -261,9 +265,33 @@ func (t *tappableImage) CreateRenderer() fyne.WidgetRenderer {
|
||||
}
|
||||
|
||||
// Tapped invokes the handler and is used for click interactions.
|
||||
func (t *tappableImage) Tapped(*fyne.PointEvent) {
|
||||
func (t *tappableImage) Tapped(ev *fyne.PointEvent) {
|
||||
if t.onTapped != nil {
|
||||
t.onTapped()
|
||||
t.onTapped(ev)
|
||||
}
|
||||
}
|
||||
|
||||
// TappedSecondary invokes the secondary handler and is used for right-click interactions.
|
||||
func (t *tappableImage) TappedSecondary(ev *fyne.PointEvent) {
|
||||
if t.onSecondaryTap != nil {
|
||||
t.onSecondaryTap(ev)
|
||||
}
|
||||
}
|
||||
|
||||
// MouseMoved forwards hover updates and is used for preview positioning.
|
||||
func (t *tappableImage) MouseMoved(ev *desktop.MouseEvent) {
|
||||
if t.onHover != nil {
|
||||
t.onHover(ev.Position)
|
||||
}
|
||||
}
|
||||
|
||||
// MouseIn is required by desktop.Hoverable.
|
||||
func (t *tappableImage) MouseIn(*desktop.MouseEvent) {}
|
||||
|
||||
// MouseOut clears hover state and is used when previews leave the canvas.
|
||||
func (t *tappableImage) MouseOut() {
|
||||
if t.onHoverEnd != nil {
|
||||
t.onHoverEnd()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ func main() {
|
||||
canvasImg := &canvas.Image{
|
||||
FillMode: canvas.ImageFillContain,
|
||||
}
|
||||
textPreviewImg := &canvas.Image{
|
||||
FillMode: canvas.ImageFillStretch,
|
||||
}
|
||||
textPreviewImg.Hide()
|
||||
textPreviewLayer := container.NewWithoutLayout(textPreviewImg)
|
||||
heightmapImg := &canvas.Image{
|
||||
FillMode: canvas.ImageFillContain,
|
||||
}
|
||||
@@ -69,6 +74,22 @@ func main() {
|
||||
var exitRoadMask *PixelMask
|
||||
var wallMask *PixelMask
|
||||
var turretMask *PixelMask
|
||||
var canvasBaseImage image.Image
|
||||
textLayer := newTextLayer(settings.Width, settings.Height)
|
||||
var pendingText *PendingTextPlacement
|
||||
refreshCanvasDisplay := func() {
|
||||
if canvasBaseImage == nil {
|
||||
canvasImg.Image = nil
|
||||
} else {
|
||||
canvasImg.Image = composeCanvasWithText(canvasBaseImage, textLayer, nil)
|
||||
}
|
||||
canvasImg.Refresh()
|
||||
}
|
||||
hideTextPreview := func() {
|
||||
textPreviewImg.Image = nil
|
||||
textPreviewImg.Hide()
|
||||
textPreviewImg.Refresh()
|
||||
}
|
||||
|
||||
// Set up application configuration directory
|
||||
configDir, err := os.UserConfigDir()
|
||||
@@ -88,7 +109,7 @@ func main() {
|
||||
}
|
||||
closing = true
|
||||
|
||||
canvasSnapshot := canvasImg.Image
|
||||
canvasSnapshot := canvasBaseImage
|
||||
heightmapSnapshot := heightmapImg.Image
|
||||
bumpmapSnapshot := bumpmapImg.Image
|
||||
|
||||
@@ -127,7 +148,10 @@ func main() {
|
||||
defer canvasFile.Close()
|
||||
img, err := png.Decode(canvasFile)
|
||||
if err == nil {
|
||||
canvasImg.Image = img
|
||||
canvasBaseImage = img
|
||||
textLayer.Clear(img.Bounds().Dx(), img.Bounds().Dy())
|
||||
hideTextPreview()
|
||||
refreshCanvasDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +375,11 @@ func main() {
|
||||
heightmapImg.Image = compositeImg
|
||||
bumpmapImg.Image = bumpMap
|
||||
|
||||
canvasImg.Image = finalImage
|
||||
canvasBaseImage = finalImage
|
||||
textLayer.Clear(settings.Width, settings.Height)
|
||||
pendingText = nil
|
||||
hideTextPreview()
|
||||
refreshCanvasDisplay()
|
||||
|
||||
}
|
||||
var generateBtn *widget.Button
|
||||
@@ -845,7 +873,7 @@ func main() {
|
||||
showSaveDialog(w, bumpmapImg.Image, settings)
|
||||
})
|
||||
exportMasksBtn := widget.NewButton("Export Masks", func() {
|
||||
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverMask, treeMask, roadMask, bridgeMask, wallMask, turretMask, buildingMask)
|
||||
showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverMask, treeMask, roadMask, bridgeMask, wallMask, turretMask, buildingMask, textLayer.Mask)
|
||||
})
|
||||
exportSettingsBtn := widget.NewButton("Export Settings", func() {
|
||||
showSettingsExportDialog(w, settings)
|
||||
@@ -1144,8 +1172,11 @@ func main() {
|
||||
heightmapImg.Refresh()
|
||||
bumpmapImg.Image = bumpMap
|
||||
bumpmapImg.Refresh()
|
||||
canvasImg.Image = finalImage
|
||||
canvasImg.Refresh()
|
||||
canvasBaseImage = finalImage
|
||||
textLayer.Clear(settings.Width, settings.Height)
|
||||
pendingText = nil
|
||||
hideTextPreview()
|
||||
refreshCanvasDisplay()
|
||||
if len(timedOutSteps) > 0 {
|
||||
errorLabel.SetText("Generation timed out after 1 minute for: " + strings.Join(timedOutSteps, ", ") + ". Partial results were used.")
|
||||
errorLabel.Show()
|
||||
@@ -1757,6 +1788,77 @@ func main() {
|
||||
removePresetBtn.Disable()
|
||||
refreshPresetOptions("No Preset")
|
||||
|
||||
textFonts := loadSystemFonts()
|
||||
textFontNames := make([]string, 0, len(textFonts))
|
||||
fontPathByName := make(map[string]string, len(textFonts))
|
||||
for _, fontInfo := range textFonts {
|
||||
textFontNames = append(textFontNames, fontInfo.Name)
|
||||
fontPathByName[fontInfo.Name] = fontInfo.Path
|
||||
}
|
||||
selectedTextFont := textFontNames[0]
|
||||
selectedTextColor := "black"
|
||||
textSizeSlider := newNumericInputSliderWithStep(minTextSizePercent, maxTextSizePercent, 2.0, textSizePercentStep, "%.1f%%", "Font Size")
|
||||
textFontLabel := widget.NewLabel("Font:")
|
||||
textFontSelect := widget.NewSelect(textFontNames, func(s string) {
|
||||
if s != "" {
|
||||
selectedTextFont = s
|
||||
}
|
||||
})
|
||||
textFontSelect.SetSelected(selectedTextFont)
|
||||
textColorLabel := widget.NewLabel("Font Color:")
|
||||
textColorSelect := widget.NewSelect([]string{"white", "grey", "black"}, func(s string) {
|
||||
if s != "" {
|
||||
selectedTextColor = s
|
||||
}
|
||||
})
|
||||
textColorSelect.SetSelected(selectedTextColor)
|
||||
textEntry := widget.NewEntry()
|
||||
textEntry.SetPlaceHolder("Enter text to place on the map")
|
||||
textInstructions := widget.NewLabel("Click Add to start placement. Move the mouse over the canvas image to preview the text at 50% opacity. Left click places it on the canvas. Right click cancels.")
|
||||
textInstructions.Wrapping = fyne.TextWrapWord
|
||||
addTextBtn := widget.NewButton("Add", func() {
|
||||
if canvasBaseImage == nil {
|
||||
textInstructions.SetText("Generate a canvas before placing text.")
|
||||
return
|
||||
}
|
||||
content := strings.TrimSpace(textEntry.Text)
|
||||
if content == "" {
|
||||
textInstructions.SetText("Enter text before clicking Add.")
|
||||
return
|
||||
}
|
||||
sizeVal, _ := textSizeSlider.value.Get()
|
||||
pendingText = &PendingTextPlacement{
|
||||
TextPlacementRequest: TextPlacementRequest{
|
||||
Content: content,
|
||||
FontName: selectedTextFont,
|
||||
FontPath: fontPathByName[selectedTextFont],
|
||||
SizePercent: sizeVal,
|
||||
ColorName: selectedTextColor,
|
||||
},
|
||||
Visible: false,
|
||||
}
|
||||
pendingText.Preview, _ = renderTextSprite(pendingText.TextPlacementRequest, 128, settings.Width, settings.Height)
|
||||
textInstructions.SetText("Placement active on the canvas. Left click places the text. Right click cancels.")
|
||||
hideTextPreview()
|
||||
})
|
||||
clearTextBtn := widget.NewButton("Clear Text", func() {
|
||||
textLayer.Clear(settings.Width, settings.Height)
|
||||
pendingText = nil
|
||||
textInstructions.SetText("All placed text has been cleared.")
|
||||
hideTextPreview()
|
||||
refreshCanvasDisplay()
|
||||
})
|
||||
textTab := container.NewTabItem("Text", container.NewVBox(
|
||||
textSizeSlider,
|
||||
textFontLabel,
|
||||
textFontSelect,
|
||||
textColorLabel,
|
||||
textColorSelect,
|
||||
container.NewBorder(nil, nil, nil, addTextBtn, textEntry),
|
||||
textInstructions,
|
||||
clearTextBtn,
|
||||
))
|
||||
|
||||
buildingsTab := container.NewTabItem("Buildings", container.NewVBox(
|
||||
numBuildingsSlider,
|
||||
minBuildingSizeSlider,
|
||||
@@ -1797,6 +1899,7 @@ func main() {
|
||||
waterTab,
|
||||
roadsTab,
|
||||
fortificationsTab,
|
||||
textTab,
|
||||
buildingsTab,
|
||||
)
|
||||
|
||||
@@ -1834,7 +1937,27 @@ func main() {
|
||||
right.Refresh()
|
||||
}
|
||||
|
||||
tappableCanvas = newTappableImage(container.NewMax(canvasImg), func() {
|
||||
tappableCanvas = newTappableImage(container.NewMax(canvasImg, textPreviewLayer), func(ev *fyne.PointEvent) {
|
||||
if pendingText != nil {
|
||||
point, ok := mapCanvasPositionToImagePoint(ev.Position, tappableCanvas.Size(), canvasBaseImage)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pendingText.Position = point
|
||||
pendingText.Visible = true
|
||||
if err := textLayer.Add(PlacedText{
|
||||
TextPlacementRequest: pendingText.TextPlacementRequest,
|
||||
Position: pendingText.Position,
|
||||
}); err != nil {
|
||||
textInstructions.SetText("Failed to place text with the selected font.")
|
||||
return
|
||||
}
|
||||
pendingText = nil
|
||||
textInstructions.SetText("Text placed on the canvas.")
|
||||
hideTextPreview()
|
||||
refreshCanvasDisplay()
|
||||
return
|
||||
}
|
||||
if settings.ImageViewState == 1 {
|
||||
settings.ImageViewState = 0
|
||||
} else {
|
||||
@@ -1842,7 +1965,45 @@ func main() {
|
||||
}
|
||||
updateRightPanel()
|
||||
})
|
||||
tappableHeightmap = newTappableImage(container.NewMax(heightmapImg), func() {
|
||||
tappableCanvas.onSecondaryTap = func(*fyne.PointEvent) {
|
||||
if pendingText == nil {
|
||||
return
|
||||
}
|
||||
pendingText = nil
|
||||
textInstructions.SetText("Text placement cancelled.")
|
||||
hideTextPreview()
|
||||
refreshCanvasDisplay()
|
||||
}
|
||||
tappableCanvas.onHover = func(pos fyne.Position) {
|
||||
if pendingText == nil || canvasBaseImage == nil {
|
||||
return
|
||||
}
|
||||
point, ok := mapCanvasPositionToImagePoint(pos, tappableCanvas.Size(), canvasBaseImage)
|
||||
pendingText.Visible = ok
|
||||
if ok {
|
||||
pendingText.Position = point
|
||||
if pendingText.Preview != nil {
|
||||
canvasPos, canvasSize, ok := mapImagePointToCanvasRect(point, pendingText.Preview, tappableCanvas.Size(), canvasBaseImage)
|
||||
if ok {
|
||||
textPreviewImg.Image = pendingText.Preview
|
||||
textPreviewImg.Move(canvasPos)
|
||||
textPreviewImg.Resize(canvasSize)
|
||||
textPreviewImg.Show()
|
||||
textPreviewImg.Refresh()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
hideTextPreview()
|
||||
}
|
||||
tappableCanvas.onHoverEnd = func() {
|
||||
if pendingText == nil {
|
||||
return
|
||||
}
|
||||
pendingText.Visible = false
|
||||
hideTextPreview()
|
||||
}
|
||||
tappableHeightmap = newTappableImage(container.NewMax(heightmapImg), func(*fyne.PointEvent) {
|
||||
if settings.ImageViewState == 2 {
|
||||
settings.ImageViewState = 0
|
||||
} else {
|
||||
@@ -1850,7 +2011,7 @@ func main() {
|
||||
}
|
||||
updateRightPanel()
|
||||
})
|
||||
tappableBumpmap = newTappableImage(container.NewMax(bumpmapImg), func() {
|
||||
tappableBumpmap = newTappableImage(container.NewMax(bumpmapImg), func(*fyne.PointEvent) {
|
||||
if settings.ImageViewState == 3 {
|
||||
settings.ImageViewState = 0
|
||||
} else {
|
||||
@@ -1910,7 +2071,7 @@ func encodeImageToWriter(w io.Writer, img image.Image, format string) error {
|
||||
}
|
||||
|
||||
// showMasksSaveDialog displays a dialog for saving the generated masks.
|
||||
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverMask, treeMask, roadMask, bridgeMask, wallMask, turretMask, buildingMask *PixelMask) {
|
||||
func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg image.Image, settings *Settings, lakes [][]image.Point, riverMask, treeMask, roadMask, bridgeMask, wallMask, turretMask, buildingMask, textMask *PixelMask) {
|
||||
// Create UI elements for the save dialog
|
||||
fileNameEntry := widget.NewEntry()
|
||||
fileNameEntry.SetPlaceHolder("masks_folder")
|
||||
@@ -1997,6 +2158,7 @@ func showMasksSaveDialog(win fyne.Window, canvasImg, heightmapImg, bumpmapImg im
|
||||
{name: "walls_mask." + imgFormat, make: func() image.Image { return maskToGray(wallMask) }},
|
||||
{name: "turrets_mask." + imgFormat, make: func() image.Image { return maskToGray(turretMask) }},
|
||||
{name: "buildings_mask." + imgFormat, make: func() image.Image { return maskToGray(buildingMask) }},
|
||||
{name: "text_mask." + imgFormat, make: func() image.Image { return maskToGray(textMask) }},
|
||||
}
|
||||
|
||||
// Save the images based on the selected packaging option
|
||||
|
||||
Reference in New Issue
Block a user