HUGE update to how drawing works, ensures consistency between visualization and sim. Also added erase tool.

This commit is contained in:
2026-05-14 09:37:53 -05:00
parent 6719f70c73
commit 589663d3d0
+258 -15
View File
@@ -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()