mirror of
https://github.com/wisplite/a-star-go.git
synced 2026-06-27 15:37:07 -05:00
HUGE update to how drawing works, ensures consistency between visualization and sim. Also added erase tool.
This commit is contained in:
@@ -15,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
|
||||||
@@ -79,11 +292,11 @@ func main() {
|
|||||||
editModeHeight := false
|
editModeHeight := false
|
||||||
generateGridError := false
|
generateGridError := false
|
||||||
|
|
||||||
toolOptions := []string{"Wall", "Start", "End"}
|
toolOptions := []string{"Wall", "Start", "End", "Erase"}
|
||||||
toolOptionsText := strings.Join(toolOptions, ";")
|
toolOptionsText := strings.Join(toolOptions, ";")
|
||||||
activeTool := int32(0)
|
activeTool := int32(0)
|
||||||
toolDropdownOpen := false
|
toolDropdownOpen := false
|
||||||
textureNeedsUpdate := true
|
var tex texSync // GPU uploads: partial rects while painting; full after path resets
|
||||||
|
|
||||||
heuristicOptions := []string{"Manhattan", "Euclidean", "Chebyshev"}
|
heuristicOptions := []string{"Manhattan", "Euclidean", "Chebyshev"}
|
||||||
heuristicOptionsText := strings.Join(heuristicOptions, ";")
|
heuristicOptionsText := strings.Join(heuristicOptions, ";")
|
||||||
@@ -154,33 +367,47 @@ func main() {
|
|||||||
if x >= 0 && x < width && y >= 0 && y < height {
|
if x >= 0 && x < width && y >= 0 && y < height {
|
||||||
// mapImage is one pixel per cell; drawing uses cell indices, not raw world coords.
|
// mapImage is one pixel per cell; drawing uses cell indices, not raw world coords.
|
||||||
switch activeTool {
|
switch activeTool {
|
||||||
case 0: // Wall — need previous cell for line segment
|
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 {
|
if lastMousePos.X != -1 && lastMousePos.Y != -1 {
|
||||||
prevX := int(lastMousePos.X / cellSize)
|
prevX := int(lastMousePos.X / cellSize)
|
||||||
prevY := int(lastMousePos.Y / cellSize)
|
prevY := int(lastMousePos.Y / cellSize)
|
||||||
rl.ImageDrawLine(mapImage, int32(prevX), int32(prevY), int32(x), int32(y), rl.NewColor(0, 0, 0, 255))
|
paintWallLine(&astar, mapImage, prevX, prevY, x, y, width, height, wallCol, &tex)
|
||||||
astar.SetGridType(x, y, 1)
|
} else {
|
||||||
textureNeedsUpdate = true
|
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)
|
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 int(startPos.X) != x || int(startPos.Y) != y {
|
||||||
if startPos.X >= 0 && startPos.Y >= 0 {
|
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))
|
rl.ImageDrawPixel(mapImage, int32(startPos.X), int32(startPos.Y), rl.NewColor(240, 240, 240, 255))
|
||||||
|
tex.markRegion(ox, oy, ox, oy, width, height)
|
||||||
}
|
}
|
||||||
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 255, 0, 255))
|
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))
|
startPos = rl.NewVector2(float32(x), float32(y))
|
||||||
astar.SetGridType(x, y, 2)
|
astar.SetGridType(x, y, 2)
|
||||||
textureNeedsUpdate = true
|
|
||||||
}
|
}
|
||||||
case 2: // End
|
case 2: // End
|
||||||
if int(endPos.X) != x || int(endPos.Y) != y {
|
if int(endPos.X) != x || int(endPos.Y) != y {
|
||||||
if endPos.X >= 0 && endPos.Y >= 0 {
|
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))
|
rl.ImageDrawPixel(mapImage, int32(endPos.X), int32(endPos.Y), rl.NewColor(240, 240, 240, 255))
|
||||||
|
tex.markRegion(ox, oy, ox, oy, width, height)
|
||||||
}
|
}
|
||||||
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(255, 0, 0, 255))
|
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))
|
endPos = rl.NewVector2(float32(x), float32(y))
|
||||||
astar.SetGridType(x, y, 3)
|
astar.SetGridType(x, y, 3)
|
||||||
textureNeedsUpdate = true
|
}
|
||||||
|
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
|
lastMousePos = worldPos
|
||||||
@@ -199,12 +426,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)
|
||||||
if textureNeedsUpdate {
|
tex.flush(mapImage, &mapTexture, width, height)
|
||||||
ptr := (*color.RGBA)(mapImage.Data)
|
|
||||||
pixels := unsafe.Slice(ptr, width*height)
|
|
||||||
rl.UpdateTexture(mapTexture, pixels)
|
|
||||||
textureNeedsUpdate = false
|
|
||||||
}
|
|
||||||
rl.DrawTextureEx(
|
rl.DrawTextureEx(
|
||||||
mapTexture,
|
mapTexture,
|
||||||
rl.NewVector2(0, 0), // Position
|
rl.NewVector2(0, 0), // Position
|
||||||
@@ -258,6 +480,7 @@ func main() {
|
|||||||
astar.RebuildGrid(width, height)
|
astar.RebuildGrid(width, height)
|
||||||
startPos = rl.NewVector2(-1, -1)
|
startPos = rl.NewVector2(-1, -1)
|
||||||
endPos = rl.NewVector2(-1, -1)
|
endPos = rl.NewVector2(-1, -1)
|
||||||
|
tex = texSync{rectBuf: tex.rectBuf}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +498,26 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset Visualization Button
|
||||||
|
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(80*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()
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate Path Button
|
// Calculate Path Button
|
||||||
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(40*scale)), (180*scale), (30*scale)), "Calculate Path") {
|
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(40*scale)), (180*scale), (30*scale)), "Calculate Path") {
|
||||||
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
|
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
|
||||||
@@ -300,7 +543,7 @@ func main() {
|
|||||||
rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255))
|
rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textureNeedsUpdate = true
|
tex.markFull()
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.EndDrawing()
|
rl.EndDrawing()
|
||||||
|
|||||||
Reference in New Issue
Block a user