diff options
| author | historia <[not public]> | 2026-06-29 04:03:38 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-29 04:03:38 -0400 |
| commit | ba34490aaed5f7c242c2b0453130fefc07d0d5d0 (patch) | |
| tree | db5818757d5080221494cead1c7e61a576d1cb65 /internal/model | |
| parent | b1e252c996a6332d391f96624a7d6f149eb097c4 (diff) | |
| download | tui-ascii-mapper-main.tar.gz | |
Diffstat (limited to 'internal/model')
| -rw-r--r-- | internal/model/enums.go | 81 | ||||
| -rw-r--r-- | internal/model/map.go | 99 | ||||
| -rw-r--r-- | internal/model/model_test.go | 99 | ||||
| -rw-r--r-- | internal/model/terrain.go | 43 | ||||
| -rw-r--r-- | internal/model/undo.go | 42 |
5 files changed, 364 insertions, 0 deletions
diff --git a/internal/model/enums.go b/internal/model/enums.go new file mode 100644 index 0000000..a29cf84 --- /dev/null +++ b/internal/model/enums.go @@ -0,0 +1,81 @@ +package model + +type Tool int + +const ( + ToolBrush Tool = iota + ToolSelect + ToolErase + ToolFill + ToolLine + ToolRect + ToolCircle + ToolText +) + +func (t Tool) String() string { + switch t { + case ToolBrush: + return "Brush" + case ToolSelect: + return "Select" + case ToolErase: + return "Erase" + case ToolFill: + return "Fill" + case ToolLine: + return "Line" + case ToolRect: + return "Rectangle" + case ToolCircle: + return "Circle" + case ToolText: + return "Text" + } + return "" +} + +type Mode int + +const ( + ModeNormal Mode = iota + ModeDialog + ModeLinePreview + ModeRectPreview + ModeCirclePreview + ModeTextEdit +) + +type DialogType int + +const ( + DialogNone DialogType = iota + DialogSaveAs + DialogOpenMap + DialogResize + DialogQuitConfirm + DialogDeleteSubmapConfirm + DialogRenameSymbol + DialogRenameMap + DialogFileSave + DialogFileOpen +) + +func Clamp(v, lo, hi int) int { + if v < lo { + return lo + } + if v > hi { + return hi + } + return v +} + +func ContainsInt(s []int, v int) bool { + for _, x := range s { + if x == v { + return true + } + } + return false +} diff --git a/internal/model/map.go b/internal/model/map.go new file mode 100644 index 0000000..de19660 --- /dev/null +++ b/internal/model/map.go @@ -0,0 +1,99 @@ +package model + +type Point struct{ X, Y int } + +type Cell struct { + Terrain int `yaml:"t"` + Color string `yaml:"c,omitempty"` + Text string `yaml:"x,omitempty"` +} + +type TextLabel struct { + Text string `yaml:"text"` + Start Point `yaml:"start"` + Color string `yaml:"color,omitempty"` +} + +type Map struct { + Name string `yaml:"name"` + Width int `yaml:"width"` + Height int `yaml:"height"` + Grid [][]Cell `yaml:"-"` + Palette []Terrain `yaml:"palette"` + TextLabels []TextLabel `yaml:"text_labels,omitempty"` + Submaps map[Point]*Map `yaml:"-"` + Parent *Map `yaml:"-"` + Filename string `yaml:"-"` +} + +func NewMap(name string, w, h int, palette []Terrain) *Map { + grid := make([][]Cell, h) + for y := range grid { + grid[y] = make([]Cell, w) + for x := range grid[y] { + grid[y][x].Terrain = -1 + } + } + return &Map{ + Name: name, + Width: w, + Height: h, + Grid: grid, + Palette: palette, + Submaps: make(map[Point]*Map), + } +} + +func (m *Map) Clone() *Map { + grid := make([][]Cell, m.Height) + for y := range grid { + grid[y] = make([]Cell, m.Width) + copy(grid[y], m.Grid[y]) + } + c := &Map{ + Name: m.Name, + Width: m.Width, + Height: m.Height, + Grid: grid, + Palette: m.Palette, + Submaps: make(map[Point]*Map), + Parent: m.Parent, + } + for _, tl := range m.TextLabels { + c.TextLabels = append(c.TextLabels, tl) + } + for pt, sub := range m.Submaps { + c.Submaps[pt] = sub + } + return c +} + +func (m *Map) InBounds(p Point) bool { + return p.X >= 0 && p.X < m.Width && p.Y >= 0 && p.Y < m.Height +} + +func (m *Map) SetCell(p Point, terrain int, color ...string) { + if !m.InBounds(p) { + return + } + text := m.Grid[p.Y][p.X].Text + clr := "" + if len(color) > 0 { + clr = color[0] + } + m.Grid[p.Y][p.X] = Cell{Terrain: terrain, Color: clr, Text: text} +} + +func (m *Map) SetText(p Point, text string) { + if !m.InBounds(p) { + return + } + m.Grid[p.Y][p.X].Text = text +} + +func (m *Map) CellAt(p Point) Cell { + if !m.InBounds(p) { + return Cell{Terrain: -1} + } + return m.Grid[p.Y][p.X] +} diff --git a/internal/model/model_test.go b/internal/model/model_test.go new file mode 100644 index 0000000..8e4ee63 --- /dev/null +++ b/internal/model/model_test.go @@ -0,0 +1,99 @@ +package model + +import "testing" + +func TestNewMap(t *testing.T) { + m := NewMap("test", 10, 5, nil) + if m.Width != 10 || m.Height != 5 { + t.Fatalf("NewMap size: got %dx%d", m.Width, m.Height) + } + if m.Grid[0][0].Terrain != -1 { + t.Fatal("NewMap: grid not initialized to -1") + } + if len(m.Grid) != 5 || len(m.Grid[0]) != 10 { + t.Fatal("NewMap: grid dimensions wrong") + } +} + +func TestClone(t *testing.T) { + m := NewMap("test", 10, 5, nil) + m2 := m.Clone() + m2.Grid[0][0].Terrain = 0 + if m.Grid[0][0].Terrain != -1 { + t.Fatal("Clone shares data") + } +} + +func TestInBounds(t *testing.T) { + m := NewMap("test", 10, 5, nil) + if !m.InBounds(Point{X: 0, Y: 0}) { + t.Fatal("InBounds(0,0) should be true") + } + if m.InBounds(Point{X: -1, Y: 0}) { + t.Fatal("InBounds(-1,0) should be false") + } + if m.InBounds(Point{X: 0, Y: 5}) { + t.Fatal("InBounds(0,5) should be false") + } + if m.InBounds(Point{X: 10, Y: 0}) { + t.Fatal("InBounds(10,0) should be false") + } +} + +func TestSetGetCell(t *testing.T) { + m := NewMap("test", 10, 5, nil) + m.SetCell(Point{X: 1, Y: 2}, 5, "red") + c := m.CellAt(Point{X: 1, Y: 2}) + if c.Terrain != 5 || c.Color != "red" { + t.Fatalf("SetCell/GetCell: got %d,%s", c.Terrain, c.Color) + } +} + +func TestUndoStack(t *testing.T) { + u := &UndoStack{} + m := NewMap("test", 3, 3, nil) + u.Push(m) + m.SetCell(Point{X: 1, Y: 1}, 2, "green") + u.Push(m) + m.SetCell(Point{X: 2, Y: 2}, 3, "blue") + + if entry := u.Undo(); entry != nil { + *entry.Target = *entry.State + } + if m.CellAt(Point{X: 2, Y: 2}).Terrain != -1 { + t.Fatal("Undo failed") + } +} + +func TestClamp(t *testing.T) { + if Clamp(5, 0, 10) != 5 { + t.Fatal("Clamp middle") + } + if Clamp(-1, 0, 10) != 0 { + t.Fatal("Clamp low") + } + if Clamp(11, 0, 10) != 10 { + t.Fatal("Clamp high") + } +} + +func TestPickColor(t *testing.T) { + tc := Terrain{Colors: []TerrainColor{{Color: "22", Weight: 100}}} + if tc.PickColor() != "22" { + t.Fatal("PickColor single failed") + } + tc2 := Terrain{} + if tc2.PickColor() != "0" { + t.Fatal("PickColor empty failed") + } +} + +func TestTerrainSymbol(t *testing.T) { + tr := Terrain{Symbol: "X", ASCII: "x"} + if tr.GetSymbol(true) != "X" { + t.Fatal("Unicode symbol") + } + if tr.GetSymbol(false) != "x" { + t.Fatal("ASCII symbol") + } +} diff --git a/internal/model/terrain.go b/internal/model/terrain.go new file mode 100644 index 0000000..7772620 --- /dev/null +++ b/internal/model/terrain.go @@ -0,0 +1,43 @@ +package model + +import "math/rand" + +type TerrainColor struct { + Color string `yaml:"color"` + Weight int `yaml:"weight"` +} + +type Terrain struct { + Name string `yaml:"name"` + Symbol string `yaml:"symbol"` + ASCII string `yaml:"ascii"` + Colors []TerrainColor `yaml:"colors"` +} + +func (t Terrain) GetSymbol(unicode bool) string { + if unicode { + return t.Symbol + } + return t.ASCII +} + +func (t Terrain) PickColor() string { + if len(t.Colors) == 0 { + return "0" + } + if len(t.Colors) == 1 { + return t.Colors[0].Color + } + total := 0 + for _, c := range t.Colors { + total += c.Weight + } + r := rand.Intn(total) + for _, c := range t.Colors { + r -= c.Weight + if r < 0 { + return c.Color + } + } + return t.Colors[0].Color +} diff --git a/internal/model/undo.go b/internal/model/undo.go new file mode 100644 index 0000000..5b0297b --- /dev/null +++ b/internal/model/undo.go @@ -0,0 +1,42 @@ +package model + +type UndoStack struct { + states []Entry + pos int +} + +type Entry struct { + Target *Map + State *Map +} + +func (u *UndoStack) Push(target *Map) { + keep := u.pos + 1 + if keep > len(u.states) { + keep = len(u.states) + } + u.states = append(u.states[:keep], Entry{Target: target, State: target.Clone()}) + u.pos = len(u.states) - 1 + if len(u.states) > 100 { + u.states = u.states[1:] + u.pos-- + } +} + +func (u *UndoStack) Undo() *Entry { + if u.pos <= 0 { + return nil + } + u.pos-- + return &u.states[u.pos] +} + +func (u *UndoStack) Redo() *Entry { + if u.pos >= len(u.states)-1 { + return nil + } + u.pos++ + return &u.states[u.pos] +} + +func (u *UndoStack) Pos() int { return u.pos } |
