diff --git a/src/common/constants.zig b/src/common/constants.zig index c87cffc..4a57651 100644 --- a/src/common/constants.zig +++ b/src/common/constants.zig @@ -24,3 +24,10 @@ 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; + +/// 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/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/common/utils.zig b/src/common/utils.zig new file mode 100644 index 0000000..8e081cb --- /dev/null +++ b/src/common/utils.zig @@ -0,0 +1,29 @@ +const Vector2 = @import("raylib").Vector2; + +const st = @import("structures.zig"); +const Node = @import("../infrastructure/node.zig").Node; + +/// 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; + + 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 +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; +} + +/// 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 1946d5d..2fedf55 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 { @@ -34,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 @@ -77,6 +90,20 @@ 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); + } + + /// 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/node_manager.zig b/src/infrastructure/node_manager.zig index e1f3821..a2ea6c2 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,18 +43,29 @@ 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); } } + /// 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 @@ -107,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/infrastructure/road.zig b/src/infrastructure/road.zig index af19395..494ca36 100644 --- a/src/infrastructure/road.zig +++ b/src/infrastructure/road.zig @@ -1,7 +1,9 @@ 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; pub const Road = struct { @@ -20,7 +22,7 @@ pub const Road = struct { .length = 0, }; - road.length = road.calculate_length(); + road.length = ut.calculate_length(start.pos, end.pos); return road; } @@ -45,9 +47,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 @@ -59,9 +77,23 @@ 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); } + + /// 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; + // As nodes change, road's length must be recalculated + self.length = ut.calculate_length(self.nodes[0].pos, self.nodes[1].pos); + return; + } + + return e.Entity.NotFound; + } }; const std = @import("std"); @@ -92,4 +124,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/infrastructure/road_manager.zig b/src/infrastructure/road_manager.zig index 21a2d33..1a0e394 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; @@ -26,19 +27,18 @@ 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); } } /// 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]; @@ -88,30 +88,11 @@ 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; } - - 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..2470716 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; @@ -27,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 @@ -38,6 +42,7 @@ pub const Simulator = struct { .auto_continue = false, .delete_mode = false, .show_connections = false, + .display_entity_info = false, .highlighted_entity = null, }; } @@ -59,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(); } @@ -74,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); }, } } @@ -107,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}); }; @@ -143,10 +150,22 @@ 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; - 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}); }; + defer self.allocator.free(intersections); + + // 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.splitRoadsByIntersections(intersections, temp, cur_node); self.node_man.temp_node = if (self.auto_continue) cur_node else null; return; @@ -196,4 +215,177 @@ 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; + + 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, + .origin = true, + }); + } + + 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, + .origin = false, + }; + + // 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 + 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; + + 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, + .origin = true, + }); + } + + const sorted_intersection = try intersections.toOwnedSlice(allocator); + std.sort.block(st.IntersectionData, sorted_intersection, start, ut.compareIntersections); + + return sorted_intersection; + } + + /// 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 { + 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}); + }; + return; + } + + 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}); + }; + + 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]; + + // 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 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: {}\n", .{ + 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}); + }; + } + + 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 = 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}); + }; + } }; \ No newline at end of file