From 680e07c976ed2993af08fb0c659c840df13dd420 Mon Sep 17 00:00:00 2001 From: Marto Date: Fri, 1 May 2026 07:49:07 +0200 Subject: [PATCH 1/8] Splitting road implementation preparation --- src/common/utils.zig | 24 ++++++++++++ src/infrastructure/node.zig | 6 +++ src/infrastructure/node_manager.zig | 15 +++++++- src/infrastructure/road.zig | 3 +- src/infrastructure/road_manager.zig | 22 +---------- src/simulator.zig | 58 +++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 src/common/utils.zig diff --git a/src/common/utils.zig b/src/common/utils.zig new file mode 100644 index 0000000..eac874a --- /dev/null +++ b/src/common/utils.zig @@ -0,0 +1,24 @@ +const Vector2 = @import("raylib").Vector2; + +const st = @import("structures.zig"); +const Node = @import("../infrastructure/node.zig").Node; + +/// Returns distance between two nodes, with the caveat that it doesn't do square root in the end; +/// that's because we are interested in relative distance and don't care for the precise number as long as it is in +/// correct relation to others +/// +/// This allows us to avoid the (square) rooting operation which is computationally expensive +pub fn calculate_length(start: Vector2, end: Vector2) f32 { + const x_diff = end.x - start.x; + const y_diff = end.y - start.y; + + return x_diff * x_diff + y_diff * y_diff; +} + +/// Comparator function that compares intersection proximity from the node in order to aid the sorting function +pub fn compareIntersections(ctx: *const Node, inter_a: st.IntersectionData, inter_b: st.IntersectionData) bool { + const distance_a = ctx.getRelativeInterDistance(inter_a); + const distance_b = ctx.getRelativeInterDistance(inter_b); + + return distance_a < distance_b; +} \ No newline at end of file diff --git a/src/infrastructure/node.zig b/src/infrastructure/node.zig index 1946d5d..a26f78c 100644 --- a/src/infrastructure/node.zig +++ b/src/infrastructure/node.zig @@ -4,6 +4,7 @@ const rl = @import("raylib"); const c = @import("../common/constants.zig"); const e = @import("../errors.zig"); const st = @import("../common/structures.zig"); +const ut = @import("../common/utils.zig"); const Road = @import("road.zig").Road; pub const Node = struct { @@ -77,6 +78,11 @@ pub const Node = struct { return e.Entity.NotFound; } + + /// Returns distance between the node and intersection object location + pub fn getRelativeInterDistance(self: *const Node, intersection: st.IntersectionData) f32 { + return ut.calculate_length(intersection.pos, self.pos); + } }; // TODO tests diff --git a/src/infrastructure/node_manager.zig b/src/infrastructure/node_manager.zig index e1f3821..444a5ba 100644 --- a/src/infrastructure/node_manager.zig +++ b/src/infrastructure/node_manager.zig @@ -50,11 +50,22 @@ pub const NodeManager = struct { } } + /// Tries to find a node which snapping radius covers the pos + /// + /// If it does it returns a reference to it, otherwise null + pub fn getNodeIfExists(self: *const NodeManager, pos: Vector2) ?*Node { + for (self.nodes.items) |node| { + if (node.withinSnapRadius(pos)) return node; + } + + return null; + } + /// Checks if there is a node pointer within the snap radius of pos coordinate; /// otherwise creates a new node and returns its pointer pub fn getSelectedNode(self: *NodeManager, allocator: std.mem.Allocator, pos: Vector2) !*Node { - for (self.nodes.items) |node| { - if (node.withinSnapRadius(pos)) return node; + if (self.getNodeIfExists(pos)) |node| { + return node; } // No node is within that position, so we must create a new one diff --git a/src/infrastructure/road.zig b/src/infrastructure/road.zig index af19395..9969b98 100644 --- a/src/infrastructure/road.zig +++ b/src/infrastructure/road.zig @@ -2,6 +2,7 @@ const rl = @import("raylib"); const c = @import("../common/constants.zig"); const st = @import("../common/structures.zig"); +const ut = @import("../common/utils.zig"); const Node = @import("node.zig").Node; pub const Road = struct { @@ -20,7 +21,7 @@ pub const Road = struct { .length = 0, }; - road.length = road.calculate_length(); + road.length = ut.calculate_length(start.pos, end.pos); return road; } diff --git a/src/infrastructure/road_manager.zig b/src/infrastructure/road_manager.zig index 21a2d33..026b1e8 100644 --- a/src/infrastructure/road_manager.zig +++ b/src/infrastructure/road_manager.zig @@ -1,8 +1,9 @@ const std = @import("std"); const rl = @import("raylib"); -const st = @import("../common/structures.zig"); const e = @import("../errors.zig"); +const st = @import("../common/structures.zig"); +const ut = @import("../common/utils.zig"); const Road = @import("road.zig").Road; const Node = @import("node.zig").Node; @@ -93,25 +94,6 @@ pub const RoadManager = struct { return null; } - - pub fn getIntersectingRoads(self: *const RoadManager, allocator: std.mem.Allocator, start: *const Node, end: *const Node) ![]st.IntersectionData { - var intersections: std.ArrayList(st.IntersectionData) = .empty; - const collision_point: rl.Vector2 = undefined; - - for (self.roads.items) |road| { - if (!rl.checkCollisionLines(start.pos, end.pos, road.nodes[0].pos, road.nodes[1].pos, &collision_point)) - continue; - - const intersection = st.IntersectionData { - .road = road, - .pos = collision_point, - }; - - try intersections.append(allocator, intersection); - } - - return intersections.toOwnedSlice(allocator); - } }; const Vector2 = @import("raylib").Vector2; diff --git a/src/simulator.zig b/src/simulator.zig index c65b149..5c556ac 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -3,7 +3,9 @@ const rl = @import("raylib"); const c = @import("common/constants.zig"); const st = @import("common/structures.zig"); +const ut = @import("common/utils.zig"); const Road = @import("infrastructure/road.zig").Road; +const Node = @import("infrastructure/node.zig").Node; const NodeManager = @import("infrastructure/node_manager.zig").NodeManager; const RoadManager = @import("infrastructure/road_manager.zig").RoadManager; @@ -143,11 +145,25 @@ pub const Simulator = struct { }; if (self.node_man.temp_node) |temp| { + // Prevents the road from being attached to 2 identical nodes (0 length road) if (temp.id == cur_node.id) return; + + // TODO replace with road splitting self.road_man.addRoad(self.allocator, temp, cur_node) catch |err| { std.debug.panic("Failed to add a new road or assigning its nodes: {}\n", .{err}); }; + const intersections = self.getIntersectingRoads(self.allocator, temp, cur_node) catch |err| { + std.debug.panic("Intersection selection failure: {}\n", .{err}); + }; + // DEBUG TODO REMOVE + std.debug.print("Displaying intersection position and the intersected road:\n", .{}); + for (0..intersections.len) |i| { + const int = intersections[i]; + std.debug.print("Road ID={d} Pos: ({d}, {d})\n", .{int.road.id, int.pos.x, int.pos.y}); + } + + self.node_man.temp_node = if (self.auto_continue) cur_node else null; return; } @@ -196,4 +212,46 @@ pub const Simulator = struct { self.highlighted_entity = null; } + + fn getIntersectingRoads(self: *const Simulator, allocator: std.mem.Allocator, start: *const Node, end: *const Node) ![]st.IntersectionData { + var intersections: std.ArrayList(st.IntersectionData) = .empty; + var collision_point: rl.Vector2 = undefined; + + outer: for (self.road_man.roads.items) |road| { + if (!rl.checkCollisionLines(start.pos, end.pos, road.nodes[0].pos, road.nodes[1].pos, &collision_point)) + continue; + + const intersection = st.IntersectionData { + .road = road, + .pos = collision_point, + }; + + // We put a 0 here, just to satisfy the constructor function + const node: Node = .init(0, intersection.pos); + // If the newly acquired intersection node is within the snapping radius of already existing nodes, + // we don't add it to the list + for (intersections.items) |inter_collision| { + if (node.withinSnapRadius(inter_collision.pos)) continue :outer; + } + + // If there is an existing node that covers our position within its snapping radius, + // then such position will not be saved as intersection + if (self.node_man.getNodeIfExists(node.pos) != null) continue :outer; + + try intersections.append(allocator, intersection); + } + + const sorted_intersection = try intersections.toOwnedSlice(allocator); + std.sort.block(st.IntersectionData, sorted_intersection, start, ut.compareIntersections); + + return sorted_intersection; + } + + fn splitRoadsByIntersections(self: *Simulator, intersections: st.IntersectionData, start: *Node, end: *Node) void { + _ = intersections; // autofix + _ = end; // autofix + _ = start; // autofix + _ = self; // autofix + + } }; \ No newline at end of file From e475814c85e764d0cc0dc6839d6b76d78c02317a Mon Sep 17 00:00:00 2001 From: Marto Date: Fri, 1 May 2026 12:54:41 +0200 Subject: [PATCH 2/8] Changed resolution, switched to LLVM backend, from selfhosted one --- build.zig | 1 + src/common/constants.zig | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 0356b98..faaa157 100644 --- a/build.zig +++ b/build.zig @@ -69,6 +69,7 @@ pub fn build(b: *std.Build) void { // don't need and to put everything under a single module. const exe = b.addExecutable(.{ .name = "base_road_network", + .use_llvm = true, .root_module = b.createModule(.{ // b.createModule defines a new module just like b.addModule but, // unlike b.addModule, it does not expose the module to consumers of diff --git a/src/common/constants.zig b/src/common/constants.zig index 80b8f07..c87cffc 100644 --- a/src/common/constants.zig +++ b/src/common/constants.zig @@ -1,9 +1,9 @@ const clr = @import("raylib").Color; /// Screen Width -pub const WIDTH = 1366; +pub const WIDTH = 1920; /// Screen Height -pub const HEIGHT = 768; +pub const HEIGHT = 1080; pub const BACKGROUND_COLOR = clr.light_gray; /// Base node radius From 2a3064b0fed0cdbdf67c3f32dbe0c6fec4d50115 Mon Sep 17 00:00:00 2001 From: Marto Date: Fri, 1 May 2026 14:46:30 +0200 Subject: [PATCH 3/8] Improvement to intersection data calculation --- src/common/utils.zig | 9 +++++ src/infrastructure/node.zig | 9 +++++ src/infrastructure/road.zig | 2 +- src/infrastructure/road_manager.zig | 2 +- src/simulator.zig | 59 ++++++++++++++++++++++++----- 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/common/utils.zig b/src/common/utils.zig index eac874a..12acd8e 100644 --- a/src/common/utils.zig +++ b/src/common/utils.zig @@ -21,4 +21,13 @@ pub fn compareIntersections(ctx: *const Node, inter_a: st.IntersectionData, inte const distance_b = ctx.getRelativeInterDistance(inter_b); return distance_a < distance_b; +} + +// felt cute, might delete it later idk +pub fn listContains(comptime T: type, element: *T, list: *[]*T) bool { + for (0..list.len) |i| { + if (list[i] == element) return true; + } + + return false; } \ No newline at end of file diff --git a/src/infrastructure/node.zig b/src/infrastructure/node.zig index a26f78c..7130840 100644 --- a/src/infrastructure/node.zig +++ b/src/infrastructure/node.zig @@ -83,6 +83,15 @@ pub const Node = struct { pub fn getRelativeInterDistance(self: *const Node, intersection: st.IntersectionData) f32 { return ut.calculate_length(intersection.pos, self.pos); } + + /// Searches for the road pointer within roads list + pub fn roadsContains(self: *const Node, road_to_search: *const Road) bool { + for (self.roads.items) |road| { + if (road == road_to_search) return true; + } + + return false; + } }; // TODO tests diff --git a/src/infrastructure/road.zig b/src/infrastructure/road.zig index 9969b98..0220eca 100644 --- a/src/infrastructure/road.zig +++ b/src/infrastructure/road.zig @@ -60,7 +60,7 @@ pub const Road = struct { } /// Checks whether pos coordinate is on the referenced road - pub fn isHighlighted(self: *const Road, pos: rl.Vector2) bool { + pub fn collides(self: *const Road, pos: rl.Vector2) bool { return rl.checkCollisionPointLine(pos, self.nodes[0].pos, self.nodes[1].pos, c.ROAD_SIZE); } }; diff --git a/src/infrastructure/road_manager.zig b/src/infrastructure/road_manager.zig index 026b1e8..9ba88c3 100644 --- a/src/infrastructure/road_manager.zig +++ b/src/infrastructure/road_manager.zig @@ -89,7 +89,7 @@ pub const RoadManager = struct { /// Returns if pos is pointing at a road, or null if it isn't at any pub fn getHighlightedRoad(self: *const RoadManager, pos: Vector2) ?*Road { for (self.roads.items) |road| { - if (road.isHighlighted(pos)) return road; + if (road.collides(pos)) return road; } return null; diff --git a/src/simulator.zig b/src/simulator.zig index 5c556ac..631637a 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -156,6 +156,8 @@ pub const Simulator = struct { const intersections = self.getIntersectingRoads(self.allocator, temp, cur_node) catch |err| { std.debug.panic("Intersection selection failure: {}\n", .{err}); }; + defer self.allocator.free(intersections); + // DEBUG TODO REMOVE std.debug.print("Displaying intersection position and the intersected road:\n", .{}); for (0..intersections.len) |i| { @@ -163,7 +165,6 @@ pub const Simulator = struct { std.debug.print("Road ID={d} Pos: ({d}, {d})\n", .{int.road.id, int.pos.x, int.pos.y}); } - self.node_man.temp_node = if (self.auto_continue) cur_node else null; return; } @@ -217,8 +218,32 @@ pub const Simulator = struct { var intersections: std.ArrayList(st.IntersectionData) = .empty; var collision_point: rl.Vector2 = undefined; + var start_node_collision: ?*Road = null; + var end_node_collision: ?*Road = null; + + // Here we will check if any road collides with start and end node + for (self.road_man.roads.items) |road| { + if (start_node_collision == null and road.collides(start.pos) and !start.roadsContains(road)) + start_node_collision = road; + if (end_node_collision == null and road.collides(end.pos) and !end.roadsContains(road)) + end_node_collision = road; + + if (start_node_collision != null and end_node_collision != null) break; + } + + // if road node is placed on the road it is added as a collision with said road + if (start_node_collision) |road| { + try intersections.append(self.allocator, .{ + .road = road, + .pos = start.pos, + }); + } + outer: for (self.road_man.roads.items) |road| { - if (!rl.checkCollisionLines(start.pos, end.pos, road.nodes[0].pos, road.nodes[1].pos, &collision_point)) + if (!rl.checkCollisionLines( + start.pos,end.pos, + road.nodes[0].pos, road.nodes[1].pos, + &collision_point)) continue; const intersection = st.IntersectionData { @@ -226,7 +251,8 @@ pub const Simulator = struct { .pos = collision_point, }; - // We put a 0 here, just to satisfy the constructor function + // We put a 0 here, just to satisfy the constructor function, + // it is not getting appended to the node list anyways const node: Node = .init(0, intersection.pos); // If the newly acquired intersection node is within the snapping radius of already existing nodes, // we don't add it to the list @@ -236,22 +262,37 @@ pub const Simulator = struct { // If there is an existing node that covers our position within its snapping radius, // then such position will not be saved as intersection - if (self.node_man.getNodeIfExists(node.pos) != null) continue :outer; + if (self.node_man.getNodeIfExists(node.pos) != null) continue; try intersections.append(allocator, intersection); } + // if end node is placed on the road it is added as a collision with said road + if (end_node_collision) |road| { + try intersections.append(self.allocator, .{ + .road = road, + .pos = end.pos, + }); + } + const sorted_intersection = try intersections.toOwnedSlice(allocator); std.sort.block(st.IntersectionData, sorted_intersection, start, ut.compareIntersections); return sorted_intersection; } - fn splitRoadsByIntersections(self: *Simulator, intersections: st.IntersectionData, start: *Node, end: *Node) void { - _ = intersections; // autofix - _ = end; // autofix - _ = start; // autofix - _ = self; // autofix + fn splitRoadsByIntersections(self: *Simulator, intersections: []st.IntersectionData, start: *Node, end: *Node) void { + if (intersections.len == 0) { + self.road_man.addRoad(self.allocator, start, end) catch |err| { + std.debug.panic("Failed to create a road connecting the origin nodes: {}\n", .{err}); + }; + return; + } + + for (0..intersections.len) |i| { + _ = i; // autofix + + } } }; \ No newline at end of file From 643712f52981adcb2108361b13fb6ea1ad143366 Mon Sep 17 00:00:00 2001 From: Marto Date: Fri, 1 May 2026 15:44:00 +0200 Subject: [PATCH 4/8] Basic Entity ID display for debugging --- src/common/constants.zig | 4 ++++ src/common/utils.zig | 24 ++++++++++-------------- src/infrastructure/node.zig | 16 ++++++++++++++-- src/infrastructure/node_manager.zig | 10 +++++----- src/infrastructure/road.zig | 18 +++++++++++++++++- src/infrastructure/road_manager.zig | 4 ++-- src/simulator.zig | 19 ++++++++++++------- 7 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/common/constants.zig b/src/common/constants.zig index c87cffc..4c6cb77 100644 --- a/src/common/constants.zig +++ b/src/common/constants.zig @@ -24,3 +24,7 @@ pub const ROAD_SIZE = 20; pub const ROAD_COLOUR = clr.black; /// Colour of the road that is highlighted pub const ROAD_HIGHLIGHTED_COLOUR = clr.green; + +pub const TEXT_SIZE = 50; +pub const ENTITY_DATA_TEXT_SIZE = TEXT_SIZE / 2; +pub const ENTITY_DATA_TEXT_COLOUR = clr.orange; \ No newline at end of file diff --git a/src/common/utils.zig b/src/common/utils.zig index 12acd8e..8e081cb 100644 --- a/src/common/utils.zig +++ b/src/common/utils.zig @@ -3,16 +3,13 @@ const Vector2 = @import("raylib").Vector2; const st = @import("structures.zig"); const Node = @import("../infrastructure/node.zig").Node; -/// Returns distance between two nodes, with the caveat that it doesn't do square root in the end; -/// that's because we are interested in relative distance and don't care for the precise number as long as it is in -/// correct relation to others -/// -/// This allows us to avoid the (square) rooting operation which is computationally expensive +/// Returns distance between two nodes, used mostly for road length and possibly for A* heuristics pub fn calculate_length(start: Vector2, end: Vector2) f32 { const x_diff = end.x - start.x; const y_diff = end.y - start.y; - return x_diff * x_diff + y_diff * y_diff; + const squared_solution = x_diff * x_diff + y_diff * y_diff; + return @sqrt(squared_solution); } /// Comparator function that compares intersection proximity from the node in order to aid the sorting function @@ -23,11 +20,10 @@ pub fn compareIntersections(ctx: *const Node, inter_a: st.IntersectionData, inte return distance_a < distance_b; } -// felt cute, might delete it later idk -pub fn listContains(comptime T: type, element: *T, list: *[]*T) bool { - for (0..list.len) |i| { - if (list[i] == element) return true; - } - - return false; -} \ No newline at end of file +/// Returns vector from P1 to P2 +pub fn getVectorP1P2(p1: Vector2, p2: Vector2) Vector2 { + return .{ + .x = p2.x - p1.x, + .y = p2.y - p1.y, + }; +} diff --git a/src/infrastructure/node.zig b/src/infrastructure/node.zig index 7130840..2fedf55 100644 --- a/src/infrastructure/node.zig +++ b/src/infrastructure/node.zig @@ -35,10 +35,22 @@ pub const Node = struct { } /// Simple function which draws the node - pub fn draw(self: *const Node, direct_colour: ?rl.Color) void { + pub fn draw(self: *const Node, direct_colour: ?rl.Color, display_info: bool) void { const colour = if (direct_colour) |clr| clr else c.NODE_COLOUR; - rl.drawCircleV(self.pos, c.NODE_RADIUS, colour); + + if (!display_info) return; + + var buf: [100]u8 = undefined; + const entity_info = std.fmt.bufPrintZ(&buf, "{d}", .{self.id}) catch |err| { + std.debug.panic("Failed to allocate space for ID???: {}\n", .{err}); + }; + + const info_x = @as(i32, @trunc(self.pos.x)) - c.NODE_RADIUS / 2; + const info_y = @as(i32, @trunc(self.pos.y)) - c.NODE_RADIUS / 2; + + rl.drawText(entity_info, info_x, info_y, + c.ENTITY_DATA_TEXT_SIZE, c.ENTITY_DATA_TEXT_COLOUR); } /// Determines whether the pos (location) is within the snapping radius of the node diff --git a/src/infrastructure/node_manager.zig b/src/infrastructure/node_manager.zig index 444a5ba..f010d51 100644 --- a/src/infrastructure/node_manager.zig +++ b/src/infrastructure/node_manager.zig @@ -33,9 +33,9 @@ pub const NodeManager = struct { } /// Regular draw function - pub fn draw(self: *const NodeManager, pos: Vector2) void { + pub fn draw(self: *const NodeManager, pos: Vector2, display_info: bool) void { for (self.nodes.items) |node| { - node.draw(null); + node.draw(null, display_info); } if (self.temp_node) |node| { @@ -43,10 +43,10 @@ pub const NodeManager = struct { var cur_node = Node.init(0, pos); // Temporary road that is to be drawn as one in the making const road: Road = .init(0, node, &cur_node); - road.draw(false); + road.draw(false, false); - node.draw(c.NODE_TEMP_COLOUR); - cur_node.draw(c.NODE_CURSOR_COLOUR); + node.draw(c.NODE_TEMP_COLOUR, display_info); + cur_node.draw(c.NODE_CURSOR_COLOUR, false); } } diff --git a/src/infrastructure/road.zig b/src/infrastructure/road.zig index 0220eca..6aae992 100644 --- a/src/infrastructure/road.zig +++ b/src/infrastructure/road.zig @@ -46,9 +46,25 @@ pub const Road = struct { /// /// In the future as we improve and make roads more complex with multiple lanes and such /// it will gradually become more complex - pub fn draw(self: *const Road, highlighted: bool) void { + pub fn draw(self: *const Road, highlighted: bool, display_info: bool) void { const colour = if (highlighted) c.ROAD_HIGHLIGHTED_COLOUR else c.ROAD_COLOUR; rl.drawLineEx(self.nodes[0].pos, self.nodes[1].pos, c.ROAD_SIZE, colour); + + if (!display_info) return; + + var buf: [100]u8 = undefined; + const entity = std.fmt.bufPrintZ(&buf, "{d}", .{self.id}) catch |err| { + std.debug.panic("Could not allocate ID into string???: {}\n", .{err}); + }; + + const distance = ut.getVectorP1P2(self.nodes[0].pos, self.nodes[1].pos); + const entity_info_pos: rl.Vector2 = .{ + .x = self.nodes[0].pos.x + distance.x / 2 - c.ROAD_SIZE / 2, + .y = self.nodes[0].pos.y + distance.y / 2 - c.ROAD_SIZE / 2, + }; + + rl.drawText(entity, @trunc(entity_info_pos.x), @trunc(entity_info_pos.y), + c.ENTITY_DATA_TEXT_SIZE, c.ENTITY_DATA_TEXT_COLOUR); } /// Important: after this function executes, this road is no longer reachable from its bounding nodes diff --git a/src/infrastructure/road_manager.zig b/src/infrastructure/road_manager.zig index 9ba88c3..55444bd 100644 --- a/src/infrastructure/road_manager.zig +++ b/src/infrastructure/road_manager.zig @@ -27,10 +27,10 @@ pub const RoadManager = struct { } /// Draws all the roads in the list, sends the information ahead whether the road drawn should be highlighted - pub fn draw(self: *const RoadManager, highlighted_road: ?*Road) void { + pub fn draw(self: *const RoadManager, highlighted_road: ?*Road, display_info: bool) void { for (self.roads.items) |road| { const is_highlighted = if (highlighted_road) |h_road| road == h_road else false; - road.draw(is_highlighted); + road.draw(is_highlighted, display_info); } } diff --git a/src/simulator.zig b/src/simulator.zig index 631637a..ccc3cb5 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -29,6 +29,8 @@ pub const Simulator = struct { /// /// Note: It only works outside of the delete mode show_connections: bool, + /// Toggle that tracks whether ID (or possibly something more in the future) of every entity is displayed in GUI + display_entity_info: bool, highlighted_entity: ?st.Entity, /// Constructor for convenience @@ -40,6 +42,7 @@ pub const Simulator = struct { .auto_continue = false, .delete_mode = false, .show_connections = false, + .display_entity_info = false, .highlighted_entity = null, }; } @@ -61,8 +64,8 @@ pub const Simulator = struct { } } - self.road_man.draw(highlighted_road); - self.node_man.draw(pos); + self.road_man.draw(highlighted_road, self.display_entity_info); + self.node_man.draw(pos, self.display_entity_info); self.drawRelatedSelectedEntities(); } @@ -76,18 +79,18 @@ pub const Simulator = struct { const node = h_entity.node; for (node.roads.items) |road| { - road.draw(true); + road.draw(true, self.display_entity_info); } - node.draw(c.NODE_RELATED_COLOUR); + node.draw(c.NODE_RELATED_COLOUR, self.display_entity_info); }, .road => { const road = h_entity.road; - road.draw(true); + road.draw(true, self.display_entity_info); - road.nodes[0].draw(c.NODE_RELATED_COLOUR); - road.nodes[1].draw(c.NODE_RELATED_COLOUR); + road.nodes[0].draw(c.NODE_RELATED_COLOUR, self.display_entity_info); + road.nodes[1].draw(c.NODE_RELATED_COLOUR, self.display_entity_info); }, } } @@ -109,6 +112,8 @@ pub const Simulator = struct { self.delete_mode = rl.isKeyDown(.left_shift); self.show_connections = rl.isKeyDown(.left_alt) and !self.delete_mode; + if (rl.isKeyReleased(.tab)) self.display_entity_info = !self.display_entity_info; + if (rl.isKeyReleased(.c)) self.clear() catch |err| { std.debug.panic("Failed to clear the entities: {}\n", .{err}); }; From afd7aa50c4e6a3ca7365e17cff8f9cd4c49ca02c Mon Sep 17 00:00:00 2001 From: Marto Date: Fri, 1 May 2026 16:44:52 +0200 Subject: [PATCH 5/8] Added comments and slightly refactored code for deletion of temp node --- src/common/constants.zig | 3 +++ src/infrastructure/node_manager.zig | 14 +++++++++----- src/simulator.zig | 2 ++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/common/constants.zig b/src/common/constants.zig index 4c6cb77..4a57651 100644 --- a/src/common/constants.zig +++ b/src/common/constants.zig @@ -25,6 +25,9 @@ pub const ROAD_COLOUR = clr.black; /// Colour of the road that is highlighted pub const ROAD_HIGHLIGHTED_COLOUR = clr.green; +/// Regular text size pub const TEXT_SIZE = 50; +/// Text size that is used for displaying entity IDs pub const ENTITY_DATA_TEXT_SIZE = TEXT_SIZE / 2; +/// Text colour in which entity IDs are displayed if toggled pub const ENTITY_DATA_TEXT_COLOUR = clr.orange; \ No newline at end of file diff --git a/src/infrastructure/node_manager.zig b/src/infrastructure/node_manager.zig index f010d51..a2ea6c2 100644 --- a/src/infrastructure/node_manager.zig +++ b/src/infrastructure/node_manager.zig @@ -118,16 +118,20 @@ pub const NodeManager = struct { return null; } + /// Essentially what it does is it sets temp node pointer to null and + /// if the node it pointed at had no road references (essentially it was a new node for road building), + /// it deletes the node from the node list as well pub fn deleteTempNode(self: *NodeManager, allocator: std.mem.Allocator) void { if (self.temp_node == null) return; const node = self.temp_node.?; - if (node.roads.items.len == 0) - self.deleteNode(allocator, node) catch |err| { - std.debug.panic("Failed to delete the temporary node: {}\n", .{err}); - }; - self.temp_node = null; + + if (node.roads.items.len != 0) return; + self.deleteNode(allocator, node) catch |err| { + std.debug.panic("Failed to delete the temporary node: {}\n", .{err}); + }; + } }; diff --git a/src/simulator.zig b/src/simulator.zig index ccc3cb5..9644f5d 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -219,6 +219,7 @@ pub const Simulator = struct { self.highlighted_entity = null; } + /// Returns array of IntersectionData struct, containing pointers to roads that got intersected and exact position fn getIntersectingRoads(self: *const Simulator, allocator: std.mem.Allocator, start: *const Node, end: *const Node) ![]st.IntersectionData { var intersections: std.ArrayList(st.IntersectionData) = .empty; var collision_point: rl.Vector2 = undefined; @@ -286,6 +287,7 @@ pub const Simulator = struct { return sorted_intersection; } + /// Takes the data about intersections and adds new nodes there alongside with linking existing roads to them fn splitRoadsByIntersections(self: *Simulator, intersections: []st.IntersectionData, start: *Node, end: *Node) void { if (intersections.len == 0) { self.road_man.addRoad(self.allocator, start, end) catch |err| { From dd64ec648a24175d2b7de5d6a9507fde3930a3fd Mon Sep 17 00:00:00 2001 From: Marto Date: Fri, 1 May 2026 17:44:30 +0200 Subject: [PATCH 6/8] Splitting function completed --- src/infrastructure/road.zig | 18 +++++++++- src/simulator.zig | 69 ++++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/infrastructure/road.zig b/src/infrastructure/road.zig index 6aae992..9a0288c 100644 --- a/src/infrastructure/road.zig +++ b/src/infrastructure/road.zig @@ -1,6 +1,7 @@ const rl = @import("raylib"); const c = @import("../common/constants.zig"); +const e = @import("../errors.zig"); const st = @import("../common/structures.zig"); const ut = @import("../common/utils.zig"); const Node = @import("node.zig").Node; @@ -79,6 +80,18 @@ pub const Road = struct { pub fn collides(self: *const Road, pos: rl.Vector2) bool { return rl.checkCollisionPointLine(pos, self.nodes[0].pos, self.nodes[1].pos, c.ROAD_SIZE); } + + /// Updates node reference old_node => new node; returns error if old_node does not exist + pub fn updateNodeReference(self: *Road, old_node: *Node, new_node: *Node) !void { + for (0..self.nodes.len) |i| { + if (self.nodes[i] != old_node) continue; + + self.nodes[i] = new_node; + return; + } + + return e.Entity.NotFound; + } }; const std = @import("std"); @@ -109,4 +122,7 @@ test "valid road nodes" { try expect(road.nodes[0].id == 34); try expect(road.nodes[1].id == 227); -} \ No newline at end of file +} + +// TODO tests +// test every case error for every function that can return an error \ No newline at end of file diff --git a/src/simulator.zig b/src/simulator.zig index 9644f5d..fbae2dc 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -288,18 +288,77 @@ pub const Simulator = struct { } /// Takes the data about intersections and adds new nodes there alongside with linking existing roads to them - fn splitRoadsByIntersections(self: *Simulator, intersections: []st.IntersectionData, start: *Node, end: *Node) void { + /// + /// Important: This function assumes the intersection array is sorted by distance from the start node (ascending) + fn splitRoadsByIntersections(self: *Simulator, intersections: []st.IntersectionData, start: *Node, end: *Node) !void { if (intersections.len == 0) { self.road_man.addRoad(self.allocator, start, end) catch |err| { - std.debug.panic("Failed to create a road connecting the origin nodes: {}\n", .{err}); + std.debug.panic("Failed creating the road out of origin nodes: {}\n", .{err}); }; - return; } - for (0..intersections.len) |i| { - _ = i; // autofix + // Here we connect the start node with the first intersection node (via road) + const first_node = self.node_man.getSelectedNode(self.allocator, intersections[0].pos) catch |err| { + std.debug.panic("Failed to add the first node of the intersection: {}\n", .{err}); + }; + self.road_man.addRoad(self.allocator, start, first_node) catch |err| { + std.debug.panic("Failed to add a road of origin (start) node and the first intersection node: {}\n", .{err}); + }; + for (0..intersections.len) |i| { + const intersection = intersections[i]; + + // The node created at the point of intersection + const new_node = self.node_man.getSelectedNode(self.allocator, intersection.pos) catch |err| { + std.debug.panic("Failed to create a node based on the intersection index {d}: {}\n", .{ + i, + err + }); + }; + + // Pointer to the node that borders the road that was intersected + // This node and the new_node will become nodes for the new road being created + const old_node_of_road = intersection.road.nodes[1]; + + // The old road that was intersected now borders the new node + // and the old node is removed from the road's end node reference, + // as is the end node's road reference + + // So the intersected road loses old node (at the far end) and gets new node that intersects it + intersection.road.updateNodeReference(old_node_of_road, new_node) catch |err| { + std.debug.panic("Failed to update the road's node references: {}\n", .{err}); + }; + + // Now we add the road (to the road list) and references the road at both bounding nodes + self.road_man.addRoad(self.allocator, new_node, old_node_of_road) catch |err| { + std.debug.panic("Failed to create a road of new node and former node of prior intersecting road", .{ + err + }); + }; + + // Here we work on creating new roads between intersection nodes and as such because we need nodes + // at 2 different intersections, it means we have to be sure next one exists + if (i == intersections.len - 1) continue; + + const next_intersection = self.node_man.getSelectedNode(self.allocator, intersections[i+1].pos) catch |err| { + std.debug.panic("Failed to create node of next intersection (current index={d}: {}\n", .{i, err}); + }; + + // Creating the road connecting current intersection with the next one + self.road_man.addRoad(self.allocator, new_node, next_intersection) catch |err| { + std.debug.panic("Failed to create the road of current and next intersection nodes: {}\n", .{err}); + }; } + + // Finally we create final road by connecting last intersection node to the end origin node + const final_intersection_pos = intersections[intersections.len - 1].pos; + const final_intersection_node = self.node_man.getSelectedNode(self.allocator, final_intersection_pos) catch |err| { + std.debug.panic("Failed to create node based on last intersection position: {}\n", .{err}); + }; + + self.road_man.addRoad(self.allocator, final_intersection_node, end) catch |err| { + std.debug.panic("Failed to create a road of final intersection and end origin node: {}\n", .{err}); + }; } }; \ No newline at end of file From f7a1340500e2fdfca771aaf54e4107609b28d608 Mon Sep 17 00:00:00 2001 From: Marto Date: Fri, 1 May 2026 19:03:31 +0200 Subject: [PATCH 7/8] Fixed the issue of newly built intersecting node not connecting to all existing (intersecting) roads --- src/infrastructure/road.zig | 2 ++ src/infrastructure/road_manager.zig | 3 +-- src/simulator.zig | 18 +++++++++++------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/infrastructure/road.zig b/src/infrastructure/road.zig index 9a0288c..494ca36 100644 --- a/src/infrastructure/road.zig +++ b/src/infrastructure/road.zig @@ -87,6 +87,8 @@ pub const Road = struct { if (self.nodes[i] != old_node) continue; self.nodes[i] = new_node; + // As nodes change, road's length must be recalculated + self.length = ut.calculate_length(self.nodes[0].pos, self.nodes[1].pos); return; } diff --git a/src/infrastructure/road_manager.zig b/src/infrastructure/road_manager.zig index 55444bd..1a0e394 100644 --- a/src/infrastructure/road_manager.zig +++ b/src/infrastructure/road_manager.zig @@ -37,9 +37,8 @@ pub const RoadManager = struct { /// Function which creates the road object, its pointer, adds it to the list /// and then also references that same road to the bounding nodes pub fn addRoad(self: *RoadManager, allocator: std.mem.Allocator, start: *Node, end: *Node) !void { - const road: Road = .init(self.getNextID(), start, end); const road_ptr = try allocator.create(Road); - road_ptr.* = road; + road_ptr.* = Road.init(self.getNextID(), start, end); try self.roads.append(allocator, road_ptr); const ref = self.roads.items[self.roads.items.len - 1]; diff --git a/src/simulator.zig b/src/simulator.zig index fbae2dc..1790102 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -153,11 +153,6 @@ pub const Simulator = struct { // Prevents the road from being attached to 2 identical nodes (0 length road) if (temp.id == cur_node.id) return; - // TODO replace with road splitting - self.road_man.addRoad(self.allocator, temp, cur_node) catch |err| { - std.debug.panic("Failed to add a new road or assigning its nodes: {}\n", .{err}); - }; - const intersections = self.getIntersectingRoads(self.allocator, temp, cur_node) catch |err| { std.debug.panic("Intersection selection failure: {}\n", .{err}); }; @@ -170,6 +165,8 @@ pub const Simulator = struct { std.debug.print("Road ID={d} Pos: ({d}, {d})\n", .{int.road.id, int.pos.x, int.pos.y}); } + self.splitRoadsByIntersections(intersections, temp, cur_node); + self.node_man.temp_node = if (self.auto_continue) cur_node else null; return; } @@ -290,7 +287,7 @@ pub const Simulator = struct { /// Takes the data about intersections and adds new nodes there alongside with linking existing roads to them /// /// Important: This function assumes the intersection array is sorted by distance from the start node (ascending) - fn splitRoadsByIntersections(self: *Simulator, intersections: []st.IntersectionData, start: *Node, end: *Node) !void { + fn splitRoadsByIntersections(self: *Simulator, intersections: []st.IntersectionData, start: *Node, end: *Node) void { if (intersections.len == 0) { self.road_man.addRoad(self.allocator, start, end) catch |err| { std.debug.panic("Failed creating the road out of origin nodes: {}\n", .{err}); @@ -329,10 +326,17 @@ pub const Simulator = struct { intersection.road.updateNodeReference(old_node_of_road, new_node) catch |err| { std.debug.panic("Failed to update the road's node references: {}\n", .{err}); }; + // Now the old node must not point at the intersection road + old_node_of_road.unreferenceRoad(intersection.road) catch |err| { + std.debug.panic("Failed to unreference the intersection road from the old node: {}\n", .{err}); + }; + new_node.referenceRoad(self.allocator, intersection.road) catch |err| { + std.debug.panic("Failed to reference the intersection road to the intersecting node: {}\n", .{err}); + }; // Now we add the road (to the road list) and references the road at both bounding nodes self.road_man.addRoad(self.allocator, new_node, old_node_of_road) catch |err| { - std.debug.panic("Failed to create a road of new node and former node of prior intersecting road", .{ + std.debug.panic("Failed to create a road of new node and former node of prior intersecting road: {}\n", .{ err }); }; From fdf672de4bf08ec71ef80e3f998780843cf831a3 Mon Sep 17 00:00:00 2001 From: Marto Date: Fri, 1 May 2026 20:46:09 +0200 Subject: [PATCH 8/8] Fixed splitting quirks when only one intersection is found and the intersection itself is origin node --- src/common/structures.zig | 14 +++++++++++++- src/simulator.zig | 35 +++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/common/structures.zig b/src/common/structures.zig index ad2bc6a..96c64ac 100644 --- a/src/common/structures.zig +++ b/src/common/structures.zig @@ -3,7 +3,12 @@ const Vector2 = @import("raylib").Vector2; const Road = @import("../infrastructure/road.zig").Road; const Node = @import("../infrastructure/node.zig").Node; -/// Can point to either entity type (node, road, etc.) +/// This is a simple equivalent to something like abstract class, so the simulator class has only one variable +/// which tracks which object is highlighted and the logic that seeks to find the current highlighted class doesn't need +/// to figure out which highlighted entity has priority in case of overlap; +/// given that car can be on node and road, and node itself is on road(s) +/// +/// TLDR: Can point to either entity type (node, road, etc.) pub const Entity = union(enum) { node: *Node, road: *Road, @@ -16,4 +21,11 @@ pub const IntersectionData = struct { pos: Vector2, /// Points to the road where the intersection occured road: *Road, + /// Tracks whether this intersection is actually an origin point + /// (In simple terms this means whether the point/node that intersects is also the start/end node) + /// + /// We do this because the referencing logic is very strict and will return an error + /// if we try to reference something that is already referenced, due to stricter approach helping with + /// earlier bug and error detection + origin: bool, }; \ No newline at end of file diff --git a/src/simulator.zig b/src/simulator.zig index 1790102..2470716 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -239,6 +239,7 @@ pub const Simulator = struct { try intersections.append(self.allocator, .{ .road = road, .pos = start.pos, + .origin = true, }); } @@ -252,6 +253,7 @@ pub const Simulator = struct { const intersection = st.IntersectionData { .road = road, .pos = collision_point, + .origin = false, }; // We put a 0 here, just to satisfy the constructor function, @@ -275,6 +277,7 @@ pub const Simulator = struct { try intersections.append(self.allocator, .{ .road = road, .pos = end.pos, + .origin = true, }); } @@ -295,13 +298,30 @@ pub const Simulator = struct { return; } - // Here we connect the start node with the first intersection node (via road) const first_node = self.node_man.getSelectedNode(self.allocator, intersections[0].pos) catch |err| { std.debug.panic("Failed to add the first node of the intersection: {}\n", .{err}); }; - self.road_man.addRoad(self.allocator, start, first_node) catch |err| { - std.debug.panic("Failed to add a road of origin (start) node and the first intersection node: {}\n", .{err}); - }; + + var override_node: ?*Node = null; + // This if statement essentially checks that IF we only have one intersection and that one is one of the origin nodes, + // it means that we have to enable one of start => intersection, or, end => intersection road building logic + // + // However due to the possibility that we link the road to itself (intersection[0] is start that we then connect + // that one to start node; so intersection[0] => start = start => start), + // we have to essentially realise which node is that first intersection and essentially store that info and only + // let the opposite node form a road with the intersection + // and that is what override_node, override_start and override_end variables are all about + if (intersections.len == 1 and intersections[0].origin) { + override_node = if (first_node == start) end else start; + } + + const override_start = override_node != null and override_node.? == start; + if (!intersections[0].origin or override_start) { + // Here we connect the start node with the first intersection node (via road) + self.road_man.addRoad(self.allocator, start, first_node) catch |err| { + std.debug.panic("Failed to add a road of origin (start) node and the first intersection node: {}\n", .{err}); + }; + } for (0..intersections.len) |i| { const intersection = intersections[i]; @@ -355,12 +375,15 @@ pub const Simulator = struct { }; } + const override_end = override_node != null and override_node.? == end; + // Finally we create final road by connecting last intersection node to the end origin node - const final_intersection_pos = intersections[intersections.len - 1].pos; - const final_intersection_node = self.node_man.getSelectedNode(self.allocator, final_intersection_pos) catch |err| { + const final_intersection = intersections[intersections.len - 1]; + const final_intersection_node = self.node_man.getSelectedNode(self.allocator, final_intersection.pos) catch |err| { std.debug.panic("Failed to create node based on last intersection position: {}\n", .{err}); }; + if (final_intersection.origin and !override_end) return; self.road_man.addRoad(self.allocator, final_intersection_node, end) catch |err| { std.debug.panic("Failed to create a road of final intersection and end origin node: {}\n", .{err}); };