aboutsummaryrefslogtreecommitdiff
path: root/internal/tools
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tools')
-rw-r--r--internal/tools/tools.go327
-rw-r--r--internal/tools/tools_test.go36
2 files changed, 363 insertions, 0 deletions
diff --git a/internal/tools/tools.go b/internal/tools/tools.go
new file mode 100644
index 0000000..77b9f27
--- /dev/null
+++ b/internal/tools/tools.go
@@ -0,0 +1,327 @@
+package tools
+
+import "tui-ascii-mapper/internal/model"
+
+func Brush(m *model.Map, center model.Point, terrain int, size int, palette []model.Terrain) {
+ half := size / 2
+ for dy := -half; dy <= half; dy++ {
+ for dx := -half; dx <= half; dx++ {
+ color := ""
+ if terrain >= 0 && terrain < len(palette) {
+ color = palette[terrain].PickColor()
+ }
+ m.SetCell(model.Point{X: center.X + dx, Y: center.Y + dy}, terrain, color)
+ }
+ }
+}
+
+func ThickenPoints(pts []model.Point, size int) []model.Point {
+ if size <= 1 {
+ return pts
+ }
+ half := size / 2
+ seen := make(map[model.Point]bool)
+ var result []model.Point
+ for _, p := range pts {
+ for dy := -half; dy <= half; dy++ {
+ for dx := -half; dx <= half; dx++ {
+ np := model.Point{X: p.X + dx, Y: p.Y + dy}
+ if !seen[np] {
+ seen[np] = true
+ result = append(result, np)
+ }
+ }
+ }
+ }
+ return result
+}
+
+func FloodFill(m *model.Map, start model.Point, terrain int, palette []model.Terrain) {
+ if !m.InBounds(start) {
+ return
+ }
+ target := m.CellAt(start).Terrain
+ if target == terrain {
+ return
+ }
+ color := ""
+ if terrain >= 0 && terrain < len(palette) {
+ color = palette[terrain].PickColor()
+ }
+ type pt struct{ x, y int }
+ stack := []pt{{start.X, start.Y}}
+ visited := make([][]bool, m.Height)
+ for i := range visited {
+ visited[i] = make([]bool, m.Width)
+ }
+ for len(stack) > 0 {
+ p := stack[len(stack)-1]
+ stack = stack[:len(stack)-1]
+ if !m.InBounds(model.Point{X: p.x, Y: p.y}) || visited[p.y][p.x] {
+ continue
+ }
+ if m.Grid[p.y][p.x].Terrain != target {
+ continue
+ }
+ visited[p.y][p.x] = true
+ m.SetCell(model.Point{X: p.x, Y: p.y}, terrain, color)
+ stack = append(stack, pt{p.x + 1, p.y}, pt{p.x - 1, p.y}, pt{p.x, p.y + 1}, pt{p.x, p.y - 1})
+ }
+}
+
+func BresenhamLine(a, b model.Point) []model.Point {
+ var pts []model.Point
+ x0, y0 := a.X, a.Y
+ x1, y1 := b.X, b.Y
+ dx := iabs(x1 - x0)
+ dy := -iabs(y1 - y0)
+ sx, sy := 1, 1
+ if x0 > x1 {
+ sx = -1
+ }
+ if y0 > y1 {
+ sy = -1
+ }
+ err := dx + dy
+ for {
+ pts = append(pts, model.Point{X: x0, Y: y0})
+ if x0 == x1 && y0 == y1 {
+ break
+ }
+ e2 := 2 * err
+ if e2 >= dy {
+ err += dy
+ x0 += sx
+ }
+ if e2 <= dx {
+ err += dx
+ y0 += sy
+ }
+ }
+ return pts
+}
+
+func iabs(x int) int {
+ if x < 0 {
+ return -x
+ }
+ return x
+}
+
+func DrawRect(a, b model.Point, filled bool) []model.Point {
+ x0, x1 := a.X, b.X
+ y0, y1 := a.Y, b.Y
+ if x0 > x1 {
+ x0, x1 = x1, x0
+ }
+ if y0 > y1 {
+ y0, y1 = y1, y0
+ }
+ var pts []model.Point
+ if filled {
+ for y := y0; y <= y1; y++ {
+ for x := x0; x <= x1; x++ {
+ pts = append(pts, model.Point{X: x, Y: y})
+ }
+ }
+ return pts
+ }
+ for x := x0; x <= x1; x++ {
+ pts = append(pts, model.Point{X: x, Y: y0}, model.Point{X: x, Y: y1})
+ }
+ for y := y0 + 1; y < y1; y++ {
+ pts = append(pts, model.Point{X: x0, Y: y}, model.Point{X: x1, Y: y})
+ }
+ return pts
+}
+
+func DrawCircle(center, edge model.Point, filled bool) []model.Point {
+ r2 := (edge.X-center.X)*(edge.X-center.X) + (edge.Y-center.Y)*(edge.Y-center.Y)
+ r := r2
+ if r < 0 {
+ return nil
+ }
+ radius := IntSqrt(r)
+ var pts []model.Point
+ for dy := -radius; dy <= radius; dy++ {
+ for dx := -radius; dx <= radius; dx++ {
+ dist2 := dx*dx + dy*dy
+ if filled {
+ if dist2 <= r {
+ pts = append(pts, model.Point{X: center.X + dx, Y: center.Y + dy})
+ }
+ } else {
+ if dist2 <= r && dist2 > (radius-1)*(radius-1) {
+ pts = append(pts, model.Point{X: center.X + dx, Y: center.Y + dy})
+ }
+ }
+ }
+ }
+ return pts
+}
+
+func DrawOval(f1, f2 model.Point, filled bool) []model.Point {
+ dx := f2.X - f1.X
+ dy := f2.Y - f1.Y
+ dist := IntSqrt(dx*dx + dy*dy)
+ if dist == 0 {
+ return nil
+ }
+ a := dist * 3 / 2
+ if a < 1 {
+ a = 1
+ }
+ a2 := a * a
+ c2 := dist * dist / 4
+ b2 := a2 - c2
+ if b2 < 0 {
+ b2 = 0
+ }
+
+ cx := (f1.X + f2.X) / 2
+ cy := (f1.Y + f2.Y) / 2
+
+ minX := cx - a - 1
+ maxX := cx + a + 1
+ minY := cy - a - 1
+ maxY := cy + a + 1
+
+ var pts []model.Point
+ for py := minY; py <= maxY; py++ {
+ for px := minX; px <= maxX; px++ {
+ d1 := distSq(px, py, f1.X, f1.Y)
+ d2 := distSq(px, py, f2.X, f2.Y)
+ sum := IntSqrt(d1) + IntSqrt(d2)
+
+ if filled {
+ if sum <= 2*a {
+ pts = append(pts, model.Point{X: px, Y: py})
+ }
+ } else {
+ if sum >= 2*a-1 && sum <= 2*a+1 {
+ pts = append(pts, model.Point{X: px, Y: py})
+ }
+ }
+ }
+ }
+ return pts
+}
+
+func distSq(x1, y1, x2, y2 int) int {
+ dx := x1 - x2
+ dy := y1 - y2
+ return dx*dx + dy*dy
+}
+
+func IntSqrt(n int) int {
+ if n <= 0 {
+ return 0
+ }
+ lo, hi := 0, n
+ for lo < hi {
+ mid := (lo + hi + 1) / 2
+ if mid*mid <= n {
+ lo = mid
+ } else {
+ hi = mid - 1
+ }
+ }
+ return lo
+}
+
+func ApplyPoints(m *model.Map, pts []model.Point, terrain int, palette []model.Terrain) {
+ color := ""
+ if terrain >= 0 && terrain < len(palette) {
+ color = palette[terrain].PickColor()
+ }
+ for _, p := range pts {
+ m.SetCell(p, terrain, color)
+ }
+}
+
+func PlaceTextLabel(m *model.Map, start model.Point, text string, color string) {
+ RemoveTextLabel(m, start)
+ tl := model.TextLabel{Text: text, Start: start, Color: color}
+ m.TextLabels = append(m.TextLabels, tl)
+ runes := []rune(text)
+ for i, r := range runes {
+ p := model.Point{X: start.X + i, Y: start.Y}
+ if m.InBounds(p) {
+ m.Grid[p.Y][p.X].Text = string(r)
+ }
+ }
+}
+
+func RemoveTextLabel(m *model.Map, start model.Point) {
+ for i, tl := range m.TextLabels {
+ if tl.Start == start {
+ m.TextLabels = append(m.TextLabels[:i], m.TextLabels[i+1:]...)
+ break
+ }
+ }
+ for y := range m.Grid {
+ for x := range m.Grid[y] {
+ if m.Grid[y][x].Text == "" {
+ continue
+ }
+ found := false
+ for _, tl := range m.TextLabels {
+ runes := []rune(tl.Text)
+ for i := range runes {
+ if tl.Start.X+i == x && tl.Start.Y == y {
+ found = true
+ break
+ }
+ }
+ if found {
+ break
+ }
+ }
+ if !found {
+ m.Grid[y][x].Text = ""
+ }
+ }
+ }
+}
+
+func FindTextLabelAt(m *model.Map, p model.Point) int {
+ for i, tl := range m.TextLabels {
+ runes := []rune(tl.Text)
+ for j := range runes {
+ if tl.Start.X+j == p.X && tl.Start.Y == p.Y {
+ return i
+ }
+ }
+ }
+ return -1
+}
+
+func MoveTextLabel(m *model.Map, oldStart, newStart model.Point) {
+ for i, tl := range m.TextLabels {
+ if tl.Start == oldStart {
+ for _, p := range LabelPositions(tl) {
+ if m.InBounds(p) {
+ m.Grid[p.Y][p.X].Text = ""
+ }
+ }
+ m.TextLabels[i].Start = newStart
+ runes := []rune(tl.Text)
+ for j, r := range runes {
+ p := model.Point{X: newStart.X + j, Y: newStart.Y}
+ if m.InBounds(p) {
+ m.Grid[p.Y][p.X].Text = string(r)
+ }
+ }
+ return
+ }
+ }
+}
+
+func LabelPositions(tl model.TextLabel) []model.Point {
+ var pts []model.Point
+ runes := []rune(tl.Text)
+ for i := range runes {
+ pts = append(pts, model.Point{X: tl.Start.X + i, Y: tl.Start.Y})
+ }
+ return pts
+}
diff --git a/internal/tools/tools_test.go b/internal/tools/tools_test.go
new file mode 100644
index 0000000..22ac3d2
--- /dev/null
+++ b/internal/tools/tools_test.go
@@ -0,0 +1,36 @@
+package tools
+
+import (
+ "fmt"
+ "testing"
+
+ "tui-ascii-mapper/internal/model"
+)
+
+func TestDemotools(t *testing.T) {
+ m := model.NewMap("test", 10, 10, nil)
+ Brush(m, model.Point{X: 5, Y: 5}, 0, 3, nil)
+ if m.Grid[5][5].Terrain != 0 {
+ t.Fatal("Brush failed")
+ }
+ pts := BresenhamLine(model.Point{X: 0, Y: 0}, model.Point{X: 3, Y: 0})
+ if len(pts) != 4 || pts[0] != (model.Point{X: 0, Y: 0}) || pts[3] != (model.Point{X: 3, Y: 0}) {
+ t.Fatalf("Line failed: %v", pts)
+ }
+ if IntSqrt(25) != 5 || IntSqrt(26) != 5 || IntSqrt(0) != 0 {
+ t.Fatal("IntSqrt failed")
+ }
+ t2 := model.Terrain{Colors: []model.TerrainColor{{Color: "22", Weight: 100}}}
+ if t2.PickColor() != "22" {
+ t.Fatal("PickColor failed")
+ }
+ PlaceTextLabel(m, model.Point{X: 2, Y: 2}, "ABC", "")
+ if m.Grid[2][2].Text != "A" || m.Grid[2][3].Text != "B" {
+ t.Fatalf("TextLabel: %s,%s", m.Grid[2][2].Text, m.Grid[2][3].Text)
+ }
+ RemoveTextLabel(m, model.Point{X: 2, Y: 2})
+ if m.Grid[2][2].Text != "" {
+ t.Fatal("TextLabel remove failed")
+ }
+ fmt.Println("tools: ok")
+}