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/tools | |
| parent | b1e252c996a6332d391f96624a7d6f149eb097c4 (diff) | |
| download | tui-ascii-mapper-main.tar.gz | |
Diffstat (limited to 'internal/tools')
| -rw-r--r-- | internal/tools/tools.go | 327 | ||||
| -rw-r--r-- | internal/tools/tools_test.go | 36 |
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") +} |
