Compare commits

...

19 Commits

Author SHA1 Message Date
wisplite 9ac99cf488 added square euclidean at the dismay of many theoretical mathematicians (it's inadmissible) (but it's really fast) 2026-05-14 11:06:48 -05:00
wisplite 754ea5bd18 added AutoCompute mode 2026-05-14 11:01:03 -05:00
wisplite 50db827e57 pre-allocate queue to save on reallocations later 2026-05-14 10:42:28 -05:00
wisplite 1291fff451 allow zooming to minimum zoom if canvas is big enough, and make scaling a little more comfortable 2026-05-14 10:27:17 -05:00
wisplite bb85e11895 finally make the zooming proportional so it isn't too fast when zooming out 2026-05-14 10:15:23 -05:00
wisplite 06f36a625f status label (WOW THIS IS FAST) 2026-05-14 10:10:51 -05:00
wisplite c20f98cf90 add error box to prevent an occasional panic when the start or end pos wasn't set right 2026-05-14 09:53:40 -05:00
wisplite 260c39eedc runtime statistics label 2026-05-14 09:51:04 -05:00
wisplite 60a28cea88 visualize explored nodes (closed path) 2026-05-14 09:43:49 -05:00
wisplite 589663d3d0 HUGE update to how drawing works, ensures consistency between visualization and sim. Also added erase tool. 2026-05-14 09:37:53 -05:00
wisplite 6719f70c73 added heuristic dropdown and fixed a bunch of bugs in the logic 2026-05-14 09:29:25 -05:00
wisplite 85855f00ff whoops, we should reset the grid to clear the closedSet, openSet, and scores 2026-05-14 09:14:02 -05:00
wisplite 5b4887f0cc working a* 2026-05-14 09:13:32 -05:00
wisplite 29bce85e82 slight rework of draw code to address a small memory leak (changes are pushed straight to vram rather than recreating an image from scratch every time) 2026-05-14 07:06:38 -05:00
wisplite 5b4c246b4f now it draws a line between the old and new mouse positions so drawn paths are continuous 2026-05-14 06:55:31 -05:00
wisplite 48f889bd44 basic drawing logic, need to add raycasting or something 2026-05-14 06:40:52 -05:00
wisplite 910f0e56cf cap the canvas size at 16000 to prevent GPU texture allocation issues (will replace with chunked textures in the multithreaded version) 2026-05-14 05:04:26 -05:00
wisplite da36fcd0ba fixed memory leak in grid generation code 2026-05-14 04:56:33 -05:00
wisplite b5a03b2461 added tool selector 2026-05-14 04:54:48 -05:00
2 changed files with 723 additions and 9 deletions
+266
View File
@@ -0,0 +1,266 @@
package main
import (
"container/heap"
"fmt"
"math"
"time"
)
type Item struct {
index int // index of the cell in the grid (y * width + x)
priority float32 // f = g + h
gScore float32 // used to break f ties toward straighter paths
}
type PriorityQueue []*Item
func (pq PriorityQueue) Len() int {
return len(pq)
}
func (pq PriorityQueue) Less(i, j int) bool {
if pq[i].priority != pq[j].priority {
return pq[i].priority < pq[j].priority
}
// Same f: prefer larger g (smaller h → closer to goal) for cleaner grid paths.
return pq[i].gScore > pq[j].gScore
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
}
func (pq *PriorityQueue) Push(x interface{}) {
*pq = append(*pq, x.(*Item))
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil
*pq = old[0 : n-1]
return item
}
type AStar struct {
gridTypes []byte
gScores []float32
parents []byte
openSet PriorityQueue
closedSet []bool
width int
height int
heuristic func(x int, y int, endX int, endY int) float32
timeTaken time.Duration
}
func (a *AStar) Init(width int, height int) {
a.gridTypes = make([]byte, width*height)
a.gScores = make([]float32, width*height)
a.parents = make([]byte, width*height)
a.openSet = make(PriorityQueue, 0, 2000000) // pre-allocate space for 2 million cells to avoid reallocations
a.closedSet = make([]bool, width*height)
a.heuristic = func(x int, y int, endX int, endY int) float32 {
return float32(math.Abs(float64(x-endX)) + math.Abs(float64(y-endY))) // Manhattan distance default
}
a.width = width
a.height = height
for i := range a.gScores {
a.gScores[i] = math.MaxFloat32
}
}
func (a *AStar) ResetGrid(withTypes bool) {
for i := range a.gScores {
if withTypes {
a.gridTypes[i] = 0
}
a.gScores[i] = math.MaxFloat32
a.parents[i] = 0
a.closedSet[i] = false
}
a.openSet = a.openSet[:0]
}
func (a *AStar) RebuildGrid(width int, height int) {
a.gridTypes = make([]byte, width*height)
a.gScores = make([]float32, width*height)
a.parents = make([]byte, width*height)
a.openSet = make(PriorityQueue, 0, 2000000)
a.closedSet = make([]bool, width*height)
a.width = width
a.height = height
}
func (a *AStar) SetHeuristic(heuristic int32) {
switch heuristic {
case 0:
a.heuristic = func(x int, y int, endX int, endY int) float32 {
return float32(math.Abs(float64(x-endX)) + math.Abs(float64(y-endY))) // Manhattan distance
}
case 1:
a.heuristic = func(x int, y int, endX int, endY int) float32 {
return float32(math.Sqrt(float64(x-endX)*float64(x-endX) + float64(y-endY)*float64(y-endY))) // Euclidean distance
}
case 2:
a.heuristic = func(x int, y int, endX int, endY int) float32 {
return float32(math.Max(float64(x-endX), float64(y-endY))) // Chebyshev distance
}
case 3:
a.heuristic = func(x int, y int, endX int, endY int) float32 {
return float32(math.Pow(float64(x-endX), 2) + math.Pow(float64(y-endY), 2)) // Squared Euclidean (extremely fast but not optimal/inadmissible)
}
}
}
func (a *AStar) SetGridType(x int, y int, gridType byte) {
/*
0 = empty
1 = wall
2 = start
3 = end
*/
a.gridTypes[y*a.width+x] = gridType
}
func (a *AStar) GetGridType(x int, y int) byte {
return a.gridTypes[y*a.width+x]
}
func (a *AStar) GetGridTypes() []byte {
return a.gridTypes
}
func (a *AStar) GetClosedSet() []bool {
return a.closedSet
}
func (a *AStar) SetGScores(x int, y int, gScore float32) {
a.gScores[y*a.width+x] = gScore
}
func (a *AStar) GetGScores(x int, y int) float32 {
return a.gScores[y*a.width+x]
}
func (a *AStar) SetParent(x int, y int, parentx int, parenty int) {
if parentx < x {
a.parents[y*a.width+x] = byte(0) // left of the node
} else if parentx > x {
a.parents[y*a.width+x] = byte(2) // right of the node
} else if parenty < y {
a.parents[y*a.width+x] = byte(1) // above the node
} else if parenty > y {
a.parents[y*a.width+x] = byte(3) // below the node
}
}
func (a *AStar) GetParent(x int, y int) byte {
return a.parents[y*a.width+x]
}
func (a *AStar) ParentIndexToXY(childx int, childy int, parent byte) (int, int) {
if parent == 0 {
return childx - 1, childy // parent left
} else if parent == 1 {
return childx, childy - 1 // parent above
} else if parent == 2 {
return childx + 1, childy // parent right
} else if parent == 3 {
return childx, childy + 1 // parent below
}
return childx, childy
}
func (a *AStar) ParentIndexToXYIndex(childx int, childy int, parent byte) int {
x, y := a.ParentIndexToXY(childx, childy, parent)
return y*a.width + x
}
func (a *AStar) GetNeighbors(x int, y int) []int {
neighbors := make([]int, 0)
if x > 0 {
neighbors = append(neighbors, y*a.width+x-1)
}
if x < a.width-1 {
neighbors = append(neighbors, y*a.width+x+1)
}
if y > 0 {
neighbors = append(neighbors, y*a.width+x-a.width)
}
if y < a.height-1 {
neighbors = append(neighbors, y*a.width+x+a.width)
}
return neighbors
}
func (a *AStar) GetTerrainCost(x int, y int) float32 {
return 1.0
}
func (a *AStar) GetEvaluatedCells() int {
cellsEvaluated := 0
for _, closed := range a.closedSet {
if closed {
cellsEvaluated++
}
}
return cellsEvaluated
}
func (a *AStar) GetTimeTaken() time.Duration {
return a.timeTaken
}
func (a *AStar) CalculatePath(startX int, startY int, endX int, endY int) [][]int {
timer := time.Now()
defer func() {
a.timeTaken = time.Since(timer)
}()
startIndex := startY*a.width + startX
endIndex := endY*a.width + endX
a.gScores[startIndex] = 0
startF := a.heuristic(startX, startY, endX, endY)
heap.Push(&a.openSet, &Item{index: startIndex, priority: startF, gScore: 0})
for a.openSet.Len() > 0 {
current := heap.Pop(&a.openSet).(*Item)
if a.closedSet[current.index] {
continue
}
if current.index == endIndex {
// We've found the goal!
fmt.Println("Found the goal!")
path := make([][]int, 0)
for currentIndex := current.index; currentIndex != startIndex; currentIndex = a.ParentIndexToXYIndex(currentIndex%a.width, currentIndex/a.width, a.parents[currentIndex]) {
x, y := a.ParentIndexToXY(currentIndex%a.width, currentIndex/a.width, a.parents[currentIndex])
path = append(path, []int{x, y})
}
return path
}
a.closedSet[current.index] = true
for _, neighborIndex := range a.GetNeighbors(current.index%a.width, current.index/a.width) {
if a.closedSet[neighborIndex] {
continue
}
if a.gridTypes[neighborIndex] == 1 {
a.gScores[neighborIndex] = math.MaxFloat32
continue
}
terrainCost := a.GetTerrainCost(neighborIndex%a.width, neighborIndex/a.width)
tentativeGScore := a.gScores[current.index] + terrainCost
if tentativeGScore < a.gScores[neighborIndex] {
a.SetParent(neighborIndex%a.width, neighborIndex/a.width, current.index%a.width, current.index/a.width)
a.gScores[neighborIndex] = tentativeGScore
priority := tentativeGScore + a.heuristic(neighborIndex%a.width, neighborIndex/a.width, endX, endY)
heap.Push(&a.openSet, &Item{index: neighborIndex, priority: priority, gScore: tentativeGScore})
}
}
}
return make([][]int, 0)
}
+457 -9
View File
@@ -1,7 +1,11 @@
package main package main
import ( import (
"image/color"
"math"
"strconv" "strconv"
"strings"
"unsafe"
rg "github.com/gen2brain/raylib-go/raygui" rg "github.com/gen2brain/raylib-go/raygui"
rl "github.com/gen2brain/raylib-go/raylib" rl "github.com/gen2brain/raylib-go/raylib"
@@ -11,6 +15,219 @@ func canvasMouse() rl.Vector2 {
return rl.GetMousePosition() return rl.GetMousePosition()
} }
// Keeps GPU texture uploads in sync with mapImage edits. Full uploads are used after
// path rebuilds; edits use bounding boxes via UpdateTextureRec.
type texSync struct {
needUpload bool
uploadFull bool
partialOk bool
x0, y0 int
x1, y1 int
rectBuf []color.RGBA
}
func absInt(v int) int {
if v < 0 {
return -v
}
return v
}
func (t *texSync) markFull() {
t.needUpload = true
t.uploadFull = true
}
func (t *texSync) markRegion(minX, minY, maxX, maxY, gridW, gridH int) {
if minX > maxX {
minX, maxX = maxX, minX
}
if minY > maxY {
minY, maxY = maxY, minY
}
if minX < 0 {
minX = 0
}
if minY < 0 {
minY = 0
}
if maxX >= gridW {
maxX = gridW - 1
}
if maxY >= gridH {
maxY = gridH - 1
}
if minX > maxX || minY > maxY {
return
}
t.needUpload = true
if t.uploadFull {
return
}
if !t.partialOk {
t.partialOk = true
t.x0, t.y0 = minX, minY
t.x1, t.y1 = maxX, maxY
return
}
if minX < t.x0 {
t.x0 = minX
}
if minY < t.y0 {
t.y0 = minY
}
if maxX > t.x1 {
t.x1 = maxX
}
if maxY > t.y1 {
t.y1 = maxY
}
}
func (t *texSync) flush(img *rl.Image, tex *rl.Texture2D, gridW, gridH int) {
if !t.needUpload {
return
}
if t.uploadFull {
ptr := (*color.RGBA)(unsafe.Pointer(img.Data))
pixels := unsafe.Slice(ptr, gridW*gridH)
rl.UpdateTexture(*tex, pixels)
t.uploadFull = false
t.partialOk = false
} else if t.partialOk {
w := t.x1 - t.x0 + 1
h := t.y1 - t.y0 + 1
n := w * h
if cap(t.rectBuf) < n {
t.rectBuf = make([]color.RGBA, n)
} else {
t.rectBuf = t.rectBuf[:n]
}
ptr := (*color.RGBA)(unsafe.Pointer(img.Data))
full := unsafe.Slice(ptr, gridW*gridH)
for row := 0; row < h; row++ {
src := (t.y0+row)*gridW + t.x0
copy(t.rectBuf[row*w:], full[src:src+w])
}
rec := rl.NewRectangle(float32(t.x0), float32(t.y0), float32(w), float32(h))
rl.UpdateTextureRec(*tex, rec, t.rectBuf)
t.partialOk = false
}
t.needUpload = false
}
// paintWallLine updates both mapImage and the A* grid for every grid cell crossed by the segment.
func paintWallLine(a *AStar, mapImage *rl.Image, x0, y0, x1, y1, gridW, gridH int, col color.RGBA, tex *texSync) {
minX := x0
if x1 < minX {
minX = x1
}
maxX := x0
if x1 > maxX {
maxX = x1
}
minY := y0
if y1 < minY {
minY = y1
}
maxY := y0
if y1 > maxY {
maxY = y1
}
tex.markRegion(minX, minY, maxX, maxY, gridW, gridH)
dx := absInt(x1 - x0)
dy := -absInt(y1 - y0)
sx := 1
sy := 1
if x0 > x1 {
sx = -1
}
if y0 > y1 {
sy = -1
}
err := dx + dy
for {
if x0 >= 0 && x0 < gridW && y0 >= 0 && y0 < gridH {
switch a.GetGridType(x0, y0) {
case 2, 3: // start / end
default:
rl.ImageDrawPixel(mapImage, int32(x0), int32(y0), col)
a.SetGridType(x0, y0, 1)
}
}
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
// paintEraseLine clears wall cells along the segment (same grid traversal as paintWallLine).
func paintEraseLine(a *AStar, mapImage *rl.Image, x0, y0, x1, y1, gridW, gridH int, empty color.RGBA, tex *texSync) {
minX := x0
if x1 < minX {
minX = x1
}
maxX := x0
if x1 > maxX {
maxX = x1
}
minY := y0
if y1 < minY {
minY = y1
}
maxY := y0
if y1 > maxY {
maxY = y1
}
tex.markRegion(minX, minY, maxX, maxY, gridW, gridH)
dx := absInt(x1 - x0)
dy := -absInt(y1 - y0)
sx := 1
sy := 1
if x0 > x1 {
sx = -1
}
if y0 > y1 {
sy = -1
}
err := dx + dy
for {
if x0 >= 0 && x0 < gridW && y0 >= 0 && y0 < gridH {
switch a.GetGridType(x0, y0) {
case 2, 3: // start / end
default:
rl.ImageDrawPixel(mapImage, int32(x0), int32(y0), empty)
a.SetGridType(x0, y0, 0)
}
}
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
func drawInfiniteGridLines(camera rl.Camera2D, canvasW float32, canvasH float32, cellSize float32, width int, height int) { func drawInfiniteGridLines(camera rl.Camera2D, canvasW float32, canvasH float32, cellSize float32, width int, height int) {
// 1. Level of Detail (LOD) Check // 1. Level of Detail (LOD) Check
// If we are zoomed out too far, don't draw the gridlines at all // If we are zoomed out too far, don't draw the gridlines at all
@@ -48,10 +265,10 @@ func drawInfiniteGridLines(camera rl.Camera2D, canvasW float32, canvasH float32,
func main() { func main() {
rl.SetConfigFlags(rl.FlagWindowResizable) rl.SetConfigFlags(rl.FlagWindowResizable)
rl.InitWindow(int32(800), int32(450), "A* Visualizer") rl.InitWindow(int32(800), int32(600), "A* Visualizer")
defer rl.CloseWindow() defer rl.CloseWindow()
scale := rl.GetWindowScaleDPI().X scale := rl.GetWindowScaleDPI().X + 0.25
if scale == 0 { if scale == 0 {
scale = 2.0 // Fallback value scale = 2.0 // Fallback value
} }
@@ -73,14 +290,36 @@ func main() {
heightInputValue := "10" heightInputValue := "10"
editModeWidth := false editModeWidth := false
editModeHeight := false editModeHeight := false
generateGridError := false
posError := false
toolOptions := []string{"Wall", "Start", "End", "Erase"}
toolOptionsText := strings.Join(toolOptions, ";")
activeTool := int32(0)
toolDropdownOpen := false
var tex texSync // GPU uploads: partial rects while painting; full after path resets
heuristicOptions := []string{"Manhattan", "Euclidean", "Chebyshev", "Squared Euclidean"}
heuristicOptionsText := strings.Join(heuristicOptions, ";")
activeHeuristic := int32(0)
heuristicDropdownOpen := false
cellSize := float32(25) cellSize := float32(25)
lastMousePos := rl.NewVector2(-1, -1)
startPos := rl.NewVector2(-1, -1)
endPos := rl.NewVector2(-1, -1)
mapImage := rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255)) mapImage := rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255))
mapTexture := rl.LoadTextureFromImage(mapImage) mapTexture := rl.LoadTextureFromImage(mapImage)
defer rl.UnloadTexture(mapTexture) defer rl.UnloadTexture(mapTexture)
defer rl.UnloadImage(mapImage) defer rl.UnloadImage(mapImage)
autoCompute := false
astar := AStar{}
astar.Init(width, height)
for !rl.WindowShouldClose() { for !rl.WindowShouldClose() {
screenWidth := float32(rl.GetScreenWidth()) screenWidth := float32(rl.GetScreenWidth())
screenHeight := float32(rl.GetScreenHeight()) screenHeight := float32(rl.GetScreenHeight())
@@ -100,9 +339,9 @@ func main() {
// 1. Where is the mouse in the world BEFORE zooming? // 1. Where is the mouse in the world BEFORE zooming?
worldPosBefore := rl.GetScreenToWorld2D(mousePos, camera) worldPosBefore := rl.GetScreenToWorld2D(mousePos, camera)
// 2. Apply the zoom // 2. Apply proportional zoom so wheel steps get smaller as the view zooms out.
camera.Zoom += float32(wheel) * 0.1 camera.Zoom *= float32(math.Pow(1.1, float64(wheel)))
if camera.Zoom < 0.01 { if camera.Zoom < 0.01 && width > 2000 && height > 2000 {
camera.Zoom = 0.01 camera.Zoom = 0.01
} }
@@ -121,6 +360,106 @@ func main() {
camera.Target.Y -= delta.Y / camera.Zoom camera.Target.Y -= delta.Y / camera.Zoom
} }
// Paint logic
if rl.IsMouseButtonDown(rl.MouseLeftButton) {
mousePos := rl.GetMousePosition()
if int(mousePos.X) < int(canvasWidth) {
worldPos := rl.GetScreenToWorld2D(rl.GetMousePosition(), camera)
x := int(worldPos.X / cellSize)
y := int(worldPos.Y / cellSize)
if x >= 0 && x < width && y >= 0 && y < height {
// mapImage is one pixel per cell; drawing uses cell indices, not raw world coords.
switch activeTool {
case 0: // Wall — Bresenham line into image + simulator grid (skips start/end cells)
wallCol := color.RGBA{A: 255}
if lastMousePos.X != -1 && lastMousePos.Y != -1 {
prevX := int(lastMousePos.X / cellSize)
prevY := int(lastMousePos.Y / cellSize)
paintWallLine(&astar, mapImage, prevX, prevY, x, y, width, height, wallCol, &tex)
} else {
paintWallLine(&astar, mapImage, x, y, x, y, width, height, wallCol, &tex)
}
case 1: // Start — must run on first paint frame (lastMousePos may be -1 after release)
if int(startPos.X) != x || int(startPos.Y) != y {
if startPos.X >= 0 && startPos.Y >= 0 {
ox, oy := int(startPos.X), int(startPos.Y)
rl.ImageDrawPixel(mapImage, int32(startPos.X), int32(startPos.Y), rl.NewColor(240, 240, 240, 255))
astar.SetGridType(ox, oy, 0)
tex.markRegion(ox, oy, ox, oy, width, height)
}
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 255, 0, 255))
tex.markRegion(x, y, x, y, width, height)
startPos = rl.NewVector2(float32(x), float32(y))
astar.SetGridType(x, y, 2)
}
case 2: // End
if int(endPos.X) != x || int(endPos.Y) != y {
if endPos.X >= 0 && endPos.Y >= 0 {
ox, oy := int(endPos.X), int(endPos.Y)
rl.ImageDrawPixel(mapImage, int32(endPos.X), int32(endPos.Y), rl.NewColor(240, 240, 240, 255))
astar.SetGridType(ox, oy, 0)
tex.markRegion(ox, oy, ox, oy, width, height)
}
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(255, 0, 0, 255))
tex.markRegion(x, y, x, y, width, height)
endPos = rl.NewVector2(float32(x), float32(y))
astar.SetGridType(x, y, 3)
}
case 3: // Erase — same cell indices as walls; ImageDrawLine used world coords before (wrong space).
emptyCol := color.RGBA{R: 240, G: 240, B: 240, A: 255}
if lastMousePos.X != -1 && lastMousePos.Y != -1 {
prevX := int(lastMousePos.X / cellSize)
prevY := int(lastMousePos.Y / cellSize)
paintEraseLine(&astar, mapImage, prevX, prevY, x, y, width, height, emptyCol, &tex)
} else {
paintEraseLine(&astar, mapImage, x, y, x, y, width, height, emptyCol, &tex)
}
}
lastMousePos = worldPos
}
}
}
if rl.IsMouseButtonReleased(rl.MouseLeftButton) {
if autoCompute && lastMousePos.X != -1 && lastMousePos.Y != -1 && int(startPos.X) >= 0 && int(startPos.X) < width && int(startPos.Y) >= 0 && int(startPos.Y) < height && int(endPos.X) >= 0 && int(endPos.X) < width && int(endPos.Y) >= 0 && int(endPos.Y) < height {
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
gridTypes := astar.GetGridTypes()
for i, gridType := range gridTypes {
// reset the map image
switch gridType {
case 0:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255))
case 1:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255))
case 2:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255))
case 3:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255))
}
}
tex.markFull()
astar.SetHeuristic(activeHeuristic)
path := astar.CalculatePath(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y))
closedSet := astar.GetClosedSet()
for i, closed := range closedSet {
if closed {
x := i % width
y := i / width
if x != int(startPos.X) || y != int(startPos.Y) {
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255))
}
}
}
for _, p := range path {
if p[0] != int(startPos.X) || p[1] != int(startPos.Y) { // we want to keep the start position green
rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255))
}
}
tex.markFull()
}
lastMousePos = rl.NewVector2(-1, -1)
}
// --- DRAWING --- // --- DRAWING ---
rl.BeginDrawing() rl.BeginDrawing()
rl.ClearBackground(rl.White) rl.ClearBackground(rl.White)
@@ -128,6 +467,7 @@ func main() {
// 1. Draw Canvas // 1. Draw Canvas
rl.BeginScissorMode(0, 0, int32(canvasWidth), int32(screenHeight)) rl.BeginScissorMode(0, 0, int32(canvasWidth), int32(screenHeight))
rl.BeginMode2D(camera) rl.BeginMode2D(camera)
tex.flush(mapImage, &mapTexture, width, height)
rl.DrawTextureEx( rl.DrawTextureEx(
mapTexture, mapTexture,
rl.NewVector2(0, 0), // Position rl.NewVector2(0, 0), // Position
@@ -143,6 +483,7 @@ func main() {
sidebarX := canvasWidth sidebarX := canvasWidth
rl.DrawRectangleRec(rl.NewRectangle(sidebarX, 0, sidebarWidth, screenHeight), rl.RayWhite) rl.DrawRectangleRec(rl.NewRectangle(sidebarX, 0, sidebarWidth, screenHeight), rl.RayWhite)
// Width Input
if rg.TextBox(rl.NewRectangle(sidebarX+(10*scale), (10*scale), (80*scale), (20*scale)), &widthInputValue, 20, editModeWidth) { if rg.TextBox(rl.NewRectangle(sidebarX+(10*scale), (10*scale), (80*scale), (20*scale)), &widthInputValue, 20, editModeWidth) {
editModeWidth = !editModeWidth editModeWidth = !editModeWidth
if editModeWidth { if editModeWidth {
@@ -151,6 +492,7 @@ func main() {
} }
rg.Label(rl.NewRectangle(sidebarX+(95*scale), (10*scale), (10*scale), (20*scale)), "x") rg.Label(rl.NewRectangle(sidebarX+(95*scale), (10*scale), (10*scale), (20*scale)), "x")
// Height Input
if rg.TextBox(rl.NewRectangle(sidebarX+(110*scale), (10*scale), (80*scale), (20*scale)), &heightInputValue, 20, editModeHeight) { if rg.TextBox(rl.NewRectangle(sidebarX+(110*scale), (10*scale), (80*scale), (20*scale)), &heightInputValue, 20, editModeHeight) {
editModeHeight = !editModeHeight editModeHeight = !editModeHeight
if editModeHeight { if editModeHeight {
@@ -158,15 +500,121 @@ func main() {
} }
} }
if generateGridError {
result := rg.MessageBox(rl.NewRectangle(screenWidth/2-(100*scale), screenHeight/2-(40*scale), (200*scale), (120*scale)), "Error", "Width and/or height must be\nless than 16000", "OK")
if result >= 0 {
generateGridError = false
}
}
if posError {
result := rg.MessageBox(rl.NewRectangle(screenWidth/2-(100*scale), screenHeight/2-(40*scale), (200*scale), (120*scale)), "Error", "Start and end positions must be\nwithin the generated grid", "OK")
if result >= 0 {
posError = false
}
}
// Generate Grid Button
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (40*scale), (180*scale), (30*scale)), "Generate Grid") { if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (40*scale), (180*scale), (30*scale)), "Generate Grid") {
width, _ = strconv.Atoi(widthInputValue) width, _ = strconv.Atoi(widthInputValue)
height, _ = strconv.Atoi(heightInputValue) height, _ = strconv.Atoi(heightInputValue)
mapImage = rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255)) if width > 16000 || height > 16000 {
mapTexture = rl.LoadTextureFromImage(mapImage) generateGridError = true
defer rl.UnloadTexture(mapTexture) } else {
defer rl.UnloadImage(mapImage) rl.UnloadTexture(mapTexture)
rl.UnloadImage(mapImage)
mapImage = rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255))
mapTexture = rl.LoadTextureFromImage(mapImage)
astar.RebuildGrid(width, height)
startPos = rl.NewVector2(-1, -1)
endPos = rl.NewVector2(-1, -1)
tex = texSync{rectBuf: tex.rectBuf}
}
} }
// Tool Selector (text must be "opt1;opt2;..." — raygui splits on ';' and needs 2+ items)
rg.Label(rl.NewRectangle(sidebarX+(10*scale), (75*scale), (180*scale), (30*scale)), "Tool:")
if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (100*scale), (180*scale), (30*scale)), toolOptionsText, &activeTool, toolDropdownOpen) {
toolDropdownOpen = !toolDropdownOpen
}
// Heuristic Selector
if !toolDropdownOpen {
rg.Label(rl.NewRectangle(sidebarX+(10*scale), (135*scale), (180*scale), (30*scale)), "Heuristic:")
if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (160*scale), (180*scale), (30*scale)), heuristicOptionsText, &activeHeuristic, heuristicDropdownOpen) {
heuristicDropdownOpen = !heuristicDropdownOpen
}
}
// Reset Visualization Button
if !toolDropdownOpen && !heuristicDropdownOpen {
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (200*scale), (180*scale), (30*scale)), "Reset Visualization") {
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
gridTypes := astar.GetGridTypes()
for i, gridType := range gridTypes {
// reset the map image
switch gridType {
case 0:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255))
case 1:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255))
case 2:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255))
case 3:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255))
}
}
tex.markFull()
}
}
// AutoCompute
autoCompute = rg.CheckBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(40*scale)), (30*scale), (30*scale)), "", autoCompute) // no title because it would overlap the button
// Calculate Path Button
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(40*scale)), (140*scale), (30*scale)), "Calculate Path") {
if int(startPos.X) < 0 || int(startPos.X) >= width || int(startPos.Y) < 0 || int(startPos.Y) >= height || int(endPos.X) < 0 || int(endPos.X) >= width || int(endPos.Y) < 0 || int(endPos.Y) >= height {
posError = true
} else {
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
astar.SetHeuristic(activeHeuristic)
gridTypes := astar.GetGridTypes()
for i, gridType := range gridTypes {
// reset the map image
switch gridType {
case 0:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255))
case 1:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255))
case 2:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255))
case 3:
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255))
}
}
path := astar.CalculatePath(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y))
closedSet := astar.GetClosedSet()
for i, closed := range closedSet {
if closed {
x := i % width
y := i / width
if x != int(startPos.X) || y != int(startPos.Y) {
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255))
}
}
}
for _, p := range path {
if p[0] != int(startPos.X) || p[1] != int(startPos.Y) { // we want to keep the start position green
rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255))
}
}
tex.markFull()
}
}
// Status Label
rg.Label(rl.NewRectangle((10*scale), (screenHeight-(30*scale)), (canvasWidth-(20*scale)), (30*scale)), "Evaluated "+strconv.Itoa(astar.GetEvaluatedCells())+" cells in "+astar.GetTimeTaken().String())
rl.EndDrawing() rl.EndDrawing()
} }
} }