commit 07b8f0ecb146ceb7dbb2cebb9bc8cc9375296888 Author: Marto Date: Sun Apr 26 15:51:04 2026 +0200 Initial commit: base-road-network copy diff --git a/src/common/constants.odin b/src/common/constants.odin new file mode 100644 index 0000000..865ffa5 --- /dev/null +++ b/src/common/constants.odin @@ -0,0 +1,34 @@ +package common + +import rl "vendor:raylib" + +// Screen Width +WIDTH :: 1366 +// Screen Height +HEIGHT :: 768 +// Default Monitor +MONITOR :: 0 + +// Thickness of the road being drawn +ROAD_SIZE :: 20 +// Radius of the node +NODE_RADIUS :: 20 +// Size of designated radius that determines when position is within node's 'sphere' +NODE_SNAP_RADIUS :: 3 +// Default text size +TEXT_SIZE :: 50 + +// Default road colour +ROAD_COLOUR :: rl.BLACK +// Highlighted road colour +ROAD_HIGHLIGHT_COLOUR :: rl.GREEN +// Node colour once it's fully built +NODE_DONE_COLOUR :: rl.BROWN +// Node colour while node is being built +NODE_BUILD_COLOUR :: rl.ORANGE +// Node colour while being able to start building but not doing that yet +NODE_CURSOR_COLOUR :: rl.BLUE +// The colour of the overlay displaying the snap radius +NODE_SNAP_COLOUR :: rl.PINK +// The default colour of the text displayed +TEXT_COLOUR :: rl.BLACK \ No newline at end of file diff --git a/src/common/structures.odin b/src/common/structures.odin new file mode 100644 index 0000000..977c072 --- /dev/null +++ b/src/common/structures.odin @@ -0,0 +1,17 @@ +package common + +import rl "vendor:raylib" + +// Stores data about intersections +Intersection_Data :: struct { + // Index of the road that is intersected + road: u32, + // The exact point of intersection + point: rl.Vector2, +} + +// Tracks the entity type +Entity :: enum { + Node, + Road, +} \ No newline at end of file diff --git a/src/draw.odin b/src/draw.odin new file mode 100644 index 0000000..add8018 --- /dev/null +++ b/src/draw.odin @@ -0,0 +1,49 @@ +package main + +import "core:fmt" +import rl "vendor:raylib" + +import "common" + +// Main drawing function +draw :: proc(self: ^Simulator, pos: rl.Vector2) { + rl.ClearBackground(rl.LIGHTGRAY) + + // draw roads + for &road, index in self.roads { + start := road.nodes[0] + end := road.nodes[1] + + road_colour: rl.Color + if road, ok := self.highlighted_road.?; ok && road == u32(index) && self.delete_mode { + road_colour = common.ROAD_HIGHLIGHT_COLOUR + } else do road_colour = common.ROAD_COLOUR + + rl.DrawLineEx(self.nodes[start].pos, self.nodes[end].pos, common.ROAD_SIZE, road_colour) + } + + // draw nodes + for &node in self.nodes { + // draws the snapping radius if key is held down + if self.show_details do rl.DrawCircleV(node.pos, common.NODE_SNAP_RADIUS * common.NODE_RADIUS, common.NODE_SNAP_COLOUR) + // draws the node + rl.DrawCircleV(node.pos, common.NODE_RADIUS, common.NODE_DONE_COLOUR) + } + + // draw temp road if exists + if val, ok := self.temp_node.?; ok { + rl.DrawLineEx(self.nodes[val].pos, pos, common.ROAD_SIZE, common.ROAD_COLOUR) + + rl.DrawCircleV(self.nodes[val].pos, common.NODE_RADIUS, common.NODE_BUILD_COLOUR) + rl.DrawCircleV(pos, common.NODE_RADIUS, common.NODE_CURSOR_COLOUR) + } + + draw_ui(self) +} + +// Drawing UI text, mostly for debugging purposes +@(private="file") +draw_ui :: proc(self: ^Simulator) { + entity_count := fmt.ctprintf("Nodes: %d\nRoads: %d", len(self.nodes), len(self.roads)) + rl.DrawText(entity_count, i32(common.WIDTH - 13 * len(entity_count)), common.HEIGHT - 2 * common.TEXT_SIZE, common.TEXT_SIZE, common.TEXT_COLOUR) +} \ No newline at end of file diff --git a/src/entity_management.odin b/src/entity_management.odin new file mode 100644 index 0000000..d2143af --- /dev/null +++ b/src/entity_management.odin @@ -0,0 +1,90 @@ +package main + +import rl "vendor:raylib" + +import "common" +import inf "infrastructure" + +// This function only returns the index to the node or if it doesn't exist bool in the tuple is false +@private +get_node_index_if_exists :: proc(self: ^Simulator, pos: rl.Vector2) -> (u32, bool) { + for &node, index in self.nodes { + if inf.node_within_snapping_radius(&node, pos) do return u32(index), true + } + + return 0, false +} + +// Given position, the function will attempt the return the pointer to the node in near vicinity, +// or if unsuccesful manually creating the node based on the position in the list and then returning the pointer to it +@private +get_node_or_new :: proc(self: ^Simulator, pos: rl.Vector2) -> u32 { + if node, ok := get_node_index_if_exists(self, pos); ok do return node + + node := inf.node_init(pos) + append(&self.nodes, node) + + return u32(len(self.nodes) - 1) +} + +// Attempts to update node reference to the road; +// Returns false if the old reference doesn't exist +@private +update_node_reference :: proc(self: ^Simulator, road_to_update: u32, old_ref: u32, new_ref: u32) -> bool { + road := &self.roads[road_to_update] + + for i in 0.. ([2]u32, bool) { + mlen: u32 + // Stores data about old and new index in case the deleted index is not last, meaning the swap occurs + index_change: [2]u32 + // Tracks whether the removal of node/road will cause a swap in the (dynamic) array + // and thus forcing the pre-swapped reference to be updated + swap_made: bool + + switch type { + case .Node: + mlen = u32(len(self.nodes)) + case .Road: + mlen = u32(len(self.roads)) + } + + last := mlen - 1 + if entity_index != last { + index_change = {last, entity_index} + swap_made = true + } + + switch type { + case .Node: + unordered_remove(&self.nodes, entity_index) + if !swap_made do return {}, false + + for &road in self.roads do inf.road_update_node_reference(&road, index_change[0], index_change[1]) + return index_change, true + + case .Road: + unordered_remove(&self.roads, entity_index) + if !swap_made do return {}, false + + for &node in self.nodes do inf.node_update_road_reference(&node, index_change[0], index_change[1]) + return index_change, true + } + + return {}, false +} \ No newline at end of file diff --git a/src/infrastructure/node.odin b/src/infrastructure/node.odin new file mode 100644 index 0000000..15b3f97 --- /dev/null +++ b/src/infrastructure/node.odin @@ -0,0 +1,61 @@ +package infrastructure + +import "../common" +import rl "vendor:raylib" + +Node :: struct { + // Whether the node is reachable + enabled: bool, + // This node's position + pos: rl.Vector2, + // All of the roads that are connected to the node itself; + // Stores the index of the Road object that is stored within Simulator struct in roads dynamic array + roads: [dynamic]u32, +} + +// Constructor +node_init :: proc(new_pos: rl.Vector2) -> Node { + return { + enabled = true, + pos = new_pos, + roads = nil, + } +} + +// Destructor +node_deinit :: proc(self: ^Node) { + delete(self.roads) +} + +// Returns whether passed pos(ition) is within this node's snapping radius +node_within_snapping_radius :: proc(self: ^Node, pos: rl.Vector2) -> bool { + return rl.CheckCollisionPointCircle( + pos, + self.pos, + common.NODE_SNAP_RADIUS * common.NODE_RADIUS, + ) +} + +// Tries to remove the road reference from the node; returns false if failed +node_unreference_road :: proc(self: ^Node, road_to_unref: u32) -> bool { + for i in 0.. bool { + for &road in self.roads { + if road != old_ref do continue + + road = new_ref + return true + } + + return false +} \ No newline at end of file diff --git a/src/infrastructure/road.odin b/src/infrastructure/road.odin new file mode 100644 index 0000000..0ab758d --- /dev/null +++ b/src/infrastructure/road.odin @@ -0,0 +1,25 @@ +package infrastructure + +Road :: struct { + // Index to nodes that limit the road + nodes: [2]u32, +} + +// Road Initialisation +road_init :: proc(start: u32, end: u32) -> Road { + return { + nodes = {start, end} + } +} + +// Updates existing node reference to a new one; returns false if old ref was not found +road_update_node_reference :: proc(self: ^Road, old_ref: u32, new_ref: u32) -> bool { + for &node in self.nodes { + if node != old_ref do continue + + node = new_ref + return true + } + + return false +} \ No newline at end of file diff --git a/src/input.odin b/src/input.odin new file mode 100644 index 0000000..ffd37b6 --- /dev/null +++ b/src/input.odin @@ -0,0 +1,62 @@ +package main + +import rl "vendor:raylib" + +import inf "infrastructure" + +// Public input function that gets called in graphics library loop +handle_input :: proc(self: ^Simulator, pos: rl.Vector2) { + handle_keyboard_input(self) + handle_mouse_input(self, pos) +} + +// General keyboard input event handler +@(private="file") +handle_keyboard_input :: proc(self: ^Simulator) { + self.show_details = rl.IsKeyDown(.LEFT_ALT) + self.auto_continue = rl.IsKeyDown(.LEFT_CONTROL) + self.delete_mode = rl.IsKeyDown(.LEFT_SHIFT) + + if (rl.IsKeyReleased(.C)) { + self.temp_node = nil + clear(&self.roads) + clear(&self.nodes) + } +} + +// Generally mouse event handler +@(private="file") +handle_mouse_input :: proc(self: ^Simulator, pos: rl.Vector2) { + if (rl.IsMouseButtonReleased(.LEFT)) { + left_click_event(self, pos) + } else if (rl.IsMouseButtonReleased(.RIGHT)) { + right_click_event(self) + } +} + +// Handles left click functionality +@(private="file") +left_click_event :: proc(self: ^Simulator, pos: rl.Vector2) { + if self.delete_mode { + if road, ok := self.highlighted_road.?; ok do delete_road(self, road) + return + } + + create_road(self, pos) +} + +// Handles right click functionality +@(private="file") +right_click_event :: proc(self: ^Simulator) { + if self.temp_node == nil do return + index := self.temp_node.? + + temp_node := &self.nodes[index] + self.temp_node = nil + if len(temp_node.roads) > 0 do return + + inf.node_deinit(temp_node) + // We can safely call the remove here as the only way it will get deleted is if it's the only node aka it was created during our creation process + // Consequently this means that it will always be on last place and never swapped with anything + unordered_remove(&self.nodes, index) +} \ No newline at end of file diff --git a/src/main.odin b/src/main.odin new file mode 100644 index 0000000..a7a6283 --- /dev/null +++ b/src/main.odin @@ -0,0 +1,29 @@ +package main + +import rl "vendor:raylib" + +import "common" + +main :: proc() { + rl.SetConfigFlags({.MSAA_4X_HINT, .WINDOW_HIGHDPI}) + rl.InitWindow(common.WIDTH, common.HEIGHT, "Base Road Network") + defer rl.CloseWindow() + + rl.SetWindowMonitor(common.MONITOR) + rl.SetTargetFPS(rl.GetMonitorRefreshRate(common.MONITOR)) + + sim: Simulator + defer deinit(&sim) + + for !rl.WindowShouldClose() { + rl.BeginDrawing() + defer rl.EndDrawing() + + pos := rl.GetMousePosition() + + handle_input(&sim, pos) + update(&sim, pos) + + draw(&sim, pos) + } +} \ No newline at end of file diff --git a/src/simulator.odin b/src/simulator.odin new file mode 100644 index 0000000..38cd873 --- /dev/null +++ b/src/simulator.odin @@ -0,0 +1,192 @@ +package main + +import rl "vendor:raylib" + +import "common" +import "core:fmt" +import inf "infrastructure" + +Simulator :: struct { + // Stores all nodes + nodes: [dynamic]inf.Node, + // Stores all roads + roads: [dynamic]inf.Road, + // Tracks the temporary node location + temp_node: Maybe(u32), + // Tracks the selected road + highlighted_road: Maybe(u32), + // Tracks whether the user wishes to see node's snapping radius + show_details: bool, + // Tracks whether after placing a road new one will start being placed + auto_continue: bool, + // Tracks whether the delete mode is activated + delete_mode: bool, +} + +// Destructor +deinit :: proc(self: ^Simulator) { + self.temp_node = nil + self.highlighted_road = nil + + delete(self.roads) + + for &node in self.nodes { + inf.node_deinit(&node) + } + delete(self.nodes) +} + +// Functionality that runs every tick regardless of input +update :: proc(self: ^Simulator, pos: rl.Vector2) { + update_highlighted_road(self, pos) +} + +// Implementation of road building after a click has been registered +@private +create_road :: proc(self: ^Simulator, pos: rl.Vector2) { + // BUILD + cur_node_index := get_node_or_new(self, pos) + + if temp, ok := self.temp_node.?; ok { + // If both values are identical this means the user has to create a new road using the same nodes + if cur_node_index == temp do return + data := get_intersecting_roads(self, temp, cur_node_index) + split_roads_by_points(self, data, temp, cur_node_index) + + self.temp_node = self.auto_continue ? cur_node_index : nil + return + } + + self.temp_node = cur_node_index +} + +// Returns data about roads that intersect the given 2 nodes (points) +@(private="file") +get_intersecting_roads :: proc(self: ^Simulator, start: u32, end: u32) -> []common.Intersection_Data { + intersections: [dynamic]common.Intersection_Data + collision_point: rl.Vector2 + + outer: for road, index in self.roads { + road_start_node := road.nodes[0] + road_end_node := road.nodes[1] + + if !rl.CheckCollisionLines(self.nodes[start].pos, self.nodes[end].pos, self.nodes[road_start_node].pos, self.nodes[road_end_node].pos, &collision_point) do continue + + // Save the collision info + data := common.Intersection_Data { + road = u32(index), + point = collision_point + } + + node := inf.node_init(data.point) + // Here we check if the intersection points that were recorded before, are already within snapping radius of our current intersected point + for collision in intersections { + if (inf.node_within_snapping_radius(&node, collision.point)) do continue outer + } + + // Here we check if our new intersected point node is too close to already established nodes + if _, ok := get_node_index_if_exists(self, data.point); ok do continue + + append(&intersections, data) + } + + return intersections[:] +} + +// Given intersection data, the function splits all existing roads and adds new nodes on intersections +@(private="file") +split_roads_by_points :: proc(self: ^Simulator, intersections: []common.Intersection_Data, start: u32, end: u32) { + if len(intersections) == 0 { + add_road(self, start, end) + return + } + + first_intersection_node := get_node_or_new(self, intersections[0].point) + add_road(self, start, first_intersection_node) + + for i in 0..