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()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 1. Level of Detail (LOD) Check
|
||||
// If we are zoomed out too far, don't draw the gridlines at all
|
||||
@@ -79,11 +292,11 @@ func main() {
|
||||
editModeHeight := false
|
||||
generateGridError := false
|
||||
|
||||
toolOptions := []string{"Wall", "Start", "End"}
|
||||
toolOptions := []string{"Wall", "Start", "End", "Erase"}
|
||||
toolOptionsText := strings.Join(toolOptions, ";")
|
||||
activeTool := int32(0)
|
||||
toolDropdownOpen := false
|
||||
textureNeedsUpdate := true
|
||||
var tex texSync // GPU uploads: partial rects while painting; full after path resets
|
||||
|
||||
heuristicOptions := []string{"Manhattan", "Euclidean", "Chebyshev"}
|
||||
heuristicOptionsText := strings.Join(heuristicOptions, ";")
|
||||
@@ -154,33 +367,47 @@ func main() {
|
||||
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 — 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 {
|
||||
prevX := int(lastMousePos.X / cellSize)
|
||||
prevY := int(lastMousePos.Y / cellSize)
|
||||
rl.ImageDrawLine(mapImage, int32(prevX), int32(prevY), int32(x), int32(y), rl.NewColor(0, 0, 0, 255))
|
||||
astar.SetGridType(x, y, 1)
|
||||
textureNeedsUpdate = true
|
||||
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))
|
||||
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)
|
||||
textureNeedsUpdate = true
|
||||
}
|
||||
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))
|
||||
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)
|
||||
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
|
||||
@@ -199,12 +426,7 @@ func main() {
|
||||
// 1. Draw Canvas
|
||||
rl.BeginScissorMode(0, 0, int32(canvasWidth), int32(screenHeight))
|
||||
rl.BeginMode2D(camera)
|
||||
if textureNeedsUpdate {
|
||||
ptr := (*color.RGBA)(mapImage.Data)
|
||||
pixels := unsafe.Slice(ptr, width*height)
|
||||
rl.UpdateTexture(mapTexture, pixels)
|
||||
textureNeedsUpdate = false
|
||||
}
|
||||
tex.flush(mapImage, &mapTexture, width, height)
|
||||
rl.DrawTextureEx(
|
||||
mapTexture,
|
||||
rl.NewVector2(0, 0), // Position
|
||||
@@ -258,6 +480,7 @@ func main() {
|
||||
astar.RebuildGrid(width, height)
|
||||
startPos = 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
|
||||
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
|
||||
@@ -300,7 +543,7 @@ func main() {
|
||||
rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255))
|
||||
}
|
||||
}
|
||||
textureNeedsUpdate = true
|
||||
tex.markFull()
|
||||
}
|
||||
|
||||
rl.EndDrawing()
|
||||
|
||||
Reference in New Issue
Block a user