package main import "fmt" // Brush applies terrain to a block centered at p. Each cell gets independent random color. func Brush(m *Map, center Point, terrain int, size int, palette []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(Point{center.X + dx, center.Y + dy}, terrain, color) } } } // ThickenPoints expands a set of points by the given brush size, returning deduplicated points. func ThickenPoints(pts []Point, size int) []Point { if size <= 1 { return pts } half := size / 2 seen := make(map[Point]bool) var result []Point for _, p := range pts { for dy := -half; dy <= half; dy++ { for dx := -half; dx <= half; dx++ { np := Point{p.X + dx, p.Y + dy} if !seen[np] { seen[np] = true result = append(result, np) } } } } return result } // FloodFill fills a contiguous area from start with terrain. func FloodFill(m *Map, start Point, terrain int, palette []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(Point{p.x, 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(Point{p.x, 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}) } } // BresenhamLine returns points along a line from a to b. func BresenhamLine(a, b Point) []Point { var pts []Point x0, y0 := a.X, a.Y x1, y1 := b.X, b.Y dx := abs(x1 - x0) dy := -abs(y1 - y0) sx, sy := 1, 1 if x0 > x1 { sx = -1 } if y0 > y1 { sy = -1 } err := dx + dy for { pts = append(pts, Point{x0, 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 abs(x int) int { if x < 0 { return -x } return x } // DrawRect returns points for the outline (or fill) of a rectangle. func DrawRect(a, b Point, filled bool) []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 []Point if filled { for y := y0; y <= y1; y++ { for x := x0; x <= x1; x++ { pts = append(pts, Point{x, y}) } } return pts } for x := x0; x <= x1; x++ { pts = append(pts, Point{x, y0}, Point{x, y1}) } for y := y0 + 1; y < y1; y++ { pts = append(pts, Point{x0, y}, Point{x1, y}) } return pts } // DrawCircle returns points for the outline (or fill) of a circle. func DrawCircle(center, edge Point, filled bool) []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 } // integer sqrt approximation, good enough for grid radius := intSqrt(r) var pts []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, Point{center.X + dx, center.Y + dy}) } } else { // outline: approximate ring if dist2 <= r && dist2 > (radius-1)*(radius-1) { pts = append(pts, Point{center.X + dx, center.Y + dy}) } } } } return pts } // DrawOval returns points for the outline (or fill) of an ellipse with two foci. func DrawOval(f1, f2 Point, filled bool) []Point { // semi-major axis: enough to pass through f2 from f1, plus a bit dx := f2.X - f1.X dy := f2.Y - f1.Y // Use distance between foci as 2c, major axis 2a = 2c * 1.5 (so oval extends) dist := intSqrt(dx*dx + dy*dy) if dist == 0 { return nil } a := dist * 3 / 2 // major semi-axis (oval extends beyond both foci) if a < 1 { a = 1 } a2 := a * a c2 := dist * dist / 4 // c = half distance between foci b2 := a2 - c2 // b² = a² - c² if b2 < 0 { b2 = 0 } // Center of ellipse cx := (f1.X + f2.X) / 2 cy := (f1.Y + f2.Y) / 2 // Bounding box minX := cx - a - 1 maxX := cx + a + 1 minY := cy - a - 1 maxY := cy + a + 1 var pts []Point for py := minY; py <= maxY; py++ { for px := minX; px <= maxX; px++ { // Distances to foci 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, Point{px, py}) } } else { // Outline: near the ellipse boundary if sum >= 2*a-1 && sum <= 2*a+1 { pts = append(pts, Point{px, 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 } // ApplyPoints writes terrain to all given points. func ApplyPoints(m *Map, pts []Point, terrain int, palette []Terrain) { color := "" if terrain >= 0 && terrain < len(palette) { color = palette[terrain].PickColor() } for _, p := range pts { m.SetCell(p, terrain, color) } } // PlaceTextLabel adds a text label at start, clearing any prior text in those cells. func PlaceTextLabel(m *Map, start Point, text string, color string) { // Remove any existing label starting at the same point RemoveTextLabel(m, start) tl := TextLabel{Text: text, Start: start, Color: color} m.TextLabels = append(m.TextLabels, tl) runes := []rune(text) for i, r := range runes { p := Point{start.X + i, start.Y} if m.InBounds(p) { m.Grid[p.Y][p.X].Text = string(r) } } } // RemoveTextLabel removes the text label starting at start. func RemoveTextLabel(m *Map, start Point) { for i, tl := range m.TextLabels { if tl.Start == start { m.TextLabels = append(m.TextLabels[:i], m.TextLabels[i+1:]...) break } } // Also clear from grid cells for y := range m.Grid { for x := range m.Grid[y] { if m.Grid[y][x].Text == "" { continue } // Check if this cell belongs to a label 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 = "" } } } } // FindTextLabelAt returns the label index that covers point p, or -1. func FindTextLabelAt(m *Map, p 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 } // MoveTextLabel moves label at oldStart to newStart. func MoveTextLabel(m *Map, oldStart, newStart Point) { for i, tl := range m.TextLabels { if tl.Start == oldStart { // Clear old cells for _, p := range LabelPositions(tl) { if m.InBounds(p) { m.Grid[p.Y][p.X].Text = "" } } m.TextLabels[i].Start = newStart // Set new cells runes := []rune(tl.Text) for j, r := range runes { p := Point{newStart.X + j, newStart.Y} if m.InBounds(p) { m.Grid[p.Y][p.X].Text = string(r) } } return } } } func LabelPositions(tl TextLabel) []Point { var pts []Point runes := []rune(tl.Text) for i := range runes { pts = append(pts, Point{tl.Start.X + i, tl.Start.Y}) } return pts } func DemoTools() { m := NewMap("test", 10, 10, nil) Brush(m, Point{5, 5}, 0, 3, nil) if m.Grid[5][5].Terrain != 0 { panic("Brush failed") } pts := BresenhamLine(Point{0, 0}, Point{3, 0}) if len(pts) != 4 || pts[0] != (Point{0, 0}) || pts[3] != (Point{3, 0}) { panic(fmt.Sprintf("Line failed: %v", pts)) } // intSqrt sanity if intSqrt(25) != 5 || intSqrt(26) != 5 || intSqrt(0) != 0 { panic("intSqrt failed") } // Color picking t := Terrain{Colors: []TerrainColor{{Color: "22", Weight: 100}}} if t.PickColor() != "22" { panic("PickColor failed") } // Text labels — Grid[y][x] PlaceTextLabel(m, Point{2, 2}, "ABC", "") if m.Grid[2][2].Text != "A" || m.Grid[2][3].Text != "B" { panic("TextLabel: " + m.Grid[2][2].Text + "," + m.Grid[2][3].Text) } RemoveTextLabel(m, Point{2, 2}) if m.Grid[2][2].Text != "" { panic("TextLabel remove failed") } fmt.Println("tools: ok") }