diff --git a/src/constants.zig b/src/constants.zig index 72b886a..936072d 100644 --- a/src/constants.zig +++ b/src/constants.zig @@ -9,7 +9,7 @@ pub const ALLOC_SIZE = 50; pub const NODE_RADIUS = 20; /// Node snapping radius designating the radius upon which new node will not be created /// but rather merged with an existing one -pub const NODE_SNAP_RADIUS = 3; +pub const NODE_SNAP_RADIUS = 3 * NODE_RADIUS; /// Road size used for display and calculation pub const ROAD_SIZE = 20; /// Default text size diff --git a/src/infrastructure/node.zig b/src/infrastructure/node.zig index cf93b51..f649956 100644 --- a/src/infrastructure/node.zig +++ b/src/infrastructure/node.zig @@ -24,17 +24,33 @@ pub const Node = struct { /// References the passed road with the node pub fn referenceRoad(self: *Node, allocator: std.mem.Allocator, road: *Road) !void { - if (self.roadInList(road)) return; + if (self.roadInList(road) != null) return; try self.roads.append(allocator, road); } + /// Tries to unrference the road passed into from the node (self) + /// + /// Returns error if the road is not within the node + pub fn unreferenceRoad(self: *Node, road: *const Road) !void { + if (self.roadInList(road)) |i| _ = self.roads.swapRemove(i); + + return error.InvalidNode; + } /// Returns bool whether the road passed is part of the roads list - fn roadInList(self: *const Node, road_to_add: *const Road) bool { - for (self.roads.items) |road| { - if (road.id == road_to_add.id) return true; + fn roadInList(self: *const Node, road_to_check: *const Road) ?usize { + for (self.roads.items, 0..) |road, i| { + std.debug.print("Road id = {d}\n", .{road.id}); + std.debug.print("Road to check id = {d}\n", .{road_to_check.id}); + // TODO fix why doesn't 0 == 0 return i + if (road.id == road_to_check.id) return i; } - return false; + return null; } + + pub fn withinSnappingRadius(self: *const Node, pos: rl.Vector2) bool { + return rl.checkCollisionPointCircle(pos, self.pos, c.NODE_SNAP_RADIUS); + } + }; \ No newline at end of file diff --git a/src/infrastructure/node_manager.zig b/src/infrastructure/node_manager.zig index c5dac55..8aaf309 100644 --- a/src/infrastructure/node_manager.zig +++ b/src/infrastructure/node_manager.zig @@ -22,14 +22,13 @@ pub const NodeManager = struct { pub fn draw(self: *const NodeManager, display_details: bool) void { for (self.nodes.items) |node| { - if (display_details) rl.drawCircleV(node.pos, c.NODE_SNAP_RADIUS * c.NODE_RADIUS, .pink); + if (display_details) rl.drawCircleV(node.pos, c.NODE_SNAP_RADIUS, .pink); rl.drawCircleV(node.pos, c.NODE_RADIUS, .brown); } const pos = rl.getMousePosition(); if (self.temp_node) |node| { - rl.drawLineEx(node.pos, pos, c.ROAD_SIZE, .black); rl.drawCircleV(node.pos, c.NODE_RADIUS, .brown); @@ -47,16 +46,22 @@ pub const NodeManager = struct { return null; } - /// Returns the pointer to the node that is in the snapping node radius of the pos - /// or creates a new node and returns pointer to it - fn getSelectedNode(self: *NodeManager, pos: rl.Vector2) *Node { + pub fn getNodeWithinRadius(self: *const NodeManager, pos: rl.Vector2) ?*Node { for (self.nodes.items) |*node| { - if (!rl.checkCollisionPointCircle(pos, node.pos, c.NODE_SNAP_RADIUS * c.NODE_RADIUS)) + if (!node.withinSnappingRadius(pos)) continue; return node; } + return null; + } + + /// Returns the pointer to the node that is in the snapping node radius of the pos + /// or creates a new node and returns pointer to it + fn getSelectedNode(self: *NodeManager, pos: rl.Vector2) *Node { + if (self.getNodeWithinRadius(pos)) |node| return node; + // We couldn't find the existing node and as such must create a new one const node: Node = .init(self.getNewID(), pos); @@ -75,9 +80,7 @@ pub const NodeManager = struct { /// generates finds the last element in the list and returns id + 1 or 0 if there are no elements in the list fn getNewID(self: *const NodeManager) usize { - const last_ref = self.getLastRef(); - - return if (last_ref) |ref| ref.*.id + 1 else 0; + return if (self.getLastRef()) |ref| ref.*.id + 1 else 0; } /// Iterates through all nodes and runs the deinit procedure on each of them @@ -88,8 +91,9 @@ pub const NodeManager = struct { } /// Removes the node from the list with all appropriate checks - pub fn removeNode(self: *NodeManager, node_to_remove: *Node) void { + pub fn removeNode(self: *NodeManager, node_to_remove: *Node) !void { // In case the node has references to the existing roads we can not remove it + // This also means we don't have to deinit it, since it has no elements if (node_to_remove.roads.items.len > 0) return; for (self.nodes.items, 0..) |*node, i| { @@ -98,5 +102,7 @@ pub const NodeManager = struct { _ = self.nodes.swapRemove(i); return; } + + return error.NodeNotExist; } }; \ No newline at end of file diff --git a/src/infrastructure/road.zig b/src/infrastructure/road.zig index eab87fb..4f6cbf1 100644 --- a/src/infrastructure/road.zig +++ b/src/infrastructure/road.zig @@ -10,4 +10,9 @@ pub const Road = struct { .nodes = .{start_node, end_node}, }; } + + pub fn unreferenceNodes(self: *Road) !void { + try self.nodes[0].unreferenceRoad(self); + try self.nodes[1].unreferenceRoad(self); + } }; \ No newline at end of file diff --git a/src/infrastructure/road_manager.zig b/src/infrastructure/road_manager.zig index 9ab8ef8..7db2802 100644 --- a/src/infrastructure/road_manager.zig +++ b/src/infrastructure/road_manager.zig @@ -2,15 +2,18 @@ const std = @import("std"); const rl = @import("raylib"); const c = @import("../constants.zig"); +const st = @import("../structures.zig"); const Road = @import("road.zig").Road; const Node = @import("node.zig").Node; pub const RoadManager = struct { roads: std.ArrayList(Road), + highlighted_road: ?*Road, pub fn init(allocator: std.mem.Allocator) !RoadManager { return .{ .roads = try .initCapacity(allocator, c.ALLOC_SIZE), + .highlighted_road = null, }; } @@ -18,9 +21,18 @@ pub const RoadManager = struct { self.roads.deinit(allocator); } - pub fn draw(self: *const RoadManager) void { + pub fn draw(self: *RoadManager, display_highlighted_road: bool) void { + self.highlighted_road = self.getSelectedRoad(rl.getMousePosition()); + for (self.roads.items) |road| { - rl.drawLineEx(road.nodes[0].*.pos, road.nodes[1].*.pos, c.ROAD_SIZE, .black); + var colour: rl.Color = .black; + if (self.highlighted_road) |hr| { + if (display_highlighted_road and road.id == hr.id) { + colour = .green; + } + } + + rl.drawLineEx(road.nodes[0].*.pos, road.nodes[1].*.pos, c.ROAD_SIZE, colour); } } @@ -42,14 +54,38 @@ pub const RoadManager = struct { /// Returns the reference to the last element in the list; returns null if there are no elements in the list fn getLastRef(self: *const RoadManager) ?*Road { const rlen = self.roads.items.len; - return if (rlen == 0) null else &self.roads.items[rlen - 1]; } - /// generates finds the last element in the list and returns id + 1 or 0 if there are no elements in the list + /// Generates finds the last element in the list and returns id + 1 or 0 if there are no elements in the list fn getNewID(self: *const RoadManager) usize { - const last_ref = self.getLastRef(); + return if (self.getLastRef()) |ref| ref.*.id + 1 else 0; + } - return if (last_ref) |ref| ref.*.id + 1 else 0; + /// Removes the road which reference is passed in; returns error if the road is invalid + pub fn remove(self: *RoadManager, road_to_remove: *Road) !void { + try road_to_remove.unreferenceNodes(); + + for (self.roads.items, 0..) |*road, i| { + if (road.id == road_to_remove.id) { + _ = self.roads.swapRemove(i); + return; + } + } + + return error.RoadNotInList; + } + + /// Returns the pointer to the road that pos is pointing at or null if there is no road in the vicinity + pub fn getSelectedRoad(self: *const RoadManager, pos: rl.Vector2) ?*Road { + for (self.roads.items) |*road| { + if (!rl.checkCollisionPointLine( + pos, road.nodes[0].pos, road.nodes[1].pos, c.ROAD_SIZE / 2)) + continue; + + return road; + } + + return null; } }; \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index a6e5cad..c6ae73d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -16,7 +16,6 @@ pub fn main() !void { rl.initWindow(c.WIDTH, c.HEIGHT, "Base Road Network"); defer rl.closeWindow(); - // Used to set to (my) default monitor const monitor = 0; rl.setWindowMonitor(monitor); diff --git a/src/simulator.zig b/src/simulator.zig index e7b2899..635bcfe 100644 --- a/src/simulator.zig +++ b/src/simulator.zig @@ -2,9 +2,13 @@ const std = @import("std"); const rl = @import("raylib"); const c = @import("constants.zig"); +const st = @import("structures.zig"); + const NodeManager = @import("infrastructure/node_manager.zig").NodeManager; const RoadManager = @import("infrastructure/road_manager.zig").RoadManager; +const Node = @import("infrastructure/node.zig").Node; + pub const Simulator = struct { allocator: std.mem.Allocator, /// Contains data and functions about nodes @@ -15,6 +19,9 @@ pub const Simulator = struct { display_details: bool, /// Tracks whether to automatically start build a new road after the creation of previous one is finished auto_continue: bool, + /// Displays intersection data for easier implementation and debugging + debug_intersection_data: std.ArrayList(st.IntersectionData), + mode: st.Mode, pub fn init(allocator: std.mem.Allocator) !Simulator { return .{ @@ -23,12 +30,15 @@ pub const Simulator = struct { .road_man = try .init(allocator), .display_details = false, .auto_continue = false, + .debug_intersection_data = .empty, + .mode = .VISUAL, }; } pub fn deinit(self: *Simulator) void { self.node_man.deinit(self.allocator); self.road_man.deinit(self.allocator); + self.debug_intersection_data.deinit(self.allocator); } /// Input handling function that is exposed to the public @@ -42,50 +52,72 @@ pub const Simulator = struct { self.display_details = rl.isKeyDown(.left_alt); self.auto_continue = rl.isKeyDown(.left_control); - if (rl.isKeyReleased(.c)) { + if (rl.isKeyReleased(.c) and self.mode == .DELETE) { + self.debug_intersection_data.clearRetainingCapacity(); self.node_man.temp_node = null; self.road_man.roads.clearRetainingCapacity(); self.node_man.clear(self.allocator); self.node_man.nodes.clearRetainingCapacity(); } + + self.mode = switch (rl.getKeyPressed()) { + .v => .VISUAL, + .d => .DELETE, + .b => .BUILD, + else => self.mode, + }; } /// every mouse event is checked here fn handleMouseInput(self: *Simulator) void { const pos = rl.getMousePosition(); - if (rl.isMouseButtonReleased(.left)) { + if (self.mode == .BUILD and rl.isMouseButtonReleased(.left)) { self.leftClickEvent(pos); } - else if (rl.isMouseButtonReleased(.right)) self.rightClickEvent(); - + else if (self.mode == .BUILD and rl.isMouseButtonReleased(.right)) { + if (self.node_man.temp_node) |node| { + // todo + self.node_man.removeNode(node) catch {}; + } + } + else if (self.mode == .DELETE and rl.isMouseButtonReleased(.left) and self.road_man.highlighted_road != null) { + // todo + self.road_man.remove(self.road_man.highlighted_road.?) catch |err| { + std.debug.panic("Failed to remove the road: {}\n", .{err}); + }; + } } fn leftClickEvent(self: *Simulator, pos: rl.Vector2) void { if (self.node_man.add(pos)) |node| { - if (self.node_man.temp_node.?.id == node.id) return; + const temp = self.node_man.temp_node.?; + if (temp.id == node.id) return; - self.road_man.add(self.allocator, self.node_man.temp_node.?, node) catch |err| { + self.road_man.add(self.allocator, temp, node) catch |err| { std.debug.panic("Error while attempting to add a road: {}\n", .{err}); }; + // get intersections made + self.getIntersectingRoads(temp, node) catch |err| { + std.debug.panic("Failed to save intersection data: {}\n", .{err}); + }; + self.node_man.temp_node = if (self.auto_continue) node else null; } } - fn rightClickEvent(self: *Simulator) void { - if (self.node_man.temp_node) |node| { - self.node_man.removeNode(node); - self.node_man.temp_node = null; - } - } - /// The main drawing function that is exposed upwards - pub fn draw(self: *const Simulator) void { - self.road_man.draw(); + pub fn draw(self: *Simulator) void { + self.road_man.draw(self.mode == .DELETE); self.node_man.draw(self.display_details); self.drawUI(); + // Display intersection 'nodes' + for (self.debug_intersection_data.items) |intersection| { + rl.drawCircleV(intersection.point, c.NODE_RADIUS / 2, .red); + } + rl.clearBackground(.light_gray); } @@ -93,6 +125,7 @@ pub const Simulator = struct { fn drawUI(self: *const Simulator) void { var buf: [1024]u8 = undefined; + // Displays how many nodes and roads are currently in the lists const entities_list = std.fmt.bufPrintZ( &buf, @@ -106,5 +139,31 @@ pub const Simulator = struct { c.WIDTH - 13 * @as(i32, @intCast(entities_list.len)), c.HEIGHT - 2 * c.TEXT_SIZE, c.TEXT_SIZE, .black); + + + // Displays the mode the simulation is currently in + rl.drawText(@tagName(self.mode), 10, c.HEIGHT - c.TEXT_SIZE, c.TEXT_SIZE, .black); + } + + /// Gets list of pointers of all roads that 'collide' with the road bounded by the nodes we pass into it + fn getIntersectingRoads(self: *Simulator, start: *const Node, end: *const Node) !void { + 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, + .point = collision_point, + }; + + const node: Node = .init(0, intersection.point); + // here we need to check if the points captured already are already within the reach + for (self.debug_intersection_data.items) |collision| { + if (node.withinSnappingRadius(collision.point)) + continue :outer; + } + + if (self.node_man.getNodeWithinRadius(intersection.point) == null) try self.debug_intersection_data.append(self.allocator, intersection); + } } }; \ No newline at end of file diff --git a/src/structures.zig b/src/structures.zig new file mode 100644 index 0000000..b499aaa --- /dev/null +++ b/src/structures.zig @@ -0,0 +1,14 @@ +const Vector2 = @import("raylib").Vector2; + +const Road = @import("infrastructure/road.zig").Road; + +pub const IntersectionData = struct { + road: *Road, + point: Vector2, +}; + +pub const Mode = enum { + VISUAL, + BUILD, + DELETE +}; \ No newline at end of file