From 589663d3d0fb5769cf73aa6c1e9dc09e40f1ddee Mon Sep 17 00:00:00 2001 From: wisplite Date: Thu, 14 May 2026 09:37:53 -0500 Subject: [PATCH] HUGE update to how drawing works, ensures consistency between visualization and sim. Also added erase tool. --- main.go | 273 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 258 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index 23e63d1..c11adb4 100644 --- a/main.go +++ b/main.go @@ -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()